mirror of
https://gitlab.com/mbugroup/lti-web-client.git
synced 2026-05-22 14:25:47 +00:00
Merge branch 'development' into 'schema/bulk-approve-marketings-expenses'
# Conflicts: # src/services/api/expense.ts
This commit is contained in:
@@ -523,7 +523,7 @@ const useSelect = <T,>(
|
||||
|
||||
const qs = new URLSearchParams({
|
||||
...(params ?? {}),
|
||||
[searchKey]: inputValue ?? '',
|
||||
[searchKey ? searchKey : 'search']: inputValue ?? '',
|
||||
[pageKey]: String(pageIndex + 1),
|
||||
[limitKey]: String(limit),
|
||||
}).toString();
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
'use client';
|
||||
|
||||
import { useMemo, useState } from 'react';
|
||||
import { useSearchParams } from 'next/navigation';
|
||||
|
||||
import { Icon } from '@iconify/react';
|
||||
import Button from '@/components/Button';
|
||||
@@ -9,6 +10,7 @@ import ExpenseRequestContent from '@/components/pages/expense/ExpenseRequestCont
|
||||
import ExpenseRealizationContent from '@/components/pages/expense/ExpenseRealizationContent';
|
||||
|
||||
import { Expense } from '@/types/api/expense';
|
||||
import { getExpenseListReturnTo } from '@/lib/expense-list-navigation';
|
||||
|
||||
interface ExpenseDetailProps {
|
||||
initialValues?: Expense;
|
||||
@@ -16,6 +18,8 @@ interface ExpenseDetailProps {
|
||||
|
||||
const ExpenseDetail: React.FC<ExpenseDetailProps> = ({ initialValues }) => {
|
||||
const [activeTab, setActiveTab] = useState<string>('request');
|
||||
const searchParams = useSearchParams();
|
||||
const returnTo = getExpenseListReturnTo(searchParams);
|
||||
|
||||
const expenseDetailTabs = useMemo(() => {
|
||||
const validTabs = [
|
||||
@@ -46,7 +50,7 @@ const ExpenseDetail: React.FC<ExpenseDetailProps> = ({ initialValues }) => {
|
||||
<section className='w-full max-w-full pb-16'>
|
||||
<header className='flex flex-col gap-4'>
|
||||
<Button
|
||||
href='/expense'
|
||||
href={returnTo}
|
||||
variant='link'
|
||||
className='w-fit p-0 text-primary'
|
||||
>
|
||||
|
||||
@@ -1,5 +1,8 @@
|
||||
'use client';
|
||||
|
||||
import { useFormik } from 'formik';
|
||||
import toast from 'react-hot-toast';
|
||||
import { useSearchParams } from 'next/navigation';
|
||||
|
||||
import Link from 'next/link';
|
||||
import { Icon } from '@iconify/react';
|
||||
@@ -16,6 +19,7 @@ import {
|
||||
} from '@/components/pages/expense/form/ExpenseRequestForm.schema';
|
||||
import { ExpenseApi } from '@/services/api/expense';
|
||||
import { isResponseSuccess } from '@/lib/api-helper';
|
||||
import { buildExpenseActionHref } from '@/lib/expense-list-navigation';
|
||||
import { ACCEPTED_FILE_TYPE, S3_PUBLIC_BASE_URL } from '@/config/constant';
|
||||
|
||||
interface ExpenseRealizationContentProps {
|
||||
@@ -25,6 +29,8 @@ interface ExpenseRealizationContentProps {
|
||||
const ExpenseRealizationContent = ({
|
||||
initialValues,
|
||||
}: ExpenseRealizationContentProps) => {
|
||||
const searchParams = useSearchParams();
|
||||
|
||||
const formik = useFormik<UploadRequestDocumentsFormValues>({
|
||||
initialValues: {
|
||||
documents: [],
|
||||
@@ -74,7 +80,11 @@ const ExpenseRealizationContent = ({
|
||||
<Button
|
||||
type='button'
|
||||
color='warning'
|
||||
href={`/expense/realization/edit/?expenseId=${initialValues?.id}`}
|
||||
href={buildExpenseActionHref(
|
||||
'/expense/realization/edit/',
|
||||
initialValues?.id as number,
|
||||
searchParams
|
||||
)}
|
||||
className='px-4 grow sm:grow-0'
|
||||
>
|
||||
<Icon icon='mdi:pencil-outline' width={24} height={24} />
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
'use client';
|
||||
|
||||
import { useState } from 'react';
|
||||
import { useRouter } from 'next/navigation';
|
||||
import { useRouter, useSearchParams } from 'next/navigation';
|
||||
import { useFormik } from 'formik';
|
||||
import toast from 'react-hot-toast';
|
||||
|
||||
@@ -31,6 +31,10 @@ import { ExpenseApi } from '@/services/api/expense';
|
||||
import { isResponseSuccess } from '@/lib/api-helper';
|
||||
import { EXPENSE_REQUEST_APPROVAL_LINE } from '@/config/approval-line';
|
||||
import { BaseApiResponse } from '@/types/api/api-general';
|
||||
import {
|
||||
buildExpenseActionHref,
|
||||
getExpenseListReturnTo,
|
||||
} from '@/lib/expense-list-navigation';
|
||||
|
||||
interface ExpenseRequestContentProps {
|
||||
initialValues?: Expense;
|
||||
@@ -40,6 +44,8 @@ const ExpenseRequestContent = ({
|
||||
initialValues,
|
||||
}: ExpenseRequestContentProps) => {
|
||||
const router = useRouter();
|
||||
const searchParams = useSearchParams();
|
||||
const returnTo = getExpenseListReturnTo(searchParams);
|
||||
|
||||
const { approvals: approvalHistory, isLoading: isLoadingApprovalHistory } =
|
||||
useApprovalSteps({
|
||||
@@ -148,7 +154,7 @@ const ExpenseRequestContent = ({
|
||||
|
||||
if (isResponseSuccess(deleteResponse)) {
|
||||
toast.success('Berhasil menghapus data biaya operasional!');
|
||||
router.push('/expense');
|
||||
router.push(returnTo);
|
||||
} else {
|
||||
toast.error('Gagal menghapus data biaya operasional!');
|
||||
}
|
||||
@@ -164,7 +170,7 @@ const ExpenseRequestContent = ({
|
||||
|
||||
if (isResponseSuccess(completeRes)) {
|
||||
toast.success(completeRes.message);
|
||||
router.push('/expense');
|
||||
router.push(returnTo);
|
||||
} else {
|
||||
toast.error(completeRes?.message as string);
|
||||
}
|
||||
@@ -204,7 +210,7 @@ const ExpenseRequestContent = ({
|
||||
|
||||
toast.success(approveResponse?.message);
|
||||
setApprovalNotes('');
|
||||
router.push('/expense');
|
||||
router.push(returnTo);
|
||||
} else {
|
||||
approveModal.closeModal();
|
||||
|
||||
@@ -239,7 +245,7 @@ const ExpenseRequestContent = ({
|
||||
|
||||
toast.success(rejectResponse.message);
|
||||
setApprovalNotes('');
|
||||
router.push('/expense');
|
||||
router.push(returnTo);
|
||||
} else {
|
||||
rejectModal.closeModal();
|
||||
|
||||
@@ -365,7 +371,11 @@ const ExpenseRequestContent = ({
|
||||
<Button
|
||||
variant='outline'
|
||||
color='info'
|
||||
href={`/expense/realization/?expenseId=${initialValues?.id}`}
|
||||
href={buildExpenseActionHref(
|
||||
'/expense/realization/',
|
||||
initialValues?.id as number,
|
||||
searchParams
|
||||
)}
|
||||
className='w-full sm:w-fit'
|
||||
>
|
||||
<Icon
|
||||
@@ -384,7 +394,11 @@ const ExpenseRequestContent = ({
|
||||
<Button
|
||||
type='button'
|
||||
color='warning'
|
||||
href={`/expense/detail/edit/?expenseId=${initialValues?.id}`}
|
||||
href={buildExpenseActionHref(
|
||||
'/expense/detail/edit/',
|
||||
initialValues?.id as number,
|
||||
searchParams
|
||||
)}
|
||||
className='px-4 grow sm:grow-0'
|
||||
>
|
||||
<Icon icon='mdi:pencil-outline' width={24} height={24} />
|
||||
|
||||
@@ -1,8 +1,12 @@
|
||||
'use client';
|
||||
|
||||
import { ChangeEventHandler, useEffect, useMemo, useState } from 'react';
|
||||
import { usePathname } from 'next/navigation';
|
||||
import { useUiStore } from '@/stores/ui/ui.store';
|
||||
import {
|
||||
ChangeEventHandler,
|
||||
useCallback,
|
||||
useEffect,
|
||||
useMemo,
|
||||
useState,
|
||||
} from 'react';
|
||||
import useSWR from 'swr';
|
||||
import {
|
||||
CellContext,
|
||||
@@ -15,8 +19,11 @@ import toast from 'react-hot-toast';
|
||||
import { Icon } from '@iconify/react';
|
||||
import Table from '@/components/Table';
|
||||
import DebouncedTextInput from '@/components/input/DebouncedTextInput';
|
||||
import DateInput from '@/components/input/DateInput';
|
||||
import SelectInput, { OptionType } from '@/components/input/SelectInput';
|
||||
import TextArea from '@/components/input/TextArea';
|
||||
import Button from '@/components/Button';
|
||||
import { useModal } from '@/components/Modal';
|
||||
import Modal, { useModal } from '@/components/Modal';
|
||||
import ConfirmationModal from '@/components/modal/ConfirmationModal';
|
||||
import PopoverButton from '@/components/popover/PopoverButton';
|
||||
import PopoverContent from '@/components/popover/PopoverContent';
|
||||
@@ -36,6 +43,36 @@ import { isResponseSuccess } from '@/lib/api-helper';
|
||||
import { useTableFilter } from '@/services/hooks/useTableFilter';
|
||||
import { BaseApiResponse } from '@/types/api/api-general';
|
||||
|
||||
type ExpenseTableFilters = {
|
||||
search: string;
|
||||
nameSort: string;
|
||||
transactionDate: string;
|
||||
realizationDate: string;
|
||||
locationId: string;
|
||||
vendorId: string;
|
||||
userId: string;
|
||||
};
|
||||
|
||||
const approvalStatusOptions = [
|
||||
{ value: 'HEAD_AREA', label: 'Approval Head Area' },
|
||||
{ value: 'UNIT_VICE_PRESIDENT', label: 'Approval Unit Vice President' },
|
||||
{ value: 'FINANCE', label: 'Approval Finance' },
|
||||
{ value: 'REALISASI', label: 'Realisasi' },
|
||||
{ value: 'SELESAI', label: 'Selesai' },
|
||||
] as const satisfies OptionType<
|
||||
'HEAD_AREA' | 'UNIT_VICE_PRESIDENT' | 'FINANCE' | 'REALISASI' | 'SELESAI'
|
||||
>[];
|
||||
|
||||
type ApprovalStatusValue =
|
||||
| 'HEAD_AREA'
|
||||
| 'UNIT_VICE_PRESIDENT'
|
||||
| 'FINANCE'
|
||||
| 'REALISASI'
|
||||
| 'SELESAI';
|
||||
|
||||
const isApprovalDateRequired = (status?: ApprovalStatusValue) =>
|
||||
status === 'REALISASI' || status === 'SELESAI';
|
||||
|
||||
const RowOptionsMenu = ({
|
||||
popoverPosition = 'bottom',
|
||||
props,
|
||||
@@ -153,17 +190,16 @@ const RowOptionsMenu = ({
|
||||
};
|
||||
|
||||
const ExpensesTable = () => {
|
||||
const { searchValue, setSearchValue, setTableState } = useUiStore();
|
||||
const pathname = usePathname();
|
||||
|
||||
const {
|
||||
state: tableFilterState,
|
||||
updateFilter,
|
||||
setPage,
|
||||
setPageSize,
|
||||
toQueryString: getTableFilterQueryString,
|
||||
} = useTableFilter({
|
||||
} = useTableFilter<ExpenseTableFilters>({
|
||||
initial: {
|
||||
page: 1,
|
||||
pageSize: 10,
|
||||
search: '',
|
||||
nameSort: '',
|
||||
transactionDate: '',
|
||||
@@ -182,6 +218,9 @@ const ExpensesTable = () => {
|
||||
vendorId: 'vendor_id',
|
||||
userId: 'user_id',
|
||||
},
|
||||
|
||||
persist: true,
|
||||
storeName: 'expense-table',
|
||||
});
|
||||
|
||||
const {
|
||||
@@ -196,6 +235,7 @@ const ExpensesTable = () => {
|
||||
const deleteModal = useModal();
|
||||
const approveModal = useModal();
|
||||
const rejectModal = useModal();
|
||||
const bulkApproveFormModal = useModal();
|
||||
|
||||
// ===== FILTER MODAL STATE =====
|
||||
const filterModal = useModal();
|
||||
@@ -207,6 +247,10 @@ const ExpensesTable = () => {
|
||||
const [isApproveLoading, setIsApproveLoading] = useState(false);
|
||||
const [isRejectLoading, setIsRejectLoading] = useState(false);
|
||||
const [, setApprovalNotes] = useState('');
|
||||
const [bulkApprovalStatus, setBulkApprovalStatus] =
|
||||
useState<OptionType<ApprovalStatusValue> | null>(null);
|
||||
const [bulkApprovalDate, setBulkApprovalDate] = useState('');
|
||||
const [bulkApprovalNotes, setBulkApprovalNotes] = useState('');
|
||||
|
||||
const [sorting, setSorting] = useState<SortingState>([]);
|
||||
const [rowSelection, setRowSelection] = useState<Record<string, boolean>>({});
|
||||
@@ -390,17 +434,45 @@ const ExpensesTable = () => {
|
||||
);
|
||||
};
|
||||
|
||||
// const bulkApproveClickHandler = () => {
|
||||
// approveModal.openModal();
|
||||
// };
|
||||
const resetBulkApproveForm = useCallback(() => {
|
||||
setBulkApprovalStatus(null);
|
||||
setBulkApprovalDate('');
|
||||
setBulkApprovalNotes('');
|
||||
}, []);
|
||||
|
||||
// const bulkRejectClickHandler = () => {
|
||||
// rejectModal.openModal();
|
||||
// };
|
||||
const openBulkApproveForm = useCallback(
|
||||
(presetStatus?: ApprovalStatusValue) => {
|
||||
resetBulkApproveForm();
|
||||
|
||||
if (presetStatus) {
|
||||
const selectedStatus = approvalStatusOptions.find(
|
||||
(option) => option.value === presetStatus
|
||||
);
|
||||
|
||||
if (selectedStatus) {
|
||||
setBulkApprovalStatus(selectedStatus);
|
||||
}
|
||||
}
|
||||
|
||||
bulkApproveFormModal.openModal();
|
||||
},
|
||||
[bulkApproveFormModal, resetBulkApproveForm]
|
||||
);
|
||||
|
||||
const bulkApproveClickHandler = () => {
|
||||
setApprovalNotes('');
|
||||
approveModal.openModal();
|
||||
openBulkApproveForm();
|
||||
};
|
||||
|
||||
const bulkApproveHeadAreaClickHandler = () => {
|
||||
openBulkApproveForm('HEAD_AREA');
|
||||
};
|
||||
|
||||
const bulkApproveUnitVicePresidentClickHandler = () => {
|
||||
openBulkApproveForm('UNIT_VICE_PRESIDENT');
|
||||
};
|
||||
|
||||
const bulkApproveFinanceClickHandler = () => {
|
||||
openBulkApproveForm('FINANCE');
|
||||
};
|
||||
|
||||
const bulkRejectClickHandler = () => {
|
||||
@@ -408,6 +480,18 @@ const ExpensesTable = () => {
|
||||
rejectModal.openModal();
|
||||
};
|
||||
|
||||
const bulkApprovalDateChangeHandler: ChangeEventHandler<HTMLInputElement> = (
|
||||
e
|
||||
) => {
|
||||
setBulkApprovalDate(e.target.value);
|
||||
};
|
||||
|
||||
const bulkApprovalNotesChangeHandler: ChangeEventHandler<
|
||||
HTMLTextAreaElement
|
||||
> = (e) => {
|
||||
setBulkApprovalNotes(e.target.value);
|
||||
};
|
||||
|
||||
const confirmationModalDeleteClickHandler = async () => {
|
||||
setIsDeleteLoading(true);
|
||||
|
||||
@@ -470,6 +554,48 @@ const ExpensesTable = () => {
|
||||
setIsApproveLoading(false);
|
||||
};
|
||||
|
||||
const bulkApproveSubmitHandler = async () => {
|
||||
if (!bulkApprovalStatus) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (isApprovalDateRequired(bulkApprovalStatus.value) && !bulkApprovalDate) {
|
||||
toast.error('Tanggal realisasi wajib diisi.');
|
||||
return;
|
||||
}
|
||||
|
||||
if (!bulkApprovalNotes.trim()) {
|
||||
toast.error('Catatan wajib diisi.');
|
||||
return;
|
||||
}
|
||||
|
||||
setIsApproveLoading(true);
|
||||
|
||||
const bulkApproveResponse = await ExpenseApi.bulkApprovals(
|
||||
selectedRowIds,
|
||||
bulkApprovalStatus.value,
|
||||
isApprovalDateRequired(bulkApprovalStatus.value) ? bulkApprovalDate : '',
|
||||
bulkApprovalNotes
|
||||
);
|
||||
|
||||
if (isResponseSuccess(bulkApproveResponse)) {
|
||||
refreshExpenses();
|
||||
bulkApproveFormModal.closeModal();
|
||||
toast.success(
|
||||
`Berhasil approve ${selectedRowIds.length} data biaya operasional!`
|
||||
);
|
||||
resetBulkApproveForm();
|
||||
setRowSelection({});
|
||||
} else {
|
||||
toast.error(
|
||||
bulkApproveResponse?.message ??
|
||||
`Gagal approve ${selectedRowIds.length} data biaya operasional!`
|
||||
);
|
||||
}
|
||||
|
||||
setIsApproveLoading(false);
|
||||
};
|
||||
|
||||
const confirmationModalRejectClickHandler = async (notes: string) => {
|
||||
setIsRejectLoading(true);
|
||||
|
||||
@@ -512,16 +638,7 @@ const ExpensesTable = () => {
|
||||
setIsRejectLoading(false);
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
updateFilter('search', searchValue);
|
||||
}, [searchValue, updateFilter]);
|
||||
|
||||
useEffect(() => {
|
||||
setTableState('expense-table', pathname);
|
||||
}, [pathname, setTableState]);
|
||||
|
||||
const searchChangeHandler: ChangeEventHandler<HTMLInputElement> = (e) => {
|
||||
setSearchValue(e.target.value);
|
||||
updateFilter('search', e.target.value);
|
||||
};
|
||||
|
||||
@@ -554,7 +671,7 @@ const ExpensesTable = () => {
|
||||
const isNameSorted = sorting.find((sortItem) => sortItem.id === 'name');
|
||||
|
||||
if (!isNameSorted) {
|
||||
updateFilter('nameSort', '');
|
||||
updateFilter('nameSort', '', false);
|
||||
} else {
|
||||
updateFilter('nameSort', isNameSorted.desc ? 'desc' : 'asc');
|
||||
}
|
||||
@@ -565,7 +682,7 @@ const ExpensesTable = () => {
|
||||
<div className='w-full'>
|
||||
<div className='w-full p-3 flex flex-row justify-between gap-3 flex-wrap border-b border-base-content/10'>
|
||||
{/* Action Buttons */}
|
||||
<div className='w-fit flex flex-row gap-3 flex-wrap'>
|
||||
<div className='w-fit flex flex-col gap-3 flex-wrap'>
|
||||
<RequirePermission permissions='lti.expense.create'>
|
||||
<Button
|
||||
href='/expense/add'
|
||||
@@ -578,14 +695,36 @@ const ExpensesTable = () => {
|
||||
</RequirePermission>
|
||||
|
||||
{selectedRowIds.length > 0 && (
|
||||
<>
|
||||
<hr className='w-px h-full border-none bg-base-content/10 sm:block hidden' />
|
||||
<div className='flex flex-row gap-3 flex-wrap'>
|
||||
<RequirePermission
|
||||
permissions={[
|
||||
'lti.expense.approve.head_area',
|
||||
'lti.expense.approve.unit_vice_president',
|
||||
'lti.expense.approve.finance',
|
||||
'lti.expense.create.realization',
|
||||
]}
|
||||
>
|
||||
<Button
|
||||
variant='outline'
|
||||
color='none'
|
||||
onClick={bulkApproveClickHandler}
|
||||
className='px-3 py-2.5 gap-1.5 text-sm text-base-content/50 border border-base-content/10 rounded-xl shadow-button-soft'
|
||||
>
|
||||
<Icon
|
||||
icon='lucide-lab:farm'
|
||||
width={20}
|
||||
height={20}
|
||||
className='text-success'
|
||||
/>
|
||||
Bulk Approve
|
||||
</Button>
|
||||
</RequirePermission>
|
||||
|
||||
<RequirePermission permissions='lti.expense.approve.head_area'>
|
||||
<Button
|
||||
variant='outline'
|
||||
color='none'
|
||||
onClick={bulkApproveClickHandler}
|
||||
onClick={bulkApproveHeadAreaClickHandler}
|
||||
disabled={!isAllSelectedRowLatestApprovalOnHeadArea}
|
||||
className='px-3 py-2.5 gap-1.5 text-sm text-base-content/50 border border-base-content/10 rounded-xl shadow-button-soft'
|
||||
>
|
||||
@@ -603,7 +742,7 @@ const ExpensesTable = () => {
|
||||
<Button
|
||||
variant='outline'
|
||||
color='none'
|
||||
onClick={bulkApproveClickHandler}
|
||||
onClick={bulkApproveUnitVicePresidentClickHandler}
|
||||
disabled={
|
||||
!isAllSelectedRowLatestApprovalOnUnitVicePresident
|
||||
}
|
||||
@@ -623,7 +762,7 @@ const ExpensesTable = () => {
|
||||
<Button
|
||||
variant='outline'
|
||||
color='none'
|
||||
onClick={bulkApproveClickHandler}
|
||||
onClick={bulkApproveFinanceClickHandler}
|
||||
disabled={!isAllSelectedRowLatestApprovalOnFinance}
|
||||
className='px-3 py-2.5 gap-1.5 text-sm text-base-content/50 border border-base-content/10 rounded-xl shadow-button-soft'
|
||||
>
|
||||
@@ -664,12 +803,12 @@ const ExpensesTable = () => {
|
||||
Reject
|
||||
</Button>
|
||||
</RequirePermission>
|
||||
</>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Search and Filter */}
|
||||
<div className='flex flex-1 flex-row justify-start sm:justify-end items-center gap-3 flex-wrap'>
|
||||
<div className='flex flex-1 flex-row justify-start sm:justify-end items-start gap-3 flex-wrap'>
|
||||
<DebouncedTextInput
|
||||
name='search'
|
||||
placeholder='Search'
|
||||
@@ -804,6 +943,100 @@ const ExpensesTable = () => {
|
||||
}}
|
||||
/>
|
||||
|
||||
<Modal
|
||||
ref={bulkApproveFormModal.ref}
|
||||
className={{
|
||||
modalBox: 'max-w-lg rounded-lg p-0',
|
||||
}}
|
||||
closeOnBackdrop
|
||||
>
|
||||
<div className='flex flex-col'>
|
||||
<div className='flex items-center justify-between border-b border-base-content/10 p-4'>
|
||||
<h4 className='text-sm font-semibold text-base-content'>
|
||||
Bulk Approve Expense
|
||||
</h4>
|
||||
<Button
|
||||
variant='ghost'
|
||||
color='none'
|
||||
onClick={() => {
|
||||
bulkApproveFormModal.closeModal();
|
||||
resetBulkApproveForm();
|
||||
}}
|
||||
className='p-1'
|
||||
>
|
||||
<Icon icon='mdi:close' width={20} height={20} />
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
<div className='flex flex-col gap-4 p-4'>
|
||||
<SelectInput
|
||||
label='Status Approval'
|
||||
options={approvalStatusOptions as OptionType[]}
|
||||
value={bulkApprovalStatus}
|
||||
onChange={(val) => {
|
||||
const nextValue = val as OptionType<ApprovalStatusValue> | null;
|
||||
setBulkApprovalStatus(nextValue);
|
||||
|
||||
if (!isApprovalDateRequired(nextValue?.value)) {
|
||||
setBulkApprovalDate('');
|
||||
}
|
||||
}}
|
||||
placeholder='Pilih status approval'
|
||||
isClearable
|
||||
/>
|
||||
|
||||
{isApprovalDateRequired(bulkApprovalStatus?.value) && (
|
||||
<DateInput
|
||||
name='bulk_approval_date'
|
||||
label='Tanggal Realisasi'
|
||||
value={bulkApprovalDate}
|
||||
onChange={bulkApprovalDateChangeHandler}
|
||||
isNestedModal
|
||||
required
|
||||
/>
|
||||
)}
|
||||
|
||||
<TextArea
|
||||
name='bulk_approval_notes'
|
||||
label='Catatan'
|
||||
value={bulkApprovalNotes}
|
||||
onChange={bulkApprovalNotesChangeHandler}
|
||||
placeholder='Masukkan catatan approval...'
|
||||
rows={4}
|
||||
required
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className='flex justify-end gap-3 border-t border-base-content/10 p-4'>
|
||||
<Button
|
||||
variant='outline'
|
||||
color='none'
|
||||
onClick={() => {
|
||||
bulkApproveFormModal.closeModal();
|
||||
resetBulkApproveForm();
|
||||
}}
|
||||
className='px-3 py-2.5'
|
||||
>
|
||||
Batal
|
||||
</Button>
|
||||
<Button
|
||||
color='success'
|
||||
onClick={bulkApproveSubmitHandler}
|
||||
isLoading={isApproveLoading}
|
||||
disabled={
|
||||
!bulkApprovalStatus ||
|
||||
!bulkApprovalNotes.trim() ||
|
||||
(isApprovalDateRequired(bulkApprovalStatus.value) &&
|
||||
!bulkApprovalDate)
|
||||
}
|
||||
className='px-3 py-2.5'
|
||||
>
|
||||
Submit
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</Modal>
|
||||
|
||||
<ExpensesFilterModal
|
||||
ref={filterModal.ref}
|
||||
onSubmit={handleFilterSubmit}
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
'use client';
|
||||
|
||||
import { useCallback, useState } from 'react';
|
||||
import { useRouter } from 'next/navigation';
|
||||
import { useRouter, useSearchParams } from 'next/navigation';
|
||||
import { useFormik } from 'formik';
|
||||
import toast from 'react-hot-toast';
|
||||
|
||||
@@ -35,6 +35,7 @@ import { isResponseError } from '@/lib/api-helper';
|
||||
import { LocationApi, SupplierApi } from '@/services/api/master-data';
|
||||
import { Supplier } from '@/types/api/master-data/supplier';
|
||||
import { ACCEPTED_FILE_TYPE } from '@/config/constant';
|
||||
import { getExpenseListReturnTo } from '@/lib/expense-list-navigation';
|
||||
import { cn } from '@/lib/helper';
|
||||
import { useFormikErrorList } from '@/services/hooks/useFormikErrorList';
|
||||
|
||||
@@ -48,6 +49,8 @@ const ExpenseRealizationForm = ({
|
||||
initialValues,
|
||||
}: ExpenseRealizationFormProps) => {
|
||||
const router = useRouter();
|
||||
const searchParams = useSearchParams();
|
||||
const returnTo = getExpenseListReturnTo(searchParams);
|
||||
|
||||
const [expenseFormErrorMessage, setExpenseFormErrorMessage] = useState('');
|
||||
|
||||
@@ -64,9 +67,9 @@ const ExpenseRealizationForm = ({
|
||||
}
|
||||
|
||||
toast.success(createExpenseRes?.message as string);
|
||||
router.push('/expense');
|
||||
router.push(returnTo);
|
||||
},
|
||||
[router]
|
||||
[initialValues?.id, returnTo, router]
|
||||
);
|
||||
|
||||
const updateExpenseHandler = useCallback(
|
||||
@@ -83,9 +86,9 @@ const ExpenseRealizationForm = ({
|
||||
|
||||
toast.success(updateExpenseRes?.message as string);
|
||||
router.refresh();
|
||||
router.push('/expense');
|
||||
router.push(returnTo);
|
||||
},
|
||||
[router]
|
||||
[returnTo, router]
|
||||
);
|
||||
|
||||
const formik = useFormik<ExpenseRealizationFormValues>({
|
||||
@@ -258,7 +261,7 @@ const ExpenseRealizationForm = ({
|
||||
<section className='w-full'>
|
||||
<header className='flex flex-col gap-4'>
|
||||
<Button
|
||||
href='/expense'
|
||||
href={returnTo}
|
||||
variant='link'
|
||||
className='w-fit p-0 text-primary'
|
||||
>
|
||||
|
||||
@@ -2,6 +2,8 @@
|
||||
|
||||
import Button from '@/components/Button';
|
||||
import CheckboxInput from '@/components/input/CheckboxInput';
|
||||
import DateInput from '@/components/input/DateInput';
|
||||
import TextArea from '@/components/input/TextArea';
|
||||
import Modal, { useModal } from '@/components/Modal';
|
||||
import ConfirmationModal from '@/components/modal/ConfirmationModal';
|
||||
import ConfirmationModalWithNotes from '@/components/modal/ConfirmationModalWithNotes';
|
||||
@@ -13,6 +15,7 @@ import {
|
||||
SalesOrderApi,
|
||||
} from '@/services/api/marketing/marketing';
|
||||
import { useTableFilter } from '@/services/hooks/useTableFilter';
|
||||
import { BaseApiResponse } from '@/types/api/api-general';
|
||||
import {
|
||||
BaseSalesOrder,
|
||||
Marketing,
|
||||
@@ -21,7 +24,7 @@ import {
|
||||
import { Icon } from '@iconify/react';
|
||||
import { CellContext, ColumnDef, Row } from '@tanstack/react-table';
|
||||
import { useRouter } from 'next/navigation';
|
||||
import { useMemo, useState } from 'react';
|
||||
import { ChangeEventHandler, useCallback, useMemo, useState } from 'react';
|
||||
import toast from 'react-hot-toast';
|
||||
import useSWR from 'swr';
|
||||
import RequirePermission from '@/components/helper/RequirePermission';
|
||||
@@ -154,12 +157,17 @@ const MarketingTable = () => {
|
||||
);
|
||||
const [selectedItem, setSelectedItem] = useState<Marketing | null>(null);
|
||||
const [rowSelection, setRowSelection] = useState<Record<string, boolean>>({});
|
||||
const [bulkDeliveryDate, setBulkDeliveryDate] = useState('');
|
||||
const [bulkDeliveryNotes, setBulkDeliveryNotes] = useState('');
|
||||
const [isSubmittingBulkDelivery, setIsSubmittingBulkDelivery] =
|
||||
useState(false);
|
||||
|
||||
const router = useRouter();
|
||||
const deleteModal = useModal();
|
||||
const confirmationModal = useModal();
|
||||
const productsModal = useModal();
|
||||
const deliveryModal = useModal();
|
||||
const bulkDeliveryModal = useModal();
|
||||
const filterModal = useModal();
|
||||
|
||||
const {
|
||||
@@ -182,6 +190,9 @@ const MarketingTable = () => {
|
||||
status: 'status',
|
||||
customer_id: 'customer_id',
|
||||
},
|
||||
|
||||
persist: true,
|
||||
storeName: 'marketing-table',
|
||||
});
|
||||
|
||||
// ===== FETCH DATA =====
|
||||
@@ -198,12 +209,14 @@ const MarketingTable = () => {
|
||||
const filterSubmitHandler = (values: MarketingFilter) => {
|
||||
updateFilter(
|
||||
'product_ids',
|
||||
values.product_ids?.map((item) => item.toString()).join(',')
|
||||
values.product_ids?.map((item) => item.toString()).join(','),
|
||||
true
|
||||
);
|
||||
updateFilter('status', values.status ? values.status.toString() : '');
|
||||
updateFilter('status', values.status ? values.status.toString() : '', true);
|
||||
updateFilter(
|
||||
'customer_id',
|
||||
values.customer_id ? values.customer_id.toString() : ''
|
||||
values.customer_id ? values.customer_id.toString() : '',
|
||||
true
|
||||
);
|
||||
};
|
||||
|
||||
@@ -211,13 +224,19 @@ const MarketingTable = () => {
|
||||
useState(false);
|
||||
|
||||
const filterResetHandler = () => {
|
||||
updateFilter('product_ids', '');
|
||||
updateFilter('status', '');
|
||||
updateFilter('customer_id', '');
|
||||
updateFilter('product_ids', '', true);
|
||||
updateFilter('status', '', true);
|
||||
updateFilter('customer_id', '', true);
|
||||
};
|
||||
|
||||
const approveClickHandler = () => {
|
||||
setApproveAction('APPROVED');
|
||||
|
||||
if (selectedApprovalStep === 2) {
|
||||
bulkDeliveryModal.openModal();
|
||||
return;
|
||||
}
|
||||
|
||||
confirmationModal.openModal();
|
||||
};
|
||||
|
||||
@@ -226,10 +245,13 @@ const MarketingTable = () => {
|
||||
confirmationModal.openModal();
|
||||
};
|
||||
|
||||
const productsClickHandler = (item: Marketing) => {
|
||||
setSelectedItem(item);
|
||||
productsModal.openModal();
|
||||
};
|
||||
const productsClickHandler = useCallback(
|
||||
(item: Marketing) => {
|
||||
setSelectedItem(item);
|
||||
productsModal.openModal();
|
||||
},
|
||||
[productsModal]
|
||||
);
|
||||
|
||||
const deleteMarketingHandler = async () => {
|
||||
const deleteMarketingRes = await MarketingApi.delete(
|
||||
@@ -251,61 +273,135 @@ const MarketingTable = () => {
|
||||
const selectedRowsData = allData.filter(
|
||||
(row) => rowSelection[row.id.toString()]
|
||||
);
|
||||
const selectedApprovalStep =
|
||||
selectedRowsData.length > 0
|
||||
? selectedRowsData[0].latest_approval.step_number
|
||||
: null;
|
||||
|
||||
const hasApprovable = selectedRowsData.some(
|
||||
(row) =>
|
||||
row.latest_approval.step_number === 1 &&
|
||||
row.latest_approval.action !== 'REJECTED'
|
||||
);
|
||||
const hasRejectable = selectedRowsData.some(
|
||||
(row) =>
|
||||
row.latest_approval.step_number === 1 &&
|
||||
row.latest_approval.action !== 'REJECTED'
|
||||
);
|
||||
const eligibleSelectedRows = selectedRowsData.filter((row) => {
|
||||
const approval = row.latest_approval;
|
||||
|
||||
if (approval.action === 'REJECTED') {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (selectedApprovalStep === null) {
|
||||
return approval.step_number === 1 || approval.step_number === 2;
|
||||
}
|
||||
|
||||
return approval.step_number === selectedApprovalStep;
|
||||
});
|
||||
|
||||
const hasApprovable = eligibleSelectedRows.length > 0;
|
||||
const hasRejectable = eligibleSelectedRows.length > 0;
|
||||
|
||||
const disableApprove = !hasApprovable;
|
||||
const disableReject = !hasRejectable;
|
||||
|
||||
const idsToProcess =
|
||||
approveAction === 'APPROVED'
|
||||
? selectedRowsData
|
||||
.filter((row) => row.latest_approval.step_number === 1)
|
||||
.map((row) => row.id)
|
||||
: selectedRowsData
|
||||
.filter((row) => row.latest_approval.step_number === 2)
|
||||
.map((row) => row.id);
|
||||
const idsToProcess = eligibleSelectedRows.map((row) => row.id);
|
||||
const nextApprovalStatus =
|
||||
selectedApprovalStep === 1
|
||||
? 'SALES_ORDER'
|
||||
: selectedApprovalStep === 2
|
||||
? 'DELIVERY_ORDER'
|
||||
: null;
|
||||
|
||||
const approveMarketingHandler = async (notes: string) => {
|
||||
let idsToProcess: number[] = [];
|
||||
|
||||
idsToProcess = selectedRowsData
|
||||
.filter((row) => row.latest_approval.step_number === 1)
|
||||
.map((row) => row.id);
|
||||
|
||||
if (idsToProcess.length === 0) {
|
||||
toast.error(`Tidak ada data yang valid untuk di ${approveAction}.`);
|
||||
confirmationModal.closeModal();
|
||||
return;
|
||||
}
|
||||
|
||||
const approveMarketingRes = await SalesOrderApi.bulkApprovals(
|
||||
idsToProcess,
|
||||
approveAction,
|
||||
notes
|
||||
);
|
||||
if (approveAction === 'APPROVED' && selectedApprovalStep !== 1) {
|
||||
toast.error('Approve tahap ini harus menggunakan tanggal pengiriman.');
|
||||
confirmationModal.closeModal();
|
||||
return;
|
||||
}
|
||||
|
||||
if (approveAction === 'APPROVED' && !nextApprovalStatus) {
|
||||
toast.error('Status approval berikutnya tidak valid.');
|
||||
confirmationModal.closeModal();
|
||||
return;
|
||||
}
|
||||
|
||||
const approveMarketingRes: BaseApiResponse<unknown> | undefined =
|
||||
approveAction === 'APPROVED'
|
||||
? await MarketingApi.bulkApprovals(
|
||||
idsToProcess,
|
||||
nextApprovalStatus as 'SALES_ORDER' | 'DELIVERY_ORDER',
|
||||
'',
|
||||
notes || `APPROVED marketing ${idsToProcess.join(', ')}`
|
||||
)
|
||||
: await SalesOrderApi.bulkApprovals(idsToProcess, approveAction, notes);
|
||||
|
||||
if (isResponseSuccess(approveMarketingRes)) {
|
||||
confirmationModal.closeModal();
|
||||
toast.success(approveMarketingRes?.message as string);
|
||||
setRowSelection({});
|
||||
}
|
||||
if (isResponseError(approveMarketingRes)) {
|
||||
confirmationModal.closeModal();
|
||||
toast.error(approveMarketingRes?.message as string);
|
||||
}
|
||||
|
||||
refreshMarketing();
|
||||
};
|
||||
|
||||
const bulkDeliveryDateChangeHandler: ChangeEventHandler<HTMLInputElement> = (
|
||||
e
|
||||
) => {
|
||||
setBulkDeliveryDate(e.target.value);
|
||||
};
|
||||
|
||||
const bulkDeliveryNotesChangeHandler: ChangeEventHandler<
|
||||
HTMLTextAreaElement
|
||||
> = (e) => {
|
||||
setBulkDeliveryNotes(e.target.value);
|
||||
};
|
||||
|
||||
const submitBulkDeliveryApprovalHandler = async (
|
||||
selectedIds: number[],
|
||||
deliveryDate: string,
|
||||
notes: string
|
||||
) => {
|
||||
if (selectedIds.length === 0) {
|
||||
toast.error('Tidak ada data yang valid untuk diproses.');
|
||||
return;
|
||||
}
|
||||
|
||||
if (!deliveryDate) {
|
||||
toast.error('Tanggal pengiriman wajib diisi.');
|
||||
return;
|
||||
}
|
||||
|
||||
setIsSubmittingBulkDelivery(true);
|
||||
|
||||
try {
|
||||
const bulkDeliveryApprovalRes = await MarketingApi.bulkApprovals(
|
||||
selectedIds,
|
||||
'DELIVERY_ORDER',
|
||||
deliveryDate,
|
||||
notes || `APPROVED delivery marketing ${selectedIds.join(', ')}`
|
||||
);
|
||||
|
||||
if (isResponseError(bulkDeliveryApprovalRes)) {
|
||||
toast.error(bulkDeliveryApprovalRes?.message as string);
|
||||
return;
|
||||
}
|
||||
|
||||
if (!isResponseSuccess(bulkDeliveryApprovalRes)) {
|
||||
toast.error('Gagal memproses bulk approve delivery.');
|
||||
return;
|
||||
}
|
||||
|
||||
toast.success(bulkDeliveryApprovalRes?.message as string);
|
||||
bulkDeliveryModal.closeModal();
|
||||
setBulkDeliveryDate('');
|
||||
setBulkDeliveryNotes('');
|
||||
setRowSelection({});
|
||||
refreshMarketing();
|
||||
} finally {
|
||||
setIsSubmittingBulkDelivery(false);
|
||||
}
|
||||
};
|
||||
|
||||
const confirmationModalDeliveryClickHandler = async (notes: string) => {
|
||||
const res = await SalesOrderApi.delivery(selectedItem?.id as number, notes);
|
||||
deliveryModal.closeModal();
|
||||
@@ -316,10 +412,24 @@ const MarketingTable = () => {
|
||||
);
|
||||
};
|
||||
|
||||
const getRowCanSelect = (row: Row<Marketing>): boolean => {
|
||||
const approval = row.original.latest_approval;
|
||||
return approval?.step_number === 1 && approval?.action !== 'REJECTED';
|
||||
};
|
||||
const getRowCanSelect = useCallback(
|
||||
(row: Row<Marketing>): boolean => {
|
||||
const approval = row.original.latest_approval;
|
||||
const isSelectableStep =
|
||||
approval?.step_number === 1 || approval?.step_number === 2;
|
||||
|
||||
if (!isSelectableStep || approval?.action === 'REJECTED') {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (selectedApprovalStep === null) {
|
||||
return true;
|
||||
}
|
||||
|
||||
return approval?.step_number === selectedApprovalStep;
|
||||
},
|
||||
[selectedApprovalStep]
|
||||
);
|
||||
|
||||
const exportToExcelHandler = async () => {
|
||||
setIsLoadingExportingToExcel(true);
|
||||
@@ -336,7 +446,22 @@ const MarketingTable = () => {
|
||||
size: 1,
|
||||
header: ({ table }) => {
|
||||
const allRows = table.getRowModel().rows;
|
||||
const selectableRows = allRows.filter(getRowCanSelect);
|
||||
const stepForBulkSelection =
|
||||
selectedApprovalStep ??
|
||||
allRows.find(getRowCanSelect)?.original.latest_approval.step_number;
|
||||
const selectableRows = allRows.filter((row) => {
|
||||
if (!getRowCanSelect(row)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!stepForBulkSelection) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return (
|
||||
row.original.latest_approval.step_number === stepForBulkSelection
|
||||
);
|
||||
});
|
||||
|
||||
const allSelected =
|
||||
selectableRows.length > 0 &&
|
||||
@@ -504,7 +629,13 @@ const MarketingTable = () => {
|
||||
},
|
||||
},
|
||||
];
|
||||
}, []);
|
||||
}, [
|
||||
deleteModal,
|
||||
deliveryModal,
|
||||
getRowCanSelect,
|
||||
productsClickHandler,
|
||||
selectedApprovalStep,
|
||||
]);
|
||||
|
||||
return (
|
||||
<>
|
||||
@@ -677,7 +808,7 @@ const MarketingTable = () => {
|
||||
<ConfirmationModalWithNotes
|
||||
ref={confirmationModal.ref}
|
||||
type={approveAction === 'APPROVED' ? 'success' : 'error'}
|
||||
text={`Apakah anda yakin ingin ${approveAction == 'APPROVED' ? 'approve' : 'reject'} data penjualan (${idsToProcess.length} data)?`}
|
||||
text={`Apakah anda yakin ingin ${approveAction == 'APPROVED' ? 'approve' : 'reject'} data penjualan tahap ${selectedApprovalStep ?? '-'} (${idsToProcess.length} data)?`}
|
||||
secondaryButton={{
|
||||
text: 'Tidak',
|
||||
onClick: confirmationModal.closeModal,
|
||||
@@ -716,6 +847,88 @@ const MarketingTable = () => {
|
||||
}}
|
||||
/>
|
||||
|
||||
<Modal
|
||||
ref={bulkDeliveryModal.ref}
|
||||
className={{
|
||||
modalBox: 'max-w-lg rounded-lg p-0',
|
||||
}}
|
||||
closeOnBackdrop
|
||||
>
|
||||
<div className='flex flex-col'>
|
||||
<div className='flex items-center justify-between border-b border-base-content/10 p-4'>
|
||||
<h4 className='text-sm font-semibold text-base-content'>
|
||||
Bulk Approve Delivery
|
||||
</h4>
|
||||
<Button
|
||||
variant='ghost'
|
||||
color='none'
|
||||
onClick={() => {
|
||||
bulkDeliveryModal.closeModal();
|
||||
setBulkDeliveryDate('');
|
||||
setBulkDeliveryNotes('');
|
||||
}}
|
||||
className='p-1'
|
||||
>
|
||||
<Icon icon='mdi:close' width={20} height={20} />
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
<div className='flex flex-col gap-4 p-4'>
|
||||
<p className='text-sm text-base-content/70'>
|
||||
Pilih tanggal pengiriman untuk approve {idsToProcess.length} data
|
||||
penjualan tahap 2.
|
||||
</p>
|
||||
|
||||
<DateInput
|
||||
name='bulk_delivery_date'
|
||||
label='Tanggal Pengiriman'
|
||||
value={bulkDeliveryDate}
|
||||
onChange={bulkDeliveryDateChangeHandler}
|
||||
isNestedModal
|
||||
required
|
||||
/>
|
||||
|
||||
<TextArea
|
||||
name='bulk_delivery_notes'
|
||||
label='Catatan'
|
||||
placeholder='Masukkan catatan approval...'
|
||||
value={bulkDeliveryNotes}
|
||||
onChange={bulkDeliveryNotesChangeHandler}
|
||||
rows={4}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className='flex justify-end gap-3 border-t border-base-content/10 p-4'>
|
||||
<Button
|
||||
variant='outline'
|
||||
color='none'
|
||||
onClick={() => {
|
||||
bulkDeliveryModal.closeModal();
|
||||
setBulkDeliveryDate('');
|
||||
setBulkDeliveryNotes('');
|
||||
}}
|
||||
className='px-3 py-2.5'
|
||||
>
|
||||
Batal
|
||||
</Button>
|
||||
<Button
|
||||
color='success'
|
||||
isLoading={isSubmittingBulkDelivery}
|
||||
onClick={() =>
|
||||
submitBulkDeliveryApprovalHandler(
|
||||
idsToProcess,
|
||||
bulkDeliveryDate,
|
||||
bulkDeliveryNotes
|
||||
)
|
||||
}
|
||||
className='px-3 py-2.5'
|
||||
>
|
||||
Submit
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</Modal>
|
||||
|
||||
<Modal
|
||||
ref={productsModal.ref}
|
||||
className={{
|
||||
|
||||
@@ -23,7 +23,6 @@ import { Icon } from '@iconify/react';
|
||||
import { CellContext, ColumnDef, SortingState } from '@tanstack/react-table';
|
||||
import { useRouter, usePathname } from 'next/navigation';
|
||||
import { ChangeEventHandler, useEffect, useMemo, useState } from 'react';
|
||||
import { useUiStore } from '@/stores/ui/ui.store';
|
||||
import toast from 'react-hot-toast';
|
||||
import useSWR from 'swr';
|
||||
import { useFormik } from 'formik';
|
||||
@@ -148,7 +147,6 @@ const RowOptionsMenu = ({
|
||||
};
|
||||
|
||||
const ProjectFlockTable = ({ refresh }: { refresh?: () => void }) => {
|
||||
const { searchValue, setSearchValue, setTableState } = useUiStore();
|
||||
const pathname = usePathname();
|
||||
|
||||
const isSuccess = useProjectFlockStore((s) => s.isSuccess);
|
||||
@@ -185,7 +183,11 @@ const ProjectFlockTable = ({ refresh }: { refresh?: () => void }) => {
|
||||
category: 'category',
|
||||
period: 'period',
|
||||
},
|
||||
|
||||
persist: true,
|
||||
storeName: 'project-flock-table',
|
||||
});
|
||||
|
||||
const router = useRouter();
|
||||
|
||||
// ===== State =====
|
||||
@@ -425,18 +427,11 @@ const ProjectFlockTable = ({ refresh }: { refresh?: () => void }) => {
|
||||
setIsDeleteLoading(false);
|
||||
setRowSelection({});
|
||||
};
|
||||
useEffect(() => {
|
||||
updateFilter('search', searchValue);
|
||||
}, [searchValue, updateFilter]);
|
||||
|
||||
useEffect(() => {
|
||||
setTableState('project-flock-table', pathname);
|
||||
}, [pathname, setTableState]);
|
||||
|
||||
const searchChangeHandler: ChangeEventHandler<HTMLInputElement> = (e) => {
|
||||
setSearchValue(e.target.value);
|
||||
updateFilter('search', e.target.value);
|
||||
};
|
||||
|
||||
const confirmApprovalHandler = async (
|
||||
notes: string,
|
||||
approvalAction: 'APPROVED' | 'REJECTED'
|
||||
|
||||
@@ -261,7 +261,7 @@ const ProjectFlockForm = ({
|
||||
isLoadingOptions: isLoadingFlocks,
|
||||
options: optionsFlock,
|
||||
loadMore: loadMoreFlock,
|
||||
} = useSelect(FlockApi.basePath, 'id', 'name', '', {
|
||||
} = useSelect(FlockApi.basePath, 'id', 'name', 'search', {
|
||||
project_category: selectedCategory,
|
||||
location_id: selectedLocation,
|
||||
area_id: selectedArea,
|
||||
@@ -279,7 +279,7 @@ const ProjectFlockForm = ({
|
||||
isLoadingOptions: isLoadingLocations,
|
||||
setInputValue: setInputValueLocation,
|
||||
loadMore: loadMoreLocation,
|
||||
} = useSelect(LocationApi.basePath, 'id', 'name', '', {
|
||||
} = useSelect(LocationApi.basePath, 'id', 'name', 'search', {
|
||||
area_id:
|
||||
selectedArea != ''
|
||||
? selectedArea
|
||||
@@ -291,7 +291,7 @@ const ProjectFlockForm = ({
|
||||
isLoadingOptions: isLoadingProductionStandards,
|
||||
setInputValue: setInputValueProductionStandard,
|
||||
loadMore: loadMoreProductionStandard,
|
||||
} = useSelect(ProductionStandardApi.basePath, 'id', 'name', '', {
|
||||
} = useSelect(ProductionStandardApi.basePath, 'id', 'name', 'search', {
|
||||
project_category: selectedCategory,
|
||||
});
|
||||
|
||||
@@ -307,7 +307,7 @@ const ProjectFlockForm = ({
|
||||
} = useSWR(kandangUrl, KandangApi.getAllFetcher);
|
||||
|
||||
const { data: periodFlocks, mutate: refreshPeriodFlocks } = useSWR(
|
||||
`${selectedFlock?.toString()}/periods`,
|
||||
selectedFlock ? `${selectedFlock?.toString()}/periods` : undefined,
|
||||
() => ProjectFlockApi.getNextPeriod(parseInt(selectedLocation as string))
|
||||
);
|
||||
|
||||
@@ -793,6 +793,7 @@ const ProjectFlockForm = ({
|
||||
formik.values.kandang_ids?.includes(kandang.id)
|
||||
)?.period
|
||||
: undefined;
|
||||
|
||||
const inputPeriod =
|
||||
(initialValues?.period ?? selectedPeriod == 0) ? 1 : selectedPeriod;
|
||||
|
||||
|
||||
@@ -611,7 +611,7 @@ const RecordingForm = ({ type = 'add', initialValues }: RecordingFormProps) => {
|
||||
} = useSelect(ProductWarehouseApi.basePath, 'id', 'product.name', 'search', {
|
||||
flags: 'PAKAN,OVK',
|
||||
limit: '100',
|
||||
available_only: 'true',
|
||||
available_only: 'false',
|
||||
location_id: stockProductsLocationId,
|
||||
...(selectedKandangId ? { kandang_id: selectedKandangId.toString() } : {}),
|
||||
});
|
||||
|
||||
@@ -3,7 +3,6 @@ import { Uniformity } from '@/types/api/production/uniformity';
|
||||
|
||||
type UniformityFormSchemaType = {
|
||||
date: string;
|
||||
week: number;
|
||||
location?: {
|
||||
value: number;
|
||||
label: string;
|
||||
@@ -45,10 +44,6 @@ const FileSchema = Yup.mixed<File>()
|
||||
export const UniformityFormSchema: Yup.ObjectSchema<UniformityFormSchemaType> =
|
||||
Yup.object({
|
||||
date: Yup.string().required('Tanggal wajib diisi!'),
|
||||
week: Yup.number()
|
||||
.min(1, 'Minggu ke wajib diisi!')
|
||||
.required('Minggu ke wajib diisi!')
|
||||
.typeError('Minggu ke wajib diisi!'),
|
||||
location: Yup.object({
|
||||
value: Yup.number().min(1).required(),
|
||||
label: Yup.string().required(),
|
||||
@@ -81,7 +76,6 @@ export type UniformityFormValues = Yup.InferType<typeof UniformityFormSchema>;
|
||||
|
||||
export type UniformityFormData = {
|
||||
date: string;
|
||||
week: number;
|
||||
project_flock_kandang_id: number;
|
||||
document: File | null;
|
||||
document_name: string;
|
||||
@@ -91,8 +85,7 @@ export const getUniformityFormInitialValues = (
|
||||
initialValues?: Partial<Uniformity>
|
||||
): UniformityFormValues => {
|
||||
return {
|
||||
date: initialValues?.week ? '' : '',
|
||||
week: initialValues?.week ?? 0,
|
||||
date: '',
|
||||
location: null,
|
||||
location_id: 0,
|
||||
project_flock: null,
|
||||
|
||||
@@ -27,7 +27,6 @@ import { LocationApi } from '@/services/api/master-data';
|
||||
import {
|
||||
ProjectFlockApi,
|
||||
ProjectFlockKandangApi,
|
||||
RecordingApi,
|
||||
} from '@/services/api/production';
|
||||
import { UniformityApi } from '@/services/api/uniformity';
|
||||
import { isResponseError, isResponseSuccess } from '@/lib/api-helper';
|
||||
@@ -40,7 +39,6 @@ import {
|
||||
ProjectFlockKandangLookup,
|
||||
ProjectFlock,
|
||||
} from '@/types/api/production/project-flock';
|
||||
import { Recording } from '@/types/api/production/recording';
|
||||
import { Kandang } from '@/types/api/master-data/kandang';
|
||||
import UniformityPreviewForm from '@/components/pages/production/uniformity/form/UniformityPreviewForm';
|
||||
import UniformityResultForm from '@/components/pages/production/uniformity/form/UniformityResultForm';
|
||||
@@ -204,23 +202,6 @@ const UniformityForm = ({
|
||||
? projectFlockKandangLookupData.data
|
||||
: undefined;
|
||||
|
||||
// ===== RECORDINGS DATA (FOR WEEK CALCULATION) =====
|
||||
const recordingsUrl = useMemo(() => {
|
||||
if (!projectFlockKandangLookup?.project_flock_kandang_id) return null;
|
||||
const params = new URLSearchParams({
|
||||
page: '1',
|
||||
limit: '100',
|
||||
project_flock_kandang_id:
|
||||
projectFlockKandangLookup.project_flock_kandang_id.toString(),
|
||||
});
|
||||
return `${RecordingApi.basePath}?${params.toString()}`;
|
||||
}, [projectFlockKandangLookup?.project_flock_kandang_id]);
|
||||
|
||||
const { data: recordingsData } = useSWR(
|
||||
recordingsUrl,
|
||||
recordingsUrl ? RecordingApi.getAllFetcher : null
|
||||
);
|
||||
|
||||
// ===== FORM CONFIGURATION =====
|
||||
const formikInitialValues = useMemo<UniformityFormValues>(
|
||||
() => getUniformityFormInitialValues(initialValues),
|
||||
@@ -246,7 +227,6 @@ const UniformityForm = ({
|
||||
|
||||
setUniformityFormData({
|
||||
date: values.date,
|
||||
week: values.week,
|
||||
project_flock_kandang_id: projectFlockKandangId,
|
||||
document: values.document as File,
|
||||
document_name: (values.document as File).name,
|
||||
@@ -475,59 +455,6 @@ const UniformityForm = ({
|
||||
generateUniformityTemplate(population, projectFlockKandangLookup);
|
||||
}, [projectFlockKandangLookup]);
|
||||
|
||||
// ===== SIDE EFFECTS =====
|
||||
useEffect(() => {
|
||||
if (
|
||||
projectFlockKandangLookup?.chick_in_date &&
|
||||
projectFlockKandangLookup?.project_flock_kandang_id
|
||||
) {
|
||||
const chickInDate = new Date(projectFlockKandangLookup.chick_in_date);
|
||||
chickInDate.setHours(0, 0, 0, 0);
|
||||
|
||||
let initialWeek = 18;
|
||||
|
||||
if (
|
||||
isResponseSuccess(recordingsData) &&
|
||||
recordingsData.data &&
|
||||
recordingsData.data.length > 0
|
||||
) {
|
||||
const sortedRecordings = [...recordingsData.data].sort(
|
||||
(a: Recording, b: Recording) =>
|
||||
new Date(a.record_datetime).getTime() -
|
||||
new Date(b.record_datetime).getTime()
|
||||
);
|
||||
|
||||
const earliestRecording = sortedRecordings[0];
|
||||
if (earliestRecording?.project_flock?.production_standart?.week) {
|
||||
initialWeek =
|
||||
earliestRecording.project_flock.production_standart.week;
|
||||
}
|
||||
}
|
||||
|
||||
if (formik.values.date) {
|
||||
const selectedDate = new Date(formik.values.date);
|
||||
selectedDate.setHours(0, 0, 0, 0);
|
||||
|
||||
const daysDiff = Math.floor(
|
||||
(selectedDate.getTime() - chickInDate.getTime()) /
|
||||
(1000 * 60 * 60 * 24)
|
||||
);
|
||||
|
||||
const weeksDiff = Math.floor(daysDiff / 7);
|
||||
|
||||
setFieldValue('week', initialWeek + weeksDiff);
|
||||
} else {
|
||||
setFieldValue('week', initialWeek);
|
||||
}
|
||||
}
|
||||
}, [
|
||||
projectFlockKandangLookup?.chick_in_date,
|
||||
projectFlockKandangLookup?.project_flock_kandang_id,
|
||||
recordingsData,
|
||||
formik.values.date,
|
||||
setFieldValue,
|
||||
]);
|
||||
|
||||
useEffect(() => {
|
||||
const unsub = subscribeValidate(() => {
|
||||
setIsValid(true);
|
||||
|
||||
@@ -63,7 +63,6 @@ const UniformityResultForm = () => {
|
||||
try {
|
||||
const payload = {
|
||||
date: uniformityFormData.date,
|
||||
week: uniformityFormData.week,
|
||||
project_flock_kandang_id: uniformityFormData.project_flock_kandang_id,
|
||||
document: uniformityFormData.document,
|
||||
};
|
||||
|
||||
@@ -294,7 +294,6 @@ const PurchaseOrderAcceptApprovalForm = ({
|
||||
item.expedition_vendor_id || item.expedition_vendor?.id || null;
|
||||
|
||||
return {
|
||||
purchase_item: null,
|
||||
purchase_item_id: item.id,
|
||||
received_date: item.received_date
|
||||
? new Date(item.received_date).toISOString().split('T')[0]
|
||||
@@ -573,7 +572,7 @@ const PurchaseOrderAcceptApprovalForm = ({
|
||||
<td>
|
||||
<SelectInput
|
||||
isClearable={true}
|
||||
value={formItem?.expedition_vendor}
|
||||
value={formItem?.expedition_vendor ?? null}
|
||||
key={`expedition-vendor-${idx}`}
|
||||
onChange={(val) =>
|
||||
expeditionVendorChangeHandler(idx, val)
|
||||
|
||||
@@ -31,10 +31,6 @@ type PurchaseRequestAcceptApprovalFormSchemaType = {
|
||||
action: 'APPROVED' | 'REJECTED';
|
||||
notes: string | null;
|
||||
items: {
|
||||
purchase_item?: {
|
||||
value: number;
|
||||
label: string;
|
||||
} | null;
|
||||
purchase_item_id: number;
|
||||
received_date: string;
|
||||
travel_number: string;
|
||||
@@ -68,10 +64,6 @@ export type PurchaseStaffApprovalItemSchema = {
|
||||
};
|
||||
|
||||
export type PurchaseAcceptApprovalItemSchema = {
|
||||
purchase_item?: {
|
||||
value: number;
|
||||
label: string;
|
||||
} | null;
|
||||
purchase_item_id: number;
|
||||
received_date: string;
|
||||
travel_number: string;
|
||||
@@ -160,12 +152,6 @@ const PurchaseManagerApprovalObjectSchema: Yup.ObjectSchema<PurchaseRequestManag
|
||||
|
||||
const PurchaseAcceptApprovalItemObjectSchema: Yup.ObjectSchema<PurchaseAcceptApprovalItemSchema> =
|
||||
Yup.object({
|
||||
purchase_item: Yup.object({
|
||||
value: Yup.number().min(1).required(),
|
||||
label: Yup.string().required(),
|
||||
})
|
||||
.nullable()
|
||||
.optional(),
|
||||
purchase_item_id: Yup.number()
|
||||
.min(1, 'Purchase item is required!')
|
||||
.required('Purchase item is required!')
|
||||
@@ -185,9 +171,8 @@ const PurchaseAcceptApprovalItemObjectSchema: Yup.ObjectSchema<PurchaseAcceptApp
|
||||
.typeError('No. Surat jalan wajib diisi!'),
|
||||
vehicle_number: Yup.string()
|
||||
.nullable()
|
||||
.when('expedition_vendor', {
|
||||
is: (expeditionVendor?: { value?: number; label?: string } | null) =>
|
||||
Boolean(expeditionVendor?.value),
|
||||
.when('expedition_vendor_id', {
|
||||
is: (expeditionVendorId?: number | null) => Boolean(expeditionVendorId),
|
||||
then: (schema) => schema.required('Nomor kendaraan wajib diisi!'),
|
||||
otherwise: (schema) => schema.optional(),
|
||||
})
|
||||
@@ -196,6 +181,7 @@ const PurchaseAcceptApprovalItemObjectSchema: Yup.ObjectSchema<PurchaseAcceptApp
|
||||
value: Yup.number().min(1).required(),
|
||||
label: Yup.string().required(),
|
||||
})
|
||||
.default(undefined)
|
||||
.nullable()
|
||||
.optional(),
|
||||
expedition_vendor_id: Yup.number()
|
||||
@@ -218,9 +204,8 @@ const PurchaseAcceptApprovalItemObjectSchema: Yup.ObjectSchema<PurchaseAcceptApp
|
||||
.typeError('Jumlah diterima harus berupa angka!'),
|
||||
transport_per_item: Yup.mixed<string | number>()
|
||||
.nullable()
|
||||
.when('expedition_vendor', {
|
||||
is: (expeditionVendor?: { value?: number; label?: string } | null) =>
|
||||
Boolean(expeditionVendor?.value),
|
||||
.when('expedition_vendor_id', {
|
||||
is: (expeditionVendorId?: number | null) => Boolean(expeditionVendorId),
|
||||
then: (schema) =>
|
||||
schema.required('Biaya transport per item wajib diisi!'),
|
||||
otherwise: (schema) => schema.optional(),
|
||||
|
||||
@@ -4,7 +4,8 @@ import { useState } from 'react';
|
||||
import Tabs from '@/components/Tabs';
|
||||
|
||||
import { useTabActionsStore } from '@/stores/tab-actions/tab-actions.store';
|
||||
import ReportExpenseTab from './tab/ReportExpenseTab';
|
||||
import ReportExpenseTab from '@/components/pages/report/expense/tab/ReportExpenseTab';
|
||||
import ReportDepreciationTab from '@/components/pages/report/expense/tab/ReportDepreciationTab';
|
||||
|
||||
const ReportExpenseTabs = () => {
|
||||
const [activeTabId, setActiveTabId] = useState<string>('1');
|
||||
@@ -16,6 +17,11 @@ const ReportExpenseTabs = () => {
|
||||
label: 'Laporan Biaya Operasional',
|
||||
content: <ReportExpenseTab tabId={'1'} />,
|
||||
},
|
||||
{
|
||||
id: '2',
|
||||
label: 'Laporan Depresiasi',
|
||||
content: <ReportDepreciationTab tabId={'2'} />,
|
||||
},
|
||||
];
|
||||
|
||||
return (
|
||||
|
||||
@@ -1,27 +1,26 @@
|
||||
import React from 'react';
|
||||
import DataStateSkeleton from '@/components/helper/skeleton/DataStateSkeleton';
|
||||
import Table from '@/components/Table';
|
||||
import { ReportExpense } from '@/types/api/report/report-expense';
|
||||
import { ColumnDef } from '@tanstack/react-table';
|
||||
|
||||
type ReportExpenseColumn =
|
||||
| ColumnDef<ReportExpense>
|
||||
type ReportSkeletonColumn<TData extends object> =
|
||||
| ColumnDef<TData>
|
||||
| {
|
||||
header: string;
|
||||
columns: Array<{
|
||||
header: string;
|
||||
accessorKey?: string;
|
||||
cell?: (props: { row: { original: ReportExpense } }) => React.ReactNode;
|
||||
cell?: (props: { row: { original: TData } }) => React.ReactNode;
|
||||
}>;
|
||||
};
|
||||
|
||||
const ReportExpenseSkeleton = ({
|
||||
const ReportExpenseSkeleton = <TData extends object>({
|
||||
columns,
|
||||
icon,
|
||||
title,
|
||||
subtitle,
|
||||
}: {
|
||||
columns: ReportExpenseColumn[];
|
||||
columns: ReportSkeletonColumn<TData>[];
|
||||
icon: React.ReactNode;
|
||||
title: string;
|
||||
subtitle: string;
|
||||
|
||||
@@ -0,0 +1,270 @@
|
||||
'use client';
|
||||
|
||||
import { RefObject, useMemo, useState } from 'react';
|
||||
import { useFormik } from 'formik';
|
||||
import * as yup from 'yup';
|
||||
|
||||
import { Icon } from '@iconify/react';
|
||||
import Modal from '@/components/Modal';
|
||||
import Button from '@/components/Button';
|
||||
import DateInput from '@/components/input/DateInput';
|
||||
import SelectInput, {
|
||||
OptionType,
|
||||
useSelect,
|
||||
} from '@/components/input/SelectInput';
|
||||
|
||||
import { AreaApi, LocationApi } from '@/services/api/master-data';
|
||||
import { ProjectFlockApi } from '@/services/api/production';
|
||||
import { Area } from '@/types/api/master-data/area';
|
||||
import { Location } from '@/types/api/master-data/location';
|
||||
import { ProjectFlock } from '@/types/api/production/project-flock';
|
||||
|
||||
export type ReportDepreciationFilterValues = {
|
||||
area_id: string | null;
|
||||
location_id: string | null;
|
||||
project_flock_id: string | null;
|
||||
period: string | null;
|
||||
};
|
||||
|
||||
export const ReportDepreciationFilterSchema = yup.object({
|
||||
area_id: yup.string().nullable(),
|
||||
location_id: yup.string().nullable(),
|
||||
project_flock_id: yup.string().nullable(),
|
||||
period: yup.string().nullable().required('Periode wajib dipilih'),
|
||||
}) as yup.ObjectSchema<ReportDepreciationFilterValues>;
|
||||
|
||||
interface ReportDepreciationFilterModalProps {
|
||||
ref: RefObject<HTMLDialogElement | null>;
|
||||
initialValues?: ReportDepreciationFilterValues;
|
||||
onSubmit?: (values: Partial<ReportDepreciationFilterValues>) => void;
|
||||
onReset?: () => void;
|
||||
}
|
||||
|
||||
const defaultInitialValues: ReportDepreciationFilterValues = {
|
||||
area_id: null,
|
||||
location_id: null,
|
||||
project_flock_id: null,
|
||||
period: null,
|
||||
};
|
||||
|
||||
const ReportDepreciationFilterModal = ({
|
||||
ref,
|
||||
initialValues,
|
||||
onSubmit,
|
||||
onReset,
|
||||
}: ReportDepreciationFilterModalProps) => {
|
||||
const [selectedAreaId, setSelectedAreaId] = useState<string | undefined>(
|
||||
initialValues?.area_id || undefined
|
||||
);
|
||||
const [selectedLocationId, setSelectedLocationId] = useState<
|
||||
string | undefined
|
||||
>(initialValues?.location_id || undefined);
|
||||
|
||||
const closeModalHandler = () => {
|
||||
ref.current?.close();
|
||||
};
|
||||
|
||||
const {
|
||||
setInputValue: setAreaInputValue,
|
||||
options: areaOptions,
|
||||
isLoadingOptions: isLoadingAreaOptions,
|
||||
loadMore: loadMoreAreas,
|
||||
} = useSelect<Area>(AreaApi.basePath, 'id', 'name', 'search');
|
||||
|
||||
const {
|
||||
setInputValue: setLocationInputValue,
|
||||
options: locationOptions,
|
||||
isLoadingOptions: isLoadingLocationOptions,
|
||||
loadMore: loadMoreLocations,
|
||||
} = useSelect<Location>(LocationApi.basePath, 'id', 'name', 'search', {
|
||||
area_id: selectedAreaId || '',
|
||||
});
|
||||
|
||||
const {
|
||||
setInputValue: setProjectFlockInputValue,
|
||||
options: projectFlockOptions,
|
||||
isLoadingOptions: isLoadingProjectFlockOptions,
|
||||
loadMore: loadMoreProjectFlocks,
|
||||
} = useSelect<ProjectFlock>(
|
||||
ProjectFlockApi.basePath,
|
||||
'id',
|
||||
'flock_name',
|
||||
'search',
|
||||
{
|
||||
location_id: selectedLocationId || '',
|
||||
}
|
||||
);
|
||||
|
||||
const formik = useFormik<ReportDepreciationFilterValues>({
|
||||
initialValues: initialValues || defaultInitialValues,
|
||||
validationSchema: ReportDepreciationFilterSchema,
|
||||
onSubmit: async (values) => {
|
||||
onSubmit?.(values);
|
||||
closeModalHandler();
|
||||
},
|
||||
onReset: (_) => {
|
||||
onReset?.();
|
||||
closeModalHandler();
|
||||
},
|
||||
});
|
||||
|
||||
const areaValue = useMemo(() => {
|
||||
if (!formik.values.area_id) return null;
|
||||
return (
|
||||
areaOptions.find((opt) => String(opt.value) === formik.values.area_id) ||
|
||||
null
|
||||
);
|
||||
}, [formik.values.area_id, areaOptions]);
|
||||
|
||||
const locationValue = useMemo(() => {
|
||||
if (!formik.values.location_id) return null;
|
||||
return (
|
||||
locationOptions.find(
|
||||
(opt) => String(opt.value) === formik.values.location_id
|
||||
) || null
|
||||
);
|
||||
}, [formik.values.location_id, locationOptions]);
|
||||
|
||||
const projectFlockValue = useMemo(() => {
|
||||
if (!formik.values.project_flock_id) return null;
|
||||
return (
|
||||
projectFlockOptions.find(
|
||||
(opt) => String(opt.value) === formik.values.project_flock_id
|
||||
) || null
|
||||
);
|
||||
}, [formik.values.project_flock_id, projectFlockOptions]);
|
||||
|
||||
const areaChangeHandler = (val: OptionType | OptionType[] | null) => {
|
||||
const areaId = val && !Array.isArray(val) ? String(val.value) : null;
|
||||
|
||||
setSelectedAreaId(areaId || undefined);
|
||||
formik.setFieldValue('area_id', areaId);
|
||||
formik.setFieldValue('location_id', null);
|
||||
formik.setFieldValue('project_flock_id', null);
|
||||
setSelectedLocationId(undefined);
|
||||
};
|
||||
|
||||
const locationChangeHandler = (val: OptionType | OptionType[] | null) => {
|
||||
const locationId = val && !Array.isArray(val) ? String(val.value) : null;
|
||||
|
||||
setSelectedLocationId(locationId || undefined);
|
||||
formik.setFieldValue('location_id', locationId);
|
||||
formik.setFieldValue('project_flock_id', null);
|
||||
};
|
||||
|
||||
const projectFlockChangeHandler = (val: OptionType | OptionType[] | null) => {
|
||||
const projectFlockId =
|
||||
val && !Array.isArray(val) ? String(val.value) : null;
|
||||
|
||||
formik.setFieldValue('project_flock_id', projectFlockId);
|
||||
};
|
||||
|
||||
return (
|
||||
<Modal
|
||||
ref={ref}
|
||||
className={{
|
||||
modalBox: 'p-0 rounded-xl xl:max-w-4/12 max-w-sm',
|
||||
}}
|
||||
>
|
||||
<form
|
||||
onSubmit={formik.handleSubmit}
|
||||
onReset={formik.handleReset}
|
||||
className='w-full flex flex-col'
|
||||
>
|
||||
<div className='p-4 flex items-center justify-between gap-2 border-b border-base-content/10'>
|
||||
<div className='flex items-center gap-2 text-primary'>
|
||||
<Icon icon='heroicons:funnel' width={20} height={20} />
|
||||
<h3 className='text-sm font-medium'>Filter Data</h3>
|
||||
</div>
|
||||
|
||||
<Button
|
||||
type='button'
|
||||
variant='ghost'
|
||||
color='none'
|
||||
onClick={closeModalHandler}
|
||||
className='p-0 text-base-content/50 hover:text-base-content'
|
||||
>
|
||||
<Icon icon='heroicons:x-mark' width={20} height={20} />
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
<div className='p-4 flex flex-col gap-1.5'>
|
||||
<SelectInput
|
||||
label='Area'
|
||||
placeholder='Pilih Area'
|
||||
options={areaOptions}
|
||||
value={areaValue}
|
||||
onChange={areaChangeHandler}
|
||||
onInputChange={setAreaInputValue}
|
||||
onMenuScrollToBottom={loadMoreAreas}
|
||||
isLoading={isLoadingAreaOptions}
|
||||
isClearable
|
||||
isSearchable={true}
|
||||
className={{ wrapper: 'w-full' }}
|
||||
/>
|
||||
|
||||
<SelectInput
|
||||
label='Lokasi'
|
||||
placeholder='Pilih Lokasi'
|
||||
options={locationOptions}
|
||||
value={locationValue}
|
||||
onChange={locationChangeHandler}
|
||||
onInputChange={setLocationInputValue}
|
||||
onMenuScrollToBottom={loadMoreLocations}
|
||||
isLoading={isLoadingLocationOptions}
|
||||
isClearable
|
||||
isSearchable={true}
|
||||
className={{ wrapper: 'w-full' }}
|
||||
/>
|
||||
|
||||
<SelectInput
|
||||
label='Project Flock'
|
||||
placeholder='Pilih Project Flock'
|
||||
options={projectFlockOptions}
|
||||
value={projectFlockValue}
|
||||
onChange={projectFlockChangeHandler}
|
||||
onInputChange={setProjectFlockInputValue}
|
||||
onMenuScrollToBottom={loadMoreProjectFlocks}
|
||||
isLoading={isLoadingProjectFlockOptions}
|
||||
isClearable
|
||||
isSearchable={true}
|
||||
className={{ wrapper: 'w-full' }}
|
||||
/>
|
||||
|
||||
<DateInput
|
||||
label='Periode'
|
||||
name='period'
|
||||
placeholder='Pilih Periode'
|
||||
value={formik.values.period || ''}
|
||||
onChange={formik.handleChange}
|
||||
onBlur={formik.handleBlur}
|
||||
isError={formik.touched.period && !!formik.errors.period}
|
||||
errorMessage={formik.errors.period}
|
||||
required
|
||||
isNestedModal
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className='p-4 flex justify-between gap-4 border-t border-base-content/10 bg-gray-50'>
|
||||
<Button
|
||||
type='reset'
|
||||
variant='soft'
|
||||
className='rounded-lg text-base-content/65 bg-transparent border-none hover:bg-base-content/10 hover:text-base-content/65 transition-colors px-3 py-2'
|
||||
>
|
||||
Reset Filter
|
||||
</Button>
|
||||
|
||||
<Button
|
||||
type='submit'
|
||||
className='min-w-40 text-sm rounded-lg py-3 text-white font-semibold'
|
||||
disabled={!formik.isValid || formik.isSubmitting}
|
||||
>
|
||||
Apply Filter
|
||||
</Button>
|
||||
</div>
|
||||
</form>
|
||||
</Modal>
|
||||
);
|
||||
};
|
||||
|
||||
export default ReportDepreciationFilterModal;
|
||||
@@ -0,0 +1,255 @@
|
||||
'use client';
|
||||
|
||||
import React, { useEffect, useMemo } from 'react';
|
||||
import useSWR from 'swr';
|
||||
import { ColumnDef } from '@tanstack/react-table';
|
||||
|
||||
import { Icon } from '@iconify/react';
|
||||
import Card from '@/components/Card';
|
||||
import Pagination from '@/components/Pagination';
|
||||
import Table from '@/components/Table';
|
||||
import ButtonFilter from '@/components/helper/ButtonFilter';
|
||||
import ReportExpenseSkeleton from '@/components/pages/report/expense/skeleton/ReportExpenseSkeleton';
|
||||
import { useModal } from '@/components/Modal';
|
||||
import ReportDepreciationFilterModal from '@/components/pages/report/expense/tab/ReportDepreciationFilterModal';
|
||||
|
||||
import { useTabActionsStore } from '@/stores/tab-actions/tab-actions.store';
|
||||
import { ReportDepreciation } from '@/types/api/report/report-expense';
|
||||
import { DepreciationReportApi } from '@/services/api/report/expense-report';
|
||||
import { useTableFilter } from '@/services/hooks/useTableFilter';
|
||||
import { isResponseSuccess } from '@/lib/api-helper';
|
||||
import { formatCurrency, formatDate, formatNumber } from '@/lib/helper';
|
||||
|
||||
interface ReportDepreciationTabProps {
|
||||
tabId: string;
|
||||
}
|
||||
|
||||
const ReportDepreciationTab = ({ tabId }: ReportDepreciationTabProps) => {
|
||||
const {
|
||||
state: tableFilterState,
|
||||
updateFilter,
|
||||
setPage,
|
||||
setPageSize,
|
||||
toQueryString: getTableFilterQueryString,
|
||||
reset: resetFilter,
|
||||
} = useTableFilter({
|
||||
initial: {
|
||||
area_id: '',
|
||||
location_id: '',
|
||||
project_flock_id: '',
|
||||
period: formatDate(Date.now(), 'YYYY-MM-DD'),
|
||||
},
|
||||
paramMap: {
|
||||
pageSize: 'limit',
|
||||
area_id: 'area_id',
|
||||
location_id: 'location_id',
|
||||
project_flock_id: 'project_flock_id',
|
||||
period: 'period',
|
||||
},
|
||||
});
|
||||
|
||||
const { data: depreciationsResponse, isLoading: isLoadingDepreciations } =
|
||||
useSWR(
|
||||
`${DepreciationReportApi.basePath}${getTableFilterQueryString()}`,
|
||||
DepreciationReportApi.getAllFetcher
|
||||
);
|
||||
|
||||
const depreciations = isResponseSuccess(depreciationsResponse)
|
||||
? depreciationsResponse.data
|
||||
: [];
|
||||
|
||||
const filterModal = useModal();
|
||||
const { ref: filterModalRef } = filterModal;
|
||||
|
||||
const setTabActions = useTabActionsStore((state) => state.setTabActions);
|
||||
const clearTabActions = useTabActionsStore((state) => state.clearTabActions);
|
||||
|
||||
const depreciationKandangColumns: ColumnDef<
|
||||
ReportDepreciation['components']['kandang'][0]
|
||||
>[] = [
|
||||
{
|
||||
accessorKey: 'kandang_name',
|
||||
header: 'Kandang',
|
||||
},
|
||||
{
|
||||
accessorKey: 'house_type',
|
||||
header: 'Tipe Kandang',
|
||||
cell: ({ row }) => row.original.house_type.toUpperCase(),
|
||||
},
|
||||
{
|
||||
accessorKey: 'depreciation_percent',
|
||||
header: 'Persentase Depresiasi',
|
||||
cell: ({ row }) => row.original.depreciation_percent + '%',
|
||||
},
|
||||
{
|
||||
accessorKey: 'depreciation_value',
|
||||
header: 'Nilai Depresiasi',
|
||||
cell: ({ row }) => formatCurrency(row.original.depreciation_value),
|
||||
},
|
||||
{
|
||||
accessorKey: 'depreciation_source',
|
||||
header: 'Asal Depresiasi',
|
||||
cell: ({ row }) => row.original.depreciation_source.toUpperCase(),
|
||||
},
|
||||
{
|
||||
accessorKey: 'cutover_date',
|
||||
header: 'Tanggal Cutover',
|
||||
cell: ({ row }) => formatDate(row.original.cutover_date, 'DD MMM YYYY'),
|
||||
},
|
||||
{
|
||||
accessorKey: 'origin_date',
|
||||
header: 'Tanggal Origin',
|
||||
cell: ({ row }) => formatDate(row.original.origin_date, 'DD MMM YYYY'),
|
||||
},
|
||||
];
|
||||
|
||||
const tabActionsElement = useMemo(
|
||||
() => (
|
||||
<div className='flex flex-row gap-3'>
|
||||
<ButtonFilter
|
||||
values={tableFilterState}
|
||||
excludeFields={['page', 'pageSize']}
|
||||
onClick={() => filterModal.openModal()}
|
||||
variant='outline'
|
||||
className='px-3 py-2.5'
|
||||
/>
|
||||
</div>
|
||||
),
|
||||
[tableFilterState]
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
setTabActions(tabId, tabActionsElement);
|
||||
}, [setTabActions, tabActionsElement, tabId]);
|
||||
|
||||
useEffect(() => {
|
||||
return () => {
|
||||
clearTabActions(tabId);
|
||||
};
|
||||
}, [clearTabActions, tabId]);
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className='w-full p-0 sm:p-3 flex flex-col gap-3'>
|
||||
{isLoadingDepreciations && (
|
||||
<div className='w-full flex flex-row justify-center items-center p-4'>
|
||||
<span className='loading loading-spinner loading-xl' />
|
||||
</div>
|
||||
)}
|
||||
|
||||
{!isLoadingDepreciations && depreciations.length === 0 && (
|
||||
<ReportExpenseSkeleton
|
||||
columns={depreciationKandangColumns}
|
||||
icon={
|
||||
<Icon
|
||||
icon='heroicons:chart-bar'
|
||||
className='text-white'
|
||||
width={20}
|
||||
height={20}
|
||||
/>
|
||||
}
|
||||
title='Data Not Yet Available'
|
||||
subtitle='Please change your filters to get the data.'
|
||||
/>
|
||||
)}
|
||||
|
||||
{!isLoadingDepreciations && depreciations.length > 0 && (
|
||||
<>
|
||||
{depreciations.map((depreciationItem, idx) => (
|
||||
<Card
|
||||
key={idx}
|
||||
title={depreciationItem.farm_name}
|
||||
subtitle={`Period: ${formatDate(depreciationItem.period, 'DD MMM YYYY')} | Depresiasi Efektif: ${formatNumber(depreciationItem.depreciation_percent_effective, 'en-US', 0, 10)}% | Nilai Depresiasi: ${formatCurrency(depreciationItem.depreciation_value)} | Total Pullet Cost: ${formatCurrency(depreciationItem.pullet_cost_day_n_total, 'IDR', 'id-ID', 0, 10)}`}
|
||||
className={{
|
||||
wrapper: 'w-full rounded-lg border-none',
|
||||
body: 'p-0',
|
||||
title:
|
||||
'px-2 py-1.5 font-normal text-sm bg-primary text-white',
|
||||
subtitle:
|
||||
'px-2 pb-1.5 bg-primary text-white text-xs font-normal',
|
||||
collapsible: 'rounded-lg',
|
||||
}}
|
||||
variant='bordered'
|
||||
collapsible={true}
|
||||
>
|
||||
<Table
|
||||
data={depreciationItem.components.kandang}
|
||||
columns={depreciationKandangColumns}
|
||||
pageSize={tableFilterState.pageSize}
|
||||
page={
|
||||
isResponseSuccess(depreciationsResponse)
|
||||
? depreciationsResponse?.meta?.page
|
||||
: 0
|
||||
}
|
||||
totalItems={
|
||||
isResponseSuccess(depreciationsResponse)
|
||||
? depreciationsResponse?.meta?.total_results
|
||||
: 0
|
||||
}
|
||||
onPageChange={setPage}
|
||||
onPageSizeChange={setPageSize}
|
||||
isLoading={isLoadingDepreciations}
|
||||
className={{
|
||||
containerClassName: 'w-full mb-0!',
|
||||
tableWrapperClassName:
|
||||
'overflow-x-auto rounded-tr-none rounded-tl-none',
|
||||
tableClassName: 'w-full table-auto text-sm',
|
||||
headerRowClassName: 'border-b border-b-gray-200 bg-gray-50',
|
||||
headerColumnClassName:
|
||||
'px-4 py-3 text-xs font-semibold text-gray-700 text-left border border-gray-200',
|
||||
bodyRowClassName:
|
||||
'hover:bg-gray-50 transition-colors border-b border-l border-r border-b-gray-200 border-l-gray-200 border-r-gray-200',
|
||||
bodyColumnClassName:
|
||||
'px-4 py-3 text-xs text-gray-900 whitespace-nowrap',
|
||||
tableFooterClassName:
|
||||
'bg-gray-100 font-semibold border border-gray-200',
|
||||
footerRowClassName: 'border-t-2 border-gray-300',
|
||||
footerColumnClassName:
|
||||
'px-4 py-3 text-xs text-gray-900 whitespace-nowrap',
|
||||
paginationClassName: 'hidden',
|
||||
}}
|
||||
/>
|
||||
</Card>
|
||||
))}
|
||||
|
||||
<Pagination
|
||||
totalItems={
|
||||
isResponseSuccess(depreciationsResponse)
|
||||
? (depreciationsResponse?.meta?.total_results ?? 0)
|
||||
: 0
|
||||
}
|
||||
itemsPerPage={tableFilterState.pageSize}
|
||||
currentPage={
|
||||
isResponseSuccess(depreciationsResponse)
|
||||
? (depreciationsResponse?.meta?.page ?? 0)
|
||||
: 0
|
||||
}
|
||||
onPrevPage={() => setPage(tableFilterState.page - 1)}
|
||||
onNextPage={() => setPage(tableFilterState.page + 1)}
|
||||
onPageChange={setPage}
|
||||
rowOptions={[10, 20, 50, 100]}
|
||||
onRowChange={setPageSize}
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<ReportDepreciationFilterModal
|
||||
ref={filterModalRef}
|
||||
initialValues={tableFilterState}
|
||||
onReset={resetFilter}
|
||||
onSubmit={(values) => {
|
||||
updateFilter('area_id', values.area_id ?? '');
|
||||
updateFilter('location_id', values.location_id ?? '');
|
||||
updateFilter('project_flock_id', values.project_flock_id ?? '');
|
||||
updateFilter(
|
||||
'period',
|
||||
values.period ? formatDate(values.period, 'YYYY-MM-DD') : ''
|
||||
);
|
||||
}}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export default ReportDepreciationTab;
|
||||
@@ -23,7 +23,7 @@ import RealizationStatusBadge from '@/components/pages/expense/RealizationStatus
|
||||
import Table from '@/components/Table';
|
||||
import { formatCurrency, formatDate } from '@/lib/helper';
|
||||
import { ReportExpense } from '@/types/api/report/report-expense';
|
||||
import { ReportExpenseApi } from '@/services/api/report';
|
||||
import { ReportExpenseApi } from '@/services/api/report/expense-report';
|
||||
import { isResponseSuccess } from '@/lib/api-helper';
|
||||
import { useTabActionsStore } from '@/stores/tab-actions/tab-actions.store';
|
||||
import Modal, { useModal } from '@/components/Modal';
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
import * as yup from 'yup';
|
||||
|
||||
export type DailyMarketingReportFilterType = {
|
||||
page?: number;
|
||||
pageSize?: number;
|
||||
search: string | null;
|
||||
area_id: string | null;
|
||||
location_id: string | null;
|
||||
@@ -14,6 +16,8 @@ export type DailyMarketingReportFilterType = {
|
||||
};
|
||||
|
||||
export const DailyMarketingReportFilterSchema = yup.object({
|
||||
page: yup.number().nullable(),
|
||||
pageSize: yup.number().nullable(),
|
||||
search: yup.string().nullable(),
|
||||
area_id: yup.string().nullable(),
|
||||
location_id: yup.string().nullable(),
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
import * as yup from 'yup';
|
||||
|
||||
export type HppPerKandangFilterType = {
|
||||
page?: number;
|
||||
pageSize?: number;
|
||||
area_id: string | null;
|
||||
location_id: string | null;
|
||||
kandang_id: string | null;
|
||||
@@ -12,6 +14,8 @@ export type HppPerKandangFilterType = {
|
||||
};
|
||||
|
||||
export const HppPerKandangFilterSchema = yup.object({
|
||||
page: yup.number().nullable(),
|
||||
pageSize: yup.number().nullable(),
|
||||
area_id: yup.string().nullable(),
|
||||
location_id: yup.string().nullable(),
|
||||
kandang_id: yup.string().nullable(),
|
||||
|
||||
@@ -53,6 +53,8 @@ interface DailyMarketingTabProps {
|
||||
}
|
||||
|
||||
interface FilterParams {
|
||||
page?: number;
|
||||
pageSize?: number;
|
||||
area_id?: string;
|
||||
location_id?: string;
|
||||
warehouse_id?: string;
|
||||
@@ -116,6 +118,8 @@ const DailyMarketingTab = ({ tabId }: DailyMarketingTabProps) => {
|
||||
// ===== FORMIK SETUP =====
|
||||
const formik = useFormik<DailyMarketingReportFilterType>({
|
||||
initialValues: {
|
||||
page: 1,
|
||||
pageSize: 10,
|
||||
search: null,
|
||||
area_id: null,
|
||||
location_id: null,
|
||||
@@ -130,6 +134,8 @@ const DailyMarketingTab = ({ tabId }: DailyMarketingTabProps) => {
|
||||
validationSchema: DailyMarketingReportFilterSchema,
|
||||
onSubmit: (values, { setSubmitting }) => {
|
||||
setFilterParams({
|
||||
page: values.page || undefined,
|
||||
pageSize: values.pageSize || undefined,
|
||||
area_id: values.area_id || undefined,
|
||||
location_id: values.location_id || undefined,
|
||||
warehouse_id: values.warehouse_id || undefined,
|
||||
@@ -222,6 +228,9 @@ const DailyMarketingTab = ({ tabId }: DailyMarketingTabProps) => {
|
||||
const params = new URLSearchParams();
|
||||
|
||||
if (searchValue) params.set('search', searchValue);
|
||||
if (filterParams.page) params.set('page', String(filterParams.page));
|
||||
if (filterParams.pageSize)
|
||||
params.set('limit', String(filterParams.pageSize));
|
||||
if (filterParams.area_id) params.set('area_id', filterParams.area_id);
|
||||
if (filterParams.location_id)
|
||||
params.set('location_id', filterParams.location_id);
|
||||
@@ -283,6 +292,7 @@ const DailyMarketingTab = ({ tabId }: DailyMarketingTabProps) => {
|
||||
if (filterParams.marketing_type)
|
||||
params.set('marketing_type', filterParams.marketing_type);
|
||||
if (filterParams.sort_by) params.set('sort_by', filterParams.sort_by);
|
||||
params.set('page', '1');
|
||||
params.set('limit', '9999999');
|
||||
|
||||
const queryString = `?${params.toString()}`;
|
||||
@@ -688,6 +698,27 @@ const DailyMarketingTab = ({ tabId }: DailyMarketingTabProps) => {
|
||||
<Table
|
||||
data={data}
|
||||
columns={getTableColumns()}
|
||||
pageSize={filterParams.pageSize}
|
||||
page={
|
||||
isResponseSuccess(dailyMarketings)
|
||||
? dailyMarketings?.meta?.page
|
||||
: 0
|
||||
}
|
||||
totalItems={
|
||||
isResponseSuccess(dailyMarketings)
|
||||
? dailyMarketings?.meta?.total_results
|
||||
: 0
|
||||
}
|
||||
onPageChange={(newPage) =>
|
||||
setFilterParams((prevVal) => ({ ...prevVal, page: newPage }))
|
||||
}
|
||||
onPageSizeChange={(newPageSize) =>
|
||||
setFilterParams((prevVal) => ({
|
||||
...prevVal,
|
||||
pageSize: newPageSize,
|
||||
}))
|
||||
}
|
||||
isLoading={isLoading}
|
||||
renderFooter={data.length > 0}
|
||||
className={{
|
||||
containerClassName: 'w-full mb-0!',
|
||||
|
||||
@@ -40,6 +40,8 @@ interface HppPerKandangTabProps {
|
||||
}
|
||||
|
||||
interface FilterParams {
|
||||
page?: number;
|
||||
pageSize?: number;
|
||||
area_id?: string;
|
||||
location_id?: string;
|
||||
kandang_id?: string;
|
||||
@@ -108,6 +110,8 @@ const HppPerKandangTab = ({ tabId }: HppPerKandangTabProps) => {
|
||||
// ===== FORMIK SETUP =====
|
||||
const formik = useFormik<HppPerKandangFilterType>({
|
||||
initialValues: {
|
||||
page: 1,
|
||||
pageSize: 10,
|
||||
area_id: null,
|
||||
location_id: null,
|
||||
kandang_id: null,
|
||||
@@ -120,6 +124,8 @@ const HppPerKandangTab = ({ tabId }: HppPerKandangTabProps) => {
|
||||
validationSchema: HppPerKandangFilterSchema,
|
||||
onSubmit: (values, { setSubmitting }) => {
|
||||
setFilterParams({
|
||||
page: values.page || undefined,
|
||||
pageSize: values.pageSize || undefined,
|
||||
area_id: values.area_id || undefined,
|
||||
location_id: values.location_id || undefined,
|
||||
kandang_id: values.kandang_id || undefined,
|
||||
@@ -257,6 +263,8 @@ const HppPerKandangTab = ({ tabId }: HppPerKandangTabProps) => {
|
||||
period: filterParams.period,
|
||||
sort_by: filterParams.sort_by,
|
||||
show_unrecorded: filterParams.show_unrecorded,
|
||||
page: filterParams.page,
|
||||
pageSize: filterParams.pageSize,
|
||||
};
|
||||
|
||||
return ['hpp-per-kandang-report', params];
|
||||
@@ -271,7 +279,9 @@ const HppPerKandangTab = ({ tabId }: HppPerKandangTabProps) => {
|
||||
params.weight_max,
|
||||
params.period,
|
||||
params.sort_by,
|
||||
params.show_unrecorded
|
||||
params.show_unrecorded,
|
||||
params.page,
|
||||
params.pageSize
|
||||
)
|
||||
);
|
||||
|
||||
@@ -321,7 +331,9 @@ const HppPerKandangTab = ({ tabId }: HppPerKandangTabProps) => {
|
||||
params.weight_max,
|
||||
params.period,
|
||||
params.sort_by,
|
||||
params.show_unrecorded
|
||||
params.show_unrecorded,
|
||||
params.page,
|
||||
params.limit
|
||||
);
|
||||
|
||||
return isResponseSuccess(response) ? response.data : null;
|
||||
@@ -466,6 +478,7 @@ const HppPerKandangTab = ({ tabId }: HppPerKandangTabProps) => {
|
||||
<div className='flex flex-row gap-3'>
|
||||
<ButtonFilter
|
||||
values={filterParams}
|
||||
excludeFields={['page', 'pageSize']}
|
||||
onClick={() => handleFilterModalOpenRef.current()}
|
||||
variant='outline'
|
||||
className='px-3 py-2.5'
|
||||
@@ -845,6 +858,25 @@ const HppPerKandangTab = ({ tabId }: HppPerKandangTabProps) => {
|
||||
<Table
|
||||
data={data}
|
||||
columns={getTableColumns()}
|
||||
pageSize={filterParams.pageSize}
|
||||
page={
|
||||
isResponseSuccess(hppPerKandang) ? hppPerKandang?.meta?.page : 0
|
||||
}
|
||||
totalItems={
|
||||
isResponseSuccess(hppPerKandang)
|
||||
? hppPerKandang?.meta?.total_results
|
||||
: 0
|
||||
}
|
||||
onPageChange={(newPage) =>
|
||||
setFilterParams((prevVal) => ({ ...prevVal, page: newPage }))
|
||||
}
|
||||
onPageSizeChange={(newPageSize) =>
|
||||
setFilterParams((prevVal) => ({
|
||||
...prevVal,
|
||||
pageSize: newPageSize,
|
||||
}))
|
||||
}
|
||||
isLoading={isLoading}
|
||||
renderFooter={data.length > 0}
|
||||
renderCustomRow={renderCustomRow}
|
||||
className={{
|
||||
|
||||
Reference in New Issue
Block a user