Compare commits

...

11 Commits

Author SHA1 Message Date
ValdiANS 192a2be8b9 fix: use project flock kandang ID instead kandang ID and use kandang name with period for kandang options in filter 2026-04-16 10:17:07 +07:00
Rivaldi A N S 11353809f0 Merge branch 'feat/expense-enhancement' into 'development'
[FEAT/FE] Expense Enhancement

See merge request mbugroup/lti-web-client!404
2026-04-15 09:43:06 +00:00
ValdiANS 6463b7a572 fix: set resetPage to false as default value in updateFilter function 2026-04-15 16:39:22 +07:00
ValdiANS 7a5ee2aca1 feat: implement return to url query param 2026-04-15 16:38:56 +07:00
ValdiANS 5e907d7e53 feat: create expense navigation helper function 2026-04-15 16:35:35 +07:00
Adnan Zahir 71edc9c68a Merge branch 'fix/recording' into 'development'
[FIX][FE]: adjust value query param get product warehouses

See merge request mbugroup/lti-web-client!403
2026-04-14 15:15:35 +07:00
MacBook Air M1 2a33fdbbbe adjust value query param get product warehouses 2026-04-14 15:05:08 +07:00
Adnan Zahir 178c659b58 Merge branch 'codex/uniformity-week-calculation' into 'development'
codex/fix: uniformity week calculation

See merge request mbugroup/lti-web-client!402
2026-04-14 14:33:04 +07:00
Rivaldi A N S c1d6436583 Merge branch 'fix/adjustment-issue-14-apr-26' into 'development'
[FIX/FE] Fix Unnecessary Error Label (OptionType) on Purchase Approval Form (Purchase)

See merge request mbugroup/lti-web-client!401
2026-04-14 06:59:11 +00:00
rstubryan 8dc62453bd fix(FE-form-object-missmatch): Refactor purchase item handling in
approval forms and schemas
2026-04-14 13:31:40 +07:00
Adnan Zahir 244d800874 codex/fix: uniformity week calculation 2026-04-14 13:10:53 +07:00
16 changed files with 191 additions and 140 deletions
@@ -173,8 +173,8 @@ const DashboardProduction = () => {
loadMore: loadMoreKandang, loadMore: loadMoreKandang,
} = useSelect<ProjectFlockKandang>( } = useSelect<ProjectFlockKandang>(
ProjectFlockKandangApi.basePath, ProjectFlockKandangApi.basePath,
'kandang_id', 'id',
'kandang.name', 'name_with_period',
'search', 'search',
{ {
location_id: location_id:
@@ -362,7 +362,7 @@ const DashboardProduction = () => {
</div> </div>
{/* Hidden container for all charts (used for PDF export in OVERVIEW mode) */} {/* Hidden container for all charts (used for PDF export in OVERVIEW mode) */}
{dashboardProductionData && ( {exporting && dashboardProductionData && (
<> <>
{/* Export Stats Charts */} {/* Export Stats Charts */}
<div <div
@@ -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} />
+94 -14
View File
@@ -1,7 +1,13 @@
'use client'; 'use client';
import { ChangeEventHandler, useEffect, useMemo, useState } from 'react'; import {
import { usePathname } from 'next/navigation'; ChangeEventHandler,
useCallback,
useEffect,
useMemo,
useState,
} from 'react';
import { usePathname, useRouter, useSearchParams } from 'next/navigation';
import { useUiStore } from '@/stores/ui/ui.store'; import { useUiStore } from '@/stores/ui/ui.store';
import useSWR from 'swr'; import useSWR from 'swr';
import { import {
@@ -31,19 +37,32 @@ import ExpenseTableSkeleton from '@/components/pages/expense/skeleton/ExpenseTab
import { Expense } from '@/types/api/expense'; import { Expense } from '@/types/api/expense';
import { ExpenseApi } from '@/services/api/expense'; import { ExpenseApi } from '@/services/api/expense';
import { buildExpenseActionHref } from '@/lib/expense-list-navigation';
import { cn, formatCurrency, formatDate } from '@/lib/helper'; import { cn, formatCurrency, formatDate } from '@/lib/helper';
import { isResponseSuccess } from '@/lib/api-helper'; 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 RowOptionsMenu = ({ const RowOptionsMenu = ({
popoverPosition = 'bottom', popoverPosition = 'bottom',
props, props,
deleteClickHandler, deleteClickHandler,
returnToSearchParams,
}: { }: {
popoverPosition: 'bottom' | 'top'; popoverPosition: 'bottom' | 'top';
props: CellContext<Expense, unknown>; props: CellContext<Expense, unknown>;
deleteClickHandler: () => void; deleteClickHandler: () => void;
returnToSearchParams: URLSearchParams;
}) => { }) => {
const popoverId = `expense#${props.row.original.id}`; const popoverId = `expense#${props.row.original.id}`;
const popoverAnchorName = `--anchor-expense#${props.row.original.id}`; const popoverAnchorName = `--anchor-expense#${props.row.original.id}`;
@@ -86,7 +105,11 @@ const RowOptionsMenu = ({
<div className='flex flex-col bg-base-100 rounded-xl'> <div className='flex flex-col bg-base-100 rounded-xl'>
<RequirePermission permissions='lti.expense.detail'> <RequirePermission permissions='lti.expense.detail'>
<Button <Button
href={`/expense/detail/?expenseId=${props.row.original.id}`} href={buildExpenseActionHref(
'/expense/detail/',
props.row.original.id,
returnToSearchParams
)}
variant='ghost' variant='ghost'
color='none' color='none'
className='p-3 justify-start text-sm font-semibold w-full' className='p-3 justify-start text-sm font-semibold w-full'
@@ -100,7 +123,11 @@ const RowOptionsMenu = ({
{showEditButton && ( {showEditButton && (
<RequirePermission permissions='lti.expense.update'> <RequirePermission permissions='lti.expense.update'>
<Button <Button
href={`/expense/detail/edit/?expenseId=${props.row.original.id}`} href={buildExpenseActionHref(
'/expense/detail/edit/',
props.row.original.id,
returnToSearchParams
)}
variant='ghost' variant='ghost'
color='none' color='none'
className='p-3 justify-start text-sm font-semibold w-full' className='p-3 justify-start text-sm font-semibold w-full'
@@ -115,7 +142,11 @@ const RowOptionsMenu = ({
{showRealizationButton && ( {showRealizationButton && (
<RequirePermission permissions='lti.expense.create.realization'> <RequirePermission permissions='lti.expense.create.realization'>
<Button <Button
href={`/expense/realization/?expenseId=${props.row.original.id}`} href={buildExpenseActionHref(
'/expense/realization/',
props.row.original.id,
returnToSearchParams
)}
variant='ghost' variant='ghost'
color='none' color='none'
className='p-3 justify-start text-sm font-semibold w-full' className='p-3 justify-start text-sm font-semibold w-full'
@@ -155,6 +186,8 @@ const RowOptionsMenu = ({
const ExpensesTable = () => { const ExpensesTable = () => {
const { searchValue, setSearchValue, setTableState } = useUiStore(); const { searchValue, setSearchValue, setTableState } = useUiStore();
const pathname = usePathname(); const pathname = usePathname();
const router = useRouter();
const searchParams = useSearchParams();
const { const {
state: tableFilterState, state: tableFilterState,
@@ -162,9 +195,11 @@ const ExpensesTable = () => {
setPage, setPage,
setPageSize, setPageSize,
toQueryString: getTableFilterQueryString, toQueryString: getTableFilterQueryString,
} = useTableFilter({ } = useTableFilter<ExpenseTableFilters>({
initial: { initial: {
search: '', page: Number(searchParams.get('page')) || 1,
pageSize: Number(searchParams.get('limit')) || 10,
search: searchValue,
nameSort: '', nameSort: '',
transactionDate: '', transactionDate: '',
realizationDate: '', realizationDate: '',
@@ -193,6 +228,54 @@ const ExpensesTable = () => {
ExpenseApi.getAllFetcher ExpenseApi.getAllFetcher
); );
const syncPaginationToUrl = useCallback(
(page: number, pageSize: number) => {
const nextQueryString = new URLSearchParams({
page: String(page),
limit: String(pageSize),
}).toString();
router.replace(
nextQueryString ? `${pathname}?${nextQueryString}` : pathname,
{
scroll: false,
}
);
},
[pathname, router]
);
const pageChangeHandler = useCallback(
(page: number) => {
setPage(page);
syncPaginationToUrl(page, tableFilterState.pageSize);
},
[setPage, syncPaginationToUrl, tableFilterState.pageSize]
);
const pageSizeChangeHandler = useCallback(
(pageSize: number) => {
setPageSize(pageSize);
syncPaginationToUrl(1, pageSize);
},
[setPageSize, syncPaginationToUrl]
);
const returnToSearchParams = useMemo(() => {
const returnToParams = new URLSearchParams();
const queryString = new URLSearchParams({
page: String(tableFilterState.page),
limit: String(tableFilterState.pageSize),
}).toString();
returnToParams.set(
'returnTo',
queryString ? `${pathname}?${queryString}` : pathname
);
return returnToParams;
}, [pathname, tableFilterState.page, tableFilterState.pageSize]);
const deleteModal = useModal(); const deleteModal = useModal();
const approveModal = useModal(); const approveModal = useModal();
const rejectModal = useModal(); const rejectModal = useModal();
@@ -373,6 +456,7 @@ const ExpensesTable = () => {
popoverPosition={isLast2Rows ? 'top' : 'bottom'} popoverPosition={isLast2Rows ? 'top' : 'bottom'}
props={props} props={props}
deleteClickHandler={deleteClickHandler} deleteClickHandler={deleteClickHandler}
returnToSearchParams={returnToSearchParams}
/> />
); );
}, },
@@ -512,10 +596,6 @@ const ExpensesTable = () => {
setIsRejectLoading(false); setIsRejectLoading(false);
}; };
useEffect(() => {
updateFilter('search', searchValue);
}, [searchValue, updateFilter]);
useEffect(() => { useEffect(() => {
setTableState('expense-table', pathname); setTableState('expense-table', pathname);
}, [pathname, setTableState]); }, [pathname, setTableState]);
@@ -554,7 +634,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');
} }
@@ -734,8 +814,8 @@ const ExpensesTable = () => {
totalItems={ totalItems={
isResponseSuccess(expenses) ? expenses?.meta?.total_results : 0 isResponseSuccess(expenses) ? expenses?.meta?.total_results : 0
} }
onPageChange={setPage} onPageChange={pageChangeHandler}
onPageSizeChange={setPageSize} onPageSizeChange={pageSizeChangeHandler}
isLoading={isLoading} isLoading={isLoading}
sorting={sorting} sorting={sorting}
setSorting={setSorting} setSorting={setSorting}
@@ -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'
> >
@@ -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(),
+39
View File
@@ -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()}`;
};
-1
View File
@@ -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 -1
View File
@@ -154,7 +154,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]
-1
View File
@@ -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 = {