Compare commits

..

3 Commits

Author SHA1 Message Date
ValdiANS 4151829cb8 fix: disabled deliver item button if is submitting and set the is loading prop 2026-06-03 14:24:39 +07:00
ValdiANS f167916a21 fix: replace throw error with axios error handling in SalesOrderService and MarketingExportService
All catch blocks in singleApproval, bulkApprovals (both classes), and
delivery now return error.response?.data for axios errors and undefined
otherwise, consistent with the BaseApiService pattern.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-03 14:20:43 +07:00
ValdiANS 97acc17ca5 feat: add inline edit for chick-in date in chickin logs
- Add updateChickinDate method to ChickinService (PATCH /production/chickins/chick-in-date)
- Add pencil icon button next to each chickin date in ChickLogsView
- Clicking the icon toggles an inline DateInput with Simpan/Batal buttons
- Save button is disabled and shows loading state while request is in flight

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-03 14:05:06 +07:00
9 changed files with 125 additions and 741 deletions
@@ -847,7 +847,8 @@ const DeliveryOrderFormModal = ({}: { initialValues?: Marketing }) => {
}
}}
className='p-3 shadow-button-soft text-base-100 rounded-lg text-sm font-semibold'
disabled={deliveryRejected}
disabled={deliveryRejected || isLoading}
isLoading={isLoading}
>
{marketing?.data?.latest_approval?.step_number === 1 &&
'Approve'}
@@ -2,16 +2,17 @@ import Alert from '@/components/Alert';
import Button from '@/components/Button';
import Card from '@/components/Card';
import RequirePermission from '@/components/helper/RequirePermission';
import DateInput from '@/components/input/DateInput';
import PillBadge from '@/components/PillBadge';
import { isResponseError, isResponseSuccess } from '@/lib/api-helper';
import { formatDate, formatNumber } from '@/lib/helper';
import { ChickinApi } from '@/services/api/production/chickin';
import { useChickinStore } from '@/stores/production/chickin/chickin.store';
import { BaseApproval } from '@/types/api/api-general';
import { ProjectFlockKandang } from '@/types/api/production/project-flock-kandang';
import { Icon } from '@iconify/react';
import { useState } from 'react';
import toast from 'react-hot-toast';
import { useChickinStore } from '@/stores/production/chickin/chickin.store';
const ChickinLogsView = ({
initialValues,
@@ -23,6 +24,9 @@ const ChickinLogsView = ({
rawDataApprovals: BaseApproval[];
}) => {
const [chickinErrorMessage, setChickinErrorMessage] = useState('');
const [editingChickinId, setEditingChickinId] = useState<number | null>(null);
const [editDate, setEditDate] = useState('');
const [isEditLoading, setIsEditLoading] = useState(false);
const { openChickinApproveModal, openChickinDeleteModal } = useChickinStore();
const handleClickApprove = () => {
@@ -44,6 +48,23 @@ const ChickinLogsView = ({
});
};
const handleSaveChickinDate = async () => {
setIsEditLoading(true);
const res = await ChickinApi.updateChickinDate(
initialValues.id as number,
formatDate(editDate, 'YYYY-MM-DD')
);
setIsEditLoading(false);
if (isResponseSuccess(res)) {
toast.success(res?.message as string);
setEditingChickinId(null);
afterSubmit && afterSubmit();
}
if (isResponseError(res)) {
toast.error(res?.message as string);
}
};
const handleDeleteChickin = (chickinId: number) => {
openChickinDeleteModal(chickinId, async () => {
const deleteRes = await ChickinApi.delete(chickinId);
@@ -133,9 +154,54 @@ const ChickinLogsView = ({
<Icon icon={'mdi:calendar'} width={14} height={14} />{' '}
<span>Tanggal Chick In</span>
</div>
<div className='text-end text-gray-500'>
{formatDate(chickin.chick_in_date, 'DD MMM YYYY')}
</div>
{editingChickinId === chickin.id ? (
<div className='flex flex-col gap-2 items-end w-1/2'>
<DateInput
name='edit_chick_in_date'
value={editDate}
isNestedModal
onChange={(e) => setEditDate(e.target.value)}
/>
<div className='flex flex-row gap-1'>
<Button
color='none'
className='btn-xs btn-ghost text-gray-500'
onClick={() => setEditingChickinId(null)}
disabled={isEditLoading}
>
Batal
</Button>
<Button
color='success'
className='btn-xs text-base-100'
onClick={handleSaveChickinDate}
isLoading={isEditLoading}
disabled={!editDate || isEditLoading}
>
Simpan
</Button>
</div>
</div>
) : (
<div className='flex flex-row gap-2 items-center'>
<span className='text-gray-500'>
{formatDate(chickin.chick_in_date, 'DD MMM YYYY')}
</span>
<button
className='btn btn-ghost btn-xs p-0 text-gray-400 hover:text-primary'
onClick={() => {
setEditingChickinId(chickin.id);
setEditDate(chickin.chick_in_date);
}}
>
<Icon
icon='mdi:pencil-outline'
width={13}
height={13}
/>
</button>
</div>
)}
</div>
{/* Kandang */}
@@ -1,11 +1,10 @@
'use client';
import React, { useCallback, useEffect, useMemo, useState } from 'react';
import React, { useEffect, useMemo } from 'react';
import useSWR from 'swr';
import { ColumnDef } from '@tanstack/react-table';
import { Icon } from '@iconify/react';
import Button from '@/components/Button';
import Card from '@/components/Card';
import Pagination from '@/components/Pagination';
import Table from '@/components/Table';
@@ -57,21 +56,11 @@ const ReportDepreciationTab = ({ tabId }: ReportDepreciationTabProps) => {
storeName: 'report-depreciation-table',
});
const [forceRecompute, setForceRecompute] = useState(false);
const {
data: depreciationsResponse,
isLoading: isLoadingDepreciations,
isValidating,
mutate,
} = useSWR(
`${DepreciationReportApi.basePath}${getTableFilterQueryString()}&force_recompute=${forceRecompute}`,
DepreciationReportApi.getAllFetcher
);
const handleRefresh = useCallback(() => {
setForceRecompute(true);
}, [mutate]);
const { data: depreciationsResponse, isLoading: isLoadingDepreciations } =
useSWR(
`${DepreciationReportApi.basePath}${getTableFilterQueryString()}`,
DepreciationReportApi.getAllFetcher
);
const depreciations = isResponseSuccess(depreciationsResponse)
? depreciationsResponse.data
@@ -125,21 +114,6 @@ const ReportDepreciationTab = ({ tabId }: ReportDepreciationTabProps) => {
const tabActionsElement = useMemo(
() => (
<div className='flex flex-row gap-3'>
<Button
variant='outline'
color='none'
onClick={handleRefresh}
disabled={isValidating}
className='rounded-lg max-h-10 font-semibold text-sm gap-1.5 text-base-content/50 border border-base-content/10 shadow-button-soft px-3 py-2.5'
>
<Icon
icon='heroicons:arrow-path'
width={20}
height={20}
className={isValidating ? 'animate-spin' : ''}
/>
Refresh
</Button>
<ButtonFilter
values={tableFilterState}
excludeFields={['page', 'pageSize']}
@@ -149,7 +123,7 @@ const ReportDepreciationTab = ({ tabId }: ReportDepreciationTabProps) => {
/>
</div>
),
[tableFilterState, handleRefresh, isValidating]
[tableFilterState]
);
useEffect(() => {
@@ -281,8 +255,6 @@ const ReportDepreciationTab = ({ tabId }: ReportDepreciationTabProps) => {
values.period ? formatDate(values.period, 'YYYY-MM-DD') : '',
true
);
setForceRecompute(false);
}}
/>
</>
@@ -4,7 +4,6 @@ import { useState } from 'react';
import Tabs from '@/components/Tabs';
import DailyMarketingReportContent from '@/components/pages/report/marketing/tab/DailyMarketingTab';
import HppPerKandangTab from '@/components/pages/report/marketing/tab/HppPerKandangTab';
import HppPerFarmTab from '@/components/pages/report/marketing/tab/HppPerFarmTab';
import { useTabActionsStore } from '@/stores/tab-actions/tab-actions.store';
const MarketingReportContent = () => {
@@ -22,11 +21,6 @@ const MarketingReportContent = () => {
label: 'HPP Harian Kandang',
content: <HppPerKandangTab tabId={'2'} />,
},
{
id: '3',
label: 'HPP Per Farm',
content: <HppPerFarmTab tabId={'3'} />,
},
];
return (
@@ -1,639 +0,0 @@
'use client';
import { useState, useMemo, useEffect, useCallback } from 'react';
import useSWR from 'swr';
import { Icon } from '@iconify/react';
import { useFormik } from 'formik';
import toast from 'react-hot-toast';
import { ColumnDef, Row, flexRender } from '@tanstack/react-table';
import { AxiosError } from 'axios';
import { SaleReportApi } from '@/services/api/report/marketing-sale';
import { LocationApi } from '@/services/api/master-data';
import { useSelect, OptionType } from '@/components/input/SelectInput';
import { httpClientFetcher, SWRHttpKey } from '@/services/http/client';
import { BaseApiResponse } from '@/types/api/api-general';
import SelectInputCheckbox from '@/components/input/SelectInputCheckbox';
import { useTableFilter } from '@/services/hooks/useTableFilter';
import { useTabActionsStore } from '@/stores/tab-actions/tab-actions.store';
import ButtonFilter from '@/components/helper/ButtonFilter';
import { formatCurrency, formatNumber } from '@/lib/helper';
import { isResponseSuccess } from '@/lib/api-helper';
import {
HppPerFarmReport,
HppPerFarmRow,
HppPerFarmFlock,
} from '@/types/api/report/hpp-per-farm';
import { HppPerKandangReport } from '@/types/api/report/hpp-per-kandang';
import Modal, { useModal } from '@/components/Modal';
import Button from '@/components/Button';
import DateInput from '@/components/input/DateInput';
import Table from '@/components/Table';
import HppPerKandangSkeleton from '@/components/pages/report/marketing/skeleton/HppPerKandangSkeleton';
interface HppPerFarmTabProps {
tabId: string;
}
const HppPerFarmTab = ({ tabId }: HppPerFarmTabProps) => {
const [dateError, setDateError] = useState('');
const [expandedLocations, setExpandedLocations] = useState<Set<number>>(
new Set()
);
const filterModal = useModal();
const setTabActions = useTabActionsStore((state) => state.setTabActions);
const clearTabActions = useTabActionsStore((state) => state.clearTabActions);
const {
state: tableFilterState,
updateFilter,
setPage,
setPageSize,
toQueryString: getTableFilterQueryString,
reset: resetFilter,
} = useTableFilter<{
start_date: string;
end_date: string;
locations: OptionType<number>[];
}>({
initial: {
start_date: '',
end_date: '',
locations: [],
},
paramMap: {
page: 'page',
pageSize: 'limit',
start_date: 'start_date',
end_date: 'end_date',
locations: 'location_id',
},
persist: true,
storeName: 'hpp-per-farm-table',
});
const {
options: locationOptions,
setInputValue: setLocationInput,
isLoadingOptions: isLoadingLocations,
loadMore: loadMoreLocations,
} = useSelect(LocationApi.basePath, 'id', 'name', 'search');
const formik = useFormik({
initialValues: {
start_date: tableFilterState.start_date,
end_date: tableFilterState.end_date,
locations: tableFilterState.locations,
},
onSubmit: (values, { setSubmitting }) => {
updateFilter('start_date', values.start_date, true);
updateFilter('end_date', values.end_date, true);
updateFilter('locations', values.locations, true);
filterModal.closeModal();
setSubmitting(false);
},
});
const DATE_ERROR_TOAST_ID = 'hpp-farm-date-range-error';
const getDateRangeError = (start: string, end: string): string => {
if (!start || !end) return '';
const startDate = new Date(start);
const endDate = new Date(end);
if (endDate < startDate)
return 'Tanggal akhir tidak boleh lebih kecil dari tanggal mulai';
const diffDays =
(endDate.getTime() - startDate.getTime()) / (1000 * 60 * 60 * 24);
if (diffDays > 31) return 'Rentang tanggal maksimal 31 hari';
return '';
};
const applyDateValidation = (start: string, end: string) => {
const error = getDateRangeError(start, end);
setDateError(error);
if (error) {
toast.error(error, { duration: Infinity, id: DATE_ERROR_TOAST_ID });
} else {
toast.dismiss(DATE_ERROR_TOAST_ID);
}
};
const formikResetHandler = () => {
resetFilter();
setDateError('');
toast.dismiss(DATE_ERROR_TOAST_ID);
formik.resetForm({
values: { start_date: '', end_date: '', locations: [] },
});
filterModal.closeModal();
};
const handleStartDateChange = (e: React.ChangeEvent<HTMLInputElement>) => {
const value = e.target.value;
formik.setFieldValue('start_date', value);
applyDateValidation(value, formik.values.end_date);
};
const handleEndDateChange = (e: React.ChangeEvent<HTMLInputElement>) => {
const value = e.target.value;
formik.setFieldValue('end_date', value);
applyDateValidation(formik.values.start_date, value);
};
const isSubmitted = !!tableFilterState.start_date;
const { data: response, isLoading } = useSWR<
BaseApiResponse<HppPerFarmReport>,
AxiosError<BaseApiResponse>,
SWRHttpKey | null
>(
isSubmitted
? `${SaleReportApi.basePath}/hpp-per-farm${getTableFilterQueryString()}`
: null,
httpClientFetcher
);
const data = isResponseSuccess(response) ? (response.data?.rows ?? []) : [];
const summary = isResponseSuccess(response)
? response.data?.summary
: undefined;
const meta =
isResponseSuccess(response) && response.meta ? response.meta : null;
const toggleLocation = useCallback((locationId: number) => {
setExpandedLocations((prev) => {
const next = new Set(prev);
if (next.has(locationId)) {
next.delete(locationId);
} else {
next.add(locationId);
}
return next;
});
}, []);
// Reset expansion when page changes
useEffect(() => {
setExpandedLocations(new Set());
}, [tableFilterState.page]);
// Inject tab actions
useEffect(() => {
setTabActions(
tabId,
<div className='flex flex-row gap-3'>
<ButtonFilter
values={{
start_date: tableFilterState.start_date,
end_date: tableFilterState.end_date,
locations: tableFilterState.locations,
}}
fieldGroups={[['start_date', 'end_date']]}
onClick={filterModal.openModal}
variant='outline'
className='px-3 py-2.5'
/>
</div>
);
}, [tabId, setTabActions, tableFilterState, filterModal.openModal]);
useEffect(() => {
return () => clearTabActions(tabId);
}, [tabId, clearTabActions]);
// Open filter modal on mount when no date set
useEffect(() => {
if (!tableFilterState.start_date) {
filterModal.openModal();
}
}, [filterModal.openModal]);
const columns = useMemo(
(): ColumnDef<HppPerFarmRow>[] => [
{
id: 'expand',
header: '',
cell: ({ row }) => {
const hasFlocks = (row.original.flocks?.length ?? 0) > 0;
if (!hasFlocks) return null;
const isExpanded = expandedLocations.has(row.original.location.id);
return (
<button
onClick={() => toggleLocation(row.original.location.id)}
className='flex items-center justify-center w-5 h-5 rounded text-base-content/50 hover:text-base-content hover:bg-base-content/10 transition-colors'
>
<Icon
icon={
isExpanded
? 'heroicons:chevron-down'
: 'heroicons:chevron-right'
}
width={14}
height={14}
/>
</button>
);
},
footer: () => null,
},
{
id: 'no',
header: 'No',
cell: (props) => props.row.index + 1,
footer: () => <div className='font-semibold text-gray-900'>TOTAL</div>,
},
{
id: 'farm',
header: 'Farm',
cell: ({ row }) => (
<div className='font-semibold'>{row.original.location.name}</div>
),
footer: () => <div className='font-semibold text-gray-900'>ALL</div>,
},
{
id: 'total_cost_rp',
header: 'Total Biaya (RP)',
cell: ({ row }) => (
<div className='text-right'>
{formatCurrency(row.original.total_cost_rp)}
</div>
),
footer: () => (
<div className='text-right font-semibold text-gray-900'>
{formatCurrency(summary?.total_cost_rp ?? 0)}
</div>
),
},
{
id: 'feed_cost_rp',
header: 'Biaya Pakan (RP)',
cell: ({ row }) => (
<div className='text-right'>
{formatCurrency(row.original.feed_cost_rp)}
</div>
),
footer: () => (
<div className='text-right font-semibold text-gray-900'>-</div>
),
},
{
id: 'ovk_cost_rp',
header: 'Biaya OVK (RP)',
cell: ({ row }) => (
<div className='text-right'>
{formatCurrency(row.original.ovk_cost_rp)}
</div>
),
footer: () => (
<div className='text-right font-semibold text-gray-900'>-</div>
),
},
{
id: 'bop_cost_rp',
header: 'BOP (RP)',
cell: ({ row }) => (
<div className='text-right'>
{formatCurrency(row.original.bop_cost_rp)}
</div>
),
footer: () => (
<div className='text-right font-semibold text-gray-900'>-</div>
),
},
{
id: 'depreciation_rp',
header: 'Penyusutan (RP)',
cell: ({ row }) => (
<div className='text-right'>
{formatCurrency(row.original.depreciation_rp)}
</div>
),
footer: () => (
<div className='text-right font-semibold text-gray-900'>-</div>
),
},
{
id: 'other_cost_rp',
header: 'Biaya Lain (RP)',
cell: ({ row }) => (
<div className='text-right'>
{formatCurrency(row.original.other_cost_rp)}
</div>
),
footer: () => (
<div className='text-right font-semibold text-gray-900'>-</div>
),
},
{
id: 'egg_weight_recording_kg',
header: 'Bobot Telur Recording (KG)',
cell: ({ row }) => (
<div className='text-right'>
{formatNumber(row.original.egg_weight_recording_kg)}
</div>
),
footer: () => (
<div className='text-right font-semibold text-gray-900'>
{formatNumber(summary?.total_egg_weight_recording_kg ?? 0)}
</div>
),
},
{
id: 'egg_weight_do_kg',
header: 'Bobot Telur DO (KG)',
cell: ({ row }) => (
<div className='text-right'>
{formatNumber(row.original.egg_weight_do_kg)}
</div>
),
footer: () => (
<div className='text-right font-semibold text-gray-900'>
{formatNumber(summary?.total_egg_weight_do_kg ?? 0)}
</div>
),
},
{
id: 'hpp_per_kg_production',
header: 'HPP/KG Produksi (RP/KG)',
cell: ({ row }) => (
<div className='text-right'>
{formatCurrency(row.original.hpp_per_kg_production)}
</div>
),
footer: () => (
<div className='text-right font-semibold text-gray-900'>
{formatCurrency(summary?.average_hpp_per_kg_production ?? 0)}
</div>
),
},
{
id: 'hpp_per_kg_sales',
header: 'HPP/KG Penjualan (RP/KG)',
cell: ({ row }) => (
<div className='text-right'>
{formatCurrency(row.original.hpp_per_kg_sales)}
</div>
),
footer: () => (
<div className='text-right font-semibold text-gray-900'>
{formatCurrency(summary?.average_hpp_per_kg_sales ?? 0)}
</div>
),
},
{
id: 'average_doc_price_rp',
header: 'Rata-rata Harga DOC (RP)',
cell: ({ row }) => (
<div className='text-right'>
{formatCurrency(row.original.average_doc_price_rp)}
</div>
),
footer: () => (
<div className='text-right font-semibold text-gray-900'>-</div>
),
},
],
[expandedLocations, toggleLocation, summary]
);
const renderCustomRow = useCallback(
(row: Row<HppPerFarmRow>): React.ReactNode => {
const isExpanded = expandedLocations.has(row.original.location.id);
const flocks = row.original.flocks ?? [];
const locationRow = (
<tr
key={row.id}
className='hover:bg-gray-50 transition-colors border-b border-l border-r border-b-gray-200 border-l-gray-200 border-r-gray-200'
>
{row.getVisibleCells().map((cell) => (
<td
key={cell.id}
className='px-4 py-3 text-xs text-gray-900 whitespace-nowrap border-gray-200'
>
{flexRender(cell.column.columnDef.cell, cell.getContext())}
</td>
))}
</tr>
);
if (!isExpanded || flocks.length === 0) {
return locationRow;
}
const flockRows = flocks.map((flock: HppPerFarmFlock, i: number) => (
<tr
key={`flock-${flock.project_flock_id}`}
className='bg-gray-50/70 hover:bg-gray-100 transition-colors border-b border-l border-r border-b-gray-200 border-l-gray-200 border-r-gray-200 [&_td]:px-4 [&_td]:py-2.5 [&_td]:text-xs [&_td]:text-gray-600 [&_td]:whitespace-nowrap'
>
<td />
<td className='text-gray-500'>{i + 1}</td>
<td className='pl-6 text-gray-700 italic'>{flock.flock_name}</td>
<td className='text-right'>{formatCurrency(flock.total_cost_rp)}</td>
<td className='text-right'>{formatCurrency(flock.feed_cost_rp)}</td>
<td className='text-right'>{formatCurrency(flock.ovk_cost_rp)}</td>
<td className='text-right'>{formatCurrency(flock.bop_cost_rp)}</td>
<td className='text-right'>
{formatCurrency(flock.depreciation_rp)}
</td>
<td className='text-right'>{formatCurrency(flock.other_cost_rp)}</td>
<td className='text-right'>
{formatNumber(flock.egg_weight_recording_kg)}
</td>
<td className='text-right'>{formatNumber(flock.egg_weight_do_kg)}</td>
<td className='text-right'>
{formatCurrency(flock.hpp_per_kg_production)}
</td>
<td className='text-right'>
{formatCurrency(flock.hpp_per_kg_sales)}
</td>
<td className='text-right'>
{formatCurrency(flock.average_doc_price_rp)}
</td>
</tr>
));
return [locationRow, ...flockRows];
},
[expandedLocations]
);
return (
<>
<div className='w-full p-0 sm:p-3 flex flex-col gap-3'>
{!isSubmitted ? (
<HppPerKandangSkeleton
columns={
columns as unknown as ColumnDef<HppPerKandangReport['rows'][0]>[]
}
icon={
<Icon
icon='heroicons:funnel'
className='text-white'
width={20}
height={20}
/>
}
title='No Filters Selected'
subtitle='Please choose filters to narrow down your results and make your search easier.'
/>
) : isLoading ? (
<HppPerKandangSkeleton
columns={
columns as unknown as ColumnDef<HppPerKandangReport['rows'][0]>[]
}
icon={
<Icon
icon='heroicons:document-report'
className='text-white'
width={20}
height={20}
/>
}
title='Memuat Data HPP Per Farm'
subtitle='Silakan tunggu sebentar...'
/>
) : data.length === 0 ? (
<HppPerKandangSkeleton
columns={
columns as unknown as ColumnDef<HppPerKandangReport['rows'][0]>[]
}
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.'
/>
) : (
<Table
data={data}
columns={columns}
pageSize={tableFilterState.pageSize}
page={tableFilterState.page}
totalItems={meta?.total_results ?? 0}
onPageChange={setPage}
onPageSizeChange={setPageSize}
isLoading={isLoading}
renderFooter={data.length > 0}
renderCustomRow={renderCustomRow}
className={{
containerClassName: 'w-full mb-0!',
tableWrapperClassName: 'overflow-x-auto',
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',
}}
/>
)}
</div>
{/* Filter Modal */}
<Modal
ref={filterModal.ref}
className={{
modal: 'p-0',
modalBox: 'p-0 rounded-[0.875rem] xl:max-w-4/12 max-w-sm',
}}
>
{/* Modal Header */}
<div className='flex items-center justify-between gap-2 border-b border-base-content/10 p-4'>
<div className='flex items-center gap-2 text-primary'>
<Icon icon='heroicons:funnel' width={20} height={20} />
<h3 className='font-medium text-sm'>Filter Data</h3>
</div>
<Button
variant='link'
onClick={filterModal.closeModal}
className='text-base-content/50 hover:text-base-content transition-colors cursor-pointer'
>
<Icon icon='heroicons:x-mark' width={20} height={20} />
</Button>
</div>
<form onSubmit={formik.handleSubmit} onReset={formikResetHandler}>
<div className='p-4 flex flex-col gap-3'>
{/* Date Range Filter */}
<div>
<label className='block text-xs font-semibold text-base-content py-2'>
Periode
</label>
<div className='flex flex-row gap-1.5 items-center justify-between'>
<DateInput
name='start_date'
value={formik.values.start_date || ''}
onChange={handleStartDateChange}
className={{ wrapper: 'w-full' }}
isNestedModal
isError={!!dateError}
/>
<hr className='w-full max-w-3 h-px border-base-content/10' />
<DateInput
name='end_date'
value={formik.values.end_date || ''}
onChange={handleEndDateChange}
className={{ wrapper: 'w-full' }}
isNestedModal
isError={!!dateError}
/>
</div>
{dateError && (
<div className='text-error text-xs mt-1'>{dateError}</div>
)}
</div>
{/* Location Filter */}
<SelectInputCheckbox
label='Lokasi'
placeholder='Pilih Lokasi'
options={locationOptions}
value={formik.values.locations}
onChange={(val) =>
formik.setFieldValue('locations', Array.isArray(val) ? val : [])
}
onInputChange={setLocationInput}
isLoading={isLoadingLocations}
isClearable
onMenuScrollToBottom={loadMoreLocations}
className={{ wrapper: 'w-full' }}
/>
</div>
{/* Modal Footer */}
<div className='flex justify-between items-center gap-4 p-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={!!dateError || formik.isSubmitting}
>
Apply Filter
</Button>
</div>
</form>
</Modal>
</>
);
};
export default HppPerFarmTab;
+20 -8
View File
@@ -45,8 +45,11 @@ export class SalesOrderService extends BaseApiService<
notes: notes || `${action} marketing ${id}`,
},
});
} catch (error) {
throw error;
} catch (error: unknown) {
if (axios.isAxiosError<BaseApiResponse<{ message: string }>>(error)) {
return error.response?.data;
}
return undefined;
}
}
@@ -68,8 +71,11 @@ export class SalesOrderService extends BaseApiService<
notes: notes || `${action} marketing ${ids.join(', ')}`,
},
});
} catch (error) {
throw error;
} catch (error: unknown) {
if (axios.isAxiosError<BaseApiResponse<{ message: string }>>(error)) {
return error.response?.data;
}
return undefined;
}
}
@@ -110,8 +116,11 @@ export class SalesOrderService extends BaseApiService<
notes: notes || `Delivery marketing ${id}`,
},
});
} catch (error) {
throw error;
} catch (error: unknown) {
if (axios.isAxiosError<BaseApiResponse<{ message: string }>>(error)) {
return error.response?.data;
}
return undefined;
}
}
}
@@ -142,8 +151,11 @@ class MarketingExportService extends BaseApiService<
notes: notes,
},
});
} catch (error) {
throw error;
} catch (error: unknown) {
if (axios.isAxiosError<BaseApiResponse<Marketing[] | Marketing>>(error)) {
return error.response?.data;
}
return undefined;
}
}
+24
View File
@@ -6,6 +6,7 @@ import {
import { BaseApiService } from '@/services/api/base';
import { BaseApiResponse } from '@/types/api/api-general';
import { httpClient } from '@/services/http/client';
import axios from 'axios';
export class ChickinService extends BaseApiService<
Chickin,
@@ -16,6 +17,29 @@ export class ChickinService extends BaseApiService<
super(basePath);
}
async updateChickinDate(
projectFlockKandangId: number,
chickInDate: string
): Promise<BaseApiResponse<{ message: string }> | undefined> {
try {
return await httpClient<BaseApiResponse<{ message: string }>>(
`${this.basePath}/chick-in-date`,
{
method: 'PATCH',
body: {
project_flock_kandang_id: projectFlockKandangId,
chick_in_date: chickInDate,
},
}
);
} catch (error: unknown) {
if (axios.isAxiosError<BaseApiResponse<{ message: string }>>(error)) {
return error.response?.data;
}
return undefined;
}
}
/**
* Approve single marketing data
*/
+2 -2
View File
@@ -5,7 +5,7 @@ import { RequestOptions } from '@/services/http/base';
import { redirectToSSO } from '@/lib/auth-helper';
const BASE_URL = process.env.NEXT_PUBLIC_API_BASE_URL ?? '';
const axiosClient = axios.create({ baseURL: BASE_URL, timeout: 300_000 });
const axiosClient = axios.create({ baseURL: BASE_URL, timeout: 60_000 });
axiosClient.interceptors.response.use(
(response) => response,
@@ -38,7 +38,7 @@ export async function httpClient<T, B = unknown>(
method: opts.method ?? 'GET',
params: opts.query,
data: opts.body,
timeout: opts.timeoutMs ?? 300_000,
timeout: opts.timeoutMs ?? 60_000,
withCredentials: isCookieAuth && !isBearerAuth,
responseType: opts.responseType,
headers: {
-46
View File
@@ -1,46 +0,0 @@
export type HppPerFarmFlock = {
project_flock_id: number;
flock_name: string;
total_cost_rp: number;
feed_cost_rp: number;
ovk_cost_rp: number;
bop_cost_rp: number;
depreciation_rp: number;
other_cost_rp: number;
egg_weight_recording_kg: number;
egg_weight_do_kg: number;
hpp_per_kg_production: number;
hpp_per_kg_sales: number;
average_doc_price_rp: number;
};
export type HppPerFarmRow = {
location: { id: number; name: string };
total_cost_rp: number;
feed_cost_rp: number;
ovk_cost_rp: number;
bop_cost_rp: number;
depreciation_rp: number;
other_cost_rp: number;
egg_weight_recording_kg: number;
egg_weight_do_kg: number;
hpp_per_kg_production: number;
hpp_per_kg_sales: number;
average_doc_price_rp: number;
flocks: HppPerFarmFlock[];
};
export type HppPerFarmSummary = {
total_cost_rp: number;
total_egg_weight_recording_kg: number;
total_egg_weight_do_kg: number;
average_hpp_per_kg_production: number;
average_hpp_per_kg_sales: number;
};
export type HppPerFarmReport = {
start_date: string;
end_date: string;
rows: HppPerFarmRow[];
summary: HppPerFarmSummary;
};