Merge branch 'development' of gitlab.com:mbugroup/lti-web-client into dev/hotfix/restu

This commit is contained in:
rstubryan
2026-01-17 20:19:51 +07:00
10 changed files with 249 additions and 92 deletions
+19 -22
View File
@@ -34,7 +34,7 @@ const FinanceDetail = ({ finance }: { finance: Finance }) => {
},
{
label: 'Pihak',
value: finance.party.id ? finance.party.name : '-',
value: finance.party?.id ? finance.party?.name : '-',
},
{
label: 'Tanggal',
@@ -56,25 +56,21 @@ const FinanceDetail = ({ finance }: { finance: Finance }) => {
},
{
label: 'Nomor Rekening',
value: `${finance.bank.alias} - ${finance.bank.account_number} - ${finance.bank.owner}`,
value: `${finance.bank?.alias} - ${finance.bank?.account_number} - ${finance.bank?.owner}`,
},
{
label: `Rekening ${formatTitleCase(finance.party.type)}`,
value: finance.party.account_number,
label: `Rekening ${formatTitleCase(finance.party?.type)}`,
value: finance.party?.account_number,
},
{
label: 'Nominal',
value: formatCurrency(finance.expense_amount),
},
{
label: 'Sisa',
value: formatCurrency(finance.income_amount),
value: formatCurrency(finance.nominal),
},
].filter((item) => {
// Hide party account number row if transaction type is INJECTION
if (
FINANCE_INJECTION_STATUS.includes(finance.transaction_type) &&
item.label === `Rekening ${formatTitleCase(finance.party.type)}`
item.label === `Rekening ${formatTitleCase(finance.party?.type)}`
) {
return false;
}
@@ -148,18 +144,19 @@ const FinanceDetail = ({ finance }: { finance: Finance }) => {
</Card>
<div className='flex flex-row gap-2 justify-end'>
{FINANCE_TRANSACTION_STATUS.includes(finance.transaction_type) && (
<RequirePermission permissions='lti.finance.payments.update'>
<Button
color='warning'
className='min-w-24'
href={`/finance/detail/edit?financeId=${finance.id}`}
>
<Icon icon='mdi:pencil-outline' />
Edit
</Button>
</RequirePermission>
)}
{FINANCE_TRANSACTION_STATUS.includes(finance.transaction_type) &&
finance.party?.type !== 'SUPPLIER' && (
<RequirePermission permissions='lti.finance.payments.update'>
<Button
color='warning'
className='min-w-24'
href={`/finance/detail/edit?financeId=${finance.id}`}
>
<Icon icon='mdi:pencil-outline' />
Edit
</Button>
</RequirePermission>
)}
{FINANCE_INITIAL_BALANCE_STATUS.includes(finance.transaction_type) && (
<RequirePermission permissions='lti.finance.initial_balances.update'>
<Button
+59 -40
View File
@@ -22,7 +22,7 @@ import {
} from '@/config/constant';
import { FinanceApi } from '@/services/api/finance';
import { isResponseSuccess } from '@/lib/api-helper';
import { BankApi } from '@/services/api/master-data';
import { BankApi, CustomerApi, SupplierApi } from '@/services/api/master-data';
import { Bank } from '@/types/api/master-data/bank';
import { useModal } from '@/components/Modal';
import ConfirmationModal from '@/components/modal/ConfirmationModal';
@@ -65,19 +65,24 @@ const RowOptionsMenu = ({
{FINANCE_TRANSACTION_STATUS.includes(
props.row.original.transaction_type
) && (
<RequirePermission permissions='lti.finance.payments.update'>
<Button
href={`/finance/detail/edit?financeId=${props.row.original.id}`}
variant='ghost'
color='warning'
className='justify-start text-sm'
>
<Icon icon='material-symbols:edit-outline' width={16} height={16} />
Edit
</Button>
</RequirePermission>
)}
) &&
props.row.original.party?.type !== 'SUPPLIER' && (
<RequirePermission permissions='lti.finance.payments.update'>
<Button
href={`/finance/detail/edit?financeId=${props.row.original.id}`}
variant='ghost'
color='warning'
className='justify-start text-sm'
>
<Icon
icon='material-symbols:edit-outline'
width={16}
height={16}
/>
Edit
</Button>
</RequirePermission>
)}
{FINANCE_INITIAL_BALANCE_STATUS.includes(
props.row.original.transaction_type
@@ -194,20 +199,25 @@ const FinanceTable = () => {
// ===== 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 {
options: partyTypeOptions,
isLoadingOptions: partyTypeIsLoadingOptions,
setInputValue: partyTypeInputValue,
loadMore: partyTypeLoadMore,
} = useSelect(
selectedTransactionType
? selectedTransactionType.value === 'CUSTOMER'
? CustomerApi.basePath
: SupplierApi.basePath
: '',
'id',
'name'
);
const sortByOptions = useMemo(() => {
return [
{ label: 'Tanggal Pembayaran', value: 'payment_date' },
@@ -336,10 +346,10 @@ const FinanceTable = () => {
},
{
header: 'Pihak',
accessorFn: (finance: Finance) => finance.party.name,
accessorFn: (finance: Finance) => finance.party?.name,
cell: (props: CellContext<Finance, unknown>) => {
if (props.row.original.party.id) {
return <span>{props.row.original.party.name}</span>;
if (props.row.original.party?.id) {
return <span>{props.row.original.party?.name}</span>;
}
return <span>{'-'}</span>;
},
@@ -360,12 +370,12 @@ const FinanceTable = () => {
{
header: 'Bank',
accessorFn: (finance: Finance) =>
`${finance.bank.alias} - ${finance.bank.account_number} - ${finance.bank.owner}`,
`${finance.bank?.alias} - ${finance.bank?.account_number} - ${finance.bank?.owner}`,
},
{
header: 'Pengeluaran (Rp)',
accessorFn: (finance: Finance) =>
formatCurrency(finance.expense_amount),
formatCurrency(Math.abs(finance.expense_amount)),
},
{
header: 'Pemasukan (Rp)',
@@ -468,25 +478,41 @@ const FinanceTable = () => {
<div className='grid grid-cols-4 gap-6'>
<SelectInput
options={transactionTypeOptions}
label='Jenis Transaksi'
label='Tipe Transaksi'
value={selectedTransactionType}
onChange={transactionTypeChangeHandler}
isClearable
/>
<SelectInput
options={partyTypeOptions}
label={
selectedTransactionType
? selectedTransactionType.value === 'CUSTOMER'
? 'Pelanggan'
: 'Supplier'
: 'Pihak'
}
value={selectedPartyType}
onChange={partyTypeChangeHandler}
onInputChange={partyTypeInputValue}
onMenuScrollToBottom={partyTypeLoadMore}
isLoading={partyTypeIsLoadingOptions}
isClearable
/>
<SelectInput
options={
isResponseSuccess(bankRawData)
? bankOptions.map((bank) => ({
label:
bankRawData.data.find((data) => data.id === bank.value)
bankRawData.data.find((data) => data.id === bank?.value)
?.alias +
' - ' +
bankRawData.data.find((data) => data.id === bank.value)
bankRawData.data.find((data) => data.id === bank?.value)
?.account_number +
' - ' +
bankRawData.data.find((data) => data.id === bank.value)
bankRawData.data.find((data) => data.id === bank?.value)
?.owner,
value: bank.value,
value: bank?.value,
}))
: []
}
@@ -497,13 +523,6 @@ const FinanceTable = () => {
onMenuScrollToBottom={bankLoadMore}
isClearable
/>
<SelectInput
options={partyTypeOptions}
label='Pihak'
value={selectedPartyType}
onChange={partyTypeChangeHandler}
isClearable
/>
<DebouncedTextInput
name='search'
label='Cari'
@@ -32,8 +32,10 @@ import {
import { Bank } from '@/types/api/master-data/bank';
import { useFormik } from 'formik';
import { useRouter } from 'next/navigation';
import { useCallback, useMemo } from 'react';
import { useCallback, useMemo, useState } from 'react';
import toast from 'react-hot-toast';
import Alert from '@/components/Alert';
import { Icon } from '@iconify/react';
interface FormFinanceAddProps {
type?: 'add' | 'edit';
@@ -51,18 +53,22 @@ const FormFinanceAdd = ({
initialValues,
}: FormFinanceAddProps) => {
const router = useRouter();
const [serverErrorMessage, setServerErrorMessage] = useState('');
const [isSupplier, setIsSupplier] = useState(
initialValues?.party?.type === 'SUPPLIER'
);
// ===== Formik =====
const formikInitialValues = useMemo((): FinanceFormValues => {
return {
party_type_option:
FINANCE_PARTY_TYPE_OPTIONS.find(
(option) => option.value === initialValues?.party.type
(option) => option.value === initialValues?.party?.type
) || null,
party_id_option: initialValues?.party
? {
label: initialValues?.party.name || '',
value: initialValues?.party.id || 0,
label: initialValues?.party?.name || '',
value: initialValues?.party?.id || 0,
}
: null,
payment_date: initialValues?.payment_date || '',
@@ -72,11 +78,11 @@ const FormFinanceAdd = ({
) || null,
bank_id_option: initialValues?.bank
? {
label: initialValues.bank.name,
value: initialValues.bank.id,
label: initialValues?.bank?.name,
value: initialValues?.bank?.id,
}
: null,
party_account_number: initialValues?.party.account_number || '',
party_account_number: initialValues?.party?.account_number || '',
reference_number: initialValues?.reference_number || '',
nominal: initialValues?.nominal.toString() || '',
notes: initialValues?.notes || '',
@@ -153,6 +159,7 @@ const FormFinanceAdd = ({
if (isResponseError(response)) {
toast.error(response.message);
setServerErrorMessage(response.message);
return;
}
@@ -168,6 +175,7 @@ const FormFinanceAdd = ({
if (isResponseError(response)) {
toast.error(response.message);
setServerErrorMessage(response.message);
return;
}
@@ -207,6 +215,7 @@ const FormFinanceAdd = ({
? formik.errors.party_type_option
: ''
}
isDisabled={type === 'edit' || isSupplier}
required
isClearable
/>
@@ -245,7 +254,7 @@ const FormFinanceAdd = ({
}
required
isClearable
isDisabled={!formik.values.party_type_option?.value}
isDisabled={!formik.values.party_type_option?.value || isSupplier}
/>
<DateInput
label='Tanggal'
@@ -263,6 +272,7 @@ const FormFinanceAdd = ({
: ''
}
required
disabled={isSupplier}
/>
<SelectInput
label='Metode Pembayaran'
@@ -284,6 +294,7 @@ const FormFinanceAdd = ({
}
required
isClearable
isDisabled={isSupplier}
/>
<SelectInput
label='Bank'
@@ -324,6 +335,7 @@ const FormFinanceAdd = ({
}
required
isClearable
isDisabled={isSupplier}
/>
<TextInput
label={`Nomor Rekening ${formik.values.party_type_option?.value ? formatTitleCase(formik.values.party_type_option.value as string) : 'Pihak'}`}
@@ -344,6 +356,7 @@ const FormFinanceAdd = ({
}
required
readOnly
disabled={isSupplier}
/>
<TextInput
label='Nomor Referensi'
@@ -363,6 +376,7 @@ const FormFinanceAdd = ({
: ''
}
required
disabled={isSupplier}
/>
<NumberInput
label='Nominal'
@@ -378,6 +392,7 @@ const FormFinanceAdd = ({
: ''
}
required
disabled={isSupplier}
/>
<TextArea
label='Catatan'
@@ -393,8 +408,18 @@ const FormFinanceAdd = ({
: ''
}
required
disabled={isSupplier}
/>
<AlertErrorList formErrorList={formErrorList} onClose={close} />
{serverErrorMessage && (
<Alert color='error'>
<Icon icon='mdi:alert' />
{serverErrorMessage}
<Button color='error' onClick={() => setServerErrorMessage('')}>
<Icon icon='mdi:close' />
</Button>
</Alert>
)}
<div className='flex justify-center gap-4'>
<Button
type='reset'
@@ -27,13 +27,7 @@ export const InitialBalanceFormSchema = Yup.object().shape({
'Pihak wajib diisi',
(value) => value !== null && value !== undefined
),
bank_id_option: Yup.mixed()
.nullable()
.test(
'is-valid-option',
'Bank wajib diisi',
(value) => value !== null && value !== undefined
),
bank_id_option: Yup.mixed().nullable(),
reference_number: Yup.string().required('Nomor referensi wajib diisi'),
initial_balance_type_option: Yup.mixed()
.nullable()
@@ -29,8 +29,9 @@ import { Bank } from '@/types/api/master-data/bank';
import { Icon } from '@iconify/react';
import { useFormik } from 'formik';
import { useRouter } from 'next/navigation';
import { useCallback, useMemo } from 'react';
import { useCallback, useMemo, useState } from 'react';
import toast from 'react-hot-toast';
import Alert from '@/components/Alert';
interface FormFinanceAddInitialBalanceProps {
type?: 'add' | 'edit';
@@ -42,6 +43,7 @@ const FormFinanceAddInitialBalance = ({
initialValues,
}: FormFinanceAddInitialBalanceProps) => {
const router = useRouter();
const [serverErrorMessage, setServerErrorMessage] = useState('');
// ===== Formik =====
const formikInitialValues = useMemo((): InitialBalanceFormValues => {
@@ -53,18 +55,18 @@ const FormFinanceAddInitialBalance = ({
return {
party_type_option:
FINANCE_PARTY_TYPE_OPTIONS.find(
(option) => option.value === initialValues?.party.type
(option) => option.value === initialValues?.party?.type
) || null,
party_id_option: initialValues?.party
? {
label: initialValues.party.name,
value: initialValues.party.id,
label: initialValues.party?.name,
value: initialValues.party?.id,
}
: null,
bank_id_option: initialValues?.bank
? {
label: initialValues.bank.name,
value: initialValues.bank.id,
label: initialValues.bank?.name,
value: initialValues.bank?.id,
}
: null,
reference_number: initialValues?.reference_number || '',
@@ -147,6 +149,7 @@ const FormFinanceAddInitialBalance = ({
if (isResponseError(response)) {
toast.error(response.message);
setServerErrorMessage(response.message);
return;
}
@@ -166,6 +169,7 @@ const FormFinanceAddInitialBalance = ({
if (isResponseError(response)) {
toast.error(response.message);
setServerErrorMessage(response.message);
return;
}
@@ -211,6 +215,7 @@ const FormFinanceAddInitialBalance = ({
: ''
}
required
isDisabled={type === 'edit'}
isClearable
/>
<SelectInput
@@ -277,7 +282,6 @@ const FormFinanceAddInitialBalance = ({
? formik.errors.bank_id_option
: ''
}
required
isClearable
/>
<TextInput
@@ -362,7 +366,18 @@ const FormFinanceAddInitialBalance = ({
}
required
/>
<AlertErrorList formErrorList={formErrorList} onClose={close} />
{serverErrorMessage && (
<Alert color='error'>
<Icon icon='mdi:alert' />
{serverErrorMessage}
<Button color='error' onClick={() => setServerErrorMessage('')}>
<Icon icon='mdi:close' />
</Button>
</Alert>
)}
<div className='flex justify-center gap-4'>
<Button
type='reset'
@@ -24,8 +24,10 @@ import {
import { Bank } from '@/types/api/master-data/bank';
import { useFormik } from 'formik';
import { useRouter } from 'next/navigation';
import { useCallback, useMemo } from 'react';
import { useCallback, useMemo, useState } from 'react';
import toast from 'react-hot-toast';
import Alert from '@/components/Alert';
import { Icon } from '@iconify/react';
interface FormFinanceInjectionProps {
type?: 'add' | 'edit';
@@ -37,14 +39,15 @@ const FormFinanceInjection = ({
initialValues,
}: FormFinanceInjectionProps) => {
const router = useRouter();
const [serverErrorMessage, setServerErrorMessage] = useState('');
// ===== Formik =====
const formikInitialValues = useMemo((): InjectionFormValues => {
return {
bank_id_option: initialValues?.bank
? {
label: initialValues.bank.name,
value: initialValues.bank.id,
label: initialValues.bank?.name,
value: initialValues.bank?.id,
}
: null,
adjustment_date: initialValues?.payment_date || '',
@@ -103,6 +106,7 @@ const FormFinanceInjection = ({
if (isResponseError(response)) {
toast.error(response.message);
setServerErrorMessage(response.message);
return;
}
@@ -119,6 +123,7 @@ const FormFinanceInjection = ({
if (isResponseError(response)) {
toast.error(response.message);
setServerErrorMessage(response.message);
return;
}
@@ -230,6 +235,15 @@ const FormFinanceInjection = ({
required
/>
<AlertErrorList formErrorList={formErrorList} onClose={close} />
{serverErrorMessage && (
<Alert color='error'>
<Icon icon='mdi:alert' />
{serverErrorMessage}
<Button color='error' onClick={() => setServerErrorMessage('')}>
<Icon icon='mdi:close' />
</Button>
</Alert>
)}
<div className='flex justify-center gap-4'>
<Button
type='reset'
@@ -91,7 +91,7 @@ const InventoryProductDetail = ({
<td>:</td>
<td>
{inventoryProduct?.tax
? formatCurrency(inventoryProduct?.tax)
? formatNumber(inventoryProduct?.tax) + '%'
: '-'}
</td>
</tr>
@@ -16,7 +16,7 @@ import { TableRowSizeSelector } from '@/components/table/TableRowSizeSelector';
import { TableToolbar } from '@/components/table/TableToolbar';
import { ROWS_OPTIONS } from '@/config/constant';
import { isResponseError, isResponseSuccess } from '@/lib/api-helper';
import { cn, formatCurrency, formatDate } from '@/lib/helper';
import { cn, formatCurrency, formatDate, formatTitleCase } from '@/lib/helper';
import {
MarketingApi,
SalesOrderApi,
@@ -33,6 +33,7 @@ import RequirePermission from '@/components/helper/RequirePermission';
import { useAuth } from '@/services/hooks/useAuth';
import { CustomerApi, ProductApi } from '@/services/api/master-data';
import { MARKETING_APPROVAL_LINE } from '@/config/approval-line';
import Badge from '@/components/Badge';
const RowsOptionsMenu = ({
type = 'dropdown',
@@ -520,8 +521,53 @@ const MarketingTable = () => {
},
},
{
accessorKey: 'latest_approval.step_name',
accessorKey: 'approval.step_name',
header: 'Status',
cell: (props) => {
const approval = props.row.original.latest_approval;
const isRejected = approval?.action == 'REJECTED';
const isApproved = approval?.action == 'APPROVED';
return (
<Badge
variant='soft'
className={{
badge:
'rounded-lg px-2 w-full flex flex-row justify-start whitespace-nowrap',
}}
color={
isRejected
? 'error'
: isApproved
? approval?.step_number == 1
? 'neutral'
: approval?.step_number == 2
? 'primary'
: approval?.step_number == 3
? 'success'
: 'neutral'
: 'neutral'
}
>
<Icon
icon='mdi:circle'
width={12}
height={12}
color={
approval?.step_number == 1
? 'neutral'
: approval?.step_number == 2
? 'primary'
: approval?.step_number == 3
? 'success'
: 'neutral'
}
/>
{isRejected
? 'Ditolak'
: formatTitleCase(approval?.step_name || '')}
</Badge>
);
},
},
{
accessorKey: 'customer.name',
@@ -16,6 +16,7 @@ import {
formatCurrency,
formatDate,
formatNumber,
formatTitleCase,
formatVechicleNumber,
} from '@/lib/helper';
import {
@@ -34,6 +35,7 @@ import toast from 'react-hot-toast';
import SalesOrderExport from '@/components/pages/marketing/pdf/SalesOrderExport';
import DeliveryOrderExport from '@/components/pages/marketing/pdf/DeliveryOrderExport';
import RequirePermission from '@/components/helper/RequirePermission';
import Badge from '@/components/Badge';
const MarketingDetail = ({
initialValues,
@@ -121,6 +123,10 @@ const MarketingDetail = ({
);
};
const approval = initialValues?.latest_approval;
const isRejected = approval?.action == 'REJECTED';
const isApproved = approval?.action == 'APPROVED';
return (
<>
<div className='flex flex-col w-full gap-4'>
@@ -230,7 +236,46 @@ const MarketingDetail = ({
<tr>
<td className='font-semibold'>Status</td>
<td>:</td>
<td>{initialValues?.latest_approval?.step_name}</td>
<td>
<Badge
variant='soft'
className={{
badge:
'rounded-lg px-2 w-fit flex flex-row justify-start whitespace-nowrap',
}}
color={
isRejected
? 'error'
: isApproved
? approval?.step_number == 1
? 'neutral'
: approval?.step_number == 2
? 'primary'
: approval?.step_number == 3
? 'success'
: 'neutral'
: 'neutral'
}
>
<Icon
icon='mdi:circle'
width={12}
height={12}
color={
approval?.step_number == 1
? 'neutral'
: approval?.step_number == 2
? 'primary'
: approval?.step_number == 3
? 'success'
: 'neutral'
}
/>
{isRejected
? 'Ditolak'
: formatTitleCase(approval?.step_name || '')}
</Badge>
</td>
</tr>
<tr>
<td className='font-semibold'>Tanggal Penjualan</td>
@@ -621,7 +621,9 @@ const MarketingForm = ({
isClearable
placeholder='Pilih Pelanggan'
isDisabled={
formType === 'add_deliver' || formType === 'edit_deliver'
formType === 'add_deliver' ||
formType === 'edit_deliver' ||
formType === 'edit'
}
/>
<DateInput