feat(FE-350): add filtering table

This commit is contained in:
randy-ar
2025-12-24 16:44:53 +07:00
parent 36ff6d04ee
commit 8c95dc8327
10 changed files with 933 additions and 2843 deletions
+1 -1
View File
@@ -1,3 +1,3 @@
npm run format
npm run lint
npm run build
npx tsc --noEmit
+1 -1
View File
@@ -11,7 +11,7 @@ const FinanceDetailPage = () => {
const financeId = useSearchParams().get('financeId');
const { data: finance } = useSWR(financeId, () =>
FinanceApi.getSingleFetcher(financeId as string)
FinanceApi.getSingle(Number(financeId))
);
if (!financeId) {
+3 -21
View File
@@ -1,30 +1,12 @@
'use client';
import FinanceTable from '@/components/pages/finance/FinanceTable';
import { isResponseSuccess } from '@/lib/api-helper';
import { FinanceApi } from '@/services/api/finance';
import useSWR from 'swr';
const Finance = () => {
const { data: finances, isLoading: isLoadingFinances } = useSWR(
`${FinanceApi.basePath}`,
() => FinanceApi.getAllFetcher()
);
if (isLoadingFinances) {
return (
<div className='w-full flex flex-row justify-center items-center p-4'>
<span className='loading loading-spinner loading-xl' />
</div>
);
}
return (
<section className='size-full p-4'>
<h1>Finance</h1>
<FinanceTable
finances={isResponseSuccess(finances) ? finances.data : []}
/>
<section className='size-full p-6'>
<div className='flex flex-row gap-4'></div>
<FinanceTable />
</section>
);
};
+174 -64
View File
@@ -1,87 +1,197 @@
'use client';
import { ReactNode, useEffect } from 'react';
import useSWR from 'swr';
import { useRouter } from 'next/navigation';
import useSWRImmutable from 'swr/immutable';
import { useAuth } from '@/services/hooks/useAuth';
import { httpClientFetcher, SWRHttpKey } from '@/services/http/client';
import { isResponseError, isResponseSuccess } from '@/lib/api-helper';
import { BaseApiResponse, GetMeResponse } from '@/types/api/api-general';
import { AxiosError } from 'axios';
import { redirectToSSO } from '@/lib/auth-helper';
import { isResponseSuccess } from '@/lib/api-helper';
import { GetMeResponse } from '@/types/api/api-general';
// TODO: delete this later, DONT HARDCODE USER DATA
const DUMMY_USER = {
id: 1,
email: 'admin@mbugroup.id',
npk: '0001',
name: 'Super Admin',
image: null,
created_at: '2025-09-30T03:24:20.899229Z',
updated_at: '2025-09-30T03:24:20.899229Z',
roles: [
{
id: 1,
key: 'mbu.super_admin',
name: 'MBU Administrator',
client: {
id: 1,
name: 'PT Mitra Berlian Unggas',
alias: 'MBU',
},
permissions: [
{
id: 1,
name: 'mbu:purchase:read',
action: 'read',
client: {
id: 1,
name: 'PT Mitra Berlian Unggas',
alias: 'MBU',
},
},
{
id: 2,
name: 'mbu:purchase:create',
action: 'create',
client: {
id: 1,
name: 'PT Mitra Berlian Unggas',
alias: 'MBU',
},
},
{
id: 3,
name: 'mbu:purchase:approve',
action: 'approve',
client: {
id: 1,
name: 'PT Mitra Berlian Unggas',
alias: 'MBU',
},
},
],
},
{
id: 2,
key: 'lti.super_admin',
name: 'LTI Administrator',
client: {
id: 2,
name: 'PT Lumbung Telur Indonesia',
alias: 'LTI',
},
permissions: [
{
id: 4,
name: 'lti:purchase:read',
action: 'read',
client: {
id: 2,
name: 'PT Lumbung Telur Indonesia',
alias: 'LTI',
},
},
{
id: 5,
name: 'lti:purchase:create',
action: 'create',
client: {
id: 2,
name: 'PT Lumbung Telur Indonesia',
alias: 'LTI',
},
},
{
id: 6,
name: 'lti:purchase:approve',
action: 'approve',
client: {
id: 2,
name: 'PT Lumbung Telur Indonesia',
alias: 'LTI',
},
},
],
},
{
id: 3,
key: 'manbu.super_admin',
name: 'MANBU Administrator',
client: {
id: 3,
name: 'PT Mandiri Berlian Unggas',
alias: 'MANBU',
},
permissions: [
{
id: 7,
name: 'manbu:purchase:read',
action: 'read',
client: {
id: 3,
name: 'PT Mandiri Berlian Unggas',
alias: 'MANBU',
},
},
{
id: 8,
name: 'manbu:purchase:create',
action: 'create',
client: {
id: 3,
name: 'PT Mandiri Berlian Unggas',
alias: 'MANBU',
},
},
{
id: 9,
name: 'manbu:purchase:approve',
action: 'approve',
client: {
id: 3,
name: 'PT Mandiri Berlian Unggas',
alias: 'MANBU',
},
},
],
},
],
};
interface RequireAuthProps {
children?: ReactNode;
}
const RequireAuth = ({ children }: RequireAuthProps) => {
const { user, setUser, setIsLoadingUser } = useAuth();
const router = useRouter();
const { setUser, setIsLoadingUser } = useAuth();
const {
data: userResponse,
isLoading: isLoadingUserResponse,
error: userErrorResponse,
} = useSWR<
GetMeResponse & { ok?: boolean },
AxiosError<BaseApiResponse>,
SWRHttpKey
>('/sso/userinfo', httpClientFetcher, {
shouldRetryOnError: false,
});
const { data: userResponse, isLoading: isLoadingUserResponse } =
useSWRImmutable<GetMeResponse & { ok?: boolean }, unknown, SWRHttpKey>(
'/auth/sso/userinfo',
httpClientFetcher,
{
shouldRetryOnError: false,
revalidateOnFocus: false,
revalidateOnReconnect: false,
refreshInterval: 0,
}
);
useEffect(() => {
setIsLoadingUser(isLoadingUserResponse);
}, [isLoadingUserResponse, setIsLoadingUser]);
useEffect(() => {
if (isResponseSuccess(userResponse)) {
setUser(userResponse.data);
} else {
// router.replace(process.env.NEXT_PUBLIC_SSO_LOGIN_URL as string);
// TODO: remove this later, DONT HARDCODE USER DATA
setUser(DUMMY_USER);
}
}, [userResponse, setUser]);
}, [userResponse, setIsLoadingUser, setUser]);
// Explicitly handle 401 redirect from the component level
useEffect(() => {
if (
isResponseError(userResponse) &&
userErrorResponse?.response?.status === 401
) {
// Clear cache to prevent stale data from rendering children
// mutate('/sso/userinfo', undefined, { revalidate: false }); // Optional: if using global mutate
setUser(undefined);
redirectToSSO();
}
}, [userErrorResponse, setUser, userResponse]);
// TODO: uncomment this later
// if (isLoadingUserResponse && !userResponse) {
// return (
// <div className='w-full flex flex-row justify-center items-center p-4'>
// <span className='loading loading-spinner loading-xl' />
// </div>
// );
// }
useEffect(() => {
setIsLoadingUser(isLoadingUserResponse);
}, [isLoadingUserResponse]);
if (
(isLoadingUserResponse && !userResponse && !userErrorResponse) ||
(!userResponse && !userErrorResponse)
) {
return (
<div className='w-full flex flex-row justify-center items-center p-4'>
<span className='loading loading-spinner loading-xl' />
</div>
);
}
if (userErrorResponse) {
return (
<div className='w-full h-screen flex flex-col justify-center items-center gap-4'>
<h2 className='text-2xl font-bold text-error'>Authentication Failed</h2>
<p className='text-gray-600'>
Please try refreshing the page or contact support if the problem
persists.
</p>
<button
className='btn btn-primary'
onClick={() => window.location.reload()}
>
Retry
</button>
</div>
);
}
return <>{isResponseSuccess(userResponse) && user && children}</>;
return <>{children}</>;
};
export default RequireAuth;
+19 -47
View File
@@ -1,14 +1,15 @@
import Card from '@/components/Card';
import { FormHeader } from '@/components/helper/form/FormHeader';
import DebouncedTextInput from '@/components/input/DebouncedTextInput';
import Table from '@/components/Table';
import { formatCurrency } from '@/lib/helper';
import { Finance, FinanceReferences } from '@/types/api/finance/finance';
import { formatCurrency, formatTitleCase } from '@/lib/helper';
import { Finance } from '@/types/api/finance/finance';
const FinanceDetail = ({ finance }: { finance: Finance }) => {
const informasiUmum = [
{
label: 'ID',
value: finance.id,
value: finance.payment_code,
},
{
label: 'Jenis Transaksi',
@@ -16,41 +17,47 @@ const FinanceDetail = ({ finance }: { finance: Finance }) => {
},
{
label: 'Pihak',
value: finance.transaction_owner.name,
value: finance.party.name,
},
{
label: 'Tanggal',
value: finance.transaction_date,
value: finance.payment_date,
},
{
label: 'Metode Pembayaran',
value: finance.payment_method,
},
{
label: 'Catatan',
value: finance.notes || '-',
},
];
const informasiTransfer = [
{
label: 'No. Referensi',
value: finance.references_number,
value: finance.reference_number,
},
{
label: 'Nomor Rekening',
value: `${finance.bank_account.alias} - ${finance.bank_account.account_number} - ${finance.bank_account.owner}`,
value: `${finance.bank.alias} - ${finance.bank.account_number} - ${finance.bank.owner}`,
},
{
label: 'Rekening Customer',
value: finance.transaction_account_number,
label: `Rekening ${formatTitleCase(finance.party.type)}`,
value: finance.party.account_number,
},
{
label: 'Nominal',
value: formatCurrency(finance.transaction_amount),
value: formatCurrency(finance.expense_amount),
},
{
label: 'Sisa',
value: formatCurrency(finance.balance_amount),
value: formatCurrency(finance.income_amount),
},
];
return (
<div className='flex flex-col gap-4'>
<div className='flex flex-col gap-6 p-6'>
<FormHeader title='' backUrl='/finance' />
<Card
title='Detail Keuangan'
className={{
@@ -100,41 +107,6 @@ const FinanceDetail = ({ finance }: { finance: Finance }) => {
}}
/>
</div>
<div className='flex flex-col gap-4'>
<div className='flex flex-row gap-4'>
<DebouncedTextInput
className={{
wrapper: 'max-w-1/4 ml-auto',
}}
name='cari'
placeholder='Cari'
/>
</div>
<Table<FinanceReferences>
data={finance.references}
columns={[
{
header: 'No.',
id: 'index',
accessorFn: (row, index) => index + 1,
},
{
header: 'No. Referensi',
id: 'references_number',
accessorKey: 'references_number',
},
{
header: 'Nominal',
id: 'nominal',
accessorFn: (row) =>
formatCurrency(Number(row.total_allocation)),
},
]}
className={{
containerClassName: 'mb-6',
}}
/>
</div>
</Card>
</div>
);
+301 -28
View File
@@ -1,39 +1,208 @@
import { ChangeEventHandler, useMemo, useState } from 'react';
import { Row } from '@tanstack/react-table';
import { useSearchParams } from 'next/navigation';
import useSWR from 'swr';
import Button from '@/components/Button';
import Card from '@/components/Card';
import Dropdown from '@/components/dropdown/Dropdown';
import DateInput from '@/components/input/DateInput';
import DebouncedTextInput from '@/components/input/DebouncedTextInput';
import SelectInput, {
OptionType,
useSelect,
} from '@/components/input/SelectInput';
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 { useTableFilter } from '@/services/hooks/useTableFilter';
import { Finance } from '@/types/api/finance/finance';
import { Row } from '@tanstack/react-table';
import { useMemo } from 'react';
import { 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';
const FinanceTable = () => {
const {
state: tableFilterState,
updateFilter,
setPage,
setPageSize,
toQueryString: getTableFilterQueryString,
} = useTableFilter({
initial: {
search: '',
transactionType: '',
bankId: '',
partyType: '',
sortBy: '',
startDate: '',
endDate: '',
},
paramMap: {
page: 'page',
pageSize: 'limit',
transactionType: 'transaction_type',
bankId: 'bank_id',
partyType: 'party_type',
sortBy: 'sort_date',
startDate: 'start_date',
endDate: 'end_date',
},
});
// ===== State =====
const [searchParams, setSearchParams] = useSearchParams();
const [pendingFilters, setPendingFilters] = useState({
search: '',
transactionType: '',
bankId: '',
partyType: '',
sortBy: '',
startDate: '',
endDate: '',
});
const [selectedTransactionType, setSelectedTransactionType] =
useState<OptionType | null>(null);
const [selectedBank, setSelectedBank] = useState<OptionType | null>(null);
const [selectedPartyType, setSelectedPartyType] = useState<OptionType | null>(
null
);
const [selectedSortBy, setSelectedSortBy] = useState<OptionType | null>(null);
// ===== Fetch Data =====
const {
data: finances,
isLoading,
mutate: refreshFinances,
} = useSWR(
`${FinanceApi.basePath}${getTableFilterQueryString()}`,
FinanceApi.getAllFetcher
);
// ===== Options =====
const transactionTypeOptions = useMemo(() => {
return [
{ label: 'Transfer', value: 'TRANSFER' },
{ label: 'Cash', value: 'CASH' },
{ label: 'Card', value: 'CARD' },
{ label: 'Cheque', value: 'CHEQUE' },
{ label: 'Saldo', value: 'SALDO' },
];
}, []);
const partyTypeOptions = useMemo(() => {
return [
{ label: 'Customer', value: 'CUSTOMER' },
{ label: 'Supplier', value: 'SUPPLIER' },
];
}, []);
const sortByOptions = useMemo(() => {
return [
{ label: 'Tanggal Pembayaran', value: 'payment_date' },
{ label: 'Tanggal Dibuat', value: 'created_at' },
];
}, []);
const { options: bankOptions, rawData: bankRawData } = useSelect<Bank>(
BankApi.basePath,
'id',
'alias',
'',
{
limit: 'limit',
}
);
// ===== Handler =====
const searchChangeHandler: ChangeEventHandler<HTMLInputElement> = (e) => {
setPendingFilters((prev) => ({ ...prev, search: e.target.value }));
};
const transactionTypeChangeHandler = (
val: OptionType | OptionType[] | null
) => {
setSelectedTransactionType(val as OptionType);
setPendingFilters((prev) => ({
...prev,
transactionType: val ? ((val as OptionType).value as string) : '',
}));
};
const bankChangeHandler = (val: OptionType | OptionType[] | null) => {
setSelectedBank(val as OptionType);
setPendingFilters((prev) => ({
...prev,
bankId: val ? ((val as OptionType).value as string) : '',
}));
};
const partyTypeChangeHandler = (val: OptionType | OptionType[] | null) => {
setSelectedPartyType(val as OptionType);
setPendingFilters((prev) => ({
...prev,
partyType: val ? ((val as OptionType).value as string) : '',
}));
};
const sortByChangeHandler = (val: OptionType | OptionType[] | null) => {
setSelectedSortBy(val as OptionType);
setPendingFilters((prev) => ({
...prev,
sortBy: val ? ((val as OptionType).value as string) : '',
}));
};
const startDateChangeHandler: ChangeEventHandler<HTMLInputElement> = (e) => {
setPendingFilters((prev) => ({ ...prev, startDate: e.target.value }));
};
const endDateChangeHandler: ChangeEventHandler<HTMLInputElement> = (e) => {
setPendingFilters((prev) => ({ ...prev, endDate: e.target.value }));
};
const pageSizeChangeHandler = (val: OptionType | OptionType[] | null) => {
const newVal = val as OptionType;
setPageSize(newVal.value as number);
};
const submitFilterHandler = () => {
updateFilter('search', pendingFilters.search);
updateFilter('transactionType', pendingFilters.transactionType);
updateFilter('bankId', pendingFilters.bankId);
updateFilter('partyType', pendingFilters.partyType);
updateFilter('sortBy', pendingFilters.sortBy);
updateFilter('startDate', pendingFilters.startDate);
updateFilter('endDate', pendingFilters.endDate);
};
const resetFilterHandler = () => {
setSelectedTransactionType(null);
setSelectedBank(null);
setSelectedPartyType(null);
setSelectedSortBy(null);
const emptyFilters = {
search: '',
transactionType: '',
bankId: '',
partyType: '',
sortBy: '',
startDate: '',
endDate: '',
};
setPendingFilters(emptyFilters);
updateFilter('search', '');
updateFilter('transactionType', '');
updateFilter('bankId', '');
updateFilter('partyType', '');
updateFilter('sortBy', '');
updateFilter('startDate', '');
updateFilter('endDate', '');
};
const FinanceTable = ({ finances }: { finances: Finance[] }) => {
const columns = useMemo(() => {
return [
{
header: 'ID',
accessorKey: 'id',
},
{
header: 'Alokasi',
accessorFn: (finance: Finance) => finance.references.length,
cell: ({ row }: { row: Row<Finance> }) => (
<Tooltip
content={row.original.references
.map((ref) => ref.references_number)
.join(', ')}
>
<span className='text-primary'>
{row.original.references.length}
</span>
</Tooltip>
),
accessorKey: 'payment_code',
},
{
header: 'References Number',
accessorKey: 'references_number',
accessorKey: 'reference_number',
},
{
header: 'Jenis Transaksi',
@@ -41,12 +210,12 @@ const FinanceTable = ({ finances }: { finances: Finance[] }) => {
},
{
header: 'Pihak',
accessorFn: (finance: Finance) => finance.transaction_owner.name,
accessorFn: (finance: Finance) => finance.party.name,
},
{
header: 'Tanggal',
accessorFn: (finance: Finance) =>
formatDate(finance.transaction_date, 'DD MMM YYYY'),
formatDate(finance.payment_date, 'DD MMM YYYY'),
},
{
header: 'Metode Pembayaran',
@@ -55,17 +224,16 @@ const FinanceTable = ({ finances }: { finances: Finance[] }) => {
{
header: 'Bank',
accessorFn: (finance: Finance) =>
`${finance.bank_account.alias} - ${finance.bank_account.account_number} - ${finance.bank_account.owner}`,
`${finance.bank.alias} - ${finance.bank.account_number} - ${finance.bank.owner}`,
},
{
header: 'Pengeluaran (Rp)',
accessorFn: (finance: Finance) =>
formatCurrency(finance.balance_amount),
formatCurrency(finance.expense_amount),
},
{
header: 'Pemasukan (Rp)',
accessorFn: (finance: Finance) =>
formatCurrency(finance.transaction_amount),
accessorFn: (finance: Finance) => formatCurrency(finance.income_amount),
},
{
header: 'Aksi',
@@ -88,8 +256,113 @@ const FinanceTable = ({ finances }: { finances: Finance[] }) => {
];
}, []);
return (
<section className='size-full p-4'>
<Table<Finance> data={finances} columns={columns} />
<section className='size-full p-6 flex flex-col gap-6'>
<div className='flex justify-end gap-2'>
<Button color='warning' className='min-w-24'>
Injection Saldo Bank
</Button>
<Button color='info' className='text-white min-w-24'>
Saldo Awal
</Button>
<Button color='primary' className='min-w-24'>
Tambah
</Button>
</div>
<Card
variant='bordered'
className={{
wrapper: 'w-full',
}}
footer={
<div className='flex justify-end gap-2'>
<Button
color='warning'
className='min-w-24'
onClick={resetFilterHandler}
>
Reset
</Button>
<Button
color='primary'
className='min-w-24'
onClick={submitFilterHandler}
>
Cari
</Button>
</div>
}
>
<div className='grid grid-cols-4 gap-6'>
<SelectInput
options={transactionTypeOptions}
label='Jenis Transaksi'
value={selectedTransactionType}
onChange={transactionTypeChangeHandler}
isClearable
/>
{isResponseSuccess(bankRawData) && (
<SelectInput
options={bankOptions.map((bank) => ({
label:
bankRawData.data.find((data) => data.id === bank.value)
?.alias +
' - ' +
bankRawData.data.find((data) => data.id === bank.value)
?.account_number +
' - ' +
bankRawData.data.find((data) => data.id === bank.value)
?.owner,
value: bank.value,
}))}
label='Bank'
value={selectedBank}
onChange={bankChangeHandler}
isClearable
/>
)}
<SelectInput
options={partyTypeOptions}
label='Pihak'
value={selectedPartyType}
onChange={partyTypeChangeHandler}
isClearable
/>
<DebouncedTextInput
name='search'
label='Cari'
placeholder='Cari'
value={pendingFilters.search}
onChange={searchChangeHandler}
/>
<SelectInput
options={sortByOptions}
label='Urutkan Berdasarkan'
value={selectedSortBy}
onChange={sortByChangeHandler}
isClearable
/>
<DateInput
name='startDate'
label='Periode Tanggal (Mulai)'
value={pendingFilters.startDate}
onChange={startDateChangeHandler}
/>
<DateInput
name='endDate'
label='Periode Tanggal (Akhir)'
value={pendingFilters.endDate}
onChange={endDateChangeHandler}
/>
</div>
</Card>
<Table<Finance>
data={isResponseSuccess(finances) ? finances.data : []}
columns={columns}
pageSize={tableFilterState.pageSize}
page={tableFilterState.page}
onPageChange={setPage}
isLoading={isLoading}
/>
</section>
);
};
File diff suppressed because it is too large Load Diff
+7 -16
View File
@@ -1,16 +1,11 @@
/**
* Dummy data for Finance[]
* Generated from: finance.json
* Generated from: finance_payments.json
*
* This file is auto-generated. Do not edit manually.
*/
import {
FinanceBankAccount,
FinanceTransactionOwner,
FinanceReferences,
Finance,
} from '../../types/api/finance/finance';
import { FinanceBank, Finance } from '../../types/api/finance/finance';
import { BaseApiResponse } from '@/types/api/api-general';
import dummyData from './finance.dummy.json';
@@ -18,9 +13,7 @@ import dummyData from './finance.dummy.json';
* Get dummy Finance[] data
* @returns Promise with BaseApiResponse containing Finance[]
*/
export async function getAllDummyFinance(): Promise<
BaseApiResponse<Finance[]>
> {
export async function getAllFetcher(): Promise<BaseApiResponse<Finance[]>> {
return new Promise((resolve) => {
setTimeout(() => {
resolve({
@@ -33,19 +26,17 @@ export async function getAllDummyFinance(): Promise<
});
}
export async function getSingleDummyFinance(
id: string
export async function getFetcher(
id: number
): Promise<BaseApiResponse<Finance>> {
console.log(dummyData as unknown as Finance[]);
return new Promise((resolve) => {
setTimeout(() => {
const data = dummyData.find((item) => item.id === id);
resolve({
code: 200,
status: 'success',
message: 'Data retrieved successfully',
data: (dummyData as unknown as Finance[]).find(
(finance) => finance.id === id
) as Finance,
data: data as unknown as Finance,
});
}, 500);
});
+4 -8
View File
@@ -4,10 +4,7 @@ import { BaseApiResponse } from '@/types/api/api-general';
import { httpClient } from '@/services/http/client';
import { Finance } from '@/types/api/finance/finance';
// DUMMY_START
import {
getAllDummyFinance,
getSingleDummyFinance,
} from '@/dummy/finance/finance.dummy';
import { getAllFetcher, getFetcher } from '@/dummy/finance/finance.dummy';
// DUMMY_END
export class FinanceApiService extends BaseApiService<
@@ -21,7 +18,7 @@ export class FinanceApiService extends BaseApiService<
async getAllFetcher(): Promise<BaseApiResponse<Finance[]>> {
// DUMMY_START
return await getAllDummyFinance();
return await getAllFetcher();
// DUMMY_END
// LIVE_START
@@ -37,10 +34,9 @@ export class FinanceApiService extends BaseApiService<
// LIVE_END
}
async getSingleFetcher(id: string): Promise<BaseApiResponse<Finance>> {
async getSingle(id: number): Promise<BaseApiResponse<Finance>> {
// DUMMY_START
console.log(id);
return await getSingleDummyFinance(id);
return await getFetcher(id);
// DUMMY_END
// LIVE_START
+19 -21
View File
@@ -1,31 +1,29 @@
export interface Finance {
id: string;
references_number: string;
bank_account: FinanceBankAccount;
id: number;
payment_code: string;
reference_number: string;
transaction_type: string;
transaction_owner: FinanceTransactionOwner;
transaction_account_number: string;
transaction_date: string;
party: FinanceParty;
payment_date: string;
created_at: string;
payment_method: string;
transaction_amount: number;
balance_amount: number;
bank: FinanceBank;
expense_amount: number;
income_amount: number;
nominal: number;
notes: string;
references: FinanceReferences[];
}
export interface FinanceReferences {
references_number: string;
total_allocation: number;
}
export interface FinanceTransactionOwner {
export interface FinanceParty {
id: number;
name: string;
}
export interface FinanceBankAccount {
alias: string;
name: string;
type: string;
account_number: string;
}
export interface FinanceBank {
id: number;
name: string;
alias: string;
owner: string;
account_number: string;
owner: string;
}