mirror of
https://gitlab.com/mbugroup/lti-web-client.git
synced 2026-05-25 15:55:48 +00:00
feat(FE-350): add filtering table
This commit is contained in:
+1
-1
@@ -1,3 +1,3 @@
|
|||||||
npm run format
|
npm run format
|
||||||
npm run lint
|
npm run lint
|
||||||
npm run build
|
npx tsc --noEmit
|
||||||
@@ -11,7 +11,7 @@ const FinanceDetailPage = () => {
|
|||||||
const financeId = useSearchParams().get('financeId');
|
const financeId = useSearchParams().get('financeId');
|
||||||
|
|
||||||
const { data: finance } = useSWR(financeId, () =>
|
const { data: finance } = useSWR(financeId, () =>
|
||||||
FinanceApi.getSingleFetcher(financeId as string)
|
FinanceApi.getSingle(Number(financeId))
|
||||||
);
|
);
|
||||||
|
|
||||||
if (!financeId) {
|
if (!financeId) {
|
||||||
|
|||||||
@@ -1,30 +1,12 @@
|
|||||||
'use client';
|
'use client';
|
||||||
|
|
||||||
import FinanceTable from '@/components/pages/finance/FinanceTable';
|
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 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 (
|
return (
|
||||||
<section className='size-full p-4'>
|
<section className='size-full p-6'>
|
||||||
<h1>Finance</h1>
|
<div className='flex flex-row gap-4'></div>
|
||||||
<FinanceTable
|
<FinanceTable />
|
||||||
finances={isResponseSuccess(finances) ? finances.data : []}
|
|
||||||
/>
|
|
||||||
</section>
|
</section>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -1,87 +1,197 @@
|
|||||||
'use client';
|
'use client';
|
||||||
|
|
||||||
import { ReactNode, useEffect } from 'react';
|
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 { useAuth } from '@/services/hooks/useAuth';
|
||||||
import { httpClientFetcher, SWRHttpKey } from '@/services/http/client';
|
import { httpClientFetcher, SWRHttpKey } from '@/services/http/client';
|
||||||
import { isResponseError, isResponseSuccess } from '@/lib/api-helper';
|
import { isResponseSuccess } from '@/lib/api-helper';
|
||||||
import { BaseApiResponse, GetMeResponse } from '@/types/api/api-general';
|
import { GetMeResponse } from '@/types/api/api-general';
|
||||||
import { AxiosError } from 'axios';
|
|
||||||
import { redirectToSSO } from '@/lib/auth-helper';
|
// 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 {
|
interface RequireAuthProps {
|
||||||
children?: ReactNode;
|
children?: ReactNode;
|
||||||
}
|
}
|
||||||
|
|
||||||
const RequireAuth = ({ children }: RequireAuthProps) => {
|
const RequireAuth = ({ children }: RequireAuthProps) => {
|
||||||
const { user, setUser, setIsLoadingUser } = useAuth();
|
const router = useRouter();
|
||||||
|
const { setUser, setIsLoadingUser } = useAuth();
|
||||||
|
|
||||||
const {
|
const { data: userResponse, isLoading: isLoadingUserResponse } =
|
||||||
data: userResponse,
|
useSWRImmutable<GetMeResponse & { ok?: boolean }, unknown, SWRHttpKey>(
|
||||||
isLoading: isLoadingUserResponse,
|
'/auth/sso/userinfo',
|
||||||
error: userErrorResponse,
|
httpClientFetcher,
|
||||||
} = useSWR<
|
{
|
||||||
GetMeResponse & { ok?: boolean },
|
shouldRetryOnError: false,
|
||||||
AxiosError<BaseApiResponse>,
|
revalidateOnFocus: false,
|
||||||
SWRHttpKey
|
revalidateOnReconnect: false,
|
||||||
>('/sso/userinfo', httpClientFetcher, {
|
refreshInterval: 0,
|
||||||
shouldRetryOnError: false,
|
}
|
||||||
});
|
);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
setIsLoadingUser(isLoadingUserResponse);
|
||||||
|
}, [isLoadingUserResponse, setIsLoadingUser]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (isResponseSuccess(userResponse)) {
|
if (isResponseSuccess(userResponse)) {
|
||||||
setUser(userResponse.data);
|
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
|
// TODO: uncomment this later
|
||||||
useEffect(() => {
|
// if (isLoadingUserResponse && !userResponse) {
|
||||||
if (
|
// return (
|
||||||
isResponseError(userResponse) &&
|
// <div className='w-full flex flex-row justify-center items-center p-4'>
|
||||||
userErrorResponse?.response?.status === 401
|
// <span className='loading loading-spinner loading-xl' />
|
||||||
) {
|
// </div>
|
||||||
// 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]);
|
|
||||||
|
|
||||||
useEffect(() => {
|
return <>{children}</>;
|
||||||
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}</>;
|
|
||||||
};
|
};
|
||||||
|
|
||||||
export default RequireAuth;
|
export default RequireAuth;
|
||||||
|
|||||||
@@ -1,14 +1,15 @@
|
|||||||
import Card from '@/components/Card';
|
import Card from '@/components/Card';
|
||||||
|
import { FormHeader } from '@/components/helper/form/FormHeader';
|
||||||
import DebouncedTextInput from '@/components/input/DebouncedTextInput';
|
import DebouncedTextInput from '@/components/input/DebouncedTextInput';
|
||||||
import Table from '@/components/Table';
|
import Table from '@/components/Table';
|
||||||
import { formatCurrency } from '@/lib/helper';
|
import { formatCurrency, formatTitleCase } from '@/lib/helper';
|
||||||
import { Finance, FinanceReferences } from '@/types/api/finance/finance';
|
import { Finance } from '@/types/api/finance/finance';
|
||||||
|
|
||||||
const FinanceDetail = ({ finance }: { finance: Finance }) => {
|
const FinanceDetail = ({ finance }: { finance: Finance }) => {
|
||||||
const informasiUmum = [
|
const informasiUmum = [
|
||||||
{
|
{
|
||||||
label: 'ID',
|
label: 'ID',
|
||||||
value: finance.id,
|
value: finance.payment_code,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
label: 'Jenis Transaksi',
|
label: 'Jenis Transaksi',
|
||||||
@@ -16,41 +17,47 @@ const FinanceDetail = ({ finance }: { finance: Finance }) => {
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
label: 'Pihak',
|
label: 'Pihak',
|
||||||
value: finance.transaction_owner.name,
|
value: finance.party.name,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
label: 'Tanggal',
|
label: 'Tanggal',
|
||||||
value: finance.transaction_date,
|
value: finance.payment_date,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
label: 'Metode Pembayaran',
|
label: 'Metode Pembayaran',
|
||||||
value: finance.payment_method,
|
value: finance.payment_method,
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
label: 'Catatan',
|
||||||
|
value: finance.notes || '-',
|
||||||
|
},
|
||||||
];
|
];
|
||||||
const informasiTransfer = [
|
const informasiTransfer = [
|
||||||
{
|
{
|
||||||
label: 'No. Referensi',
|
label: 'No. Referensi',
|
||||||
value: finance.references_number,
|
value: finance.reference_number,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
label: 'Nomor Rekening',
|
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',
|
label: `Rekening ${formatTitleCase(finance.party.type)}`,
|
||||||
value: finance.transaction_account_number,
|
value: finance.party.account_number,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
label: 'Nominal',
|
label: 'Nominal',
|
||||||
value: formatCurrency(finance.transaction_amount),
|
value: formatCurrency(finance.expense_amount),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
label: 'Sisa',
|
label: 'Sisa',
|
||||||
value: formatCurrency(finance.balance_amount),
|
value: formatCurrency(finance.income_amount),
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
return (
|
return (
|
||||||
<div className='flex flex-col gap-4'>
|
<div className='flex flex-col gap-6 p-6'>
|
||||||
|
<FormHeader title='' backUrl='/finance' />
|
||||||
|
|
||||||
<Card
|
<Card
|
||||||
title='Detail Keuangan'
|
title='Detail Keuangan'
|
||||||
className={{
|
className={{
|
||||||
@@ -100,41 +107,6 @@ const FinanceDetail = ({ finance }: { finance: Finance }) => {
|
|||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
</div>
|
</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>
|
</Card>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -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 Button from '@/components/Button';
|
||||||
|
import Card from '@/components/Card';
|
||||||
import Dropdown from '@/components/dropdown/Dropdown';
|
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 Menu from '@/components/menu/Menu';
|
||||||
import MenuItem from '@/components/menu/MenuItem';
|
import MenuItem from '@/components/menu/MenuItem';
|
||||||
import Table from '@/components/Table';
|
import Table from '@/components/Table';
|
||||||
import Tooltip from '@/components/Tooltip';
|
import Tooltip from '@/components/Tooltip';
|
||||||
import { formatCurrency, formatDate } from '@/lib/helper';
|
import { formatCurrency, formatDate } from '@/lib/helper';
|
||||||
|
import { useTableFilter } from '@/services/hooks/useTableFilter';
|
||||||
import { Finance } from '@/types/api/finance/finance';
|
import { Finance } from '@/types/api/finance/finance';
|
||||||
import { Row } from '@tanstack/react-table';
|
import { ROWS_OPTIONS } from '@/config/constant';
|
||||||
import { useMemo } from 'react';
|
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(() => {
|
const columns = useMemo(() => {
|
||||||
return [
|
return [
|
||||||
{
|
{
|
||||||
header: 'ID',
|
header: 'ID',
|
||||||
accessorKey: 'id',
|
accessorKey: 'payment_code',
|
||||||
},
|
|
||||||
{
|
|
||||||
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>
|
|
||||||
),
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
header: 'References Number',
|
header: 'References Number',
|
||||||
accessorKey: 'references_number',
|
accessorKey: 'reference_number',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
header: 'Jenis Transaksi',
|
header: 'Jenis Transaksi',
|
||||||
@@ -41,12 +210,12 @@ const FinanceTable = ({ finances }: { finances: Finance[] }) => {
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
header: 'Pihak',
|
header: 'Pihak',
|
||||||
accessorFn: (finance: Finance) => finance.transaction_owner.name,
|
accessorFn: (finance: Finance) => finance.party.name,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
header: 'Tanggal',
|
header: 'Tanggal',
|
||||||
accessorFn: (finance: Finance) =>
|
accessorFn: (finance: Finance) =>
|
||||||
formatDate(finance.transaction_date, 'DD MMM YYYY'),
|
formatDate(finance.payment_date, 'DD MMM YYYY'),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
header: 'Metode Pembayaran',
|
header: 'Metode Pembayaran',
|
||||||
@@ -55,17 +224,16 @@ const FinanceTable = ({ finances }: { finances: Finance[] }) => {
|
|||||||
{
|
{
|
||||||
header: 'Bank',
|
header: 'Bank',
|
||||||
accessorFn: (finance: Finance) =>
|
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)',
|
header: 'Pengeluaran (Rp)',
|
||||||
accessorFn: (finance: Finance) =>
|
accessorFn: (finance: Finance) =>
|
||||||
formatCurrency(finance.balance_amount),
|
formatCurrency(finance.expense_amount),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
header: 'Pemasukan (Rp)',
|
header: 'Pemasukan (Rp)',
|
||||||
accessorFn: (finance: Finance) =>
|
accessorFn: (finance: Finance) => formatCurrency(finance.income_amount),
|
||||||
formatCurrency(finance.transaction_amount),
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
header: 'Aksi',
|
header: 'Aksi',
|
||||||
@@ -88,8 +256,113 @@ const FinanceTable = ({ finances }: { finances: Finance[] }) => {
|
|||||||
];
|
];
|
||||||
}, []);
|
}, []);
|
||||||
return (
|
return (
|
||||||
<section className='size-full p-4'>
|
<section className='size-full p-6 flex flex-col gap-6'>
|
||||||
<Table<Finance> data={finances} columns={columns} />
|
<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>
|
</section>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
+404
-2636
File diff suppressed because it is too large
Load Diff
@@ -1,16 +1,11 @@
|
|||||||
/**
|
/**
|
||||||
* Dummy data for Finance[]
|
* Dummy data for Finance[]
|
||||||
* Generated from: finance.json
|
* Generated from: finance_payments.json
|
||||||
*
|
*
|
||||||
* This file is auto-generated. Do not edit manually.
|
* This file is auto-generated. Do not edit manually.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import {
|
import { FinanceBank, Finance } from '../../types/api/finance/finance';
|
||||||
FinanceBankAccount,
|
|
||||||
FinanceTransactionOwner,
|
|
||||||
FinanceReferences,
|
|
||||||
Finance,
|
|
||||||
} from '../../types/api/finance/finance';
|
|
||||||
import { BaseApiResponse } from '@/types/api/api-general';
|
import { BaseApiResponse } from '@/types/api/api-general';
|
||||||
import dummyData from './finance.dummy.json';
|
import dummyData from './finance.dummy.json';
|
||||||
|
|
||||||
@@ -18,9 +13,7 @@ import dummyData from './finance.dummy.json';
|
|||||||
* Get dummy Finance[] data
|
* Get dummy Finance[] data
|
||||||
* @returns Promise with BaseApiResponse containing Finance[]
|
* @returns Promise with BaseApiResponse containing Finance[]
|
||||||
*/
|
*/
|
||||||
export async function getAllDummyFinance(): Promise<
|
export async function getAllFetcher(): Promise<BaseApiResponse<Finance[]>> {
|
||||||
BaseApiResponse<Finance[]>
|
|
||||||
> {
|
|
||||||
return new Promise((resolve) => {
|
return new Promise((resolve) => {
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
resolve({
|
resolve({
|
||||||
@@ -33,19 +26,17 @@ export async function getAllDummyFinance(): Promise<
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function getSingleDummyFinance(
|
export async function getFetcher(
|
||||||
id: string
|
id: number
|
||||||
): Promise<BaseApiResponse<Finance>> {
|
): Promise<BaseApiResponse<Finance>> {
|
||||||
console.log(dummyData as unknown as Finance[]);
|
|
||||||
return new Promise((resolve) => {
|
return new Promise((resolve) => {
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
|
const data = dummyData.find((item) => item.id === id);
|
||||||
resolve({
|
resolve({
|
||||||
code: 200,
|
code: 200,
|
||||||
status: 'success',
|
status: 'success',
|
||||||
message: 'Data retrieved successfully',
|
message: 'Data retrieved successfully',
|
||||||
data: (dummyData as unknown as Finance[]).find(
|
data: data as unknown as Finance,
|
||||||
(finance) => finance.id === id
|
|
||||||
) as Finance,
|
|
||||||
});
|
});
|
||||||
}, 500);
|
}, 500);
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -4,10 +4,7 @@ import { BaseApiResponse } from '@/types/api/api-general';
|
|||||||
import { httpClient } from '@/services/http/client';
|
import { httpClient } from '@/services/http/client';
|
||||||
import { Finance } from '@/types/api/finance/finance';
|
import { Finance } from '@/types/api/finance/finance';
|
||||||
// DUMMY_START
|
// DUMMY_START
|
||||||
import {
|
import { getAllFetcher, getFetcher } from '@/dummy/finance/finance.dummy';
|
||||||
getAllDummyFinance,
|
|
||||||
getSingleDummyFinance,
|
|
||||||
} from '@/dummy/finance/finance.dummy';
|
|
||||||
// DUMMY_END
|
// DUMMY_END
|
||||||
|
|
||||||
export class FinanceApiService extends BaseApiService<
|
export class FinanceApiService extends BaseApiService<
|
||||||
@@ -21,7 +18,7 @@ export class FinanceApiService extends BaseApiService<
|
|||||||
|
|
||||||
async getAllFetcher(): Promise<BaseApiResponse<Finance[]>> {
|
async getAllFetcher(): Promise<BaseApiResponse<Finance[]>> {
|
||||||
// DUMMY_START
|
// DUMMY_START
|
||||||
return await getAllDummyFinance();
|
return await getAllFetcher();
|
||||||
// DUMMY_END
|
// DUMMY_END
|
||||||
|
|
||||||
// LIVE_START
|
// LIVE_START
|
||||||
@@ -37,10 +34,9 @@ export class FinanceApiService extends BaseApiService<
|
|||||||
// LIVE_END
|
// LIVE_END
|
||||||
}
|
}
|
||||||
|
|
||||||
async getSingleFetcher(id: string): Promise<BaseApiResponse<Finance>> {
|
async getSingle(id: number): Promise<BaseApiResponse<Finance>> {
|
||||||
// DUMMY_START
|
// DUMMY_START
|
||||||
console.log(id);
|
return await getFetcher(id);
|
||||||
return await getSingleDummyFinance(id);
|
|
||||||
// DUMMY_END
|
// DUMMY_END
|
||||||
|
|
||||||
// LIVE_START
|
// LIVE_START
|
||||||
|
|||||||
Vendored
+19
-21
@@ -1,31 +1,29 @@
|
|||||||
export interface Finance {
|
export interface Finance {
|
||||||
id: string;
|
id: number;
|
||||||
references_number: string;
|
payment_code: string;
|
||||||
bank_account: FinanceBankAccount;
|
reference_number: string;
|
||||||
transaction_type: string;
|
transaction_type: string;
|
||||||
transaction_owner: FinanceTransactionOwner;
|
party: FinanceParty;
|
||||||
transaction_account_number: string;
|
payment_date: string;
|
||||||
transaction_date: string;
|
created_at: string;
|
||||||
payment_method: string;
|
payment_method: string;
|
||||||
transaction_amount: number;
|
bank: FinanceBank;
|
||||||
balance_amount: number;
|
expense_amount: number;
|
||||||
|
income_amount: number;
|
||||||
|
nominal: number;
|
||||||
notes: string;
|
notes: string;
|
||||||
references: FinanceReferences[];
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface FinanceReferences {
|
export interface FinanceParty {
|
||||||
references_number: string;
|
|
||||||
total_allocation: number;
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface FinanceTransactionOwner {
|
|
||||||
id: number;
|
id: number;
|
||||||
name: string;
|
name: string;
|
||||||
}
|
type: string;
|
||||||
|
account_number: string;
|
||||||
export interface FinanceBankAccount {
|
}
|
||||||
alias: string;
|
export interface FinanceBank {
|
||||||
name: string;
|
id: number;
|
||||||
|
name: string;
|
||||||
|
alias: string;
|
||||||
|
owner: string;
|
||||||
account_number: string;
|
account_number: string;
|
||||||
owner: string;
|
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user