Compare commits

..

25 Commits

Author SHA1 Message Date
MacBook Air M1 89a6312761 adjust laporan depresiasi 2026-06-10 22:18:44 +07:00
MacBook Air M1 7061031cd9 show total price when pending is 0 2026-06-10 21:15:59 +07:00
Giovanni Gabriel Septriadi 2e99a7b2f7 Merge branch 'feat/recording-detail-used-product-total-price' into 'development'
[FEA/FE] Recording Detail Used Product Total Price

See merge request mbugroup/lti-web-client!511
2026-06-10 13:20:35 +00:00
ValdiANS 61cd56be22 feat: add total_price to recording stock type and display it in recording detail form 2026-06-10 14:50:50 +07:00
Giovanni Gabriel Septriadi 76ee17abb4 Merge branch 'development' into 'production'
feat: add HPP Per Farm report tab with expandable flock rows

See merge request mbugroup/lti-web-client!510
2026-06-06 01:49:42 +00:00
Rivaldi A N S 2f604c9966 Merge branch 'feat/depreciation-report-v2' into 'development'
[FEAT/FE] Depreciation Report V2

See merge request mbugroup/lti-web-client!509
2026-06-05 09:41:53 +00:00
ValdiANS 55737bb96f chore: resolve merge conflict with development in ReportDepreciationTab
Development had added a forceRecompute/Refresh mechanism on top of V1 API.
Kept our V2 implementation which supersedes it — V2 uses a different
endpoint and response shape that doesn't support force_recompute.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-05 16:39:48 +07:00
ValdiANS 1ab1c9b027 Merge branch 'development' into feat/depreciation-report-v2 2026-06-05 16:27:49 +07:00
ValdiANS ab6ad7d7b1 feat: migrate depreciation report to V2 API with daily breakdown view
- Add V2 types (ReportDepreciationV2Item, DepreciationV2Meta, DepreciationV2Response) for the new per-day response shape
- Add DepreciationReportV2Api service pointing to /reports/expense/v2/depreciation
- Require projectFlock in filter (was optional); auto-open filter modal on first load when none is selected
- Replace multi-card farm loop with a single project flock card showing farm_name and period only in the header
- Replace kandang sub-table with daily depreciation rows: date, day_n, chickin_date, depreciation_value, pullet_cost_day_n_total, multiplication_percentage, total_value_pullet_after_depreciation
- Add Total Hari (limit) NumberInput field (default 10) to filter modal; remove pagination
- Switch storeName to report-depreciation-v2-table to avoid loading stale localStorage state

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-05 16:14:57 +07:00
Giovanni Gabriel Septriadi 3641d517ed Merge branch 'rc/00' into 'production'
Rc/00

See merge request mbugroup/lti-web-client!508
2026-06-05 02:44:29 +00:00
Giovanni Gabriel Septriadi fce4d52214 Merge branch 'fix/laying-transition-logic-removal' into 'rc/00'
Fix/laying transition logic removal

See merge request mbugroup/lti-web-client!507
2026-06-05 02:41:10 +00:00
Giovanni Gabriel Septriadi 68cadc42fc Merge branch 'rc/00' into 'production'
feat: add date range, filter by, and warehouse filter to marketing table

See merge request mbugroup/lti-web-client!506
2026-06-04 17:33:52 +00:00
Giovanni Gabriel Septriadi e2354b5ea7 Merge branch 'feat/enable-edit-chick-in-date' into 'rc/00'
feat: add inline edit for chick-in date in chickin logs

See merge request mbugroup/lti-web-client!503
2026-06-04 16:53:13 +00:00
Giovanni Gabriel Septriadi 8f88677191 Merge branch 'feat/marketing-filter-range-date' into 'rc/00'
feat: add date range, filter by, and warehouse filter to marketing table

See merge request mbugroup/lti-web-client!504
2026-06-04 16:52:49 +00:00
Rivaldi A N S 16c5c6c887 Merge branch 'feat/hpp-per-farm' into 'development'
[FEAT/FE] HPP Per Farm

See merge request mbugroup/lti-web-client!505
2026-06-04 08:00:40 +00:00
Rivaldi A N S 9b19e306bf Merge branch 'feat/enable-edit-chick-in-date' into 'development'
[FEAT/FE] Enable Edit Chick-In Date

See merge request mbugroup/lti-web-client!502
2026-06-03 07:26:17 +00:00
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
Rivaldi A N S 5348d47e3c Merge branch 'feat/marketing-filter-range-date' into 'development'
[FEAT/FE] Marketing Filter Range Date

See merge request mbugroup/lti-web-client!501
2026-06-02 06:28:04 +00:00
ValdiANS e73af7e252 feat: add date range, filter by, and warehouse filter to marketing table
- Add start_date and end_date range inputs to the marketing filter modal
  with validation that prevents end date from being earlier than start date
- Add 'Filter Berdasarkan' single-select radio (so_date / created_at)
  to let users choose which date field the range applies to
- Add single-select Gudang (warehouse) filter backed by WarehouseApi,
  serialized as warehouse_id query param
- Wire all three new filters into useTableFilter (paramMap, persist,
  excludeKeysFromUrl for label-only fields) and propagate through
  filterSubmitHandler, filterResetHandler, and marketingFilterInitialValues
  so filter state survives page refreshes

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-02 13:22:01 +07:00
Rivaldi A N S 80f8b190fd Merge branch 'fix/laying-transition-logic-removal' into 'development'
[FIX/FE] Laying Transition Logic Removal

See merge request mbugroup/lti-web-client!500
2026-06-02 02:47:02 +00:00
ValdiANS 7b4bd7605b fix: remove transition restriction for recording 2026-06-02 09:45:19 +07:00
Rivaldi A N S 9bd646294b Merge branch 'fix/laying-transition-logic-removal' into 'development'
[FIX/FE] Laying Transition Restrict Logic Removal

See merge request mbugroup/lti-web-client!499
2026-05-30 02:19:24 +00:00
ValdiANS 366260608f fix: remove transition restrict logic 2026-05-30 09:13:56 +07:00
15 changed files with 671 additions and 259 deletions
@@ -847,7 +847,8 @@ const DeliveryOrderFormModal = ({}: { initialValues?: Marketing }) => {
} }
}} }}
className='p-3 shadow-button-soft text-base-100 rounded-lg text-sm font-semibold' 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 && {marketing?.data?.latest_approval?.step_number === 1 &&
'Approve'} 'Approve'}
@@ -1,6 +1,6 @@
'use client'; 'use client';
import { RefObject, useCallback, useMemo } from 'react'; import { RefObject, useCallback, useMemo, useState } from 'react';
import { useFormik } from 'formik'; import { useFormik } from 'formik';
import { Icon } from '@iconify/react'; import { Icon } from '@iconify/react';
import Modal from '@/components/Modal'; import Modal from '@/components/Modal';
@@ -9,6 +9,8 @@ import SelectInput, {
OptionType, OptionType,
useSelect, useSelect,
} from '@/components/input/SelectInput'; } from '@/components/input/SelectInput';
import DateInput from '@/components/input/DateInput';
import SelectInputRadio from '@/components/input/SelectInputRadio';
import { MARKETING_APPROVAL_LINE } from '@/config/approval-line'; import { MARKETING_APPROVAL_LINE } from '@/config/approval-line';
import { import {
MarketingFilterFormValues, MarketingFilterFormValues,
@@ -17,12 +19,17 @@ import {
import { MarketingFilter } from '@/types/api/marketing/marketing'; import { MarketingFilter } from '@/types/api/marketing/marketing';
import SelectInputCheckbox from '@/components/input/SelectInputCheckbox'; import SelectInputCheckbox from '@/components/input/SelectInputCheckbox';
import { MarketingApi } from '@/services/api/marketing/marketing'; import { MarketingApi } from '@/services/api/marketing/marketing';
import { CustomerApi, ProductApi } from '@/services/api/master-data'; import {
CustomerApi,
ProductApi,
WarehouseApi,
} from '@/services/api/master-data';
import { isResponseSuccess } from '@/lib/api-helper'; import { isResponseSuccess } from '@/lib/api-helper';
import { BaseMarketing, BaseSalesOrder } from '@/types/api/marketing/marketing'; import { BaseMarketing, BaseSalesOrder } from '@/types/api/marketing/marketing';
import { ProjectFlockApi } from '@/services/api/production'; import { ProjectFlockApi } from '@/services/api/production';
import { ProjectFlock } from '@/types/api/production/project-flock'; import { ProjectFlock } from '@/types/api/production/project-flock';
import { Product } from '@/types/api/master-data/product'; import { Product } from '@/types/api/master-data/product';
import { Warehouse } from '@/types/api/master-data/warehouse';
interface MarketingFilterModal { interface MarketingFilterModal {
ref: RefObject<HTMLDialogElement | null>; ref: RefObject<HTMLDialogElement | null>;
@@ -34,6 +41,10 @@ interface MarketingFilterModal {
customer: OptionType<number> | null; customer: OptionType<number> | null;
project_flock: OptionType<number> | null; project_flock: OptionType<number> | null;
project_flock_kandang: OptionType<number> | null; project_flock_kandang: OptionType<number> | null;
warehouse: OptionType<number> | null;
start_date: string;
end_date: string;
filter_by: OptionType<string> | null;
}; };
} }
@@ -79,6 +90,13 @@ const MarketingFilterModal = ({
'search' 'search'
); );
const {
options: warehouseOptions,
isLoadingOptions: isLoadingWarehouseOptions,
setInputValue: setWarehouseInputValue,
loadMore: loadMoreWarehouses,
} = useSelect<Warehouse>(WarehouseApi.basePath, 'id', 'name', 'search');
const statusOptions = [ const statusOptions = [
...MARKETING_APPROVAL_LINE.map((item) => ({ ...MARKETING_APPROVAL_LINE.map((item) => ({
value: item.step_name.split(' ').join('_').toUpperCase(), value: item.step_name.split(' ').join('_').toUpperCase(),
@@ -87,6 +105,13 @@ const MarketingFilterModal = ({
{ value: 'DITOLAK', label: 'Ditolak' }, { value: 'DITOLAK', label: 'Ditolak' },
]; ];
const filterByOptions = [
{ value: 'so_date', label: 'Tanggal SO' },
{ value: 'created_at', label: 'Tanggal Dibuat' },
];
const [hasDateError, setHasDateError] = useState(false);
const formik = useFormik<MarketingFilterFormValues>({ const formik = useFormik<MarketingFilterFormValues>({
initialValues: initialValues || { initialValues: initialValues || {
product_ids: [], product_ids: [],
@@ -94,6 +119,10 @@ const MarketingFilterModal = ({
customer: null, customer: null,
project_flock: null, project_flock: null,
project_flock_kandang: null, project_flock_kandang: null,
warehouse: null,
start_date: '',
end_date: '',
filter_by: null,
}, },
validationSchema: MarketingFilterSchema, validationSchema: MarketingFilterSchema,
@@ -111,6 +140,12 @@ const MarketingFilterModal = ({
Number(values.project_flock_kandang?.value) || undefined, Number(values.project_flock_kandang?.value) || undefined,
project_flock_kandang_name: project_flock_kandang_name:
values.project_flock_kandang?.label || undefined, values.project_flock_kandang?.label || undefined,
warehouse_id: Number(values.warehouse?.value) || undefined,
warehouse_name: values.warehouse?.label || undefined,
start_date: values.start_date || undefined,
end_date: values.end_date || undefined,
filter_by: values.filter_by?.value || undefined,
filter_by_name: values.filter_by?.label || undefined,
}; };
onSubmit?.(formattedValues); onSubmit?.(formattedValues);
@@ -133,12 +168,37 @@ const MarketingFilterModal = ({
customer: null, customer: null,
project_flock: null, project_flock: null,
project_flock_kandang: null, project_flock_kandang: null,
warehouse: null,
start_date: '',
end_date: '',
filter_by: null,
}, },
}); });
setHasDateError(false);
onReset?.(); onReset?.();
closeModalHandler(); closeModalHandler();
}, [resetForm, onReset, closeModalHandler]); }, [resetForm, onReset, closeModalHandler]);
const handleStartDateChange = (e: React.ChangeEvent<HTMLInputElement>) => {
const value = e.target.value;
formik.setFieldValue('start_date', value);
if (value && formik.values.end_date) {
setHasDateError(new Date(formik.values.end_date) < new Date(value));
} else {
setHasDateError(false);
}
};
const handleEndDateChange = (e: React.ChangeEvent<HTMLInputElement>) => {
const value = e.target.value;
formik.setFieldValue('end_date', value);
if (value && formik.values.start_date) {
setHasDateError(new Date(value) < new Date(formik.values.start_date));
} else {
setHasDateError(false);
}
};
const productChangeHandler = (val: OptionType | OptionType[] | null) => { const productChangeHandler = (val: OptionType | OptionType[] | null) => {
formik.setFieldValue('product_ids', val as OptionType[]); formik.setFieldValue('product_ids', val as OptionType[]);
}; };
@@ -207,6 +267,44 @@ const MarketingFilterModal = ({
{/* Modal Body */} {/* Modal Body */}
<div className='p-4 flex flex-col gap-1.5'> <div className='p-4 flex flex-col gap-1.5'>
<div>
<label className='block text-xs font-semibold text-base-content py-2'>
Tanggal
</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
/>
<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={hasDateError}
/>
</div>
</div>
<SelectInputRadio
label='Filter Berdasarkan'
placeholder='Pilih Filter Berdasarkan'
options={filterByOptions}
value={formik.values.filter_by ?? null}
onChange={(val) =>
formik.setFieldValue(
'filter_by',
!Array.isArray(val) ? (val ?? null) : null
)
}
isClearable
/>
{/* select multiple product */} {/* select multiple product */}
<SelectInputCheckbox <SelectInputCheckbox
label='Product' label='Product'
@@ -272,6 +370,22 @@ const MarketingFilterModal = ({
} }
isDisabled={!formik.values.project_flock} isDisabled={!formik.values.project_flock}
/> />
<SelectInput
label='Gudang'
isClearable
placeholder='Pilih Gudang'
options={warehouseOptions}
isLoading={isLoadingWarehouseOptions}
value={formik.values.warehouse}
onChange={(val) =>
formik.setFieldValue(
'warehouse',
!Array.isArray(val) ? (val as OptionType<number> | null) : null
)
}
onInputChange={setWarehouseInputValue}
onMenuScrollToBottom={loadMoreWarehouses}
/>
</div> </div>
{/* Modal Footer */} {/* Modal Footer */}
@@ -288,6 +402,7 @@ const MarketingFilterModal = ({
<Button <Button
type='submit' type='submit'
className='p-3 rounded-lg w-fit sm:w-full max-w-40 text-base-100 text-sm' className='p-3 rounded-lg w-fit sm:w-full max-w-40 text-base-100 text-sm'
disabled={hasDateError}
> >
Apply Filter Apply Filter
</Button> </Button>
@@ -203,6 +203,12 @@ const MarketingTable = () => {
project_flock_name: '', project_flock_name: '',
project_flock_kandang_id: '', project_flock_kandang_id: '',
project_flock_kandang_name: '', project_flock_kandang_name: '',
warehouse_id: '',
warehouse_name: '',
start_date: '',
end_date: '',
filter_by: '',
filter_by_name: '',
sort_by: '', sort_by: '',
order_by: '', order_by: '',
}, },
@@ -214,6 +220,10 @@ const MarketingTable = () => {
customer_id: 'customer_id', customer_id: 'customer_id',
project_flock_id: 'project_flock_id', project_flock_id: 'project_flock_id',
project_flock_kandang_id: 'project_flock_kandang_id', project_flock_kandang_id: 'project_flock_kandang_id',
warehouse_id: 'warehouse_id',
start_date: 'start_date',
end_date: 'end_date',
filter_by: 'filter_by',
sort_by: 'sort_by', sort_by: 'sort_by',
order_by: 'sort_order', order_by: 'sort_order',
}, },
@@ -223,6 +233,8 @@ const MarketingTable = () => {
'customer_name', 'customer_name',
'project_flock_name', 'project_flock_name',
'project_flock_kandang_name', 'project_flock_kandang_name',
'warehouse_name',
'filter_by_name',
], ],
persist: true, persist: true,
@@ -293,6 +305,16 @@ const MarketingTable = () => {
values.project_flock_kandang_name ?? '', values.project_flock_kandang_name ?? '',
true true
); );
updateFilter(
'warehouse_id',
values.warehouse_id ? values.warehouse_id.toString() : '',
true
);
updateFilter('warehouse_name', values.warehouse_name ?? '', true);
updateFilter('start_date', values.start_date ?? '', true);
updateFilter('end_date', values.end_date ?? '', true);
updateFilter('filter_by', values.filter_by ?? '', true);
updateFilter('filter_by_name', values.filter_by_name ?? '', true);
}; };
const [isLoadingExportingToExcel, setIsLoadingExportingToExcel] = const [isLoadingExportingToExcel, setIsLoadingExportingToExcel] =
@@ -311,6 +333,12 @@ const MarketingTable = () => {
updateFilter('project_flock_name', '', true); updateFilter('project_flock_name', '', true);
updateFilter('project_flock_kandang_id', '', true); updateFilter('project_flock_kandang_id', '', true);
updateFilter('project_flock_kandang_name', '', true); updateFilter('project_flock_kandang_name', '', true);
updateFilter('warehouse_id', '', true);
updateFilter('warehouse_name', '', true);
updateFilter('start_date', '', true);
updateFilter('end_date', '', true);
updateFilter('filter_by', '', true);
updateFilter('filter_by_name', '', true);
}; };
const approveClickHandler = () => { const approveClickHandler = () => {
@@ -433,6 +461,20 @@ const MarketingTable = () => {
label: tableFilterState.project_flock_kandang_name, label: tableFilterState.project_flock_kandang_name,
} }
: null, : null,
warehouse: tableFilterState.warehouse_id
? {
value: Number(tableFilterState.warehouse_id),
label: tableFilterState.warehouse_name,
}
: null,
start_date: tableFilterState.start_date,
end_date: tableFilterState.end_date,
filter_by: tableFilterState.filter_by
? {
value: tableFilterState.filter_by,
label: tableFilterState.filter_by_name,
}
: null,
}; };
const approveMarketingHandler = async (notes: string) => { const approveMarketingHandler = async (notes: string) => {
@@ -707,7 +749,7 @@ const MarketingTable = () => {
}, },
{ {
accessorKey: 'so_date', accessorKey: 'so_date',
header: 'Tanggal', header: 'Tanggal SO',
cell: (props) => { cell: (props) => {
return formatDate(props.row.original.so_date, 'DD MMM yyyy'); return formatDate(props.row.original.so_date, 'DD MMM yyyy');
}, },
@@ -753,18 +795,17 @@ const MarketingTable = () => {
cell: (props) => props.row.original.customer.name, cell: (props) => props.row.original.customer.name,
}, },
{ {
accessorKey: 'grand_total', accessorKey: 'grand_total_so',
accessorFn: (row) => header: 'Grand Total SO',
row.sales_order
?.map((product) => product.total_price)
.reduce((a, b) => a + b, 0) ?? 0,
header: 'Grand Total',
cell: (props) => { cell: (props) => {
return formatCurrency( return formatCurrency(props.row.original?.grand_total_so);
props.row.original?.sales_order },
?.map((product) => product.total_price) },
.reduce((a, b) => a + b, 0) ?? 0 {
); accessorKey: 'grand_total_do',
header: 'Grand Total DO',
cell: (props) => {
return formatCurrency(props.row.original?.grand_total_do);
}, },
}, },
{ {
@@ -911,6 +952,8 @@ const MarketingTable = () => {
'customer_name', 'customer_name',
'project_flock_name', 'project_flock_name',
'project_flock_kandang_name', 'project_flock_kandang_name',
'warehouse_name',
'filter_by_name',
'sort_by', 'sort_by',
'order_by', 'order_by',
]} ]}
@@ -1,4 +1,4 @@
import { array, mixed, object } from 'yup'; import { array, mixed, object, string } from 'yup';
import { OptionType } from '@/components/input/SelectInput'; import { OptionType } from '@/components/input/SelectInput';
export const MarketingFilterSchema = object({ export const MarketingFilterSchema = object({
@@ -7,6 +7,10 @@ export const MarketingFilterSchema = object({
customer: mixed<OptionType<number>>().nullable(), customer: mixed<OptionType<number>>().nullable(),
project_flock: mixed<OptionType<number>>().nullable(), project_flock: mixed<OptionType<number>>().nullable(),
project_flock_kandang: mixed<OptionType<number>>().nullable(), project_flock_kandang: mixed<OptionType<number>>().nullable(),
warehouse: mixed<OptionType<number>>().nullable(),
start_date: string().optional(),
end_date: string().optional(),
filter_by: mixed<OptionType<string>>().nullable(),
}); });
export type MarketingFilterFormValues = { export type MarketingFilterFormValues = {
@@ -15,4 +19,8 @@ export type MarketingFilterFormValues = {
customer: OptionType<number> | null; customer: OptionType<number> | null;
project_flock: OptionType<number> | null; project_flock: OptionType<number> | null;
project_flock_kandang: OptionType<number> | null; project_flock_kandang: OptionType<number> | null;
warehouse: OptionType<number> | null;
start_date: string;
end_date: string;
filter_by: OptionType<string> | null;
}; };
@@ -2,16 +2,17 @@ import Alert from '@/components/Alert';
import Button from '@/components/Button'; import Button from '@/components/Button';
import Card from '@/components/Card'; import Card from '@/components/Card';
import RequirePermission from '@/components/helper/RequirePermission'; import RequirePermission from '@/components/helper/RequirePermission';
import DateInput from '@/components/input/DateInput';
import PillBadge from '@/components/PillBadge'; import PillBadge from '@/components/PillBadge';
import { isResponseError, isResponseSuccess } from '@/lib/api-helper'; import { isResponseError, isResponseSuccess } from '@/lib/api-helper';
import { formatDate, formatNumber } from '@/lib/helper'; import { formatDate, formatNumber } from '@/lib/helper';
import { ChickinApi } from '@/services/api/production/chickin'; import { ChickinApi } from '@/services/api/production/chickin';
import { useChickinStore } from '@/stores/production/chickin/chickin.store';
import { BaseApproval } from '@/types/api/api-general'; import { BaseApproval } from '@/types/api/api-general';
import { ProjectFlockKandang } from '@/types/api/production/project-flock-kandang'; import { ProjectFlockKandang } from '@/types/api/production/project-flock-kandang';
import { Icon } from '@iconify/react'; import { Icon } from '@iconify/react';
import { useState } from 'react'; import { useState } from 'react';
import toast from 'react-hot-toast'; import toast from 'react-hot-toast';
import { useChickinStore } from '@/stores/production/chickin/chickin.store';
const ChickinLogsView = ({ const ChickinLogsView = ({
initialValues, initialValues,
@@ -23,6 +24,9 @@ const ChickinLogsView = ({
rawDataApprovals: BaseApproval[]; rawDataApprovals: BaseApproval[];
}) => { }) => {
const [chickinErrorMessage, setChickinErrorMessage] = useState(''); 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 { openChickinApproveModal, openChickinDeleteModal } = useChickinStore();
const handleClickApprove = () => { 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) => { const handleDeleteChickin = (chickinId: number) => {
openChickinDeleteModal(chickinId, async () => { openChickinDeleteModal(chickinId, async () => {
const deleteRes = await ChickinApi.delete(chickinId); const deleteRes = await ChickinApi.delete(chickinId);
@@ -133,9 +154,54 @@ const ChickinLogsView = ({
<Icon icon={'mdi:calendar'} width={14} height={14} />{' '} <Icon icon={'mdi:calendar'} width={14} height={14} />{' '}
<span>Tanggal Chick In</span> <span>Tanggal Chick In</span>
</div> </div>
<div className='text-end text-gray-500'> {editingChickinId === chickin.id ? (
{formatDate(chickin.chick_in_date, 'DD MMM YYYY')} <div className='flex flex-col gap-2 items-end w-1/2'>
</div> <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> </div>
{/* Kandang */} {/* Kandang */}
@@ -463,13 +463,16 @@ const RecordingForm = ({ type = 'add', initialValues }: RecordingFormProps) => {
}, [selectedKandang]); }, [selectedKandang]);
// ===== TRANSITION RESTRICTION LOGIC ===== // ===== TRANSITION RESTRICTION LOGIC =====
const isTransitionPeriod = useMemo(() => { // const isTransitionPeriod = useMemo(() => {
return ( // return (
initialValues?.is_transition ?? // initialValues?.is_transition ??
projectFlockKandangLookup?.is_transition ?? // projectFlockKandangLookup?.is_transition ??
false // false
); // );
}, [initialValues, projectFlockKandangLookup]); // }, [initialValues, projectFlockKandangLookup]);
// set to false by request: 30 May 2026, 09:11
const isTransitionPeriod = false;
const recordingRestriction = useMemo(() => { const recordingRestriction = useMemo(() => {
let isLaying: boolean; let isLaying: boolean;
@@ -483,10 +486,13 @@ const RecordingForm = ({ type = 'add', initialValues }: RecordingFormProps) => {
false; false;
} }
const isTransition = // const isTransition =
initialValues?.is_transition ?? // initialValues?.is_transition ??
projectFlockKandangLookup?.is_transition ?? // projectFlockKandangLookup?.is_transition ??
false; // false;
// set to false by request: 30 May 2026, 09:11
const isTransition = false;
const currentIsLaying = const currentIsLaying =
type === 'edit' type === 'edit'
@@ -1503,6 +1509,9 @@ const RecordingForm = ({ type = 'add', initialValues }: RecordingFormProps) => {
const { pendingQty } = getStockPendingInfo( const { pendingQty } = getStockPendingInfo(
stock.product_warehouse_id.value stock.product_warehouse_id.value
); );
const totalPrice = initialValues?.stocks?.[stockIdx].total_price;
const showTotalPrice =
type === 'detail' && typeof totalPrice === 'number';
if (isDetail) { if (isDetail) {
if (pendingQty > 0) { if (pendingQty > 0) {
@@ -1510,7 +1519,15 @@ const RecordingForm = ({ type = 'add', initialValues }: RecordingFormProps) => {
<span className='text-sm text-gray-600 whitespace-nowrap'> <span className='text-sm text-gray-600 whitespace-nowrap'>
(tersedia: {formatNumber(availableStock)} | pending:{' '} (tersedia: {formatNumber(availableStock)} | pending:{' '}
<span className='text-error'>{formatNumber(pendingQty)}</span> | <span className='text-error'>{formatNumber(pendingQty)}</span> |
pakai: {formatNumber(requestedUsage)}) pakai: {formatNumber(requestedUsage)}
{showTotalPrice && <> | total harga: {totalPrice}</>})
</span>
);
}
if (showTotalPrice) {
return (
<span className='text-sm text-gray-600 whitespace-nowrap'>
(total harga: {totalPrice})
</span> </span>
); );
} }
@@ -1523,13 +1540,15 @@ const RecordingForm = ({ type = 'add', initialValues }: RecordingFormProps) => {
(tersedia: {formatNumber(availableStock)} | pakai:{' '} (tersedia: {formatNumber(availableStock)} | pakai:{' '}
{formatNumber(requestedUsage)} | sisa:{' '} {formatNumber(requestedUsage)} | sisa:{' '}
{formatNumber(Math.max(remainingStock, 0))} | dipinjam:{' '} {formatNumber(Math.max(remainingStock, 0))} | dipinjam:{' '}
{formatNumber(Math.max(-remainingStock, 0))}) {formatNumber(Math.max(-remainingStock, 0))}
{showTotalPrice && <> | total harga: {totalPrice}</>})
</span> </span>
); );
} }
return ( return (
<span className='text-sm text-gray-600 whitespace-nowrap'> <span className='text-sm text-gray-600 whitespace-nowrap'>
(tersedia: {formatNumber(availableStock)}) (tersedia: {formatNumber(availableStock)}
{showTotalPrice && <> | total harga: {totalPrice}</>})
</span> </span>
); );
}, },
@@ -11,63 +11,72 @@ export const getRecordingRestriction = (
isTransition: boolean, isTransition: boolean,
currentIsLaying?: boolean currentIsLaying?: boolean
): RecordingRestriction => { ): RecordingRestriction => {
if (isTransition && !isLaying) { // if (isTransition && !isLaying) {
const isLayingKandangInTransition = currentIsLaying === true; // const isLayingKandangInTransition = currentIsLaying === true;
if (isLayingKandangInTransition) { // if (isLayingKandangInTransition) {
return { // return {
canEditStock: false, // canEditStock: false,
canEditDepletion: true, // canEditDepletion: true,
canEditEgg: true, // canEditEgg: true,
isLocked: false, // isLocked: false,
lockReason: undefined, // lockReason: undefined,
}; // };
} else { // } else {
return { // return {
canEditStock: true, // canEditStock: true,
canEditDepletion: false, // canEditDepletion: false,
canEditEgg: false, // canEditEgg: false,
isLocked: false, // isLocked: false,
lockReason: undefined, // lockReason: undefined,
}; // };
} // }
} // }
if (!isLaying && !isTransition && currentIsLaying) { // if (!isLaying && !isTransition && currentIsLaying) {
return { // return {
canEditStock: false, // canEditStock: false,
canEditDepletion: false, // canEditDepletion: false,
canEditEgg: false, // canEditEgg: false,
isLocked: true, // isLocked: true,
lockReason: // lockReason:
'Recording Growing telah terkunci karena Project Flock sudah masuk fase Laying', // 'Recording Growing telah terkunci karena Project Flock sudah masuk fase Laying',
}; // };
} // }
if (!isLaying && !isTransition) { // if (!isLaying && !isTransition) {
return { // return {
canEditStock: true, // canEditStock: true,
canEditDepletion: true, // canEditDepletion: true,
canEditEgg: false, // canEditEgg: false,
isLocked: false, // isLocked: false,
lockReason: undefined, // lockReason: undefined,
}; // };
} // }
if (isLaying && !isTransition) { // if (isLaying && !isTransition) {
return { // return {
canEditStock: true, // canEditStock: true,
canEditDepletion: true, // canEditDepletion: true,
canEditEgg: true, // canEditEgg: true,
isLocked: false, // isLocked: false,
lockReason: undefined, // lockReason: undefined,
}; // };
} // }
// return {
// canEditStock: false,
// canEditDepletion: false,
// canEditEgg: false,
// isLocked: true,
// lockReason: 'Kondisi transisi tidak valid',
// };
// remove recording transition restriction by request: 30 May 2026, 09:11
return { return {
canEditStock: false, canEditStock: true,
canEditDepletion: false, canEditDepletion: true,
canEditEgg: false, canEditEgg: true,
isLocked: true, isLocked: false,
lockReason: 'Kondisi transisi tidak valid', lockReason: undefined,
}; };
}; };
@@ -8,6 +8,7 @@ import { Icon } from '@iconify/react';
import Modal from '@/components/Modal'; import Modal from '@/components/Modal';
import Button from '@/components/Button'; import Button from '@/components/Button';
import DateInput from '@/components/input/DateInput'; import DateInput from '@/components/input/DateInput';
import NumberInput from '@/components/input/NumberInput';
import SelectInput, { import SelectInput, {
OptionType, OptionType,
useSelect, useSelect,
@@ -24,13 +25,20 @@ export type ReportDepreciationFilterValues = {
location?: OptionType<string>; location?: OptionType<string>;
projectFlock?: OptionType<string>; projectFlock?: OptionType<string>;
period: string | null; period: string | null;
totalDays: number;
}; };
export const ReportDepreciationFilterSchema = yup.object({ export const ReportDepreciationFilterSchema = yup.object({
area: yup.mixed<OptionType<string>>().optional(), area: yup.mixed<OptionType<string>>().optional(),
location: yup.mixed<OptionType<string>>().optional(), location: yup.mixed<OptionType<string>>().optional(),
projectFlock: yup.mixed<OptionType<string>>().optional(), projectFlock: yup
.mixed<OptionType<string>>()
.required('Project Flock wajib dipilih'),
period: yup.string().nullable().required('Periode wajib dipilih'), period: yup.string().nullable().required('Periode wajib dipilih'),
totalDays: yup
.number()
.min(1, 'Minimal 1 hari')
.required('Total Hari wajib diisi'),
}); });
interface ReportDepreciationFilterModalProps { interface ReportDepreciationFilterModalProps {
@@ -47,6 +55,7 @@ const defaultInitialValues: (
location: undefined, location: undefined,
projectFlock: undefined, projectFlock: undefined,
period: initialValues?.period ?? null, period: initialValues?.period ?? null,
totalDays: initialValues?.totalDays ?? 10,
}); });
const ReportDepreciationFilterModal = ({ const ReportDepreciationFilterModal = ({
@@ -196,6 +205,14 @@ const ReportDepreciationFilterModal = ({
isClearable isClearable
isSearchable={true} isSearchable={true}
className={{ wrapper: 'w-full' }} className={{ wrapper: 'w-full' }}
isError={
formik.touched.projectFlock && !!formik.errors.projectFlock
}
errorMessage={
formik.touched.projectFlock
? (formik.errors.projectFlock as string)
: undefined
}
/> />
<DateInput <DateInput
@@ -210,6 +227,31 @@ const ReportDepreciationFilterModal = ({
required required
isNestedModal isNestedModal
/> />
<NumberInput
label='Total Hari'
name='totalDays'
placeholder='Masukkan total hari'
value={formik.values.totalDays}
onChange={(e) => {
const val = Number(e.target.value);
formik.setFieldValue(
'totalDays',
isNaN(val) || val < 1 ? 1 : Math.floor(val)
);
}}
onBlur={formik.handleBlur}
decimalScale={0}
allowNegative={false}
thousandSeparator=''
isError={formik.touched.totalDays && !!formik.errors.totalDays}
errorMessage={
formik.touched.totalDays
? (formik.errors.totalDays as string)
: undefined
}
className={{ wrapper: 'w-full' }}
/>
</div> </div>
<div className='p-4 flex justify-between gap-4 border-t border-base-content/10 bg-gray-50'> <div className='p-4 flex justify-between gap-4 border-t border-base-content/10 bg-gray-50'>
@@ -1,13 +1,11 @@
'use client'; 'use client';
import React, { useCallback, useEffect, useMemo, useState } from 'react'; import React, { useEffect, useMemo, useRef } from 'react';
import useSWR from 'swr'; import useSWR from 'swr';
import { ColumnDef } from '@tanstack/react-table'; import { ColumnDef } from '@tanstack/react-table';
import { Icon } from '@iconify/react'; import { Icon } from '@iconify/react';
import Button from '@/components/Button';
import Card from '@/components/Card'; import Card from '@/components/Card';
import Pagination from '@/components/Pagination';
import Table from '@/components/Table'; import Table from '@/components/Table';
import ButtonFilter from '@/components/helper/ButtonFilter'; import ButtonFilter from '@/components/helper/ButtonFilter';
import ReportExpenseSkeleton from '@/components/pages/report/expense/skeleton/ReportExpenseSkeleton'; import ReportExpenseSkeleton from '@/components/pages/report/expense/skeleton/ReportExpenseSkeleton';
@@ -15,11 +13,14 @@ import { useModal } from '@/components/Modal';
import ReportDepreciationFilterModal from '@/components/pages/report/expense/tab/ReportDepreciationFilterModal'; import ReportDepreciationFilterModal from '@/components/pages/report/expense/tab/ReportDepreciationFilterModal';
import { useTabActionsStore } from '@/stores/tab-actions/tab-actions.store'; import { useTabActionsStore } from '@/stores/tab-actions/tab-actions.store';
import { ReportDepreciation } from '@/types/api/report/report-expense'; import {
import { DepreciationReportApi } from '@/services/api/report/expense-report'; DepreciationV2Response,
ReportDepreciationV2Item,
} from '@/types/api/report/report-expense';
import { DepreciationReportV2Api } from '@/services/api/report/expense-report';
import { useTableFilter } from '@/services/hooks/useTableFilter'; import { useTableFilter } from '@/services/hooks/useTableFilter';
import { OptionType } from '@/components/input/SelectInput'; import { OptionType } from '@/components/input/SelectInput';
import { isResponseSuccess } from '@/lib/api-helper'; import { httpClientFetcher } from '@/services/http/client';
import { formatCurrency, formatDate, formatNumber } from '@/lib/helper'; import { formatCurrency, formatDate, formatNumber } from '@/lib/helper';
interface ReportDepreciationTabProps { interface ReportDepreciationTabProps {
@@ -30,8 +31,6 @@ const ReportDepreciationTab = ({ tabId }: ReportDepreciationTabProps) => {
const { const {
state: tableFilterState, state: tableFilterState,
updateFilter, updateFilter,
setPage,
setPageSize,
toQueryString: getTableFilterQueryString, toQueryString: getTableFilterQueryString,
reset: resetFilter, reset: resetFilter,
} = useTableFilter<{ } = useTableFilter<{
@@ -39,107 +38,121 @@ const ReportDepreciationTab = ({ tabId }: ReportDepreciationTabProps) => {
location?: OptionType<string>; location?: OptionType<string>;
projectFlock?: OptionType<string>; projectFlock?: OptionType<string>;
period: string; period: string;
totalDays: number;
}>({ }>({
initial: { initial: {
area: undefined, area: undefined,
location: undefined, location: undefined,
projectFlock: undefined, projectFlock: undefined,
period: formatDate(Date.now(), 'YYYY-MM-DD'), period: formatDate(Date.now(), 'YYYY-MM-DD'),
totalDays: 10,
}, },
paramMap: { paramMap: {
pageSize: 'limit',
area: 'area_id', area: 'area_id',
location: 'location_id', location: 'location_id',
projectFlock: 'project_flock_id', projectFlock: 'project_flock_id',
period: 'period', period: 'period',
totalDays: 'limit',
}, },
persist: true, persist: true,
storeName: 'report-depreciation-table', storeName: 'report-depreciation-v2-table',
}); });
const [forceRecompute, setForceRecompute] = useState(false); const swrKey = tableFilterState.projectFlock
? `${DepreciationReportV2Api.basePath}${getTableFilterQueryString()}`
: null;
const { const { data: depreciationsResponse, isLoading: isLoadingDepreciations } =
data: depreciationsResponse, useSWR<DepreciationV2Response>(swrKey, httpClientFetcher);
isLoading: isLoadingDepreciations,
isValidating,
mutate,
} = useSWR(
`${DepreciationReportApi.basePath}${getTableFilterQueryString()}&force_recompute=${forceRecompute}`,
DepreciationReportApi.getAllFetcher
);
const handleRefresh = useCallback(() => { const depreciationMeta =
setForceRecompute(true); depreciationsResponse?.status === 'success'
}, [mutate]); ? depreciationsResponse.meta
: null;
const depreciations = isResponseSuccess(depreciationsResponse) const depreciationData =
? depreciationsResponse.data depreciationsResponse?.status === 'success'
: []; ? depreciationsResponse.data
: [];
const filterModal = useModal(); const filterModal = useModal();
const { ref: filterModalRef } = filterModal; const { ref: filterModalRef } = filterModal;
const initialOpenRef = useRef(false);
useEffect(() => {
if (!initialOpenRef.current) {
initialOpenRef.current = true;
if (!tableFilterState.projectFlock) {
filterModal.openModal();
}
}
}, []);
const setTabActions = useTabActionsStore((state) => state.setTabActions); const setTabActions = useTabActionsStore((state) => state.setTabActions);
const clearTabActions = useTabActionsStore((state) => state.clearTabActions); const clearTabActions = useTabActionsStore((state) => state.clearTabActions);
const depreciationKandangColumns: ColumnDef< const depreciationColumns: ColumnDef<ReportDepreciationV2Item>[] = useMemo(
ReportDepreciation['components']['kandang'][0] () => [
>[] = [ {
{ accessorKey: 'date',
accessorKey: 'kandang_name', header: 'Tanggal',
header: 'Kandang', cell: ({ row }) => formatDate(row.original.date, 'DD MMM YYYY'),
}, },
{ {
accessorKey: 'house_type', accessorKey: 'day_n',
header: 'Tipe Kandang', header: 'Hari ke-',
cell: ({ row }) => row.original.house_type.toUpperCase(), },
}, {
{ accessorKey: 'chickin_date',
accessorKey: 'depreciation_percent', header: 'Tanggal Chick-in',
header: 'Persentase Depresiasi', cell: ({ row }) => formatDate(row.original.chickin_date, 'DD MMM YYYY'),
cell: ({ row }) => row.original.depreciation_percent + '%', },
}, {
{ accessorKey: 'depreciation_value',
accessorKey: 'depreciation_value', header: 'Nilai Depresiasi',
header: 'Nilai Depresiasi', cell: ({ row }) => formatCurrency(row.original.depreciation_value),
cell: ({ row }) => formatCurrency(row.original.depreciation_value), },
}, {
{ accessorKey: 'pullet_cost_day_n_total',
accessorKey: 'depreciation_source', header: 'Total Harga Pullet Hari ke-N',
header: 'Asal Depresiasi', cell: ({ row }) =>
cell: ({ row }) => row.original.depreciation_source.toUpperCase(), formatCurrency(
}, row.original.pullet_cost_day_n_total,
{ 'IDR',
accessorKey: 'cutover_date', 'id-ID',
header: 'Tanggal Cutover', 0,
cell: ({ row }) => formatDate(row.original.cutover_date, 'DD MMM YYYY'), 2
}, ),
{ },
accessorKey: 'origin_date', {
header: 'Tanggal Origin', accessorKey: 'multiplication_percentage',
cell: ({ row }) => formatDate(row.original.origin_date, 'DD MMM YYYY'), header: 'Persentase Multiplikasi',
}, cell: ({ row }) =>
]; formatNumber(
row.original.multiplication_percentage * 100,
'en-US',
0,
4
) + '%',
},
{
accessorKey: 'total_value_pullet_after_depreciation',
header: 'Total Nilai Pullet Setelah Depresiasi',
cell: ({ row }) =>
formatCurrency(
row.original.total_value_pullet_after_depreciation,
'IDR',
'id-ID',
0,
2
),
},
],
[]
);
const tabActionsElement = useMemo( const tabActionsElement = useMemo(
() => ( () => (
<div className='flex flex-row gap-3'> <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 <ButtonFilter
values={tableFilterState} values={tableFilterState}
excludeFields={['page', 'pageSize']} excludeFields={['page', 'pageSize']}
@@ -149,7 +162,7 @@ const ReportDepreciationTab = ({ tabId }: ReportDepreciationTabProps) => {
/> />
</div> </div>
), ),
[tableFilterState, handleRefresh, isValidating] [tableFilterState]
); );
useEffect(() => { useEffect(() => {
@@ -171,9 +184,9 @@ const ReportDepreciationTab = ({ tabId }: ReportDepreciationTabProps) => {
</div> </div>
)} )}
{!isLoadingDepreciations && depreciations.length === 0 && ( {!isLoadingDepreciations && !tableFilterState.projectFlock && (
<ReportExpenseSkeleton <ReportExpenseSkeleton
columns={depreciationKandangColumns} columns={depreciationColumns}
icon={ icon={
<Icon <Icon
icon='heroicons:chart-bar' icon='heroicons:chart-bar'
@@ -182,90 +195,75 @@ const ReportDepreciationTab = ({ tabId }: ReportDepreciationTabProps) => {
height={20} height={20}
/> />
} }
title='Data Not Yet Available' title='Pilih Project Flock'
subtitle='Please change your filters to get the data.' subtitle='Silakan pilih Project Flock pada filter untuk melihat data depresiasi.'
/> />
)} )}
{!isLoadingDepreciations && depreciations.length > 0 && ( {!isLoadingDepreciations &&
<> tableFilterState.projectFlock &&
{depreciations.map((depreciationItem, idx) => ( depreciationData.length === 0 && (
<Card <ReportExpenseSkeleton
key={idx} columns={depreciationColumns}
title={depreciationItem.farm_name} icon={
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)}`} <Icon
className={{ icon='heroicons:chart-bar'
wrapper: 'w-full rounded-lg border-none', className='text-white'
body: 'p-0', width={20}
title: height={20}
'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} title='Data Not Yet Available'
currentPage={ subtitle='Please change your filters to get the data.'
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}
/> />
</> )}
)}
{!isLoadingDepreciations &&
depreciationData.length > 0 &&
depreciationMeta && (
<Card
title={depreciationMeta.farm_name}
subtitle={`Periode: ${formatDate(depreciationMeta.period, 'DD MMM YYYY')}`}
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={depreciationData}
columns={depreciationColumns}
pageSize={depreciationData.length}
page={1}
totalItems={depreciationData.length}
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>
)}
</div> </div>
<ReportDepreciationFilterModal <ReportDepreciationFilterModal
@@ -281,8 +279,7 @@ const ReportDepreciationTab = ({ tabId }: ReportDepreciationTabProps) => {
values.period ? formatDate(values.period, 'YYYY-MM-DD') : '', values.period ? formatDate(values.period, 'YYYY-MM-DD') : '',
true true
); );
updateFilter('totalDays', values.totalDays ?? 10, true);
setForceRecompute(false);
}} }}
/> />
</> </>
+20 -8
View File
@@ -45,8 +45,11 @@ export class SalesOrderService extends BaseApiService<
notes: notes || `${action} marketing ${id}`, notes: notes || `${action} marketing ${id}`,
}, },
}); });
} catch (error) { } catch (error: unknown) {
throw error; 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(', ')}`, notes: notes || `${action} marketing ${ids.join(', ')}`,
}, },
}); });
} catch (error) { } catch (error: unknown) {
throw error; 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}`, notes: notes || `Delivery marketing ${id}`,
}, },
}); });
} catch (error) { } catch (error: unknown) {
throw error; if (axios.isAxiosError<BaseApiResponse<{ message: string }>>(error)) {
return error.response?.data;
}
return undefined;
} }
} }
} }
@@ -142,8 +151,11 @@ class MarketingExportService extends BaseApiService<
notes: notes, notes: notes,
}, },
}); });
} catch (error) { } catch (error: unknown) {
throw error; 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 { BaseApiService } from '@/services/api/base';
import { BaseApiResponse } from '@/types/api/api-general'; import { BaseApiResponse } from '@/types/api/api-general';
import { httpClient } from '@/services/http/client'; import { httpClient } from '@/services/http/client';
import axios from 'axios';
export class ChickinService extends BaseApiService< export class ChickinService extends BaseApiService<
Chickin, Chickin,
@@ -16,6 +17,29 @@ export class ChickinService extends BaseApiService<
super(basePath); 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 * Approve single marketing data
*/ */
@@ -4,6 +4,7 @@ import { httpClient, httpClientFetcher } from '@/services/http/client';
import { BaseApiResponse } from '@/types/api/api-general'; import { BaseApiResponse } from '@/types/api/api-general';
import { import {
ReportDepreciation, ReportDepreciation,
ReportDepreciationV2Item,
ReportExpense, ReportExpense,
} from '@/types/api/report/report-expense'; } from '@/types/api/report/report-expense';
@@ -57,3 +58,9 @@ export const DepreciationReportApi = new BaseApiService<
unknown, unknown,
unknown unknown
>('/reports/expense/depreciation'); >('/reports/expense/depreciation');
export const DepreciationReportV2Api = new BaseApiService<
ReportDepreciationV2Item,
unknown,
unknown
>('/reports/expense/v2/depreciation');
+8
View File
@@ -23,6 +23,8 @@ export type BaseMarketing = {
latest_approval: BaseApproval; latest_approval: BaseApproval;
sales_order: BaseSalesOrder[]; sales_order: BaseSalesOrder[];
delivery_order: BaseDeliveryOrder[]; delivery_order: BaseDeliveryOrder[];
grand_total_do: number;
grand_total_so: number;
}; };
export type BaseSalesOrder = { export type BaseSalesOrder = {
@@ -104,6 +106,12 @@ export type MarketingFilter = {
project_flock_name?: string; project_flock_name?: string;
project_flock_kandang_id?: number; project_flock_kandang_id?: number;
project_flock_kandang_name?: string; project_flock_kandang_name?: string;
start_date?: string;
end_date?: string;
filter_by?: string;
filter_by_name?: string;
warehouse_id?: number;
warehouse_name?: string;
}; };
/** /**
+1
View File
@@ -67,6 +67,7 @@ export type RecordingStock = {
qty?: number; qty?: number;
usage_amount?: number; usage_amount?: number;
pending_qty: number; pending_qty: number;
total_price?: number;
product_warehouse: ProductWarehouse; product_warehouse: ProductWarehouse;
}; };
+60
View File
@@ -90,3 +90,63 @@ export type ReportDepreciationSearchParams = {
farm: string | null; farm: string | null;
period: string | null; period: string | null;
}; };
export type ReportDepreciationV2KandangItem = {
kandang_id: number;
kandang_name: string;
transfer_id: number;
depreciation_percent: number;
pullet_cost_day_n: number;
depreciation_value: number;
chickin_date: string;
project_flock_kandang_id: number;
depreciation_source: string;
transfer_date: string;
source_project_flock_id: number;
house_type: string;
multiplication_percentage: number;
cutover_date: string;
origin_date: string;
standard_effective_date: string;
population: number;
transfer_qty: number;
total_value_pullet_after_depreciation: number;
manual_input_id: number;
start_schedule_day: number;
day_n: number;
};
export type ReportDepreciationV2Item = {
date: string;
depreciation_percent_effective: number;
depreciation_value: number;
pullet_cost_day_n_total: number;
multiplication_percentage: number;
day_n: number;
chickin_date: string;
total_value_pullet_after_depreciation: number;
standard_effective_date: string;
total_population: number;
components: {
kandang_count: number;
total_population: number;
kandang: ReportDepreciationV2KandangItem[];
};
};
export type DepreciationV2Meta = {
project_flock_id: number;
farm_name: string;
location_id: number;
period: string;
limit: number;
total_days: number;
};
export type DepreciationV2Response = {
code: number;
status: string;
message: string;
meta: DepreciationV2Meta;
data: ReportDepreciationV2Item[];
};