mirror of
https://gitlab.com/mbugroup/lti-web-client.git
synced 2026-05-25 07:45: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({
|
const qs = new URLSearchParams({
|
||||||
...(params ?? {}),
|
...(params ?? {}),
|
||||||
[searchKey]: inputValue ?? '',
|
[searchKey ? searchKey : 'search']: inputValue ?? '',
|
||||||
[pageKey]: String(pageIndex + 1),
|
[pageKey]: String(pageIndex + 1),
|
||||||
[limitKey]: String(limit),
|
[limitKey]: String(limit),
|
||||||
}).toString();
|
}).toString();
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
'use client';
|
'use client';
|
||||||
|
|
||||||
import { useMemo, useState } from 'react';
|
import { useMemo, useState } from 'react';
|
||||||
|
import { useSearchParams } from 'next/navigation';
|
||||||
|
|
||||||
import { Icon } from '@iconify/react';
|
import { Icon } from '@iconify/react';
|
||||||
import Button from '@/components/Button';
|
import Button from '@/components/Button';
|
||||||
@@ -9,6 +10,7 @@ import ExpenseRequestContent from '@/components/pages/expense/ExpenseRequestCont
|
|||||||
import ExpenseRealizationContent from '@/components/pages/expense/ExpenseRealizationContent';
|
import ExpenseRealizationContent from '@/components/pages/expense/ExpenseRealizationContent';
|
||||||
|
|
||||||
import { Expense } from '@/types/api/expense';
|
import { Expense } from '@/types/api/expense';
|
||||||
|
import { getExpenseListReturnTo } from '@/lib/expense-list-navigation';
|
||||||
|
|
||||||
interface ExpenseDetailProps {
|
interface ExpenseDetailProps {
|
||||||
initialValues?: Expense;
|
initialValues?: Expense;
|
||||||
@@ -16,6 +18,8 @@ interface ExpenseDetailProps {
|
|||||||
|
|
||||||
const ExpenseDetail: React.FC<ExpenseDetailProps> = ({ initialValues }) => {
|
const ExpenseDetail: React.FC<ExpenseDetailProps> = ({ initialValues }) => {
|
||||||
const [activeTab, setActiveTab] = useState<string>('request');
|
const [activeTab, setActiveTab] = useState<string>('request');
|
||||||
|
const searchParams = useSearchParams();
|
||||||
|
const returnTo = getExpenseListReturnTo(searchParams);
|
||||||
|
|
||||||
const expenseDetailTabs = useMemo(() => {
|
const expenseDetailTabs = useMemo(() => {
|
||||||
const validTabs = [
|
const validTabs = [
|
||||||
@@ -46,7 +50,7 @@ const ExpenseDetail: React.FC<ExpenseDetailProps> = ({ initialValues }) => {
|
|||||||
<section className='w-full max-w-full pb-16'>
|
<section className='w-full max-w-full pb-16'>
|
||||||
<header className='flex flex-col gap-4'>
|
<header className='flex flex-col gap-4'>
|
||||||
<Button
|
<Button
|
||||||
href='/expense'
|
href={returnTo}
|
||||||
variant='link'
|
variant='link'
|
||||||
className='w-fit p-0 text-primary'
|
className='w-fit p-0 text-primary'
|
||||||
>
|
>
|
||||||
|
|||||||
@@ -1,5 +1,8 @@
|
|||||||
|
'use client';
|
||||||
|
|
||||||
import { useFormik } from 'formik';
|
import { useFormik } from 'formik';
|
||||||
import toast from 'react-hot-toast';
|
import toast from 'react-hot-toast';
|
||||||
|
import { useSearchParams } from 'next/navigation';
|
||||||
|
|
||||||
import Link from 'next/link';
|
import Link from 'next/link';
|
||||||
import { Icon } from '@iconify/react';
|
import { Icon } from '@iconify/react';
|
||||||
@@ -16,6 +19,7 @@ import {
|
|||||||
} from '@/components/pages/expense/form/ExpenseRequestForm.schema';
|
} from '@/components/pages/expense/form/ExpenseRequestForm.schema';
|
||||||
import { ExpenseApi } from '@/services/api/expense';
|
import { ExpenseApi } from '@/services/api/expense';
|
||||||
import { isResponseSuccess } from '@/lib/api-helper';
|
import { isResponseSuccess } from '@/lib/api-helper';
|
||||||
|
import { buildExpenseActionHref } from '@/lib/expense-list-navigation';
|
||||||
import { ACCEPTED_FILE_TYPE, S3_PUBLIC_BASE_URL } from '@/config/constant';
|
import { ACCEPTED_FILE_TYPE, S3_PUBLIC_BASE_URL } from '@/config/constant';
|
||||||
|
|
||||||
interface ExpenseRealizationContentProps {
|
interface ExpenseRealizationContentProps {
|
||||||
@@ -25,6 +29,8 @@ interface ExpenseRealizationContentProps {
|
|||||||
const ExpenseRealizationContent = ({
|
const ExpenseRealizationContent = ({
|
||||||
initialValues,
|
initialValues,
|
||||||
}: ExpenseRealizationContentProps) => {
|
}: ExpenseRealizationContentProps) => {
|
||||||
|
const searchParams = useSearchParams();
|
||||||
|
|
||||||
const formik = useFormik<UploadRequestDocumentsFormValues>({
|
const formik = useFormik<UploadRequestDocumentsFormValues>({
|
||||||
initialValues: {
|
initialValues: {
|
||||||
documents: [],
|
documents: [],
|
||||||
@@ -74,7 +80,11 @@ const ExpenseRealizationContent = ({
|
|||||||
<Button
|
<Button
|
||||||
type='button'
|
type='button'
|
||||||
color='warning'
|
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'
|
className='px-4 grow sm:grow-0'
|
||||||
>
|
>
|
||||||
<Icon icon='mdi:pencil-outline' width={24} height={24} />
|
<Icon icon='mdi:pencil-outline' width={24} height={24} />
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
'use client';
|
'use client';
|
||||||
|
|
||||||
import { useState } from 'react';
|
import { useState } from 'react';
|
||||||
import { useRouter } from 'next/navigation';
|
import { useRouter, useSearchParams } from 'next/navigation';
|
||||||
import { useFormik } from 'formik';
|
import { useFormik } from 'formik';
|
||||||
import toast from 'react-hot-toast';
|
import toast from 'react-hot-toast';
|
||||||
|
|
||||||
@@ -31,6 +31,10 @@ import { ExpenseApi } from '@/services/api/expense';
|
|||||||
import { isResponseSuccess } from '@/lib/api-helper';
|
import { isResponseSuccess } from '@/lib/api-helper';
|
||||||
import { EXPENSE_REQUEST_APPROVAL_LINE } from '@/config/approval-line';
|
import { EXPENSE_REQUEST_APPROVAL_LINE } from '@/config/approval-line';
|
||||||
import { BaseApiResponse } from '@/types/api/api-general';
|
import { BaseApiResponse } from '@/types/api/api-general';
|
||||||
|
import {
|
||||||
|
buildExpenseActionHref,
|
||||||
|
getExpenseListReturnTo,
|
||||||
|
} from '@/lib/expense-list-navigation';
|
||||||
|
|
||||||
interface ExpenseRequestContentProps {
|
interface ExpenseRequestContentProps {
|
||||||
initialValues?: Expense;
|
initialValues?: Expense;
|
||||||
@@ -40,6 +44,8 @@ const ExpenseRequestContent = ({
|
|||||||
initialValues,
|
initialValues,
|
||||||
}: ExpenseRequestContentProps) => {
|
}: ExpenseRequestContentProps) => {
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
|
const searchParams = useSearchParams();
|
||||||
|
const returnTo = getExpenseListReturnTo(searchParams);
|
||||||
|
|
||||||
const { approvals: approvalHistory, isLoading: isLoadingApprovalHistory } =
|
const { approvals: approvalHistory, isLoading: isLoadingApprovalHistory } =
|
||||||
useApprovalSteps({
|
useApprovalSteps({
|
||||||
@@ -148,7 +154,7 @@ const ExpenseRequestContent = ({
|
|||||||
|
|
||||||
if (isResponseSuccess(deleteResponse)) {
|
if (isResponseSuccess(deleteResponse)) {
|
||||||
toast.success('Berhasil menghapus data biaya operasional!');
|
toast.success('Berhasil menghapus data biaya operasional!');
|
||||||
router.push('/expense');
|
router.push(returnTo);
|
||||||
} else {
|
} else {
|
||||||
toast.error('Gagal menghapus data biaya operasional!');
|
toast.error('Gagal menghapus data biaya operasional!');
|
||||||
}
|
}
|
||||||
@@ -164,7 +170,7 @@ const ExpenseRequestContent = ({
|
|||||||
|
|
||||||
if (isResponseSuccess(completeRes)) {
|
if (isResponseSuccess(completeRes)) {
|
||||||
toast.success(completeRes.message);
|
toast.success(completeRes.message);
|
||||||
router.push('/expense');
|
router.push(returnTo);
|
||||||
} else {
|
} else {
|
||||||
toast.error(completeRes?.message as string);
|
toast.error(completeRes?.message as string);
|
||||||
}
|
}
|
||||||
@@ -204,7 +210,7 @@ const ExpenseRequestContent = ({
|
|||||||
|
|
||||||
toast.success(approveResponse?.message);
|
toast.success(approveResponse?.message);
|
||||||
setApprovalNotes('');
|
setApprovalNotes('');
|
||||||
router.push('/expense');
|
router.push(returnTo);
|
||||||
} else {
|
} else {
|
||||||
approveModal.closeModal();
|
approveModal.closeModal();
|
||||||
|
|
||||||
@@ -239,7 +245,7 @@ const ExpenseRequestContent = ({
|
|||||||
|
|
||||||
toast.success(rejectResponse.message);
|
toast.success(rejectResponse.message);
|
||||||
setApprovalNotes('');
|
setApprovalNotes('');
|
||||||
router.push('/expense');
|
router.push(returnTo);
|
||||||
} else {
|
} else {
|
||||||
rejectModal.closeModal();
|
rejectModal.closeModal();
|
||||||
|
|
||||||
@@ -365,7 +371,11 @@ const ExpenseRequestContent = ({
|
|||||||
<Button
|
<Button
|
||||||
variant='outline'
|
variant='outline'
|
||||||
color='info'
|
color='info'
|
||||||
href={`/expense/realization/?expenseId=${initialValues?.id}`}
|
href={buildExpenseActionHref(
|
||||||
|
'/expense/realization/',
|
||||||
|
initialValues?.id as number,
|
||||||
|
searchParams
|
||||||
|
)}
|
||||||
className='w-full sm:w-fit'
|
className='w-full sm:w-fit'
|
||||||
>
|
>
|
||||||
<Icon
|
<Icon
|
||||||
@@ -384,7 +394,11 @@ const ExpenseRequestContent = ({
|
|||||||
<Button
|
<Button
|
||||||
type='button'
|
type='button'
|
||||||
color='warning'
|
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'
|
className='px-4 grow sm:grow-0'
|
||||||
>
|
>
|
||||||
<Icon icon='mdi:pencil-outline' width={24} height={24} />
|
<Icon icon='mdi:pencil-outline' width={24} height={24} />
|
||||||
|
|||||||
@@ -1,8 +1,12 @@
|
|||||||
'use client';
|
'use client';
|
||||||
|
|
||||||
import { ChangeEventHandler, useEffect, useMemo, useState } from 'react';
|
import {
|
||||||
import { usePathname } from 'next/navigation';
|
ChangeEventHandler,
|
||||||
import { useUiStore } from '@/stores/ui/ui.store';
|
useCallback,
|
||||||
|
useEffect,
|
||||||
|
useMemo,
|
||||||
|
useState,
|
||||||
|
} from 'react';
|
||||||
import useSWR from 'swr';
|
import useSWR from 'swr';
|
||||||
import {
|
import {
|
||||||
CellContext,
|
CellContext,
|
||||||
@@ -15,8 +19,11 @@ import toast from 'react-hot-toast';
|
|||||||
import { Icon } from '@iconify/react';
|
import { Icon } from '@iconify/react';
|
||||||
import Table from '@/components/Table';
|
import Table from '@/components/Table';
|
||||||
import DebouncedTextInput from '@/components/input/DebouncedTextInput';
|
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 Button from '@/components/Button';
|
||||||
import { useModal } from '@/components/Modal';
|
import Modal, { useModal } from '@/components/Modal';
|
||||||
import ConfirmationModal from '@/components/modal/ConfirmationModal';
|
import ConfirmationModal from '@/components/modal/ConfirmationModal';
|
||||||
import PopoverButton from '@/components/popover/PopoverButton';
|
import PopoverButton from '@/components/popover/PopoverButton';
|
||||||
import PopoverContent from '@/components/popover/PopoverContent';
|
import PopoverContent from '@/components/popover/PopoverContent';
|
||||||
@@ -36,6 +43,36 @@ import { isResponseSuccess } from '@/lib/api-helper';
|
|||||||
import { useTableFilter } from '@/services/hooks/useTableFilter';
|
import { useTableFilter } from '@/services/hooks/useTableFilter';
|
||||||
import { BaseApiResponse } from '@/types/api/api-general';
|
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 = ({
|
const RowOptionsMenu = ({
|
||||||
popoverPosition = 'bottom',
|
popoverPosition = 'bottom',
|
||||||
props,
|
props,
|
||||||
@@ -153,17 +190,16 @@ const RowOptionsMenu = ({
|
|||||||
};
|
};
|
||||||
|
|
||||||
const ExpensesTable = () => {
|
const ExpensesTable = () => {
|
||||||
const { searchValue, setSearchValue, setTableState } = useUiStore();
|
|
||||||
const pathname = usePathname();
|
|
||||||
|
|
||||||
const {
|
const {
|
||||||
state: tableFilterState,
|
state: tableFilterState,
|
||||||
updateFilter,
|
updateFilter,
|
||||||
setPage,
|
setPage,
|
||||||
setPageSize,
|
setPageSize,
|
||||||
toQueryString: getTableFilterQueryString,
|
toQueryString: getTableFilterQueryString,
|
||||||
} = useTableFilter({
|
} = useTableFilter<ExpenseTableFilters>({
|
||||||
initial: {
|
initial: {
|
||||||
|
page: 1,
|
||||||
|
pageSize: 10,
|
||||||
search: '',
|
search: '',
|
||||||
nameSort: '',
|
nameSort: '',
|
||||||
transactionDate: '',
|
transactionDate: '',
|
||||||
@@ -182,6 +218,9 @@ const ExpensesTable = () => {
|
|||||||
vendorId: 'vendor_id',
|
vendorId: 'vendor_id',
|
||||||
userId: 'user_id',
|
userId: 'user_id',
|
||||||
},
|
},
|
||||||
|
|
||||||
|
persist: true,
|
||||||
|
storeName: 'expense-table',
|
||||||
});
|
});
|
||||||
|
|
||||||
const {
|
const {
|
||||||
@@ -196,6 +235,7 @@ const ExpensesTable = () => {
|
|||||||
const deleteModal = useModal();
|
const deleteModal = useModal();
|
||||||
const approveModal = useModal();
|
const approveModal = useModal();
|
||||||
const rejectModal = useModal();
|
const rejectModal = useModal();
|
||||||
|
const bulkApproveFormModal = useModal();
|
||||||
|
|
||||||
// ===== FILTER MODAL STATE =====
|
// ===== FILTER MODAL STATE =====
|
||||||
const filterModal = useModal();
|
const filterModal = useModal();
|
||||||
@@ -207,6 +247,10 @@ const ExpensesTable = () => {
|
|||||||
const [isApproveLoading, setIsApproveLoading] = useState(false);
|
const [isApproveLoading, setIsApproveLoading] = useState(false);
|
||||||
const [isRejectLoading, setIsRejectLoading] = useState(false);
|
const [isRejectLoading, setIsRejectLoading] = useState(false);
|
||||||
const [, setApprovalNotes] = useState('');
|
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 [sorting, setSorting] = useState<SortingState>([]);
|
||||||
const [rowSelection, setRowSelection] = useState<Record<string, boolean>>({});
|
const [rowSelection, setRowSelection] = useState<Record<string, boolean>>({});
|
||||||
@@ -390,17 +434,45 @@ const ExpensesTable = () => {
|
|||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
// const bulkApproveClickHandler = () => {
|
const resetBulkApproveForm = useCallback(() => {
|
||||||
// approveModal.openModal();
|
setBulkApprovalStatus(null);
|
||||||
// };
|
setBulkApprovalDate('');
|
||||||
|
setBulkApprovalNotes('');
|
||||||
|
}, []);
|
||||||
|
|
||||||
// const bulkRejectClickHandler = () => {
|
const openBulkApproveForm = useCallback(
|
||||||
// rejectModal.openModal();
|
(presetStatus?: ApprovalStatusValue) => {
|
||||||
// };
|
resetBulkApproveForm();
|
||||||
|
|
||||||
|
if (presetStatus) {
|
||||||
|
const selectedStatus = approvalStatusOptions.find(
|
||||||
|
(option) => option.value === presetStatus
|
||||||
|
);
|
||||||
|
|
||||||
|
if (selectedStatus) {
|
||||||
|
setBulkApprovalStatus(selectedStatus);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
bulkApproveFormModal.openModal();
|
||||||
|
},
|
||||||
|
[bulkApproveFormModal, resetBulkApproveForm]
|
||||||
|
);
|
||||||
|
|
||||||
const bulkApproveClickHandler = () => {
|
const bulkApproveClickHandler = () => {
|
||||||
setApprovalNotes('');
|
openBulkApproveForm();
|
||||||
approveModal.openModal();
|
};
|
||||||
|
|
||||||
|
const bulkApproveHeadAreaClickHandler = () => {
|
||||||
|
openBulkApproveForm('HEAD_AREA');
|
||||||
|
};
|
||||||
|
|
||||||
|
const bulkApproveUnitVicePresidentClickHandler = () => {
|
||||||
|
openBulkApproveForm('UNIT_VICE_PRESIDENT');
|
||||||
|
};
|
||||||
|
|
||||||
|
const bulkApproveFinanceClickHandler = () => {
|
||||||
|
openBulkApproveForm('FINANCE');
|
||||||
};
|
};
|
||||||
|
|
||||||
const bulkRejectClickHandler = () => {
|
const bulkRejectClickHandler = () => {
|
||||||
@@ -408,6 +480,18 @@ const ExpensesTable = () => {
|
|||||||
rejectModal.openModal();
|
rejectModal.openModal();
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const bulkApprovalDateChangeHandler: ChangeEventHandler<HTMLInputElement> = (
|
||||||
|
e
|
||||||
|
) => {
|
||||||
|
setBulkApprovalDate(e.target.value);
|
||||||
|
};
|
||||||
|
|
||||||
|
const bulkApprovalNotesChangeHandler: ChangeEventHandler<
|
||||||
|
HTMLTextAreaElement
|
||||||
|
> = (e) => {
|
||||||
|
setBulkApprovalNotes(e.target.value);
|
||||||
|
};
|
||||||
|
|
||||||
const confirmationModalDeleteClickHandler = async () => {
|
const confirmationModalDeleteClickHandler = async () => {
|
||||||
setIsDeleteLoading(true);
|
setIsDeleteLoading(true);
|
||||||
|
|
||||||
@@ -470,6 +554,48 @@ const ExpensesTable = () => {
|
|||||||
setIsApproveLoading(false);
|
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) => {
|
const confirmationModalRejectClickHandler = async (notes: string) => {
|
||||||
setIsRejectLoading(true);
|
setIsRejectLoading(true);
|
||||||
|
|
||||||
@@ -512,16 +638,7 @@ const ExpensesTable = () => {
|
|||||||
setIsRejectLoading(false);
|
setIsRejectLoading(false);
|
||||||
};
|
};
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
updateFilter('search', searchValue);
|
|
||||||
}, [searchValue, updateFilter]);
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
setTableState('expense-table', pathname);
|
|
||||||
}, [pathname, setTableState]);
|
|
||||||
|
|
||||||
const searchChangeHandler: ChangeEventHandler<HTMLInputElement> = (e) => {
|
const searchChangeHandler: ChangeEventHandler<HTMLInputElement> = (e) => {
|
||||||
setSearchValue(e.target.value);
|
|
||||||
updateFilter('search', e.target.value);
|
updateFilter('search', e.target.value);
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -554,7 +671,7 @@ const ExpensesTable = () => {
|
|||||||
const isNameSorted = sorting.find((sortItem) => sortItem.id === 'name');
|
const isNameSorted = sorting.find((sortItem) => sortItem.id === 'name');
|
||||||
|
|
||||||
if (!isNameSorted) {
|
if (!isNameSorted) {
|
||||||
updateFilter('nameSort', '');
|
updateFilter('nameSort', '', false);
|
||||||
} else {
|
} else {
|
||||||
updateFilter('nameSort', isNameSorted.desc ? 'desc' : 'asc');
|
updateFilter('nameSort', isNameSorted.desc ? 'desc' : 'asc');
|
||||||
}
|
}
|
||||||
@@ -565,7 +682,7 @@ const ExpensesTable = () => {
|
|||||||
<div className='w-full'>
|
<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'>
|
<div className='w-full p-3 flex flex-row justify-between gap-3 flex-wrap border-b border-base-content/10'>
|
||||||
{/* Action Buttons */}
|
{/* 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'>
|
<RequirePermission permissions='lti.expense.create'>
|
||||||
<Button
|
<Button
|
||||||
href='/expense/add'
|
href='/expense/add'
|
||||||
@@ -578,14 +695,36 @@ const ExpensesTable = () => {
|
|||||||
</RequirePermission>
|
</RequirePermission>
|
||||||
|
|
||||||
{selectedRowIds.length > 0 && (
|
{selectedRowIds.length > 0 && (
|
||||||
<>
|
<div className='flex flex-row gap-3 flex-wrap'>
|
||||||
<hr className='w-px h-full border-none bg-base-content/10 sm:block hidden' />
|
<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'>
|
<RequirePermission permissions='lti.expense.approve.head_area'>
|
||||||
<Button
|
<Button
|
||||||
variant='outline'
|
variant='outline'
|
||||||
color='none'
|
color='none'
|
||||||
onClick={bulkApproveClickHandler}
|
onClick={bulkApproveHeadAreaClickHandler}
|
||||||
disabled={!isAllSelectedRowLatestApprovalOnHeadArea}
|
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'
|
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
|
<Button
|
||||||
variant='outline'
|
variant='outline'
|
||||||
color='none'
|
color='none'
|
||||||
onClick={bulkApproveClickHandler}
|
onClick={bulkApproveUnitVicePresidentClickHandler}
|
||||||
disabled={
|
disabled={
|
||||||
!isAllSelectedRowLatestApprovalOnUnitVicePresident
|
!isAllSelectedRowLatestApprovalOnUnitVicePresident
|
||||||
}
|
}
|
||||||
@@ -623,7 +762,7 @@ const ExpensesTable = () => {
|
|||||||
<Button
|
<Button
|
||||||
variant='outline'
|
variant='outline'
|
||||||
color='none'
|
color='none'
|
||||||
onClick={bulkApproveClickHandler}
|
onClick={bulkApproveFinanceClickHandler}
|
||||||
disabled={!isAllSelectedRowLatestApprovalOnFinance}
|
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'
|
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
|
Reject
|
||||||
</Button>
|
</Button>
|
||||||
</RequirePermission>
|
</RequirePermission>
|
||||||
</>
|
</div>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Search and Filter */}
|
{/* 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
|
<DebouncedTextInput
|
||||||
name='search'
|
name='search'
|
||||||
placeholder='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
|
<ExpensesFilterModal
|
||||||
ref={filterModal.ref}
|
ref={filterModal.ref}
|
||||||
onSubmit={handleFilterSubmit}
|
onSubmit={handleFilterSubmit}
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
'use client';
|
'use client';
|
||||||
|
|
||||||
import { useCallback, useState } from 'react';
|
import { useCallback, useState } from 'react';
|
||||||
import { useRouter } from 'next/navigation';
|
import { useRouter, useSearchParams } from 'next/navigation';
|
||||||
import { useFormik } from 'formik';
|
import { useFormik } from 'formik';
|
||||||
import toast from 'react-hot-toast';
|
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 { LocationApi, SupplierApi } from '@/services/api/master-data';
|
||||||
import { Supplier } from '@/types/api/master-data/supplier';
|
import { Supplier } from '@/types/api/master-data/supplier';
|
||||||
import { ACCEPTED_FILE_TYPE } from '@/config/constant';
|
import { ACCEPTED_FILE_TYPE } from '@/config/constant';
|
||||||
|
import { getExpenseListReturnTo } from '@/lib/expense-list-navigation';
|
||||||
import { cn } from '@/lib/helper';
|
import { cn } from '@/lib/helper';
|
||||||
import { useFormikErrorList } from '@/services/hooks/useFormikErrorList';
|
import { useFormikErrorList } from '@/services/hooks/useFormikErrorList';
|
||||||
|
|
||||||
@@ -48,6 +49,8 @@ const ExpenseRealizationForm = ({
|
|||||||
initialValues,
|
initialValues,
|
||||||
}: ExpenseRealizationFormProps) => {
|
}: ExpenseRealizationFormProps) => {
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
|
const searchParams = useSearchParams();
|
||||||
|
const returnTo = getExpenseListReturnTo(searchParams);
|
||||||
|
|
||||||
const [expenseFormErrorMessage, setExpenseFormErrorMessage] = useState('');
|
const [expenseFormErrorMessage, setExpenseFormErrorMessage] = useState('');
|
||||||
|
|
||||||
@@ -64,9 +67,9 @@ const ExpenseRealizationForm = ({
|
|||||||
}
|
}
|
||||||
|
|
||||||
toast.success(createExpenseRes?.message as string);
|
toast.success(createExpenseRes?.message as string);
|
||||||
router.push('/expense');
|
router.push(returnTo);
|
||||||
},
|
},
|
||||||
[router]
|
[initialValues?.id, returnTo, router]
|
||||||
);
|
);
|
||||||
|
|
||||||
const updateExpenseHandler = useCallback(
|
const updateExpenseHandler = useCallback(
|
||||||
@@ -83,9 +86,9 @@ const ExpenseRealizationForm = ({
|
|||||||
|
|
||||||
toast.success(updateExpenseRes?.message as string);
|
toast.success(updateExpenseRes?.message as string);
|
||||||
router.refresh();
|
router.refresh();
|
||||||
router.push('/expense');
|
router.push(returnTo);
|
||||||
},
|
},
|
||||||
[router]
|
[returnTo, router]
|
||||||
);
|
);
|
||||||
|
|
||||||
const formik = useFormik<ExpenseRealizationFormValues>({
|
const formik = useFormik<ExpenseRealizationFormValues>({
|
||||||
@@ -258,7 +261,7 @@ const ExpenseRealizationForm = ({
|
|||||||
<section className='w-full'>
|
<section className='w-full'>
|
||||||
<header className='flex flex-col gap-4'>
|
<header className='flex flex-col gap-4'>
|
||||||
<Button
|
<Button
|
||||||
href='/expense'
|
href={returnTo}
|
||||||
variant='link'
|
variant='link'
|
||||||
className='w-fit p-0 text-primary'
|
className='w-fit p-0 text-primary'
|
||||||
>
|
>
|
||||||
|
|||||||
@@ -2,6 +2,8 @@
|
|||||||
|
|
||||||
import Button from '@/components/Button';
|
import Button from '@/components/Button';
|
||||||
import CheckboxInput from '@/components/input/CheckboxInput';
|
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 Modal, { useModal } from '@/components/Modal';
|
||||||
import ConfirmationModal from '@/components/modal/ConfirmationModal';
|
import ConfirmationModal from '@/components/modal/ConfirmationModal';
|
||||||
import ConfirmationModalWithNotes from '@/components/modal/ConfirmationModalWithNotes';
|
import ConfirmationModalWithNotes from '@/components/modal/ConfirmationModalWithNotes';
|
||||||
@@ -13,6 +15,7 @@ import {
|
|||||||
SalesOrderApi,
|
SalesOrderApi,
|
||||||
} from '@/services/api/marketing/marketing';
|
} from '@/services/api/marketing/marketing';
|
||||||
import { useTableFilter } from '@/services/hooks/useTableFilter';
|
import { useTableFilter } from '@/services/hooks/useTableFilter';
|
||||||
|
import { BaseApiResponse } from '@/types/api/api-general';
|
||||||
import {
|
import {
|
||||||
BaseSalesOrder,
|
BaseSalesOrder,
|
||||||
Marketing,
|
Marketing,
|
||||||
@@ -21,7 +24,7 @@ import {
|
|||||||
import { Icon } from '@iconify/react';
|
import { Icon } from '@iconify/react';
|
||||||
import { CellContext, ColumnDef, Row } from '@tanstack/react-table';
|
import { CellContext, ColumnDef, Row } from '@tanstack/react-table';
|
||||||
import { useRouter } from 'next/navigation';
|
import { useRouter } from 'next/navigation';
|
||||||
import { useMemo, useState } from 'react';
|
import { ChangeEventHandler, useCallback, useMemo, useState } from 'react';
|
||||||
import toast from 'react-hot-toast';
|
import toast from 'react-hot-toast';
|
||||||
import useSWR from 'swr';
|
import useSWR from 'swr';
|
||||||
import RequirePermission from '@/components/helper/RequirePermission';
|
import RequirePermission from '@/components/helper/RequirePermission';
|
||||||
@@ -154,12 +157,17 @@ const MarketingTable = () => {
|
|||||||
);
|
);
|
||||||
const [selectedItem, setSelectedItem] = useState<Marketing | null>(null);
|
const [selectedItem, setSelectedItem] = useState<Marketing | null>(null);
|
||||||
const [rowSelection, setRowSelection] = useState<Record<string, boolean>>({});
|
const [rowSelection, setRowSelection] = useState<Record<string, boolean>>({});
|
||||||
|
const [bulkDeliveryDate, setBulkDeliveryDate] = useState('');
|
||||||
|
const [bulkDeliveryNotes, setBulkDeliveryNotes] = useState('');
|
||||||
|
const [isSubmittingBulkDelivery, setIsSubmittingBulkDelivery] =
|
||||||
|
useState(false);
|
||||||
|
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
const deleteModal = useModal();
|
const deleteModal = useModal();
|
||||||
const confirmationModal = useModal();
|
const confirmationModal = useModal();
|
||||||
const productsModal = useModal();
|
const productsModal = useModal();
|
||||||
const deliveryModal = useModal();
|
const deliveryModal = useModal();
|
||||||
|
const bulkDeliveryModal = useModal();
|
||||||
const filterModal = useModal();
|
const filterModal = useModal();
|
||||||
|
|
||||||
const {
|
const {
|
||||||
@@ -182,6 +190,9 @@ const MarketingTable = () => {
|
|||||||
status: 'status',
|
status: 'status',
|
||||||
customer_id: 'customer_id',
|
customer_id: 'customer_id',
|
||||||
},
|
},
|
||||||
|
|
||||||
|
persist: true,
|
||||||
|
storeName: 'marketing-table',
|
||||||
});
|
});
|
||||||
|
|
||||||
// ===== FETCH DATA =====
|
// ===== FETCH DATA =====
|
||||||
@@ -198,12 +209,14 @@ const MarketingTable = () => {
|
|||||||
const filterSubmitHandler = (values: MarketingFilter) => {
|
const filterSubmitHandler = (values: MarketingFilter) => {
|
||||||
updateFilter(
|
updateFilter(
|
||||||
'product_ids',
|
'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(
|
updateFilter(
|
||||||
'customer_id',
|
'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);
|
useState(false);
|
||||||
|
|
||||||
const filterResetHandler = () => {
|
const filterResetHandler = () => {
|
||||||
updateFilter('product_ids', '');
|
updateFilter('product_ids', '', true);
|
||||||
updateFilter('status', '');
|
updateFilter('status', '', true);
|
||||||
updateFilter('customer_id', '');
|
updateFilter('customer_id', '', true);
|
||||||
};
|
};
|
||||||
|
|
||||||
const approveClickHandler = () => {
|
const approveClickHandler = () => {
|
||||||
setApproveAction('APPROVED');
|
setApproveAction('APPROVED');
|
||||||
|
|
||||||
|
if (selectedApprovalStep === 2) {
|
||||||
|
bulkDeliveryModal.openModal();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
confirmationModal.openModal();
|
confirmationModal.openModal();
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -226,10 +245,13 @@ const MarketingTable = () => {
|
|||||||
confirmationModal.openModal();
|
confirmationModal.openModal();
|
||||||
};
|
};
|
||||||
|
|
||||||
const productsClickHandler = (item: Marketing) => {
|
const productsClickHandler = useCallback(
|
||||||
setSelectedItem(item);
|
(item: Marketing) => {
|
||||||
productsModal.openModal();
|
setSelectedItem(item);
|
||||||
};
|
productsModal.openModal();
|
||||||
|
},
|
||||||
|
[productsModal]
|
||||||
|
);
|
||||||
|
|
||||||
const deleteMarketingHandler = async () => {
|
const deleteMarketingHandler = async () => {
|
||||||
const deleteMarketingRes = await MarketingApi.delete(
|
const deleteMarketingRes = await MarketingApi.delete(
|
||||||
@@ -251,61 +273,135 @@ const MarketingTable = () => {
|
|||||||
const selectedRowsData = allData.filter(
|
const selectedRowsData = allData.filter(
|
||||||
(row) => rowSelection[row.id.toString()]
|
(row) => rowSelection[row.id.toString()]
|
||||||
);
|
);
|
||||||
|
const selectedApprovalStep =
|
||||||
|
selectedRowsData.length > 0
|
||||||
|
? selectedRowsData[0].latest_approval.step_number
|
||||||
|
: null;
|
||||||
|
|
||||||
const hasApprovable = selectedRowsData.some(
|
const eligibleSelectedRows = selectedRowsData.filter((row) => {
|
||||||
(row) =>
|
const approval = row.latest_approval;
|
||||||
row.latest_approval.step_number === 1 &&
|
|
||||||
row.latest_approval.action !== 'REJECTED'
|
if (approval.action === 'REJECTED') {
|
||||||
);
|
return false;
|
||||||
const hasRejectable = selectedRowsData.some(
|
}
|
||||||
(row) =>
|
|
||||||
row.latest_approval.step_number === 1 &&
|
if (selectedApprovalStep === null) {
|
||||||
row.latest_approval.action !== 'REJECTED'
|
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 disableApprove = !hasApprovable;
|
||||||
const disableReject = !hasRejectable;
|
const disableReject = !hasRejectable;
|
||||||
|
|
||||||
const idsToProcess =
|
const idsToProcess = eligibleSelectedRows.map((row) => row.id);
|
||||||
approveAction === 'APPROVED'
|
const nextApprovalStatus =
|
||||||
? selectedRowsData
|
selectedApprovalStep === 1
|
||||||
.filter((row) => row.latest_approval.step_number === 1)
|
? 'SALES_ORDER'
|
||||||
.map((row) => row.id)
|
: selectedApprovalStep === 2
|
||||||
: selectedRowsData
|
? 'DELIVERY_ORDER'
|
||||||
.filter((row) => row.latest_approval.step_number === 2)
|
: null;
|
||||||
.map((row) => row.id);
|
|
||||||
|
|
||||||
const approveMarketingHandler = async (notes: string) => {
|
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) {
|
if (idsToProcess.length === 0) {
|
||||||
toast.error(`Tidak ada data yang valid untuk di ${approveAction}.`);
|
toast.error(`Tidak ada data yang valid untuk di ${approveAction}.`);
|
||||||
confirmationModal.closeModal();
|
confirmationModal.closeModal();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const approveMarketingRes = await SalesOrderApi.bulkApprovals(
|
if (approveAction === 'APPROVED' && selectedApprovalStep !== 1) {
|
||||||
idsToProcess,
|
toast.error('Approve tahap ini harus menggunakan tanggal pengiriman.');
|
||||||
approveAction,
|
confirmationModal.closeModal();
|
||||||
notes
|
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)) {
|
if (isResponseSuccess(approveMarketingRes)) {
|
||||||
confirmationModal.closeModal();
|
confirmationModal.closeModal();
|
||||||
toast.success(approveMarketingRes?.message as string);
|
toast.success(approveMarketingRes?.message as string);
|
||||||
setRowSelection({});
|
setRowSelection({});
|
||||||
}
|
}
|
||||||
if (isResponseError(approveMarketingRes)) {
|
|
||||||
confirmationModal.closeModal();
|
|
||||||
toast.error(approveMarketingRes?.message as string);
|
|
||||||
}
|
|
||||||
refreshMarketing();
|
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 confirmationModalDeliveryClickHandler = async (notes: string) => {
|
||||||
const res = await SalesOrderApi.delivery(selectedItem?.id as number, notes);
|
const res = await SalesOrderApi.delivery(selectedItem?.id as number, notes);
|
||||||
deliveryModal.closeModal();
|
deliveryModal.closeModal();
|
||||||
@@ -316,10 +412,24 @@ const MarketingTable = () => {
|
|||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
const getRowCanSelect = (row: Row<Marketing>): boolean => {
|
const getRowCanSelect = useCallback(
|
||||||
const approval = row.original.latest_approval;
|
(row: Row<Marketing>): boolean => {
|
||||||
return approval?.step_number === 1 && approval?.action !== 'REJECTED';
|
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 () => {
|
const exportToExcelHandler = async () => {
|
||||||
setIsLoadingExportingToExcel(true);
|
setIsLoadingExportingToExcel(true);
|
||||||
@@ -336,7 +446,22 @@ const MarketingTable = () => {
|
|||||||
size: 1,
|
size: 1,
|
||||||
header: ({ table }) => {
|
header: ({ table }) => {
|
||||||
const allRows = table.getRowModel().rows;
|
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 =
|
const allSelected =
|
||||||
selectableRows.length > 0 &&
|
selectableRows.length > 0 &&
|
||||||
@@ -504,7 +629,13 @@ const MarketingTable = () => {
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
}, []);
|
}, [
|
||||||
|
deleteModal,
|
||||||
|
deliveryModal,
|
||||||
|
getRowCanSelect,
|
||||||
|
productsClickHandler,
|
||||||
|
selectedApprovalStep,
|
||||||
|
]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
@@ -677,7 +808,7 @@ const MarketingTable = () => {
|
|||||||
<ConfirmationModalWithNotes
|
<ConfirmationModalWithNotes
|
||||||
ref={confirmationModal.ref}
|
ref={confirmationModal.ref}
|
||||||
type={approveAction === 'APPROVED' ? 'success' : 'error'}
|
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={{
|
secondaryButton={{
|
||||||
text: 'Tidak',
|
text: 'Tidak',
|
||||||
onClick: confirmationModal.closeModal,
|
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
|
<Modal
|
||||||
ref={productsModal.ref}
|
ref={productsModal.ref}
|
||||||
className={{
|
className={{
|
||||||
|
|||||||
@@ -23,7 +23,6 @@ import { Icon } from '@iconify/react';
|
|||||||
import { CellContext, ColumnDef, SortingState } from '@tanstack/react-table';
|
import { CellContext, ColumnDef, SortingState } from '@tanstack/react-table';
|
||||||
import { useRouter, usePathname } from 'next/navigation';
|
import { useRouter, usePathname } from 'next/navigation';
|
||||||
import { ChangeEventHandler, useEffect, useMemo, useState } from 'react';
|
import { ChangeEventHandler, useEffect, useMemo, useState } from 'react';
|
||||||
import { useUiStore } from '@/stores/ui/ui.store';
|
|
||||||
import toast from 'react-hot-toast';
|
import toast from 'react-hot-toast';
|
||||||
import useSWR from 'swr';
|
import useSWR from 'swr';
|
||||||
import { useFormik } from 'formik';
|
import { useFormik } from 'formik';
|
||||||
@@ -148,7 +147,6 @@ const RowOptionsMenu = ({
|
|||||||
};
|
};
|
||||||
|
|
||||||
const ProjectFlockTable = ({ refresh }: { refresh?: () => void }) => {
|
const ProjectFlockTable = ({ refresh }: { refresh?: () => void }) => {
|
||||||
const { searchValue, setSearchValue, setTableState } = useUiStore();
|
|
||||||
const pathname = usePathname();
|
const pathname = usePathname();
|
||||||
|
|
||||||
const isSuccess = useProjectFlockStore((s) => s.isSuccess);
|
const isSuccess = useProjectFlockStore((s) => s.isSuccess);
|
||||||
@@ -185,7 +183,11 @@ const ProjectFlockTable = ({ refresh }: { refresh?: () => void }) => {
|
|||||||
category: 'category',
|
category: 'category',
|
||||||
period: 'period',
|
period: 'period',
|
||||||
},
|
},
|
||||||
|
|
||||||
|
persist: true,
|
||||||
|
storeName: 'project-flock-table',
|
||||||
});
|
});
|
||||||
|
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
|
|
||||||
// ===== State =====
|
// ===== State =====
|
||||||
@@ -425,18 +427,11 @@ const ProjectFlockTable = ({ refresh }: { refresh?: () => void }) => {
|
|||||||
setIsDeleteLoading(false);
|
setIsDeleteLoading(false);
|
||||||
setRowSelection({});
|
setRowSelection({});
|
||||||
};
|
};
|
||||||
useEffect(() => {
|
|
||||||
updateFilter('search', searchValue);
|
|
||||||
}, [searchValue, updateFilter]);
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
setTableState('project-flock-table', pathname);
|
|
||||||
}, [pathname, setTableState]);
|
|
||||||
|
|
||||||
const searchChangeHandler: ChangeEventHandler<HTMLInputElement> = (e) => {
|
const searchChangeHandler: ChangeEventHandler<HTMLInputElement> = (e) => {
|
||||||
setSearchValue(e.target.value);
|
|
||||||
updateFilter('search', e.target.value);
|
updateFilter('search', e.target.value);
|
||||||
};
|
};
|
||||||
|
|
||||||
const confirmApprovalHandler = async (
|
const confirmApprovalHandler = async (
|
||||||
notes: string,
|
notes: string,
|
||||||
approvalAction: 'APPROVED' | 'REJECTED'
|
approvalAction: 'APPROVED' | 'REJECTED'
|
||||||
|
|||||||
@@ -261,7 +261,7 @@ const ProjectFlockForm = ({
|
|||||||
isLoadingOptions: isLoadingFlocks,
|
isLoadingOptions: isLoadingFlocks,
|
||||||
options: optionsFlock,
|
options: optionsFlock,
|
||||||
loadMore: loadMoreFlock,
|
loadMore: loadMoreFlock,
|
||||||
} = useSelect(FlockApi.basePath, 'id', 'name', '', {
|
} = useSelect(FlockApi.basePath, 'id', 'name', 'search', {
|
||||||
project_category: selectedCategory,
|
project_category: selectedCategory,
|
||||||
location_id: selectedLocation,
|
location_id: selectedLocation,
|
||||||
area_id: selectedArea,
|
area_id: selectedArea,
|
||||||
@@ -279,7 +279,7 @@ const ProjectFlockForm = ({
|
|||||||
isLoadingOptions: isLoadingLocations,
|
isLoadingOptions: isLoadingLocations,
|
||||||
setInputValue: setInputValueLocation,
|
setInputValue: setInputValueLocation,
|
||||||
loadMore: loadMoreLocation,
|
loadMore: loadMoreLocation,
|
||||||
} = useSelect(LocationApi.basePath, 'id', 'name', '', {
|
} = useSelect(LocationApi.basePath, 'id', 'name', 'search', {
|
||||||
area_id:
|
area_id:
|
||||||
selectedArea != ''
|
selectedArea != ''
|
||||||
? selectedArea
|
? selectedArea
|
||||||
@@ -291,7 +291,7 @@ const ProjectFlockForm = ({
|
|||||||
isLoadingOptions: isLoadingProductionStandards,
|
isLoadingOptions: isLoadingProductionStandards,
|
||||||
setInputValue: setInputValueProductionStandard,
|
setInputValue: setInputValueProductionStandard,
|
||||||
loadMore: loadMoreProductionStandard,
|
loadMore: loadMoreProductionStandard,
|
||||||
} = useSelect(ProductionStandardApi.basePath, 'id', 'name', '', {
|
} = useSelect(ProductionStandardApi.basePath, 'id', 'name', 'search', {
|
||||||
project_category: selectedCategory,
|
project_category: selectedCategory,
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -307,7 +307,7 @@ const ProjectFlockForm = ({
|
|||||||
} = useSWR(kandangUrl, KandangApi.getAllFetcher);
|
} = useSWR(kandangUrl, KandangApi.getAllFetcher);
|
||||||
|
|
||||||
const { data: periodFlocks, mutate: refreshPeriodFlocks } = useSWR(
|
const { data: periodFlocks, mutate: refreshPeriodFlocks } = useSWR(
|
||||||
`${selectedFlock?.toString()}/periods`,
|
selectedFlock ? `${selectedFlock?.toString()}/periods` : undefined,
|
||||||
() => ProjectFlockApi.getNextPeriod(parseInt(selectedLocation as string))
|
() => ProjectFlockApi.getNextPeriod(parseInt(selectedLocation as string))
|
||||||
);
|
);
|
||||||
|
|
||||||
@@ -793,6 +793,7 @@ const ProjectFlockForm = ({
|
|||||||
formik.values.kandang_ids?.includes(kandang.id)
|
formik.values.kandang_ids?.includes(kandang.id)
|
||||||
)?.period
|
)?.period
|
||||||
: undefined;
|
: undefined;
|
||||||
|
|
||||||
const inputPeriod =
|
const inputPeriod =
|
||||||
(initialValues?.period ?? selectedPeriod == 0) ? 1 : selectedPeriod;
|
(initialValues?.period ?? selectedPeriod == 0) ? 1 : selectedPeriod;
|
||||||
|
|
||||||
|
|||||||
@@ -611,7 +611,7 @@ const RecordingForm = ({ type = 'add', initialValues }: RecordingFormProps) => {
|
|||||||
} = useSelect(ProductWarehouseApi.basePath, 'id', 'product.name', 'search', {
|
} = useSelect(ProductWarehouseApi.basePath, 'id', 'product.name', 'search', {
|
||||||
flags: 'PAKAN,OVK',
|
flags: 'PAKAN,OVK',
|
||||||
limit: '100',
|
limit: '100',
|
||||||
available_only: 'true',
|
available_only: 'false',
|
||||||
location_id: stockProductsLocationId,
|
location_id: stockProductsLocationId,
|
||||||
...(selectedKandangId ? { kandang_id: selectedKandangId.toString() } : {}),
|
...(selectedKandangId ? { kandang_id: selectedKandangId.toString() } : {}),
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -3,7 +3,6 @@ import { Uniformity } from '@/types/api/production/uniformity';
|
|||||||
|
|
||||||
type UniformityFormSchemaType = {
|
type UniformityFormSchemaType = {
|
||||||
date: string;
|
date: string;
|
||||||
week: number;
|
|
||||||
location?: {
|
location?: {
|
||||||
value: number;
|
value: number;
|
||||||
label: string;
|
label: string;
|
||||||
@@ -45,10 +44,6 @@ const FileSchema = Yup.mixed<File>()
|
|||||||
export const UniformityFormSchema: Yup.ObjectSchema<UniformityFormSchemaType> =
|
export const UniformityFormSchema: Yup.ObjectSchema<UniformityFormSchemaType> =
|
||||||
Yup.object({
|
Yup.object({
|
||||||
date: Yup.string().required('Tanggal wajib diisi!'),
|
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({
|
location: Yup.object({
|
||||||
value: Yup.number().min(1).required(),
|
value: Yup.number().min(1).required(),
|
||||||
label: Yup.string().required(),
|
label: Yup.string().required(),
|
||||||
@@ -81,7 +76,6 @@ export type UniformityFormValues = Yup.InferType<typeof UniformityFormSchema>;
|
|||||||
|
|
||||||
export type UniformityFormData = {
|
export type UniformityFormData = {
|
||||||
date: string;
|
date: string;
|
||||||
week: number;
|
|
||||||
project_flock_kandang_id: number;
|
project_flock_kandang_id: number;
|
||||||
document: File | null;
|
document: File | null;
|
||||||
document_name: string;
|
document_name: string;
|
||||||
@@ -91,8 +85,7 @@ export const getUniformityFormInitialValues = (
|
|||||||
initialValues?: Partial<Uniformity>
|
initialValues?: Partial<Uniformity>
|
||||||
): UniformityFormValues => {
|
): UniformityFormValues => {
|
||||||
return {
|
return {
|
||||||
date: initialValues?.week ? '' : '',
|
date: '',
|
||||||
week: initialValues?.week ?? 0,
|
|
||||||
location: null,
|
location: null,
|
||||||
location_id: 0,
|
location_id: 0,
|
||||||
project_flock: null,
|
project_flock: null,
|
||||||
|
|||||||
@@ -27,7 +27,6 @@ import { LocationApi } from '@/services/api/master-data';
|
|||||||
import {
|
import {
|
||||||
ProjectFlockApi,
|
ProjectFlockApi,
|
||||||
ProjectFlockKandangApi,
|
ProjectFlockKandangApi,
|
||||||
RecordingApi,
|
|
||||||
} from '@/services/api/production';
|
} from '@/services/api/production';
|
||||||
import { UniformityApi } from '@/services/api/uniformity';
|
import { UniformityApi } from '@/services/api/uniformity';
|
||||||
import { isResponseError, isResponseSuccess } from '@/lib/api-helper';
|
import { isResponseError, isResponseSuccess } from '@/lib/api-helper';
|
||||||
@@ -40,7 +39,6 @@ import {
|
|||||||
ProjectFlockKandangLookup,
|
ProjectFlockKandangLookup,
|
||||||
ProjectFlock,
|
ProjectFlock,
|
||||||
} from '@/types/api/production/project-flock';
|
} from '@/types/api/production/project-flock';
|
||||||
import { Recording } from '@/types/api/production/recording';
|
|
||||||
import { Kandang } from '@/types/api/master-data/kandang';
|
import { Kandang } from '@/types/api/master-data/kandang';
|
||||||
import UniformityPreviewForm from '@/components/pages/production/uniformity/form/UniformityPreviewForm';
|
import UniformityPreviewForm from '@/components/pages/production/uniformity/form/UniformityPreviewForm';
|
||||||
import UniformityResultForm from '@/components/pages/production/uniformity/form/UniformityResultForm';
|
import UniformityResultForm from '@/components/pages/production/uniformity/form/UniformityResultForm';
|
||||||
@@ -204,23 +202,6 @@ const UniformityForm = ({
|
|||||||
? projectFlockKandangLookupData.data
|
? projectFlockKandangLookupData.data
|
||||||
: undefined;
|
: 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 =====
|
// ===== FORM CONFIGURATION =====
|
||||||
const formikInitialValues = useMemo<UniformityFormValues>(
|
const formikInitialValues = useMemo<UniformityFormValues>(
|
||||||
() => getUniformityFormInitialValues(initialValues),
|
() => getUniformityFormInitialValues(initialValues),
|
||||||
@@ -246,7 +227,6 @@ const UniformityForm = ({
|
|||||||
|
|
||||||
setUniformityFormData({
|
setUniformityFormData({
|
||||||
date: values.date,
|
date: values.date,
|
||||||
week: values.week,
|
|
||||||
project_flock_kandang_id: projectFlockKandangId,
|
project_flock_kandang_id: projectFlockKandangId,
|
||||||
document: values.document as File,
|
document: values.document as File,
|
||||||
document_name: (values.document as File).name,
|
document_name: (values.document as File).name,
|
||||||
@@ -475,59 +455,6 @@ const UniformityForm = ({
|
|||||||
generateUniformityTemplate(population, projectFlockKandangLookup);
|
generateUniformityTemplate(population, projectFlockKandangLookup);
|
||||||
}, [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(() => {
|
useEffect(() => {
|
||||||
const unsub = subscribeValidate(() => {
|
const unsub = subscribeValidate(() => {
|
||||||
setIsValid(true);
|
setIsValid(true);
|
||||||
|
|||||||
@@ -63,7 +63,6 @@ const UniformityResultForm = () => {
|
|||||||
try {
|
try {
|
||||||
const payload = {
|
const payload = {
|
||||||
date: uniformityFormData.date,
|
date: uniformityFormData.date,
|
||||||
week: uniformityFormData.week,
|
|
||||||
project_flock_kandang_id: uniformityFormData.project_flock_kandang_id,
|
project_flock_kandang_id: uniformityFormData.project_flock_kandang_id,
|
||||||
document: uniformityFormData.document,
|
document: uniformityFormData.document,
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -294,7 +294,6 @@ const PurchaseOrderAcceptApprovalForm = ({
|
|||||||
item.expedition_vendor_id || item.expedition_vendor?.id || null;
|
item.expedition_vendor_id || item.expedition_vendor?.id || null;
|
||||||
|
|
||||||
return {
|
return {
|
||||||
purchase_item: null,
|
|
||||||
purchase_item_id: item.id,
|
purchase_item_id: item.id,
|
||||||
received_date: item.received_date
|
received_date: item.received_date
|
||||||
? new Date(item.received_date).toISOString().split('T')[0]
|
? new Date(item.received_date).toISOString().split('T')[0]
|
||||||
@@ -573,7 +572,7 @@ const PurchaseOrderAcceptApprovalForm = ({
|
|||||||
<td>
|
<td>
|
||||||
<SelectInput
|
<SelectInput
|
||||||
isClearable={true}
|
isClearable={true}
|
||||||
value={formItem?.expedition_vendor}
|
value={formItem?.expedition_vendor ?? null}
|
||||||
key={`expedition-vendor-${idx}`}
|
key={`expedition-vendor-${idx}`}
|
||||||
onChange={(val) =>
|
onChange={(val) =>
|
||||||
expeditionVendorChangeHandler(idx, val)
|
expeditionVendorChangeHandler(idx, val)
|
||||||
|
|||||||
@@ -31,10 +31,6 @@ type PurchaseRequestAcceptApprovalFormSchemaType = {
|
|||||||
action: 'APPROVED' | 'REJECTED';
|
action: 'APPROVED' | 'REJECTED';
|
||||||
notes: string | null;
|
notes: string | null;
|
||||||
items: {
|
items: {
|
||||||
purchase_item?: {
|
|
||||||
value: number;
|
|
||||||
label: string;
|
|
||||||
} | null;
|
|
||||||
purchase_item_id: number;
|
purchase_item_id: number;
|
||||||
received_date: string;
|
received_date: string;
|
||||||
travel_number: string;
|
travel_number: string;
|
||||||
@@ -68,10 +64,6 @@ export type PurchaseStaffApprovalItemSchema = {
|
|||||||
};
|
};
|
||||||
|
|
||||||
export type PurchaseAcceptApprovalItemSchema = {
|
export type PurchaseAcceptApprovalItemSchema = {
|
||||||
purchase_item?: {
|
|
||||||
value: number;
|
|
||||||
label: string;
|
|
||||||
} | null;
|
|
||||||
purchase_item_id: number;
|
purchase_item_id: number;
|
||||||
received_date: string;
|
received_date: string;
|
||||||
travel_number: string;
|
travel_number: string;
|
||||||
@@ -160,12 +152,6 @@ const PurchaseManagerApprovalObjectSchema: Yup.ObjectSchema<PurchaseRequestManag
|
|||||||
|
|
||||||
const PurchaseAcceptApprovalItemObjectSchema: Yup.ObjectSchema<PurchaseAcceptApprovalItemSchema> =
|
const PurchaseAcceptApprovalItemObjectSchema: Yup.ObjectSchema<PurchaseAcceptApprovalItemSchema> =
|
||||||
Yup.object({
|
Yup.object({
|
||||||
purchase_item: Yup.object({
|
|
||||||
value: Yup.number().min(1).required(),
|
|
||||||
label: Yup.string().required(),
|
|
||||||
})
|
|
||||||
.nullable()
|
|
||||||
.optional(),
|
|
||||||
purchase_item_id: Yup.number()
|
purchase_item_id: Yup.number()
|
||||||
.min(1, 'Purchase item is required!')
|
.min(1, 'Purchase item is required!')
|
||||||
.required('Purchase item is required!')
|
.required('Purchase item is required!')
|
||||||
@@ -185,9 +171,8 @@ const PurchaseAcceptApprovalItemObjectSchema: Yup.ObjectSchema<PurchaseAcceptApp
|
|||||||
.typeError('No. Surat jalan wajib diisi!'),
|
.typeError('No. Surat jalan wajib diisi!'),
|
||||||
vehicle_number: Yup.string()
|
vehicle_number: Yup.string()
|
||||||
.nullable()
|
.nullable()
|
||||||
.when('expedition_vendor', {
|
.when('expedition_vendor_id', {
|
||||||
is: (expeditionVendor?: { value?: number; label?: string } | null) =>
|
is: (expeditionVendorId?: number | null) => Boolean(expeditionVendorId),
|
||||||
Boolean(expeditionVendor?.value),
|
|
||||||
then: (schema) => schema.required('Nomor kendaraan wajib diisi!'),
|
then: (schema) => schema.required('Nomor kendaraan wajib diisi!'),
|
||||||
otherwise: (schema) => schema.optional(),
|
otherwise: (schema) => schema.optional(),
|
||||||
})
|
})
|
||||||
@@ -196,6 +181,7 @@ const PurchaseAcceptApprovalItemObjectSchema: Yup.ObjectSchema<PurchaseAcceptApp
|
|||||||
value: Yup.number().min(1).required(),
|
value: Yup.number().min(1).required(),
|
||||||
label: Yup.string().required(),
|
label: Yup.string().required(),
|
||||||
})
|
})
|
||||||
|
.default(undefined)
|
||||||
.nullable()
|
.nullable()
|
||||||
.optional(),
|
.optional(),
|
||||||
expedition_vendor_id: Yup.number()
|
expedition_vendor_id: Yup.number()
|
||||||
@@ -218,9 +204,8 @@ const PurchaseAcceptApprovalItemObjectSchema: Yup.ObjectSchema<PurchaseAcceptApp
|
|||||||
.typeError('Jumlah diterima harus berupa angka!'),
|
.typeError('Jumlah diterima harus berupa angka!'),
|
||||||
transport_per_item: Yup.mixed<string | number>()
|
transport_per_item: Yup.mixed<string | number>()
|
||||||
.nullable()
|
.nullable()
|
||||||
.when('expedition_vendor', {
|
.when('expedition_vendor_id', {
|
||||||
is: (expeditionVendor?: { value?: number; label?: string } | null) =>
|
is: (expeditionVendorId?: number | null) => Boolean(expeditionVendorId),
|
||||||
Boolean(expeditionVendor?.value),
|
|
||||||
then: (schema) =>
|
then: (schema) =>
|
||||||
schema.required('Biaya transport per item wajib diisi!'),
|
schema.required('Biaya transport per item wajib diisi!'),
|
||||||
otherwise: (schema) => schema.optional(),
|
otherwise: (schema) => schema.optional(),
|
||||||
|
|||||||
@@ -4,7 +4,8 @@ import { useState } from 'react';
|
|||||||
import Tabs from '@/components/Tabs';
|
import Tabs from '@/components/Tabs';
|
||||||
|
|
||||||
import { useTabActionsStore } from '@/stores/tab-actions/tab-actions.store';
|
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 ReportExpenseTabs = () => {
|
||||||
const [activeTabId, setActiveTabId] = useState<string>('1');
|
const [activeTabId, setActiveTabId] = useState<string>('1');
|
||||||
@@ -16,6 +17,11 @@ const ReportExpenseTabs = () => {
|
|||||||
label: 'Laporan Biaya Operasional',
|
label: 'Laporan Biaya Operasional',
|
||||||
content: <ReportExpenseTab tabId={'1'} />,
|
content: <ReportExpenseTab tabId={'1'} />,
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
id: '2',
|
||||||
|
label: 'Laporan Depresiasi',
|
||||||
|
content: <ReportDepreciationTab tabId={'2'} />,
|
||||||
|
},
|
||||||
];
|
];
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
|||||||
@@ -1,27 +1,26 @@
|
|||||||
import React from 'react';
|
import React from 'react';
|
||||||
import DataStateSkeleton from '@/components/helper/skeleton/DataStateSkeleton';
|
import DataStateSkeleton from '@/components/helper/skeleton/DataStateSkeleton';
|
||||||
import Table from '@/components/Table';
|
import Table from '@/components/Table';
|
||||||
import { ReportExpense } from '@/types/api/report/report-expense';
|
|
||||||
import { ColumnDef } from '@tanstack/react-table';
|
import { ColumnDef } from '@tanstack/react-table';
|
||||||
|
|
||||||
type ReportExpenseColumn =
|
type ReportSkeletonColumn<TData extends object> =
|
||||||
| ColumnDef<ReportExpense>
|
| ColumnDef<TData>
|
||||||
| {
|
| {
|
||||||
header: string;
|
header: string;
|
||||||
columns: Array<{
|
columns: Array<{
|
||||||
header: string;
|
header: string;
|
||||||
accessorKey?: 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,
|
columns,
|
||||||
icon,
|
icon,
|
||||||
title,
|
title,
|
||||||
subtitle,
|
subtitle,
|
||||||
}: {
|
}: {
|
||||||
columns: ReportExpenseColumn[];
|
columns: ReportSkeletonColumn<TData>[];
|
||||||
icon: React.ReactNode;
|
icon: React.ReactNode;
|
||||||
title: string;
|
title: string;
|
||||||
subtitle: 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 Table from '@/components/Table';
|
||||||
import { formatCurrency, formatDate } from '@/lib/helper';
|
import { formatCurrency, formatDate } from '@/lib/helper';
|
||||||
import { ReportExpense } from '@/types/api/report/report-expense';
|
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 { isResponseSuccess } from '@/lib/api-helper';
|
||||||
import { useTabActionsStore } from '@/stores/tab-actions/tab-actions.store';
|
import { useTabActionsStore } from '@/stores/tab-actions/tab-actions.store';
|
||||||
import Modal, { useModal } from '@/components/Modal';
|
import Modal, { useModal } from '@/components/Modal';
|
||||||
|
|||||||
@@ -1,6 +1,8 @@
|
|||||||
import * as yup from 'yup';
|
import * as yup from 'yup';
|
||||||
|
|
||||||
export type DailyMarketingReportFilterType = {
|
export type DailyMarketingReportFilterType = {
|
||||||
|
page?: number;
|
||||||
|
pageSize?: number;
|
||||||
search: string | null;
|
search: string | null;
|
||||||
area_id: string | null;
|
area_id: string | null;
|
||||||
location_id: string | null;
|
location_id: string | null;
|
||||||
@@ -14,6 +16,8 @@ export type DailyMarketingReportFilterType = {
|
|||||||
};
|
};
|
||||||
|
|
||||||
export const DailyMarketingReportFilterSchema = yup.object({
|
export const DailyMarketingReportFilterSchema = yup.object({
|
||||||
|
page: yup.number().nullable(),
|
||||||
|
pageSize: yup.number().nullable(),
|
||||||
search: yup.string().nullable(),
|
search: yup.string().nullable(),
|
||||||
area_id: yup.string().nullable(),
|
area_id: yup.string().nullable(),
|
||||||
location_id: yup.string().nullable(),
|
location_id: yup.string().nullable(),
|
||||||
|
|||||||
@@ -1,6 +1,8 @@
|
|||||||
import * as yup from 'yup';
|
import * as yup from 'yup';
|
||||||
|
|
||||||
export type HppPerKandangFilterType = {
|
export type HppPerKandangFilterType = {
|
||||||
|
page?: number;
|
||||||
|
pageSize?: number;
|
||||||
area_id: string | null;
|
area_id: string | null;
|
||||||
location_id: string | null;
|
location_id: string | null;
|
||||||
kandang_id: string | null;
|
kandang_id: string | null;
|
||||||
@@ -12,6 +14,8 @@ export type HppPerKandangFilterType = {
|
|||||||
};
|
};
|
||||||
|
|
||||||
export const HppPerKandangFilterSchema = yup.object({
|
export const HppPerKandangFilterSchema = yup.object({
|
||||||
|
page: yup.number().nullable(),
|
||||||
|
pageSize: yup.number().nullable(),
|
||||||
area_id: yup.string().nullable(),
|
area_id: yup.string().nullable(),
|
||||||
location_id: yup.string().nullable(),
|
location_id: yup.string().nullable(),
|
||||||
kandang_id: yup.string().nullable(),
|
kandang_id: yup.string().nullable(),
|
||||||
|
|||||||
@@ -53,6 +53,8 @@ interface DailyMarketingTabProps {
|
|||||||
}
|
}
|
||||||
|
|
||||||
interface FilterParams {
|
interface FilterParams {
|
||||||
|
page?: number;
|
||||||
|
pageSize?: number;
|
||||||
area_id?: string;
|
area_id?: string;
|
||||||
location_id?: string;
|
location_id?: string;
|
||||||
warehouse_id?: string;
|
warehouse_id?: string;
|
||||||
@@ -116,6 +118,8 @@ const DailyMarketingTab = ({ tabId }: DailyMarketingTabProps) => {
|
|||||||
// ===== FORMIK SETUP =====
|
// ===== FORMIK SETUP =====
|
||||||
const formik = useFormik<DailyMarketingReportFilterType>({
|
const formik = useFormik<DailyMarketingReportFilterType>({
|
||||||
initialValues: {
|
initialValues: {
|
||||||
|
page: 1,
|
||||||
|
pageSize: 10,
|
||||||
search: null,
|
search: null,
|
||||||
area_id: null,
|
area_id: null,
|
||||||
location_id: null,
|
location_id: null,
|
||||||
@@ -130,6 +134,8 @@ const DailyMarketingTab = ({ tabId }: DailyMarketingTabProps) => {
|
|||||||
validationSchema: DailyMarketingReportFilterSchema,
|
validationSchema: DailyMarketingReportFilterSchema,
|
||||||
onSubmit: (values, { setSubmitting }) => {
|
onSubmit: (values, { setSubmitting }) => {
|
||||||
setFilterParams({
|
setFilterParams({
|
||||||
|
page: values.page || undefined,
|
||||||
|
pageSize: values.pageSize || undefined,
|
||||||
area_id: values.area_id || undefined,
|
area_id: values.area_id || undefined,
|
||||||
location_id: values.location_id || undefined,
|
location_id: values.location_id || undefined,
|
||||||
warehouse_id: values.warehouse_id || undefined,
|
warehouse_id: values.warehouse_id || undefined,
|
||||||
@@ -222,6 +228,9 @@ const DailyMarketingTab = ({ tabId }: DailyMarketingTabProps) => {
|
|||||||
const params = new URLSearchParams();
|
const params = new URLSearchParams();
|
||||||
|
|
||||||
if (searchValue) params.set('search', searchValue);
|
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.area_id) params.set('area_id', filterParams.area_id);
|
||||||
if (filterParams.location_id)
|
if (filterParams.location_id)
|
||||||
params.set('location_id', filterParams.location_id);
|
params.set('location_id', filterParams.location_id);
|
||||||
@@ -283,6 +292,7 @@ const DailyMarketingTab = ({ tabId }: DailyMarketingTabProps) => {
|
|||||||
if (filterParams.marketing_type)
|
if (filterParams.marketing_type)
|
||||||
params.set('marketing_type', filterParams.marketing_type);
|
params.set('marketing_type', filterParams.marketing_type);
|
||||||
if (filterParams.sort_by) params.set('sort_by', filterParams.sort_by);
|
if (filterParams.sort_by) params.set('sort_by', filterParams.sort_by);
|
||||||
|
params.set('page', '1');
|
||||||
params.set('limit', '9999999');
|
params.set('limit', '9999999');
|
||||||
|
|
||||||
const queryString = `?${params.toString()}`;
|
const queryString = `?${params.toString()}`;
|
||||||
@@ -688,6 +698,27 @@ const DailyMarketingTab = ({ tabId }: DailyMarketingTabProps) => {
|
|||||||
<Table
|
<Table
|
||||||
data={data}
|
data={data}
|
||||||
columns={getTableColumns()}
|
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}
|
renderFooter={data.length > 0}
|
||||||
className={{
|
className={{
|
||||||
containerClassName: 'w-full mb-0!',
|
containerClassName: 'w-full mb-0!',
|
||||||
|
|||||||
@@ -40,6 +40,8 @@ interface HppPerKandangTabProps {
|
|||||||
}
|
}
|
||||||
|
|
||||||
interface FilterParams {
|
interface FilterParams {
|
||||||
|
page?: number;
|
||||||
|
pageSize?: number;
|
||||||
area_id?: string;
|
area_id?: string;
|
||||||
location_id?: string;
|
location_id?: string;
|
||||||
kandang_id?: string;
|
kandang_id?: string;
|
||||||
@@ -108,6 +110,8 @@ const HppPerKandangTab = ({ tabId }: HppPerKandangTabProps) => {
|
|||||||
// ===== FORMIK SETUP =====
|
// ===== FORMIK SETUP =====
|
||||||
const formik = useFormik<HppPerKandangFilterType>({
|
const formik = useFormik<HppPerKandangFilterType>({
|
||||||
initialValues: {
|
initialValues: {
|
||||||
|
page: 1,
|
||||||
|
pageSize: 10,
|
||||||
area_id: null,
|
area_id: null,
|
||||||
location_id: null,
|
location_id: null,
|
||||||
kandang_id: null,
|
kandang_id: null,
|
||||||
@@ -120,6 +124,8 @@ const HppPerKandangTab = ({ tabId }: HppPerKandangTabProps) => {
|
|||||||
validationSchema: HppPerKandangFilterSchema,
|
validationSchema: HppPerKandangFilterSchema,
|
||||||
onSubmit: (values, { setSubmitting }) => {
|
onSubmit: (values, { setSubmitting }) => {
|
||||||
setFilterParams({
|
setFilterParams({
|
||||||
|
page: values.page || undefined,
|
||||||
|
pageSize: values.pageSize || undefined,
|
||||||
area_id: values.area_id || undefined,
|
area_id: values.area_id || undefined,
|
||||||
location_id: values.location_id || undefined,
|
location_id: values.location_id || undefined,
|
||||||
kandang_id: values.kandang_id || undefined,
|
kandang_id: values.kandang_id || undefined,
|
||||||
@@ -257,6 +263,8 @@ const HppPerKandangTab = ({ tabId }: HppPerKandangTabProps) => {
|
|||||||
period: filterParams.period,
|
period: filterParams.period,
|
||||||
sort_by: filterParams.sort_by,
|
sort_by: filterParams.sort_by,
|
||||||
show_unrecorded: filterParams.show_unrecorded,
|
show_unrecorded: filterParams.show_unrecorded,
|
||||||
|
page: filterParams.page,
|
||||||
|
pageSize: filterParams.pageSize,
|
||||||
};
|
};
|
||||||
|
|
||||||
return ['hpp-per-kandang-report', params];
|
return ['hpp-per-kandang-report', params];
|
||||||
@@ -271,7 +279,9 @@ const HppPerKandangTab = ({ tabId }: HppPerKandangTabProps) => {
|
|||||||
params.weight_max,
|
params.weight_max,
|
||||||
params.period,
|
params.period,
|
||||||
params.sort_by,
|
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.weight_max,
|
||||||
params.period,
|
params.period,
|
||||||
params.sort_by,
|
params.sort_by,
|
||||||
params.show_unrecorded
|
params.show_unrecorded,
|
||||||
|
params.page,
|
||||||
|
params.limit
|
||||||
);
|
);
|
||||||
|
|
||||||
return isResponseSuccess(response) ? response.data : null;
|
return isResponseSuccess(response) ? response.data : null;
|
||||||
@@ -466,6 +478,7 @@ const HppPerKandangTab = ({ tabId }: HppPerKandangTabProps) => {
|
|||||||
<div className='flex flex-row gap-3'>
|
<div className='flex flex-row gap-3'>
|
||||||
<ButtonFilter
|
<ButtonFilter
|
||||||
values={filterParams}
|
values={filterParams}
|
||||||
|
excludeFields={['page', 'pageSize']}
|
||||||
onClick={() => handleFilterModalOpenRef.current()}
|
onClick={() => handleFilterModalOpenRef.current()}
|
||||||
variant='outline'
|
variant='outline'
|
||||||
className='px-3 py-2.5'
|
className='px-3 py-2.5'
|
||||||
@@ -845,6 +858,25 @@ const HppPerKandangTab = ({ tabId }: HppPerKandangTabProps) => {
|
|||||||
<Table
|
<Table
|
||||||
data={data}
|
data={data}
|
||||||
columns={getTableColumns()}
|
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}
|
renderFooter={data.length > 0}
|
||||||
renderCustomRow={renderCustomRow}
|
renderCustomRow={renderCustomRow}
|
||||||
className={{
|
className={{
|
||||||
|
|||||||
+314
-29
@@ -40,11 +40,12 @@ import { isResponseError, isResponseSuccess } from '@/lib/api-helper';
|
|||||||
import Table from '@/components/Table';
|
import Table from '@/components/Table';
|
||||||
import { DailyChecklist } from '@/types/api/daily-checklist/daily-checklist';
|
import { DailyChecklist } from '@/types/api/daily-checklist/daily-checklist';
|
||||||
import { cn } from '@/lib/helper';
|
import { cn } from '@/lib/helper';
|
||||||
import { ColumnDef } from '@tanstack/react-table';
|
import { ColumnDef, Row } from '@tanstack/react-table';
|
||||||
import { useSelect } from '@/components/input/SelectInput';
|
import { useSelect } from '@/components/input/SelectInput';
|
||||||
import DebouncedTextInput from '@/components/input/DebouncedTextInput';
|
import DebouncedTextInput from '@/components/input/DebouncedTextInput';
|
||||||
import RequirePermission from '@/components/helper/RequirePermission';
|
import RequirePermission from '@/components/helper/RequirePermission';
|
||||||
import { DailyChecklistKandangApi } from '@/services/api/daily-checklist/kandang';
|
import { DailyChecklistKandangApi } from '@/services/api/daily-checklist/kandang';
|
||||||
|
import CheckboxInput from '@/components/input/CheckboxInput';
|
||||||
|
|
||||||
const STATUS_OPTIONS = [
|
const STATUS_OPTIONS = [
|
||||||
{ value: 'ALL', label: 'Semua Status' },
|
{ value: 'ALL', label: 'Semua Status' },
|
||||||
@@ -87,6 +88,9 @@ export function ListDailyChecklistContent() {
|
|||||||
date_from: 'date_from',
|
date_from: 'date_from',
|
||||||
date_to: 'date_to',
|
date_to: 'date_to',
|
||||||
},
|
},
|
||||||
|
|
||||||
|
persist: true,
|
||||||
|
storeName: 'list-daily-checklist-content-table',
|
||||||
});
|
});
|
||||||
|
|
||||||
const {
|
const {
|
||||||
@@ -122,12 +126,29 @@ export function ListDailyChecklistContent() {
|
|||||||
|
|
||||||
// Modals
|
// Modals
|
||||||
const [showApproveModal, setShowApproveModal] = useState(false);
|
const [showApproveModal, setShowApproveModal] = useState(false);
|
||||||
|
const [showBulkApproveModal, setShowBulkApproveModal] = useState(false);
|
||||||
const [showRejectModal, setShowRejectModal] = useState(false);
|
const [showRejectModal, setShowRejectModal] = useState(false);
|
||||||
|
const [showBulkRejectModal, setShowBulkRejectModal] = useState(false);
|
||||||
const [showDeleteModal, setShowDeleteModal] = useState(false);
|
const [showDeleteModal, setShowDeleteModal] = useState(false);
|
||||||
const [selectedItem, setSelectedItem] = useState<DailyChecklist | null>(null);
|
const [selectedItem, setSelectedItem] = useState<DailyChecklist | null>(null);
|
||||||
const [rejectReason, setRejectReason] = useState('');
|
const [rejectReason, setRejectReason] = useState('');
|
||||||
const [actionLoading, setActionLoading] = useState(false);
|
const [actionLoading, setActionLoading] = useState(false);
|
||||||
|
|
||||||
|
const [rowSelection, setRowSelection] = useState<Record<string, boolean>>({});
|
||||||
|
const selectedRowIds = Object.keys(rowSelection);
|
||||||
|
|
||||||
|
const selectedRowItems = selectedRowIds.map((itemId) =>
|
||||||
|
checklistList.find((item) => item.id === parseInt(itemId))
|
||||||
|
);
|
||||||
|
|
||||||
|
const tableEnableRowSelectionHandler: (
|
||||||
|
row: Row<DailyChecklist>
|
||||||
|
) => boolean = (row) => {
|
||||||
|
return (
|
||||||
|
row.original.status !== 'APPROVED' && row.original.status !== 'REJECTED'
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
const handleDetail = (item: DailyChecklist) => {
|
const handleDetail = (item: DailyChecklist) => {
|
||||||
router.push(
|
router.push(
|
||||||
`/daily-checklist/list-daily-checklist/detail?checklistId=${item.id}`
|
`/daily-checklist/list-daily-checklist/detail?checklistId=${item.id}`
|
||||||
@@ -149,21 +170,22 @@ export function ListDailyChecklistContent() {
|
|||||||
setShowApproveModal(true);
|
setShowApproveModal(true);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const handleBulkApprove = () => {
|
||||||
|
setShowBulkApproveModal(true);
|
||||||
|
};
|
||||||
|
|
||||||
const handleReject = (item: DailyChecklist) => {
|
const handleReject = (item: DailyChecklist) => {
|
||||||
setSelectedItem(item);
|
setSelectedItem(item);
|
||||||
setRejectReason('');
|
setRejectReason('');
|
||||||
setShowRejectModal(true);
|
setShowRejectModal(true);
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleDelete = (item: DailyChecklist) => {
|
const handleBulkReject = () => {
|
||||||
// ✅ VALIDATION: Only DRAFT can be deleted
|
setRejectReason('');
|
||||||
if (item.status !== 'DRAFT') {
|
setShowBulkRejectModal(true);
|
||||||
toast.error('Hanya checklist dengan status DRAFT yang bisa dihapus', {
|
};
|
||||||
description: `Status saat ini: ${item.status}`,
|
|
||||||
});
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
|
const handleDelete = (item: DailyChecklist) => {
|
||||||
setSelectedItem(item);
|
setSelectedItem(item);
|
||||||
setShowDeleteModal(true);
|
setShowDeleteModal(true);
|
||||||
};
|
};
|
||||||
@@ -195,6 +217,31 @@ export function ListDailyChecklistContent() {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const confirmBulkApprove = async () => {
|
||||||
|
if (!selectedRowIds.length) return;
|
||||||
|
|
||||||
|
try {
|
||||||
|
setActionLoading(true);
|
||||||
|
|
||||||
|
const approveRes = await DailyChecklistApi.bulkApprove(selectedRowIds);
|
||||||
|
|
||||||
|
if (isResponseError(approveRes)) {
|
||||||
|
toast.error('Gagal approve checklist: ' + approveRes.message);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
refreshChecklistList();
|
||||||
|
toast.success('Checklist berhasil di-approve');
|
||||||
|
setShowBulkApproveModal(false);
|
||||||
|
setRowSelection({});
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error approving checklist:', error);
|
||||||
|
toast.error('Terjadi kesalahan');
|
||||||
|
} finally {
|
||||||
|
setActionLoading(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
const confirmReject = async () => {
|
const confirmReject = async () => {
|
||||||
if (!selectedItem) return;
|
if (!selectedItem) return;
|
||||||
|
|
||||||
@@ -229,6 +276,40 @@ export function ListDailyChecklistContent() {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const confirmBulkReject = async () => {
|
||||||
|
if (!selectedRowIds.length) return;
|
||||||
|
|
||||||
|
if (!rejectReason.trim()) {
|
||||||
|
toast.error('Alasan reject harus diisi');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
setActionLoading(true);
|
||||||
|
|
||||||
|
const rejectRes = await DailyChecklistApi.bulkReject(
|
||||||
|
selectedRowIds,
|
||||||
|
rejectReason
|
||||||
|
);
|
||||||
|
|
||||||
|
if (isResponseError(rejectRes)) {
|
||||||
|
toast.error('Gagal reject checklist: ' + rejectRes.message);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
refreshChecklistList();
|
||||||
|
toast.success('Checklist berhasil di-reject');
|
||||||
|
setShowBulkRejectModal(false);
|
||||||
|
setRowSelection({});
|
||||||
|
setRejectReason('');
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error rejecting checklist:', error);
|
||||||
|
toast.error('Terjadi kesalahan');
|
||||||
|
} finally {
|
||||||
|
setActionLoading(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
const confirmDelete = async () => {
|
const confirmDelete = async () => {
|
||||||
if (!selectedItem) return;
|
if (!selectedItem) return;
|
||||||
|
|
||||||
@@ -325,6 +406,37 @@ export function ListDailyChecklistContent() {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const checklistListColumns: ColumnDef<DailyChecklist>[] = [
|
const checklistListColumns: ColumnDef<DailyChecklist>[] = [
|
||||||
|
{
|
||||||
|
id: 'select',
|
||||||
|
header: ({ table }) => (
|
||||||
|
<div className='w-full flex flex-row justify-center'>
|
||||||
|
<CheckboxInput
|
||||||
|
name='allRow'
|
||||||
|
checked={table.getIsAllRowsSelected()}
|
||||||
|
indeterminate={table.getIsSomeRowsSelected()}
|
||||||
|
onChange={table.getToggleAllRowsSelectedHandler()}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
),
|
||||||
|
cell: ({ row }) => {
|
||||||
|
const isCheckboxDisabled =
|
||||||
|
!row.getCanSelect() ||
|
||||||
|
row.original.status === 'APPROVED' ||
|
||||||
|
row.original.status === 'REJECTED';
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
<CheckboxInput
|
||||||
|
name='row'
|
||||||
|
checked={row.getIsSelected()}
|
||||||
|
disabled={isCheckboxDisabled}
|
||||||
|
indeterminate={row.getIsSomeSelected()}
|
||||||
|
onChange={row.getToggleSelectedHandler()}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
},
|
||||||
|
},
|
||||||
{
|
{
|
||||||
accessorKey: 'date',
|
accessorKey: 'date',
|
||||||
header: 'Tanggal',
|
header: 'Tanggal',
|
||||||
@@ -437,19 +549,17 @@ export function ListDailyChecklistContent() {
|
|||||||
</RequirePermission>
|
</RequirePermission>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{row.original.status === 'DRAFT' && (
|
<RequirePermission permissions='lti.daily_checklist.create'>
|
||||||
<RequirePermission permissions='lti.daily_checklist.create'>
|
<Button
|
||||||
<Button
|
size='sm'
|
||||||
size='sm'
|
variant='destructive'
|
||||||
variant='destructive'
|
onClick={() => handleDelete(row.original)}
|
||||||
onClick={() => handleDelete(row.original)}
|
className='bg-red-600 hover:bg-red-700 text-white'
|
||||||
className='bg-red-600 hover:bg-red-700 text-white'
|
>
|
||||||
>
|
<Trash2 className='w-4 h-4 mr-1' />
|
||||||
<Trash2 className='w-4 h-4 mr-1' />
|
Hapus
|
||||||
Hapus
|
</Button>
|
||||||
</Button>
|
</RequirePermission>
|
||||||
</RequirePermission>
|
|
||||||
)}
|
|
||||||
</div>
|
</div>
|
||||||
),
|
),
|
||||||
},
|
},
|
||||||
@@ -459,13 +569,39 @@ export function ListDailyChecklistContent() {
|
|||||||
<div className='min-h-screen'>
|
<div className='min-h-screen'>
|
||||||
<div className='p-6'>
|
<div className='p-6'>
|
||||||
{/* Page Title */}
|
{/* Page Title */}
|
||||||
<div className='mb-6'>
|
<div className='mb-6 flex flex-row justify-between items-center gap-3'>
|
||||||
<h1 className='text-2xl font-semibold text-gray-900'>
|
<div>
|
||||||
List Daily Checklist
|
<h1 className='text-2xl font-semibold text-gray-900'>
|
||||||
</h1>
|
List Daily Checklist
|
||||||
<p className='text-sm text-gray-600 mt-1'>
|
</h1>
|
||||||
Daftar semua checklist harian
|
<p className='text-sm text-gray-600 mt-1'>
|
||||||
</p>
|
Daftar semua checklist harian
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<RequirePermission permissions='lti.daily_checklist.create'>
|
||||||
|
{selectedRowIds.length > 0 && (
|
||||||
|
<div className='flex flex-row items-center gap-3'>
|
||||||
|
<Button
|
||||||
|
size='sm'
|
||||||
|
onClick={handleBulkApprove}
|
||||||
|
className='bg-green-600 hover:bg-green-700 text-white'
|
||||||
|
>
|
||||||
|
<CheckCircle className='w-4 h-4 mr-1' />
|
||||||
|
Bulk Approve {`(${selectedRowIds.length}) item`}
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
size='sm'
|
||||||
|
variant='destructive'
|
||||||
|
onClick={handleBulkReject}
|
||||||
|
className='bg-red-600 hover:bg-red-700 text-white'
|
||||||
|
>
|
||||||
|
<XCircle className='w-4 h-4 mr-1' />
|
||||||
|
Bulk Reject {`(${selectedRowIds.length}) item`}
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</RequirePermission>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Main Card */}
|
{/* Main Card */}
|
||||||
@@ -588,6 +724,10 @@ export function ListDailyChecklistContent() {
|
|||||||
}
|
}
|
||||||
onPageChange={setPage}
|
onPageChange={setPage}
|
||||||
isLoading={isLoadingChecklistList}
|
isLoading={isLoadingChecklistList}
|
||||||
|
rowSelection={rowSelection}
|
||||||
|
setRowSelection={setRowSelection}
|
||||||
|
enableRowSelection={tableEnableRowSelectionHandler}
|
||||||
|
withCheckbox
|
||||||
className={{
|
className={{
|
||||||
containerClassName: cn({
|
containerClassName: cn({
|
||||||
'w-full mb-20':
|
'w-full mb-20':
|
||||||
@@ -666,6 +806,76 @@ export function ListDailyChecklistContent() {
|
|||||||
</DialogContent>
|
</DialogContent>
|
||||||
</Dialog>
|
</Dialog>
|
||||||
|
|
||||||
|
{/* Bulk Approve Modal */}
|
||||||
|
<Dialog
|
||||||
|
open={showBulkApproveModal}
|
||||||
|
onOpenChange={setShowBulkApproveModal}
|
||||||
|
>
|
||||||
|
<DialogContent className='sm:max-w-md max-h-[80vh] overflow-y-auto bg-white rounded-xl shadow-lg'>
|
||||||
|
<DialogHeader>
|
||||||
|
<DialogTitle>Approve Checklist</DialogTitle>
|
||||||
|
<DialogDescription>
|
||||||
|
Apakah Anda yakin ingin approve {selectedRowIds.length} checklist
|
||||||
|
ini?
|
||||||
|
</DialogDescription>
|
||||||
|
</DialogHeader>
|
||||||
|
|
||||||
|
<div className='max-h-[60vh] overflow-y-auto flex flex-col gap-3'>
|
||||||
|
{selectedRowItems.map((item) => (
|
||||||
|
<div
|
||||||
|
key={item?.id ?? 0}
|
||||||
|
className='bg-gray-50 rounded-lg p-4 space-y-2'
|
||||||
|
>
|
||||||
|
<div className='flex justify-between text-sm'>
|
||||||
|
<span className='text-gray-600'>Tanggal:</span>
|
||||||
|
<span className='font-medium text-gray-900'>
|
||||||
|
{formatDate(item?.date ?? '')}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<div className='flex justify-between text-sm'>
|
||||||
|
<span className='text-gray-600'>Kandang:</span>
|
||||||
|
<span className='font-medium text-gray-900'>
|
||||||
|
{item?.kandang?.name ?? '-'}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<div className='flex justify-between text-sm'>
|
||||||
|
<span className='text-gray-600'>Kategori:</span>
|
||||||
|
<span className='font-medium text-gray-900'>
|
||||||
|
{item?.category
|
||||||
|
? (CATEGORY_LABELS[item.category] ?? item?.category)
|
||||||
|
: item?.category}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<div className='flex justify-between text-sm'>
|
||||||
|
<span className='text-gray-600'>Progress:</span>
|
||||||
|
<span className='font-medium text-gray-900'>
|
||||||
|
{item?.progress}%
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<DialogFooter className='flex gap-2'>
|
||||||
|
<Button
|
||||||
|
variant='outline'
|
||||||
|
onClick={() => setShowBulkApproveModal(false)}
|
||||||
|
disabled={actionLoading}
|
||||||
|
className='border-gray-200'
|
||||||
|
>
|
||||||
|
Batal
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
onClick={confirmBulkApprove}
|
||||||
|
disabled={actionLoading}
|
||||||
|
className='bg-green-600 hover:bg-green-700 text-white'
|
||||||
|
>
|
||||||
|
{actionLoading ? 'Memproses...' : 'Ya, Approve'}
|
||||||
|
</Button>
|
||||||
|
</DialogFooter>
|
||||||
|
</DialogContent>
|
||||||
|
</Dialog>
|
||||||
|
|
||||||
{/* Reject Modal */}
|
{/* Reject Modal */}
|
||||||
<Dialog open={showRejectModal} onOpenChange={setShowRejectModal}>
|
<Dialog open={showRejectModal} onOpenChange={setShowRejectModal}>
|
||||||
<DialogContent className='sm:max-w-md bg-white rounded-xl shadow-lg'>
|
<DialogContent className='sm:max-w-md bg-white rounded-xl shadow-lg'>
|
||||||
@@ -735,6 +945,81 @@ export function ListDailyChecklistContent() {
|
|||||||
</DialogContent>
|
</DialogContent>
|
||||||
</Dialog>
|
</Dialog>
|
||||||
|
|
||||||
|
{/* Bulk Reject Modal */}
|
||||||
|
<Dialog open={showBulkRejectModal} onOpenChange={setShowBulkRejectModal}>
|
||||||
|
<DialogContent className='sm:max-w-md bg-white rounded-xl shadow-lg'>
|
||||||
|
<DialogHeader>
|
||||||
|
<DialogTitle>Reject Checklist</DialogTitle>
|
||||||
|
<DialogDescription>
|
||||||
|
Berikan alasan reject untuk checklist ini
|
||||||
|
</DialogDescription>
|
||||||
|
</DialogHeader>
|
||||||
|
|
||||||
|
<div className='max-h-[60vh] overflow-y-auto flex flex-col gap-3'>
|
||||||
|
{selectedRowItems.map((item) => (
|
||||||
|
<div
|
||||||
|
key={item?.id ?? 0}
|
||||||
|
className='bg-gray-50 rounded-lg p-4 space-y-2 mb-4'
|
||||||
|
>
|
||||||
|
<div className='flex justify-between text-sm'>
|
||||||
|
<span className='text-gray-600'>Tanggal:</span>
|
||||||
|
<span className='font-medium text-gray-900'>
|
||||||
|
{formatDate(item?.date ?? '')}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<div className='flex justify-between text-sm'>
|
||||||
|
<span className='text-gray-600'>Kandang:</span>
|
||||||
|
<span className='font-medium text-gray-900'>
|
||||||
|
{item?.kandang?.name ?? '-'}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<div className='flex justify-between text-sm'>
|
||||||
|
<span className='text-gray-600'>Kategori:</span>
|
||||||
|
<span className='font-medium text-gray-900'>
|
||||||
|
{item?.category
|
||||||
|
? CATEGORY_LABELS[item.category] || item?.category
|
||||||
|
: item?.category}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<Label htmlFor='reject-reason'>
|
||||||
|
Alasan Reject <span className='text-red-500'>*</span>
|
||||||
|
</Label>
|
||||||
|
<Textarea
|
||||||
|
id='reject-reason'
|
||||||
|
value={rejectReason}
|
||||||
|
onChange={(e) => setRejectReason(e.target.value)}
|
||||||
|
placeholder='Tuliskan alasan reject...'
|
||||||
|
className='mt-1.5 border-gray-200 min-h-[100px]'
|
||||||
|
disabled={actionLoading}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<DialogFooter className='flex gap-2'>
|
||||||
|
<Button
|
||||||
|
variant='outline'
|
||||||
|
onClick={() => setShowBulkRejectModal(false)}
|
||||||
|
disabled={actionLoading}
|
||||||
|
className='border-gray-200'
|
||||||
|
>
|
||||||
|
Batal
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
onClick={confirmBulkReject}
|
||||||
|
disabled={actionLoading}
|
||||||
|
variant='destructive'
|
||||||
|
className='bg-red-600 hover:bg-red-700 text-white'
|
||||||
|
>
|
||||||
|
{actionLoading ? 'Memproses...' : 'Ya, Reject'}
|
||||||
|
</Button>
|
||||||
|
</DialogFooter>
|
||||||
|
</DialogContent>
|
||||||
|
</Dialog>
|
||||||
|
|
||||||
{/* Delete Modal */}
|
{/* Delete Modal */}
|
||||||
<Dialog open={showDeleteModal} onOpenChange={setShowDeleteModal}>
|
<Dialog open={showDeleteModal} onOpenChange={setShowDeleteModal}>
|
||||||
<DialogContent className='sm:max-w-md bg-white rounded-xl shadow-lg'>
|
<DialogContent className='sm:max-w-md bg-white rounded-xl shadow-lg'>
|
||||||
|
|||||||
@@ -49,9 +49,8 @@ import { cn } from '@/lib/helper';
|
|||||||
import { useTableFilter } from '@/services/hooks/useTableFilter';
|
import { useTableFilter } from '@/services/hooks/useTableFilter';
|
||||||
import { ColumnDef } from '@tanstack/react-table';
|
import { ColumnDef } from '@tanstack/react-table';
|
||||||
import { useSelect } from '@/components/input/SelectInput';
|
import { useSelect } from '@/components/input/SelectInput';
|
||||||
import { KandangApi, LocationApi } from '@/services/api/master-data';
|
import { LocationApi } from '@/services/api/master-data';
|
||||||
import DebouncedTextInput from '@/components/input/DebouncedTextInput';
|
import DebouncedTextInput from '@/components/input/DebouncedTextInput';
|
||||||
import { BaseDailyChecklistKandang } from '@/types/api/daily-checklist/kandang';
|
|
||||||
import { UserApi } from '@/services/api/user';
|
import { UserApi } from '@/services/api/user';
|
||||||
|
|
||||||
export function MasterKandangContent() {
|
export function MasterKandangContent() {
|
||||||
@@ -108,12 +107,6 @@ export function MasterKandangContent() {
|
|||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
|
||||||
const {
|
|
||||||
options: kandangOptions,
|
|
||||||
isLoadingMore: isLoadingKandangOptionsMore,
|
|
||||||
loadMore: loadMoreKandang,
|
|
||||||
} = useSelect(KandangApi.basePath, 'id', 'name');
|
|
||||||
|
|
||||||
const [showModal, setShowModal] = useState(false);
|
const [showModal, setShowModal] = useState(false);
|
||||||
const [showDeleteConfirm, setShowDeleteConfirm] = useState(false);
|
const [showDeleteConfirm, setShowDeleteConfirm] = useState(false);
|
||||||
const [kandangToDelete, setKandangToDelete] = useState<number | null>(null);
|
const [kandangToDelete, setKandangToDelete] = useState<number | null>(null);
|
||||||
|
|||||||
@@ -0,0 +1,39 @@
|
|||||||
|
type SearchParamsLike = {
|
||||||
|
get: (name: string) => string | null;
|
||||||
|
};
|
||||||
|
|
||||||
|
const EXPENSE_LIST_PATH = '/expense';
|
||||||
|
|
||||||
|
export const getExpenseListReturnTo = (searchParams: SearchParamsLike) => {
|
||||||
|
const existingReturnTo = searchParams.get('returnTo');
|
||||||
|
|
||||||
|
if (existingReturnTo?.startsWith(EXPENSE_LIST_PATH)) {
|
||||||
|
return existingReturnTo;
|
||||||
|
}
|
||||||
|
|
||||||
|
const params = new URLSearchParams();
|
||||||
|
const page = searchParams.get('page');
|
||||||
|
const limit = searchParams.get('limit');
|
||||||
|
|
||||||
|
if (page) params.set('page', page);
|
||||||
|
if (limit) params.set('limit', limit);
|
||||||
|
|
||||||
|
const queryString = params.toString();
|
||||||
|
|
||||||
|
return queryString
|
||||||
|
? `${EXPENSE_LIST_PATH}?${queryString}`
|
||||||
|
: EXPENSE_LIST_PATH;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const buildExpenseActionHref = (
|
||||||
|
path: string,
|
||||||
|
expenseId: number | string,
|
||||||
|
searchParams: SearchParamsLike
|
||||||
|
) => {
|
||||||
|
const params = new URLSearchParams({
|
||||||
|
expenseId: String(expenseId),
|
||||||
|
returnTo: getExpenseListReturnTo(searchParams),
|
||||||
|
});
|
||||||
|
|
||||||
|
return `${path}?${params.toString()}`;
|
||||||
|
};
|
||||||
@@ -192,6 +192,29 @@ export class DailyChecklistApiService extends BaseApiService<
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async bulkApprove(ids: string[]) {
|
||||||
|
try {
|
||||||
|
const formData = new FormData();
|
||||||
|
|
||||||
|
formData.append('ids', ids.join(','));
|
||||||
|
formData.append('status', 'APPROVED');
|
||||||
|
formData.append('reject_reason', '');
|
||||||
|
|
||||||
|
const approvePath = `${this.basePath}/bulk-update`;
|
||||||
|
const approveRes = await httpClient<BaseApiResponse>(approvePath, {
|
||||||
|
method: 'PATCH',
|
||||||
|
body: formData,
|
||||||
|
});
|
||||||
|
|
||||||
|
return approveRes;
|
||||||
|
} catch (error) {
|
||||||
|
if (axios.isAxiosError<BaseApiResponse>(error)) {
|
||||||
|
return error.response?.data;
|
||||||
|
}
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
async reject(id: string, rejectReason: string) {
|
async reject(id: string, rejectReason: string) {
|
||||||
try {
|
try {
|
||||||
const formData = new FormData();
|
const formData = new FormData();
|
||||||
@@ -215,6 +238,29 @@ export class DailyChecklistApiService extends BaseApiService<
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async bulkReject(ids: string[], rejectReason: string) {
|
||||||
|
try {
|
||||||
|
const formData = new FormData();
|
||||||
|
|
||||||
|
formData.append('ids', ids.join(','));
|
||||||
|
formData.append('status', 'REJECTED');
|
||||||
|
formData.append('reject_reason', rejectReason);
|
||||||
|
|
||||||
|
const rejectPath = `${this.basePath}/bulk-update`;
|
||||||
|
const rejectRes = await httpClient<BaseApiResponse>(rejectPath, {
|
||||||
|
method: 'PATCH',
|
||||||
|
body: formData,
|
||||||
|
});
|
||||||
|
|
||||||
|
return rejectRes;
|
||||||
|
} catch (error) {
|
||||||
|
if (axios.isAxiosError<BaseApiResponse>(error)) {
|
||||||
|
return error.response?.data;
|
||||||
|
}
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
async uploadImage(
|
async uploadImage(
|
||||||
id: number,
|
id: number,
|
||||||
status: string,
|
status: string,
|
||||||
|
|||||||
@@ -126,6 +126,29 @@ class MarketingExportService extends BaseApiService<
|
|||||||
super(basePath);
|
super(basePath);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async bulkApprovals(
|
||||||
|
ids: number[],
|
||||||
|
status: 'SALES_ORDER' | 'DELIVERY_ORDER',
|
||||||
|
date: string, // YYYY-MM-DD
|
||||||
|
notes: string
|
||||||
|
): Promise<BaseApiResponse<Marketing[] | Marketing> | undefined> {
|
||||||
|
try {
|
||||||
|
const path = `${this.basePath}/approvals/bulk`;
|
||||||
|
|
||||||
|
return await httpClient<BaseApiResponse<Marketing[] | Marketing>>(path, {
|
||||||
|
method: 'POST',
|
||||||
|
body: {
|
||||||
|
approvable_ids: ids,
|
||||||
|
status: status,
|
||||||
|
date: date,
|
||||||
|
notes: notes,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
} catch (error) {
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Export to Excel
|
* Export to Excel
|
||||||
*/
|
*/
|
||||||
|
|||||||
@@ -95,6 +95,8 @@ export class RecordingService extends BaseApiService<
|
|||||||
const params = new URLSearchParams(initialQueryString);
|
const params = new URLSearchParams(initialQueryString);
|
||||||
|
|
||||||
params.set('export', 'excel');
|
params.set('export', 'excel');
|
||||||
|
params.set('page', '1');
|
||||||
|
params.set('limit', '99999999999');
|
||||||
|
|
||||||
const queryString = `?${params.toString()}`;
|
const queryString = `?${params.toString()}`;
|
||||||
|
|
||||||
|
|||||||
@@ -1,7 +1,10 @@
|
|||||||
import { BaseApiService } from '@/services/api/base';
|
import { BaseApiService } from '@/services/api/base';
|
||||||
import { httpClientFetcher } from '@/services/http/client';
|
import { httpClientFetcher } from '@/services/http/client';
|
||||||
import { BaseApiResponse } from '@/types/api/api-general';
|
import { BaseApiResponse } from '@/types/api/api-general';
|
||||||
import { ReportExpense } from '@/types/api/report/report-expense';
|
import {
|
||||||
|
ReportDepreciation,
|
||||||
|
ReportExpense,
|
||||||
|
} from '@/types/api/report/report-expense';
|
||||||
|
|
||||||
export class ReportExpenseApiService extends BaseApiService<
|
export class ReportExpenseApiService extends BaseApiService<
|
||||||
ReportExpense,
|
ReportExpense,
|
||||||
@@ -20,3 +23,9 @@ export class ReportExpenseApiService extends BaseApiService<
|
|||||||
}
|
}
|
||||||
|
|
||||||
export const ReportExpenseApi = new ReportExpenseApiService('/reports/expense');
|
export const ReportExpenseApi = new ReportExpenseApiService('/reports/expense');
|
||||||
|
|
||||||
|
export const DepreciationReportApi = new BaseApiService<
|
||||||
|
ReportDepreciation,
|
||||||
|
unknown,
|
||||||
|
unknown
|
||||||
|
>('/reports/expense/depreciation');
|
||||||
@@ -56,7 +56,6 @@ export class UniformityApiService extends BaseApiService<
|
|||||||
): Promise<BaseApiResponse<UniformityDetail> | undefined> {
|
): Promise<BaseApiResponse<UniformityDetail> | undefined> {
|
||||||
const formData = new FormData();
|
const formData = new FormData();
|
||||||
formData.append('date', payload.date);
|
formData.append('date', payload.date);
|
||||||
formData.append('week', payload.week.toString());
|
|
||||||
formData.append(
|
formData.append(
|
||||||
'project_flock_kandang_id',
|
'project_flock_kandang_id',
|
||||||
payload.project_flock_kandang_id.toString()
|
payload.project_flock_kandang_id.toString()
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
import { useCallback, useMemo, useReducer } from 'react';
|
import { useCallback, useEffect, useMemo, useReducer } from 'react';
|
||||||
|
import { useTableFilterStore } from '@/stores/table/table-filter.store';
|
||||||
|
|
||||||
/** Core filter shape (page + pageSize) extended by your custom fields */
|
/** Core filter shape (page + pageSize) extended by your custom fields */
|
||||||
export type TableFilterState<TExtra extends Record<string, unknown>> = {
|
export type TableFilterState<TExtra extends Record<string, unknown>> = {
|
||||||
@@ -30,6 +31,9 @@ export type UseTableFilterOptions<TExtra extends Record<string, unknown>> = {
|
|||||||
paramMap?: Partial<Record<keyof TableFilterState<TExtra>, string>>;
|
paramMap?: Partial<Record<keyof TableFilterState<TExtra>, string>>;
|
||||||
/** If true, `toSearchParams`/`toQueryString` will omit values equal to defaults */
|
/** If true, `toSearchParams`/`toQueryString` will omit values equal to defaults */
|
||||||
omitDefaultsInUrl?: boolean;
|
omitDefaultsInUrl?: boolean;
|
||||||
|
|
||||||
|
persist?: boolean;
|
||||||
|
storeName?: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
function clampToInt(n: number, min = 1) {
|
function clampToInt(n: number, min = 1) {
|
||||||
@@ -90,9 +94,37 @@ function shallowEqual<T extends Record<string, unknown>>(
|
|||||||
export function useTableFilter<TExtra extends Record<string, unknown>>(
|
export function useTableFilter<TExtra extends Record<string, unknown>>(
|
||||||
options?: UseTableFilterOptions<TExtra>
|
options?: UseTableFilterOptions<TExtra>
|
||||||
) {
|
) {
|
||||||
const defaults = useMemo(
|
if (options?.persist && !options?.storeName) {
|
||||||
() => createInitialState<TExtra>(options),
|
throw new Error(
|
||||||
[options]
|
'storeName is required if persist is true in useTableFilter!'
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const storeName = options?.storeName ?? '';
|
||||||
|
const persistedState = useTableFilterStore(
|
||||||
|
useCallback(
|
||||||
|
(storeState) =>
|
||||||
|
storeName
|
||||||
|
? (storeState.data[storeName] as Partial<TableFilterState<TExtra>>)
|
||||||
|
: undefined,
|
||||||
|
[storeName]
|
||||||
|
)
|
||||||
|
);
|
||||||
|
const setTableData = useTableFilterStore(
|
||||||
|
(storeState) => storeState.setTableData
|
||||||
|
);
|
||||||
|
|
||||||
|
const defaults = useMemo(() => {
|
||||||
|
return createInitialState<TExtra>(options);
|
||||||
|
}, [options]);
|
||||||
|
|
||||||
|
const initialState = useMemo(
|
||||||
|
() =>
|
||||||
|
({
|
||||||
|
...defaults,
|
||||||
|
...(persistedState as object),
|
||||||
|
}) as TableFilterState<TExtra>,
|
||||||
|
[defaults, persistedState]
|
||||||
);
|
);
|
||||||
|
|
||||||
const [state, dispatch] = useReducer(
|
const [state, dispatch] = useReducer(
|
||||||
@@ -106,15 +138,22 @@ export function useTableFilter<TExtra extends Record<string, unknown>>(
|
|||||||
case 'SET_PAGE_SIZE': {
|
case 'SET_PAGE_SIZE': {
|
||||||
const pageSize = clampToInt(a.pageSize);
|
const pageSize = clampToInt(a.pageSize);
|
||||||
const page = a.resetPage ? 1 : s.page;
|
const page = a.resetPage ? 1 : s.page;
|
||||||
|
|
||||||
return { ...s, pageSize, page };
|
return { ...s, pageSize, page };
|
||||||
}
|
}
|
||||||
case 'SET_FILTERS': {
|
case 'SET_FILTERS': {
|
||||||
const page = a.resetPage ? 1 : s.page;
|
const page = a.resetPage ? 1 : s.page;
|
||||||
|
|
||||||
return { ...s, ...a.filters, page };
|
return { ...s, ...a.filters, page };
|
||||||
}
|
}
|
||||||
case 'UPDATE_FILTER': {
|
case 'UPDATE_FILTER': {
|
||||||
const page = a.resetPage ? 1 : s.page;
|
const page = a.resetPage ? 1 : s.page;
|
||||||
return { ...s, [a.key]: a.value, page } as TableFilterState<TExtra>;
|
|
||||||
|
return {
|
||||||
|
...s,
|
||||||
|
[a.key]: a.value,
|
||||||
|
page,
|
||||||
|
} as TableFilterState<TExtra>;
|
||||||
}
|
}
|
||||||
case 'REPLACE_ALL':
|
case 'REPLACE_ALL':
|
||||||
return {
|
return {
|
||||||
@@ -128,12 +167,19 @@ export function useTableFilter<TExtra extends Record<string, unknown>>(
|
|||||||
return s;
|
return s;
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
defaults
|
initialState
|
||||||
);
|
);
|
||||||
|
|
||||||
// Notify consumer on change (stable ref)
|
useEffect(() => {
|
||||||
|
if (!options?.persist || !storeName) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
setTableData(storeName, state);
|
||||||
|
}, [options?.persist, setTableData, state, storeName]);
|
||||||
|
|
||||||
const onChange = options?.onChange;
|
const onChange = options?.onChange;
|
||||||
useMemo(() => {
|
useEffect(() => {
|
||||||
if (onChange) onChange(state);
|
if (onChange) onChange(state);
|
||||||
}, [state, onChange]);
|
}, [state, onChange]);
|
||||||
|
|
||||||
@@ -154,7 +200,7 @@ export function useTableFilter<TExtra extends Record<string, unknown>>(
|
|||||||
);
|
);
|
||||||
|
|
||||||
const updateFilter = useCallback(
|
const updateFilter = useCallback(
|
||||||
<K extends keyof TExtra>(key: K, value: TExtra[K], resetPage = true) => {
|
<K extends keyof TExtra>(key: K, value: TExtra[K], resetPage = false) => {
|
||||||
dispatch({ type: 'UPDATE_FILTER', key, value, resetPage });
|
dispatch({ type: 'UPDATE_FILTER', key, value, resetPage });
|
||||||
},
|
},
|
||||||
[dispatch]
|
[dispatch]
|
||||||
|
|||||||
@@ -0,0 +1,60 @@
|
|||||||
|
import { create } from 'zustand';
|
||||||
|
import { createJSONStorage, devtools, persist } from 'zustand/middleware';
|
||||||
|
import { TableFilterStore } from '@/types/stores';
|
||||||
|
|
||||||
|
type TableFilterStoreState = TableFilterStore<
|
||||||
|
Record<string, Record<string, unknown>>
|
||||||
|
>;
|
||||||
|
|
||||||
|
export const useTableFilterStore = create<TableFilterStoreState>()(
|
||||||
|
devtools(
|
||||||
|
persist(
|
||||||
|
(set) => ({
|
||||||
|
data: {},
|
||||||
|
|
||||||
|
setData: (newData) => {
|
||||||
|
set({ data: newData });
|
||||||
|
},
|
||||||
|
|
||||||
|
setTableData: (key, tableData) => {
|
||||||
|
set((state) => ({
|
||||||
|
data: {
|
||||||
|
...state.data,
|
||||||
|
[key]: tableData,
|
||||||
|
},
|
||||||
|
}));
|
||||||
|
},
|
||||||
|
|
||||||
|
setTableDataField: (key, field, value) => {
|
||||||
|
set((state) => ({
|
||||||
|
data: {
|
||||||
|
...state.data,
|
||||||
|
[key]: {
|
||||||
|
...state.data[key],
|
||||||
|
[field]: value,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}));
|
||||||
|
},
|
||||||
|
|
||||||
|
setSearchValue: (key, searchValue) => {
|
||||||
|
set((state) => ({
|
||||||
|
data: {
|
||||||
|
...state.data,
|
||||||
|
[key]: {
|
||||||
|
...state.data[key],
|
||||||
|
|
||||||
|
// search key
|
||||||
|
search: searchValue,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}));
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
{
|
||||||
|
name: 'table-filter-store',
|
||||||
|
storage: createJSONStorage(() => sessionStorage),
|
||||||
|
}
|
||||||
|
)
|
||||||
|
)
|
||||||
|
);
|
||||||
@@ -1,7 +1,7 @@
|
|||||||
'use client';
|
'use client';
|
||||||
|
|
||||||
import { create } from 'zustand';
|
import { create } from 'zustand';
|
||||||
import { devtools, persist } from 'zustand/middleware';
|
import { createJSONStorage, devtools, persist } from 'zustand/middleware';
|
||||||
|
|
||||||
import { UIStore } from '@/types/stores';
|
import { UIStore } from '@/types/stores';
|
||||||
import { createMainUiSlice } from '@/stores/ui/slices/main.slice';
|
import { createMainUiSlice } from '@/stores/ui/slices/main.slice';
|
||||||
@@ -20,6 +20,7 @@ export const useUiStore = create<UIStore>()(
|
|||||||
}),
|
}),
|
||||||
{
|
{
|
||||||
name: 'search-store',
|
name: 'search-store',
|
||||||
|
storage: createJSONStorage(() => sessionStorage),
|
||||||
partialize: (state) => ({
|
partialize: (state) => ({
|
||||||
key: state.key,
|
key: state.key,
|
||||||
path: state.path,
|
path: state.path,
|
||||||
|
|||||||
-1
@@ -146,7 +146,6 @@ export type CreateUniformityPayload = {
|
|||||||
date: string;
|
date: string;
|
||||||
project_flock_kandang_id: number;
|
project_flock_kandang_id: number;
|
||||||
document: File;
|
document: File;
|
||||||
week: number;
|
|
||||||
};
|
};
|
||||||
|
|
||||||
export type VerifyUniformityPayload = {
|
export type VerifyUniformityPayload = {
|
||||||
|
|||||||
+38
@@ -52,3 +52,41 @@ export type ReportExpenseSearchParams = {
|
|||||||
category: string | null;
|
category: string | null;
|
||||||
search: string;
|
search: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export type ReportDepreciation = {
|
||||||
|
project_flock_id: number;
|
||||||
|
farm_name: string;
|
||||||
|
period: string;
|
||||||
|
depreciation_percent_effective: number;
|
||||||
|
depreciation_value: number;
|
||||||
|
pullet_cost_day_n_total: number;
|
||||||
|
hpp?: number;
|
||||||
|
components: {
|
||||||
|
kandang: {
|
||||||
|
kandang_id: number;
|
||||||
|
hpp?: number;
|
||||||
|
transfer_id: number;
|
||||||
|
cutover_date: string;
|
||||||
|
kandang_name: string;
|
||||||
|
manual_input_id: number;
|
||||||
|
depreciation_value: number;
|
||||||
|
start_schedule_day: number;
|
||||||
|
depreciation_percent: number;
|
||||||
|
source_project_flock_id: number;
|
||||||
|
day_n: number;
|
||||||
|
transfer_date: string;
|
||||||
|
pullet_cost_day_n: number;
|
||||||
|
depreciation_source: string;
|
||||||
|
project_flock_kandang_id: number;
|
||||||
|
transfer_qty: number;
|
||||||
|
house_type: string;
|
||||||
|
origin_date: string;
|
||||||
|
}[];
|
||||||
|
kandang_count: number;
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
export type ReportDepreciationSearchParams = {
|
||||||
|
farm: string | null;
|
||||||
|
period: string | null;
|
||||||
|
};
|
||||||
|
|||||||
Vendored
+8
@@ -117,3 +117,11 @@ export type ProjectFlockSlice = {
|
|||||||
setCreatedProjectFlock: (data: ProjectFlock | null) => void;
|
setCreatedProjectFlock: (data: ProjectFlock | null) => void;
|
||||||
resetProjectFlock: () => void;
|
resetProjectFlock: () => void;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export type TableFilterStore<T = Record<string, Record<string, unknown>>> = {
|
||||||
|
data: T;
|
||||||
|
setData: (newData: T) => void;
|
||||||
|
setTableData: (key: string, tableData: Record<string, unknown>) => void;
|
||||||
|
setTableDataField: (key: string, field: string, value: unknown) => void;
|
||||||
|
setSearchValue: (key: string, searchValue: string) => void;
|
||||||
|
};
|
||||||
|
|||||||
Reference in New Issue
Block a user