mirror of
https://gitlab.com/mbugroup/lti-web-client.git
synced 2026-05-20 13:32:00 +00:00
Merge branch 'development' of gitlab.com:mbugroup/lti-web-client into feat/FE/US-281/TASK-316-317-slicing-ui-and-integrate-api-daily-recording-growing-uniformity-page
This commit is contained in:
@@ -0,0 +1,11 @@
|
||||
import ProductionResultContent from '@/components/pages/report/production-result/ProductionResultContent';
|
||||
|
||||
const ProductionResultReportPage = () => {
|
||||
return (
|
||||
<section className='w-full max-w-7xl pb-16'>
|
||||
<ProductionResultContent />
|
||||
</section>
|
||||
);
|
||||
};
|
||||
|
||||
export default ProductionResultReportPage;
|
||||
@@ -25,9 +25,9 @@ const ClosingGeneralInformationTable = ({
|
||||
<td>{initialValue?.period}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Kategori</td>
|
||||
<td>Project Flock</td>
|
||||
<td>:</td>
|
||||
<td>{initialValue?.project_category}</td>
|
||||
<td>{initialValue?.project_flock?.name}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Populasi</td>
|
||||
|
||||
@@ -126,28 +126,6 @@ const ClosingsTable = () => {
|
||||
accessorKey: 'shed_label',
|
||||
header: 'Jumlah Kandang',
|
||||
},
|
||||
{
|
||||
accessorKey: 'sales_paid_amount',
|
||||
header: 'Jumlah Sudah Bayar',
|
||||
cell: (props) => (
|
||||
<span className='text-success'>
|
||||
{formatCurrency(props.row.original.sales_paid_amount)}
|
||||
</span>
|
||||
),
|
||||
},
|
||||
{
|
||||
accessorKey: 'sales_remaining_amount',
|
||||
header: 'Jumlah Sisa Bayar',
|
||||
cell: (props) => (
|
||||
<span className='text-error'>
|
||||
{formatCurrency(props.row.original.sales_remaining_amount)}
|
||||
</span>
|
||||
),
|
||||
},
|
||||
{
|
||||
accessorKey: 'sales_payment_status',
|
||||
header: 'Status Pembayaran',
|
||||
},
|
||||
{
|
||||
accessorKey: 'project_status',
|
||||
header: 'Status',
|
||||
|
||||
@@ -12,11 +12,6 @@ type RecordingGrowingFormSchemaType = {
|
||||
label: string;
|
||||
} | null;
|
||||
project_flock_kandang_id: number;
|
||||
body_weights: {
|
||||
weight: number | string;
|
||||
avg_weight: number | string;
|
||||
qty: number | string;
|
||||
}[];
|
||||
stocks: {
|
||||
product_warehouse_id: number;
|
||||
qty: number | string;
|
||||
@@ -35,12 +30,6 @@ type RecordingLayingFormSchemaType = RecordingGrowingFormSchemaType & {
|
||||
}[];
|
||||
};
|
||||
|
||||
export type BodyWeightSchema = {
|
||||
weight: number | string;
|
||||
avg_weight: number | string;
|
||||
qty: number | string;
|
||||
};
|
||||
|
||||
export type StockSchema = {
|
||||
product_warehouse_id: number;
|
||||
qty: number | string;
|
||||
@@ -57,20 +46,6 @@ export type EggSchema = {
|
||||
weight: number | string;
|
||||
};
|
||||
|
||||
const BodyWeightObjectSchema: Yup.ObjectSchema<BodyWeightSchema> = Yup.object({
|
||||
weight: Yup.number()
|
||||
.required('Berat ayam total wajib diisi!')
|
||||
.min(1, 'Berat ayam total minimal 1 gram!')
|
||||
.typeError('Berat ayam total harus berupa angka!'),
|
||||
avg_weight: Yup.number()
|
||||
.required('Berat ayam rata-rata wajib diisi!')
|
||||
.typeError('Berat ayam rata-rata harus berupa angka!'),
|
||||
qty: Yup.number()
|
||||
.required('Jumlah ayam wajib diisi!')
|
||||
.min(1, 'Jumlah ayam minimal 1 ekor!')
|
||||
.typeError('Jumlah ayam harus berupa angka!'),
|
||||
});
|
||||
|
||||
const StockObjectSchema: Yup.ObjectSchema<StockSchema> = Yup.object({
|
||||
product_warehouse_id: Yup.number()
|
||||
.required('Produk wajib diisi!')
|
||||
@@ -140,10 +115,6 @@ export const RecordingGrowingFormSchema: Yup.ObjectSchema<RecordingGrowingFormSc
|
||||
return true;
|
||||
}
|
||||
),
|
||||
body_weights: Yup.array()
|
||||
.of(BodyWeightObjectSchema)
|
||||
.min(1, 'Minimal harus ada 1 data bobot badan!')
|
||||
.required('Data bobot badan wajib diisi!'),
|
||||
stocks: Yup.array()
|
||||
.of(StockObjectSchema)
|
||||
.min(1, 'Minimal harus ada 1 data stok!')
|
||||
@@ -196,7 +167,6 @@ export type RecordingLayingFormValues = Yup.InferType<
|
||||
>;
|
||||
|
||||
type RecordingFormData = Partial<Recording> & {
|
||||
body_weights?: CreateGrowingRecordingPayload['body_weights'];
|
||||
stocks?: CreateGrowingRecordingPayload['stocks'] | Recording['stocks'];
|
||||
depletions?:
|
||||
| CreateGrowingRecordingPayload['depletions']
|
||||
@@ -216,19 +186,6 @@ export const getRecordingGrowingFormInitialValues = (
|
||||
}
|
||||
: null,
|
||||
project_flock_kandang_id: initialValues?.project_flock_kandang_id ?? 0,
|
||||
body_weights: initialValues?.body_weights?.map(
|
||||
(bw: NonNullable<CreateGrowingRecordingPayload['body_weights']>[0]) => ({
|
||||
weight: bw.avg_weight * bw.qty,
|
||||
avg_weight: bw.avg_weight,
|
||||
qty: bw.qty,
|
||||
})
|
||||
) ?? [
|
||||
{
|
||||
weight: '',
|
||||
avg_weight: '',
|
||||
qty: '',
|
||||
},
|
||||
],
|
||||
stocks: initialValues?.stocks?.map((stock) => ({
|
||||
product_warehouse_id: stock.product_warehouse_id,
|
||||
qty:
|
||||
|
||||
@@ -60,6 +60,7 @@ import {
|
||||
GROWING_RECORDING_APPROVAL_LINE,
|
||||
LAYING_RECORDING_APPROVAL_LINE,
|
||||
} from '@/config/approval-line';
|
||||
import Table from '@/components/Table';
|
||||
|
||||
interface RecordingFormProps {
|
||||
type?: 'add' | 'edit' | 'detail';
|
||||
@@ -71,16 +72,10 @@ const RecordingForm = ({ type = 'add', initialValues }: RecordingFormProps) => {
|
||||
const router = useRouter();
|
||||
|
||||
// ===== STATE MANAGEMENT =====
|
||||
const [selectedBodyWeights, setSelectedBodyWeights] = useState<number[]>([]);
|
||||
const [selectedStocks, setSelectedStocks] = useState<number[]>([]);
|
||||
const [selectedDepletions, setSelectedDepletions] = useState<number[]>([]);
|
||||
const [selectedEggs, setSelectedEggs] = useState<number[]>([]);
|
||||
|
||||
const [editingAverageIndex] = useState<number | null>(null);
|
||||
const [manuallyEditedRows, setManuallyEditedRows] = useState<Set<number>>(
|
||||
new Set()
|
||||
);
|
||||
|
||||
const [locationSearchValue, setLocationSearchValue] = useState('');
|
||||
const [selectedLocation, setSelectedLocation] = useState<OptionType | null>(
|
||||
null
|
||||
@@ -122,19 +117,6 @@ const RecordingForm = ({ type = 'add', initialValues }: RecordingFormProps) => {
|
||||
(values: RecordingGrowingFormValues) => {
|
||||
return {
|
||||
project_flock_kandang_id: values.project_flock_kandang_id,
|
||||
body_weights: (values.body_weights ?? []).map((bw) => {
|
||||
const qty = Number(bw.qty) || 0;
|
||||
const weight = Number(bw.weight) || 0;
|
||||
const totalWeight = qty * weight;
|
||||
return {
|
||||
avg_weight:
|
||||
typeof bw.avg_weight === 'number'
|
||||
? bw.avg_weight
|
||||
: parseFloat(String(bw.avg_weight)) || 0,
|
||||
qty: qty,
|
||||
total_weight: parseFloat(String(totalWeight)) || 0,
|
||||
};
|
||||
}),
|
||||
stocks: (values.stocks ?? []).map((stock) => ({
|
||||
product_warehouse_id: stock.product_warehouse_id,
|
||||
qty: Number(stock.qty) || 0,
|
||||
@@ -152,15 +134,6 @@ const RecordingForm = ({ type = 'add', initialValues }: RecordingFormProps) => {
|
||||
(values: RecordingLayingFormValues) => {
|
||||
return {
|
||||
project_flock_kandang_id: values.project_flock_kandang_id,
|
||||
body_weights: (values.body_weights ?? []).map((bw) => {
|
||||
return {
|
||||
avg_weight:
|
||||
typeof bw.avg_weight === 'number'
|
||||
? bw.avg_weight
|
||||
: parseFloat(String(bw.avg_weight)) || 0,
|
||||
qty: Number(bw.qty) || 0,
|
||||
};
|
||||
}),
|
||||
stocks: (values.stocks ?? []).map((stock) => ({
|
||||
product_warehouse_id: stock.product_warehouse_id,
|
||||
qty: Number(stock.qty) || 0,
|
||||
@@ -587,28 +560,6 @@ const RecordingForm = ({ type = 'add', initialValues }: RecordingFormProps) => {
|
||||
return recordedIds;
|
||||
}, [existingRecordings, today]);
|
||||
|
||||
const getLatestTotalChickQty = useCallback(
|
||||
(projectFlockKandangId: number) => {
|
||||
if (!isResponseSuccess(existingRecordings)) return null;
|
||||
|
||||
const projectFlockRecordings = existingRecordings.data.filter(
|
||||
(recording) =>
|
||||
recording.project_flock_kandang_id === projectFlockKandangId
|
||||
);
|
||||
|
||||
if (projectFlockRecordings.length === 0) return null;
|
||||
|
||||
projectFlockRecordings.sort(
|
||||
(a, b) =>
|
||||
new Date(b.record_datetime).getTime() -
|
||||
new Date(a.record_datetime).getTime()
|
||||
);
|
||||
|
||||
return projectFlockRecordings[0].total_chick_qty;
|
||||
},
|
||||
[existingRecordings]
|
||||
);
|
||||
|
||||
const unifiedStockProducts = useMemo(() => {
|
||||
const options: OptionType[] = [];
|
||||
if (isResponseSuccess(stockProducts) && selectedKandang) {
|
||||
@@ -808,25 +759,6 @@ const RecordingForm = ({ type = 'add', initialValues }: RecordingFormProps) => {
|
||||
});
|
||||
|
||||
// ===== HELPER FUNCTIONS =====
|
||||
const getTotalChickQtyError = useCallback(
|
||||
(qty: number) => {
|
||||
if (type === 'detail') return null;
|
||||
if (!formik.values.project_flock_kandang_id) return null;
|
||||
|
||||
const totalChickQty = getLatestTotalChickQty(
|
||||
formik.values.project_flock_kandang_id
|
||||
);
|
||||
if (!totalChickQty) return null;
|
||||
|
||||
if (qty > totalChickQty) {
|
||||
return `Jumlah ayam tidak boleh melebihi total ayam tersedia! Maksimal: ${formatNumber(totalChickQty)}`;
|
||||
}
|
||||
|
||||
return null;
|
||||
},
|
||||
[formik.values.project_flock_kandang_id, getLatestTotalChickQty, type]
|
||||
);
|
||||
|
||||
useCallback((): OptionType | null => {
|
||||
if (
|
||||
!formik.values.project_flock_kandang ||
|
||||
@@ -1193,124 +1125,6 @@ const RecordingForm = ({ type = 'add', initialValues }: RecordingFormProps) => {
|
||||
setIsRejectLoading(false);
|
||||
};
|
||||
|
||||
const addBodyWeight = () => {
|
||||
const newBodyWeights = [
|
||||
...(formik.values.body_weights || []),
|
||||
{
|
||||
weight: '',
|
||||
avg_weight: '',
|
||||
qty: '',
|
||||
},
|
||||
];
|
||||
formik.setFieldValue('body_weights', newBodyWeights);
|
||||
};
|
||||
|
||||
const handleWeightChange = (idx: number, value: number) => {
|
||||
formik.setFieldValue(`body_weights.${idx}.weight`, value);
|
||||
|
||||
setManuallyEditedRows((prev) => {
|
||||
const newSet = new Set(prev);
|
||||
newSet.delete(idx);
|
||||
return newSet;
|
||||
});
|
||||
|
||||
const currentWeight = formik.values.body_weights?.[idx];
|
||||
if (currentWeight) {
|
||||
const qty = Number(currentWeight.qty) || 0;
|
||||
const totalWeight = qty * value;
|
||||
|
||||
if (qty > 0 && value > 0) {
|
||||
const avgWeight = parseFloat((value / qty).toFixed(2));
|
||||
formik.setFieldValue(`body_weights.${idx}.avg_weight`, avgWeight);
|
||||
} else {
|
||||
formik.setFieldValue(`body_weights.${idx}.avg_weight`, '');
|
||||
}
|
||||
|
||||
formik.setFieldValue(`body_weights.${idx}.total_weight`, totalWeight);
|
||||
}
|
||||
};
|
||||
|
||||
const handleAvgWeightChange = (idx: number, value: number) => {
|
||||
formik.setFieldValue(`body_weights.${idx}.avg_weight`, value);
|
||||
|
||||
setManuallyEditedRows((prev) => {
|
||||
const newSet = new Set(prev);
|
||||
newSet.add(idx);
|
||||
return newSet;
|
||||
});
|
||||
|
||||
const currentWeight = formik.values.body_weights?.[idx];
|
||||
if (currentWeight) {
|
||||
const qty = Number(currentWeight.qty) || 0;
|
||||
if (qty > 0 && value > 0) {
|
||||
const totalWeight = value * qty;
|
||||
formik.setFieldValue(`body_weights.${idx}.weight`, totalWeight);
|
||||
formik.setFieldValue(`body_weights.${idx}.total_weight`, totalWeight);
|
||||
} else {
|
||||
formik.setFieldValue(`body_weights.${idx}.weight`, 0);
|
||||
formik.setFieldValue(`body_weights.${idx}.total_weight`, 0);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const handleQtyChange = (idx: number, value: number) => {
|
||||
formik.setFieldValue(`body_weights.${idx}.qty`, value);
|
||||
|
||||
setManuallyEditedRows((prev) => {
|
||||
const newSet = new Set(prev);
|
||||
newSet.delete(idx);
|
||||
return newSet;
|
||||
});
|
||||
|
||||
const currentWeight = formik.values.body_weights?.[idx];
|
||||
if (currentWeight) {
|
||||
const weight = Number(currentWeight.weight) || 0;
|
||||
const totalWeight = value * weight;
|
||||
|
||||
if (value > 0 && weight > 0) {
|
||||
const avgWeight = parseFloat((weight / value).toFixed(2));
|
||||
formik.setFieldValue(`body_weights.${idx}.avg_weight`, avgWeight);
|
||||
} else {
|
||||
formik.setFieldValue(`body_weights.${idx}.avg_weight`, '');
|
||||
}
|
||||
|
||||
formik.setFieldValue(`body_weights.${idx}.total_weight`, totalWeight);
|
||||
}
|
||||
};
|
||||
|
||||
const handleWeightChangeWrapper =
|
||||
(idx: number) => (e: React.ChangeEvent<HTMLInputElement>) => {
|
||||
const value = parseFloat(e.target.value) || 0;
|
||||
handleWeightChange(idx, value);
|
||||
};
|
||||
|
||||
const handleAvgWeightChangeWrapper =
|
||||
(idx: number) => (e: React.ChangeEvent<HTMLInputElement>) => {
|
||||
const value = parseFloat(e.target.value) || 0;
|
||||
handleAvgWeightChange(idx, value);
|
||||
};
|
||||
|
||||
const handleQtyChangeWrapper =
|
||||
(idx: number) => (e: React.ChangeEvent<HTMLInputElement>) => {
|
||||
const value = parseFloat(e.target.value) || 0;
|
||||
handleQtyChange(idx, value);
|
||||
};
|
||||
|
||||
const removeBodyWeight = (idx: number) => {
|
||||
const updatedBodyWeights = formik.values.body_weights?.filter(
|
||||
(_, i) => i !== idx
|
||||
);
|
||||
formik.setFieldValue('body_weights', updatedBodyWeights);
|
||||
};
|
||||
|
||||
const removeSelectedBodyWeights = () => {
|
||||
const updatedBodyWeights = formik.values.body_weights?.filter(
|
||||
(_, idx) => !selectedBodyWeights.includes(idx)
|
||||
);
|
||||
formik.setFieldValue('body_weights', updatedBodyWeights);
|
||||
setSelectedBodyWeights([]);
|
||||
};
|
||||
|
||||
const addStock = () => {
|
||||
const newStocks = [
|
||||
...(formik.values.stocks || []),
|
||||
@@ -1435,45 +1249,6 @@ const RecordingForm = ({ type = 'add', initialValues }: RecordingFormProps) => {
|
||||
}
|
||||
}, [isLayingCategory, type]);
|
||||
|
||||
const bodyWeightValues = useMemo(() => {
|
||||
if (!formik.values.body_weights) return [];
|
||||
return formik.values.body_weights.map((w) => ({
|
||||
weight: w.weight,
|
||||
qty: w.qty,
|
||||
}));
|
||||
}, [formik.values.body_weights]);
|
||||
|
||||
useEffect(() => {
|
||||
if (formik.values.body_weights && editingAverageIndex === null) {
|
||||
const updatedBodyWeights = formik.values.body_weights.map(
|
||||
(weight, idx) => {
|
||||
if (idx === editingAverageIndex || manuallyEditedRows.has(idx)) {
|
||||
return weight;
|
||||
}
|
||||
const qty = Number(weight.qty) || 0;
|
||||
const weightValue = Number(weight.weight) || 0;
|
||||
return {
|
||||
...weight,
|
||||
avg_weight:
|
||||
qty > 0 && weightValue > 0
|
||||
? parseFloat((weightValue / qty).toFixed(2))
|
||||
: 0,
|
||||
};
|
||||
}
|
||||
);
|
||||
const hasChanges = updatedBodyWeights.some(
|
||||
(updated, idx) =>
|
||||
idx !== editingAverageIndex &&
|
||||
!manuallyEditedRows.has(idx) &&
|
||||
updated.avg_weight !==
|
||||
(formik.values.body_weights[idx]?.avg_weight || 0)
|
||||
);
|
||||
if (hasChanges) {
|
||||
formik.setFieldValue('body_weights', updatedBodyWeights, false);
|
||||
}
|
||||
}
|
||||
}, [bodyWeightValues, editingAverageIndex, manuallyEditedRows]);
|
||||
|
||||
return (
|
||||
<>
|
||||
<section className='w-full'>
|
||||
@@ -1763,266 +1538,241 @@ const RecordingForm = ({ type = 'add', initialValues }: RecordingFormProps) => {
|
||||
</Card>
|
||||
)}
|
||||
|
||||
{/* Body Weights Table */}
|
||||
<Card
|
||||
title='Bobot Badan'
|
||||
className={{
|
||||
wrapper: 'w-full mb-4 shadow',
|
||||
title: 'mb-4',
|
||||
}}
|
||||
>
|
||||
<div className='overflow-x-auto'>
|
||||
<table className='table'>
|
||||
<thead>
|
||||
<tr>
|
||||
{(type as 'add' | 'edit' | 'detail') !== 'detail' && (
|
||||
<th>
|
||||
<CheckboxInput
|
||||
name='select-all-body-weights'
|
||||
checked={
|
||||
formik.values.body_weights?.length ===
|
||||
selectedBodyWeights.length &&
|
||||
formik.values.body_weights?.length > 0
|
||||
}
|
||||
onChange={(
|
||||
e: React.ChangeEvent<HTMLInputElement>
|
||||
) => {
|
||||
if (e.target.checked) {
|
||||
setSelectedBodyWeights(
|
||||
formik.values.body_weights?.map(
|
||||
(_, idx) => idx
|
||||
) ?? []
|
||||
);
|
||||
} else {
|
||||
setSelectedBodyWeights([]);
|
||||
}
|
||||
}}
|
||||
classNames={{
|
||||
wrapper: 'flex justify-center',
|
||||
checkbox: 'checkbox checkbox-sm',
|
||||
}}
|
||||
/>
|
||||
</th>
|
||||
)}
|
||||
<th>
|
||||
Berat Ayam (gram)
|
||||
<span
|
||||
className='tooltip tooltip-error tooltip-bottom '
|
||||
data-tip='required'
|
||||
>
|
||||
<span className='text-error'>*</span>
|
||||
</span>
|
||||
</th>
|
||||
<th>
|
||||
Jumlah Ayam
|
||||
<span
|
||||
className='tooltip tooltip-error tooltip-bottom'
|
||||
data-tip='required'
|
||||
>
|
||||
<span className='text-error'>*</span>
|
||||
</span>
|
||||
</th>
|
||||
<th>
|
||||
Rata-rata Berat Ayam (gram)
|
||||
<span
|
||||
className='tooltip tooltip-error tooltip-bottom '
|
||||
data-tip='required'
|
||||
>
|
||||
<span className='text-error'>*</span>
|
||||
</span>
|
||||
</th>
|
||||
{(type as 'add' | 'edit' | 'detail') !== 'detail' && (
|
||||
<th>Action</th>
|
||||
)}
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{formik.values.body_weights?.map((bw, idx) => (
|
||||
<tr key={`body-weight-${idx}`}>
|
||||
{(type as 'add' | 'edit' | 'detail') !== 'detail' && (
|
||||
<td className='align-middle!'>
|
||||
<CheckboxInput
|
||||
name={`body-weight-${idx}`}
|
||||
checked={selectedBodyWeights.includes(idx)}
|
||||
onChange={(
|
||||
e: React.ChangeEvent<HTMLInputElement>
|
||||
) => {
|
||||
if (e.target.checked) {
|
||||
setSelectedBodyWeights([
|
||||
...selectedBodyWeights,
|
||||
idx,
|
||||
]);
|
||||
} else {
|
||||
setSelectedBodyWeights(
|
||||
selectedBodyWeights.filter((i) => i !== idx)
|
||||
);
|
||||
}
|
||||
}}
|
||||
classNames={{
|
||||
wrapper: 'flex justify-center',
|
||||
checkbox: 'checkbox checkbox-sm',
|
||||
}}
|
||||
/>
|
||||
{/* FCR & Mortality Metrics - Detail View Only */}
|
||||
{type === 'detail' && initialValues && (
|
||||
<div
|
||||
className={`grid gap-6 mb-6 grid-cols-1 ${
|
||||
initialValues.project_flock_category === 'LAYING'
|
||||
? 'xl:grid-cols-3'
|
||||
: 'xl:grid-cols-2'
|
||||
}`}
|
||||
>
|
||||
{/* FCR Section */}
|
||||
<div className='border border-gray-200 rounded-lg bg-white'>
|
||||
<div className='px-4 py-3 border-b border-gray-200'>
|
||||
<span className='card-title font-bold text-xl'>FCR</span>
|
||||
</div>
|
||||
<div className='p-4'>
|
||||
<table className='w-full text-sm'>
|
||||
<thead>
|
||||
<tr className='border-b border-gray-200'>
|
||||
<th className='text-left py-2 font-semibold text-gray-600'></th>
|
||||
<th className='text-center py-2 font-semibold text-gray-600'>
|
||||
AKTUAL
|
||||
</th>
|
||||
<th className='text-center py-2 font-semibold text-gray-600'>
|
||||
STANDAR
|
||||
</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr>
|
||||
<td className='py-3 font-medium'>FCR</td>
|
||||
<td className='text-center py-3'>
|
||||
<span className='font-semibold'>
|
||||
{initialValues.fcr_value &&
|
||||
initialValues.fcr_value > 0
|
||||
? formatNumber(initialValues.fcr_value)
|
||||
: '-'}
|
||||
</span>
|
||||
</td>
|
||||
)}
|
||||
<td>
|
||||
<NumberInput
|
||||
required
|
||||
name={`body_weights.${idx}.weight`}
|
||||
value={bw.weight ?? ''}
|
||||
onChange={handleWeightChangeWrapper(idx)}
|
||||
onBlur={formik.handleBlur}
|
||||
decimalScale={2}
|
||||
allowNegative={false}
|
||||
thousandSeparator=','
|
||||
decimalSeparator='.'
|
||||
inputSuffix='gram'
|
||||
placeholder='Masukkan berat total...'
|
||||
isError={
|
||||
isRepeaterInputError('body_weights', 'weight', idx)
|
||||
.isError
|
||||
}
|
||||
errorMessage={
|
||||
isRepeaterInputError('body_weights', 'weight', idx)
|
||||
.errorMessage
|
||||
}
|
||||
readOnly={type === 'detail'}
|
||||
className={{
|
||||
wrapper: 'w-full min-w-32',
|
||||
}}
|
||||
/>
|
||||
</td>
|
||||
<td>
|
||||
<NumberInput
|
||||
required
|
||||
name={`body_weights.${idx}.qty`}
|
||||
value={bw.qty ?? ''}
|
||||
onChange={handleQtyChangeWrapper(idx)}
|
||||
onBlur={formik.handleBlur}
|
||||
decimalScale={0}
|
||||
allowNegative={false}
|
||||
thousandSeparator=','
|
||||
decimalSeparator='.'
|
||||
placeholder='Masukkan jumlah ayam...'
|
||||
bottomLabel={
|
||||
formik.values.project_flock_kandang_id &&
|
||||
type === 'add'
|
||||
? `Total Ayam: ${
|
||||
getLatestTotalChickQty(
|
||||
formik.values.project_flock_kandang_id
|
||||
) !== null
|
||||
? formatNumber(
|
||||
getLatestTotalChickQty(
|
||||
formik.values.project_flock_kandang_id
|
||||
)!
|
||||
)
|
||||
: 'N/A'
|
||||
}`
|
||||
: undefined
|
||||
}
|
||||
isError={
|
||||
isRepeaterInputError('body_weights', 'qty', idx)
|
||||
.isError ||
|
||||
(bw.qty
|
||||
? getTotalChickQtyError(Number(bw.qty)) !== null
|
||||
: false)
|
||||
}
|
||||
errorMessage={
|
||||
isRepeaterInputError('body_weights', 'qty', idx)
|
||||
.errorMessage ||
|
||||
(bw.qty
|
||||
? getTotalChickQtyError(Number(bw.qty)) ||
|
||||
undefined
|
||||
: undefined)
|
||||
}
|
||||
readOnly={type === 'detail'}
|
||||
className={{
|
||||
wrapper: 'w-full min-w-24',
|
||||
}}
|
||||
/>
|
||||
</td>
|
||||
<td>
|
||||
<NumberInput
|
||||
required
|
||||
name={`body_weights.${idx}.avg_weight`}
|
||||
value={bw.avg_weight ?? ''}
|
||||
onChange={handleAvgWeightChangeWrapper(idx)}
|
||||
onBlur={formik.handleBlur}
|
||||
decimalScale={2}
|
||||
allowNegative={false}
|
||||
thousandSeparator=','
|
||||
decimalSeparator='.'
|
||||
inputSuffix='gram'
|
||||
placeholder='Masukkan berat rata-rata...'
|
||||
isError={
|
||||
isRepeaterInputError(
|
||||
'body_weights',
|
||||
'avg_weight',
|
||||
idx
|
||||
).isError
|
||||
}
|
||||
errorMessage={
|
||||
isRepeaterInputError(
|
||||
'body_weights',
|
||||
'avg_weight',
|
||||
idx
|
||||
).errorMessage
|
||||
}
|
||||
readOnly={type === 'detail'}
|
||||
className={{
|
||||
wrapper: 'w-full min-w-32',
|
||||
}}
|
||||
/>
|
||||
</td>
|
||||
|
||||
{(type as 'add' | 'edit' | 'detail') !== 'detail' && (
|
||||
<td>
|
||||
<div className='flex items-center'>
|
||||
<Button
|
||||
type='button'
|
||||
color='error'
|
||||
onClick={() => removeBodyWeight(idx)}
|
||||
>
|
||||
<Icon
|
||||
icon='mdi:trash-can'
|
||||
width={24}
|
||||
height={24}
|
||||
/>
|
||||
</Button>
|
||||
</div>
|
||||
<td className='text-center py-3 text-gray-600'>
|
||||
{initialValues.fcr_std && initialValues.fcr_std > 0
|
||||
? formatNumber(initialValues.fcr_std)
|
||||
: '-'}
|
||||
</td>
|
||||
)}
|
||||
</tr>
|
||||
))}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
{(type as 'add' | 'edit' | 'detail') !== 'detail' && (
|
||||
<div className='flex justify-center items-center mt-4 gap-4'>
|
||||
{selectedBodyWeights.length > 0 && (
|
||||
<Button
|
||||
type='button'
|
||||
color='error'
|
||||
onClick={removeSelectedBodyWeights}
|
||||
disabled={selectedBodyWeights.length === 0}
|
||||
className='w-fit'
|
||||
>
|
||||
<Icon icon='mdi:trash-can' width={24} height={24} />
|
||||
Hapus Terpilih ({selectedBodyWeights.length})
|
||||
</Button>
|
||||
)}
|
||||
<Button
|
||||
type='button'
|
||||
color='success'
|
||||
onClick={addBodyWeight}
|
||||
className='w-fit'
|
||||
>
|
||||
<Icon icon='ic:round-plus' width={24} height={24} />
|
||||
Tambah Bobot Badan
|
||||
</Button>
|
||||
</tr>
|
||||
<tr>
|
||||
<td className='py-3 font-medium'>Feed Intake (KG)</td>
|
||||
<td className='text-center py-3'>
|
||||
<span className='font-semibold'>
|
||||
{initialValues.feed_intake &&
|
||||
initialValues.feed_intake > 0
|
||||
? formatNumber(initialValues.feed_intake)
|
||||
: '-'}
|
||||
</span>
|
||||
</td>
|
||||
<td className='text-center py-3 text-gray-600'>
|
||||
{initialValues.feed_intake_std &&
|
||||
initialValues.feed_intake_std > 0
|
||||
? formatNumber(initialValues.feed_intake_std)
|
||||
: '-'}
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</Card>
|
||||
|
||||
{/* Mortality Section */}
|
||||
<div className='border border-gray-200 rounded-lg bg-white'>
|
||||
<div className='px-4 py-3 border-b border-gray-200'>
|
||||
<span className='card-title font-bold text-xl'>
|
||||
Mortalitas
|
||||
</span>
|
||||
</div>
|
||||
<div className='p-4'>
|
||||
<table className='w-full text-sm'>
|
||||
<thead>
|
||||
<tr className='border-b border-gray-200'>
|
||||
<th
|
||||
colSpan={2}
|
||||
className='text-center py-2 font-semibold text-gray-600'
|
||||
>
|
||||
DEPLESI KUMULATIF
|
||||
</th>
|
||||
</tr>
|
||||
<tr className='border-b border-gray-200'>
|
||||
<th className='text-center py-2 font-semibold text-xs text-gray-500'>
|
||||
Total
|
||||
</th>
|
||||
<th className='text-center py-2 font-semibold text-xs text-gray-500'>
|
||||
(%)
|
||||
</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr>
|
||||
<td className='text-center py-3 border-r border-gray-100'>
|
||||
<span className='font-semibold'>
|
||||
{initialValues.total_depletion_qty &&
|
||||
initialValues.total_depletion_qty > 0
|
||||
? formatNumber(initialValues.total_depletion_qty)
|
||||
: '-'}
|
||||
</span>
|
||||
</td>
|
||||
<td className='text-center py-3 text-gray-600'>
|
||||
{initialValues.cum_depletion_rate &&
|
||||
initialValues.cum_depletion_rate > 0
|
||||
? initialValues.cum_depletion_rate.toFixed(2)
|
||||
: '-'}
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td
|
||||
colSpan={2}
|
||||
className='text-center py-3 border-r border-gray-200 text-gray-600'
|
||||
>
|
||||
Total Ayam
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td
|
||||
colSpan={2}
|
||||
className='text-center py-3 font-semibold'
|
||||
>
|
||||
{initialValues.total_chick_qty &&
|
||||
initialValues.total_chick_qty > 0
|
||||
? formatNumber(initialValues.total_chick_qty)
|
||||
: '-'}
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Egg Production Section - Only for LAYING category */}
|
||||
{type === 'detail' &&
|
||||
initialValues &&
|
||||
initialValues.project_flock_category === 'LAYING' && (
|
||||
<div className='border border-gray-200 rounded-lg bg-white'>
|
||||
<div className='px-4 py-3 border-b border-gray-200'>
|
||||
<span className='card-title font-bold text-xl'>
|
||||
Produksi
|
||||
</span>
|
||||
</div>
|
||||
<div className='p-4'>
|
||||
<table className='w-full text-sm'>
|
||||
<thead>
|
||||
<tr className='border-b border-gray-200'>
|
||||
<th className='text-left py-2 font-semibold text-gray-600'></th>
|
||||
<th className='text-center py-2 font-semibold text-gray-600'>
|
||||
AKTUAL
|
||||
</th>
|
||||
<th className='text-center py-2 font-semibold text-gray-600'>
|
||||
STANDAR
|
||||
</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr>
|
||||
<td className='py-3 font-medium'>Egg Mass</td>
|
||||
<td className='text-center py-3'>
|
||||
<span className='font-semibold'>
|
||||
{initialValues.egg_mesh &&
|
||||
initialValues.egg_mesh > 0
|
||||
? formatNumber(initialValues.egg_mesh)
|
||||
: '-'}
|
||||
</span>
|
||||
</td>
|
||||
<td className='text-center py-3 text-gray-600'>
|
||||
{initialValues.egg_mesh_std &&
|
||||
initialValues.egg_mesh_std > 0
|
||||
? formatNumber(initialValues.egg_mesh_std)
|
||||
: '-'}
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td className='py-3 font-medium'>
|
||||
Egg Weight (KG)
|
||||
</td>
|
||||
<td className='text-center py-3'>
|
||||
<span className='font-semibold'>
|
||||
{initialValues.egg_weight &&
|
||||
initialValues.egg_weight > 0
|
||||
? formatNumber(initialValues.egg_weight)
|
||||
: '-'}
|
||||
</span>
|
||||
</td>
|
||||
<td className='text-center py-3 text-gray-600'>
|
||||
{initialValues.egg_weight_std &&
|
||||
initialValues.egg_weight_std > 0
|
||||
? formatNumber(initialValues.egg_weight_std)
|
||||
: '-'}
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td className='py-3 font-medium'>Hen Day</td>
|
||||
<td className='text-center py-3'>
|
||||
<span className='font-semibold'>
|
||||
{initialValues.hand_day &&
|
||||
initialValues.hand_day > 0
|
||||
? formatNumber(initialValues.hand_day)
|
||||
: '-'}
|
||||
</span>
|
||||
</td>
|
||||
<td className='text-center py-3 text-gray-600'>
|
||||
{initialValues.hand_day_std !== undefined &&
|
||||
initialValues.hand_day_std > 0
|
||||
? `${initialValues.hand_day_std}%`
|
||||
: '-'}
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td className='py-3 font-medium'>Hen House</td>
|
||||
<td className='text-center py-3'>
|
||||
<span className='font-semibold'>
|
||||
{initialValues.hand_house &&
|
||||
initialValues.hand_house > 0
|
||||
? formatNumber(initialValues.hand_house)
|
||||
: '-'}
|
||||
</span>
|
||||
</td>
|
||||
<td className='text-center py-3 text-gray-600'>
|
||||
{initialValues.hand_house_std !== undefined &&
|
||||
initialValues.hand_house_std > 0
|
||||
? `${initialValues.hand_house_std}%`
|
||||
: '-'}
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Stocks Table */}
|
||||
<Card
|
||||
|
||||
@@ -0,0 +1,409 @@
|
||||
'use client';
|
||||
|
||||
import { useState } from 'react';
|
||||
import toast from 'react-hot-toast';
|
||||
|
||||
import { Icon } from '@iconify/react';
|
||||
import Button from '@/components/Button';
|
||||
import Dropdown from '@/components/dropdown/Dropdown';
|
||||
import SelectInput, {
|
||||
OptionType,
|
||||
useSelect,
|
||||
} from '@/components/input/SelectInput';
|
||||
import Menu from '@/components/menu/Menu';
|
||||
import MenuItem from '@/components/menu/MenuItem';
|
||||
import Card from '@/components/Card';
|
||||
import ProductionResultProjectFlockKandangTable from '@/components/pages/report/production-result/ProductionResultProjectFlockKandangTable';
|
||||
|
||||
import { BaseKandang } from '@/types/api/master-data/kandang';
|
||||
import { AreaApi, LocationApi } from '@/services/api/master-data';
|
||||
import {
|
||||
ProjectFlockApi,
|
||||
ProjectFlockKandangApi,
|
||||
} from '@/services/api/production';
|
||||
import { ProjectFlockKandang } from '@/types/api/production/project-flock-kandang';
|
||||
import { isResponseError } from '@/lib/api-helper';
|
||||
import Pagination from '@/components/Pagination';
|
||||
|
||||
const ProductionResultContent = () => {
|
||||
const [projectFlockKandangs, setProjectFlockKandangs] = useState<
|
||||
ProjectFlockKandang[] | null
|
||||
>(null);
|
||||
const [projectFlockKandangMetadata, setProjectFlockKandangMetadata] =
|
||||
useState<
|
||||
| {
|
||||
page: number;
|
||||
limit: number;
|
||||
total_pages: number;
|
||||
total_results: number;
|
||||
}
|
||||
| undefined
|
||||
>(undefined);
|
||||
|
||||
const [page, setPage] = useState(1);
|
||||
const [pageSize, setPageSize] = useState(10);
|
||||
|
||||
const [isLoadingSearch, setIsLoadingSearch] = useState(false);
|
||||
|
||||
const [isLoadingExportingToExcel, setIsLoadingExportingToExcel] =
|
||||
useState(false);
|
||||
|
||||
const [selectedArea, setSelectedArea] = useState<OptionType | null>(null);
|
||||
const [selectedLocation, setSelectedLocation] = useState<OptionType | null>(
|
||||
null
|
||||
);
|
||||
const [selectedProjectFlock, setSelectedProjectFlock] =
|
||||
useState<OptionType | null>(null);
|
||||
const [selectedProjectFlockKandang, setSelectedProjectFlockKandang] =
|
||||
useState<OptionType | null>(null);
|
||||
|
||||
const {
|
||||
setInputValue: setAreaInputValue,
|
||||
options: areaOptions,
|
||||
isLoadingOptions: isLoadingAreaOptions,
|
||||
} = useSelect<BaseKandang>(AreaApi.basePath, 'id', 'name');
|
||||
|
||||
const areaChangeHandler = (val: OptionType | OptionType[] | null) => {
|
||||
setSelectedArea(val as OptionType);
|
||||
|
||||
setSelectedLocation(null);
|
||||
|
||||
setSelectedProjectFlock(null);
|
||||
|
||||
setSelectedProjectFlockKandang(null);
|
||||
};
|
||||
|
||||
const {
|
||||
setInputValue: setLocationInputValue,
|
||||
options: locationOptions,
|
||||
isLoadingOptions: isLoadingLocationOptions,
|
||||
} = useSelect<BaseKandang>(LocationApi.basePath, 'id', 'name', 'search', {
|
||||
area_id: selectedArea ? ((selectedArea as OptionType).value as string) : '',
|
||||
});
|
||||
|
||||
const locationChangeHandler = (val: OptionType | OptionType[] | null) => {
|
||||
setSelectedLocation(val as OptionType);
|
||||
|
||||
setSelectedProjectFlock(null);
|
||||
|
||||
setSelectedProjectFlockKandang(null);
|
||||
};
|
||||
|
||||
const {
|
||||
setInputValue: setProjectFlockInputValue,
|
||||
options: projectFlockOptions,
|
||||
isLoadingOptions: isLoadingProjectFlockOptions,
|
||||
} = useSelect<BaseKandang>(
|
||||
ProjectFlockApi.basePath,
|
||||
'id',
|
||||
'flock_name',
|
||||
'search',
|
||||
{
|
||||
area_id: selectedArea
|
||||
? ((selectedArea as OptionType).value as string)
|
||||
: '',
|
||||
location_id: selectedLocation
|
||||
? ((selectedLocation as OptionType).value as string)
|
||||
: '',
|
||||
category: 'LAYING',
|
||||
}
|
||||
);
|
||||
|
||||
const projectFlockChangeHandler = (val: OptionType | OptionType[] | null) => {
|
||||
setSelectedProjectFlock(val as OptionType);
|
||||
|
||||
setSelectedProjectFlockKandang(null);
|
||||
};
|
||||
|
||||
const {
|
||||
setInputValue: setProjectFlockKandangInputValue,
|
||||
options: projectFlockKandangOptions,
|
||||
isLoadingOptions: isLoadingProjectFlockKandangOptions,
|
||||
} = useSelect<BaseKandang>(
|
||||
ProjectFlockKandangApi.basePath,
|
||||
'id',
|
||||
'kandang.name',
|
||||
'search',
|
||||
{
|
||||
area_id: selectedArea
|
||||
? ((selectedArea as OptionType).value as string)
|
||||
: '',
|
||||
location_id: selectedLocation
|
||||
? ((selectedLocation as OptionType).value as string)
|
||||
: '',
|
||||
project_flock_id: selectedProjectFlock
|
||||
? ((selectedProjectFlock as OptionType).value as string)
|
||||
: '',
|
||||
}
|
||||
);
|
||||
|
||||
const projectFlockKandangChangeHandler = (
|
||||
val: OptionType | OptionType[] | null
|
||||
) => {
|
||||
setSelectedProjectFlockKandang(val as OptionType);
|
||||
};
|
||||
|
||||
const exportToExcelHandler = async () => {
|
||||
setIsLoadingExportingToExcel(true);
|
||||
// TODO: Implement export functionality in API service first if needed
|
||||
toast.error('Fitur export belum tersedia');
|
||||
setIsLoadingExportingToExcel(false);
|
||||
};
|
||||
|
||||
const searchHandler = async () => {
|
||||
setProjectFlockKandangs(null);
|
||||
setIsLoadingSearch(true);
|
||||
|
||||
try {
|
||||
if (selectedProjectFlockKandang) {
|
||||
const projectFlockKandangResponse =
|
||||
await ProjectFlockKandangApi.getSingle(
|
||||
selectedProjectFlockKandang?.value as number
|
||||
);
|
||||
|
||||
if (
|
||||
!projectFlockKandangResponse ||
|
||||
isResponseError(projectFlockKandangResponse)
|
||||
) {
|
||||
throw new Error();
|
||||
}
|
||||
|
||||
setProjectFlockKandangs([projectFlockKandangResponse.data]);
|
||||
setProjectFlockKandangMetadata(projectFlockKandangResponse.meta);
|
||||
setIsLoadingSearch(false);
|
||||
return;
|
||||
}
|
||||
|
||||
const projectFlockKandangsResponse = await ProjectFlockKandangApi.getAll({
|
||||
area_id: selectedArea?.value,
|
||||
project_flock_id: selectedProjectFlock?.value,
|
||||
});
|
||||
|
||||
if (
|
||||
!projectFlockKandangsResponse ||
|
||||
isResponseError(projectFlockKandangsResponse)
|
||||
) {
|
||||
throw new Error();
|
||||
}
|
||||
|
||||
setProjectFlockKandangs(projectFlockKandangsResponse.data);
|
||||
setProjectFlockKandangMetadata(projectFlockKandangsResponse.meta);
|
||||
setIsLoadingSearch(false);
|
||||
} catch (error) {
|
||||
toast.error('Gagal mencari data! Coba lagi.');
|
||||
setProjectFlockKandangs(null);
|
||||
setProjectFlockKandangMetadata(undefined);
|
||||
setIsLoadingSearch(false);
|
||||
}
|
||||
};
|
||||
|
||||
const resetHandler = () => {
|
||||
setProjectFlockKandangs(null);
|
||||
setSelectedArea(null);
|
||||
setSelectedLocation(null);
|
||||
setSelectedProjectFlock(null);
|
||||
setSelectedProjectFlockKandang(null);
|
||||
// resetFilter();
|
||||
};
|
||||
|
||||
return (
|
||||
<div className='w-full p-4'>
|
||||
<Card
|
||||
variant='bordered'
|
||||
className={{
|
||||
wrapper: 'w-full',
|
||||
}}
|
||||
>
|
||||
<div>
|
||||
<h2 className='text-xl font-bold text-center'>
|
||||
Laporan Hasil Produksi
|
||||
</h2>
|
||||
</div>
|
||||
|
||||
{/* Filters */}
|
||||
<div className='flex flex-col gap-4 mb-6 mt-4'>
|
||||
<div className='grid grid-cols-12 gap-4'>
|
||||
<SelectInput
|
||||
label='Area'
|
||||
placeholder='Pilih Area'
|
||||
options={areaOptions}
|
||||
isLoading={isLoadingAreaOptions}
|
||||
value={selectedArea}
|
||||
onChange={areaChangeHandler}
|
||||
onInputChange={setAreaInputValue}
|
||||
isClearable
|
||||
className={{
|
||||
wrapper: 'col-span-12 sm:col-span-6 lg:col-span-4',
|
||||
}}
|
||||
/>
|
||||
|
||||
<SelectInput
|
||||
label='Lokasi'
|
||||
placeholder={
|
||||
selectedArea ? 'Pilih Lokasi' : 'Pilih Area terlebih dahulu'
|
||||
}
|
||||
options={locationOptions}
|
||||
isLoading={isLoadingLocationOptions}
|
||||
value={selectedLocation}
|
||||
onChange={locationChangeHandler}
|
||||
onInputChange={setLocationInputValue}
|
||||
isClearable
|
||||
isDisabled={!selectedArea}
|
||||
className={{
|
||||
wrapper: 'col-span-12 sm:col-span-6 lg:col-span-4',
|
||||
}}
|
||||
/>
|
||||
|
||||
<SelectInput
|
||||
label='Project Flock'
|
||||
placeholder={
|
||||
selectedArea && selectedLocation
|
||||
? 'Pilih Project Flock'
|
||||
: 'Pilih Area dan Lokasi terlebih dahulu'
|
||||
}
|
||||
options={projectFlockOptions}
|
||||
isLoading={isLoadingProjectFlockOptions}
|
||||
value={selectedProjectFlock}
|
||||
onChange={projectFlockChangeHandler}
|
||||
onInputChange={setProjectFlockInputValue}
|
||||
isClearable
|
||||
isDisabled={!selectedArea || !selectedLocation}
|
||||
className={{
|
||||
wrapper: 'col-span-12 sm:col-span-6 lg:col-span-4',
|
||||
}}
|
||||
/>
|
||||
|
||||
<SelectInput
|
||||
label='Project Flock Kandang'
|
||||
placeholder={
|
||||
selectedProjectFlock
|
||||
? 'Pilih Project Flock Kandang'
|
||||
: 'Pilih Project Flock terlebih dahulu'
|
||||
}
|
||||
options={projectFlockKandangOptions}
|
||||
isLoading={isLoadingProjectFlockKandangOptions}
|
||||
value={selectedProjectFlockKandang}
|
||||
onChange={projectFlockKandangChangeHandler}
|
||||
onInputChange={setProjectFlockKandangInputValue}
|
||||
isClearable
|
||||
isDisabled={!selectedProjectFlock}
|
||||
className={{
|
||||
wrapper: 'col-span-12 sm:col-span-6 lg:col-span-4',
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className='grid grid-cols-12 gap-4'>
|
||||
<div className='col-span-12 flex flex-wrap sm:justify-end items-end gap-2'>
|
||||
<Button
|
||||
onClick={searchHandler}
|
||||
isLoading={isLoadingSearch}
|
||||
disabled={
|
||||
!selectedArea || !selectedLocation || !selectedProjectFlock
|
||||
}
|
||||
className='flex-1 sm:flex-none'
|
||||
>
|
||||
<Icon icon='heroicons-outline:search' width={20} height={20} />
|
||||
Cari
|
||||
</Button>
|
||||
<Button
|
||||
color='warning'
|
||||
onClick={resetHandler}
|
||||
className='flex-1 sm:flex-none'
|
||||
>
|
||||
<Icon icon='heroicons-outline:refresh' width={20} height={20} />
|
||||
Reset
|
||||
</Button>
|
||||
|
||||
<Dropdown
|
||||
align='end'
|
||||
direction='bottom'
|
||||
trigger={
|
||||
<Button>
|
||||
Export{' '}
|
||||
<Icon
|
||||
icon='heroicons-outline:download'
|
||||
width={20}
|
||||
height={20}
|
||||
/>
|
||||
</Button>
|
||||
}
|
||||
>
|
||||
<Menu>
|
||||
<MenuItem
|
||||
title='Export to Excel'
|
||||
icon='icon-park-outline:excel'
|
||||
isLoading={isLoadingExportingToExcel}
|
||||
onClick={exportToExcelHandler}
|
||||
className='text-nowrap'
|
||||
/>
|
||||
</Menu>
|
||||
</Dropdown>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</Card>
|
||||
|
||||
<div className='mt-4'>
|
||||
{isLoadingSearch && (
|
||||
<span className='loading loading-dots loading-xl block mx-auto text-gray-400' />
|
||||
)}
|
||||
|
||||
{!isLoadingSearch && !projectFlockKandangs && (
|
||||
<p className='text-center text-gray-500'>
|
||||
Silakan pilih filter dan klik tombol Cari untuk menampilkan data.
|
||||
</p>
|
||||
)}
|
||||
|
||||
{!isLoadingSearch && projectFlockKandangs?.length === 0 && (
|
||||
<p className='text-center text-gray-500'>
|
||||
Tidak ada data kandang project flock yang dapat ditampilkan.
|
||||
</p>
|
||||
)}
|
||||
|
||||
{!isLoadingSearch && projectFlockKandangs && (
|
||||
<Card
|
||||
variant='bordered'
|
||||
className={{
|
||||
wrapper: 'w-full',
|
||||
}}
|
||||
>
|
||||
{projectFlockKandangs.map((projectFlockKandang) => (
|
||||
<ProductionResultProjectFlockKandangTable
|
||||
key={projectFlockKandang.id}
|
||||
projectFlockKandangId={projectFlockKandang.id}
|
||||
kandangName={projectFlockKandang.kandang.name}
|
||||
/>
|
||||
))}
|
||||
|
||||
<div className='max-w-sm ml-auto mt-5'>
|
||||
<Pagination
|
||||
totalItems={projectFlockKandangMetadata?.total_results || 0}
|
||||
itemsPerPage={projectFlockKandangMetadata?.limit || 0}
|
||||
currentPage={projectFlockKandangMetadata?.page || 0}
|
||||
onPrevPage={() =>
|
||||
setPage((currPage) =>
|
||||
currPage > 1 ? currPage - 1 : currPage
|
||||
)
|
||||
}
|
||||
onNextPage={() =>
|
||||
setPage((currPage) =>
|
||||
projectFlockKandangMetadata?.total_pages &&
|
||||
currPage < projectFlockKandangMetadata.total_pages
|
||||
? currPage + 1
|
||||
: currPage
|
||||
)
|
||||
}
|
||||
onPageChange={(pageNumber) => setPage(pageNumber)}
|
||||
rowOptions={[10, 20, 50, 100]}
|
||||
onRowChange={setPageSize}
|
||||
/>
|
||||
</div>
|
||||
</Card>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default ProductionResultContent;
|
||||
+364
@@ -0,0 +1,364 @@
|
||||
'use client';
|
||||
|
||||
import { useEffect, useState } from 'react';
|
||||
import useSWR from 'swr';
|
||||
import { ColumnDef, SortingState } from '@tanstack/react-table';
|
||||
|
||||
import { Icon } from '@iconify/react';
|
||||
import Table from '@/components/Table';
|
||||
import Card from '@/components/Card';
|
||||
import Collapse from '@/components/Collapse';
|
||||
|
||||
import { cn, formatNumber } from '@/lib/helper';
|
||||
import { isResponseSuccess } from '@/lib/api-helper';
|
||||
import { ProductionResult } from '@/types/api/report/production-result';
|
||||
import { useTableFilter } from '@/services/hooks/useTableFilter';
|
||||
import { ProductionResultReportApi } from '@/services/api/report/production-result';
|
||||
|
||||
interface ProductionResultProjectFlockKandangTableProps {
|
||||
projectFlockKandangId?: number;
|
||||
kandangName?: string;
|
||||
}
|
||||
|
||||
const ProductionResultProjectFlockKandangTable = ({
|
||||
projectFlockKandangId,
|
||||
kandangName,
|
||||
}: ProductionResultProjectFlockKandangTableProps) => {
|
||||
const {
|
||||
state: tableFilterState,
|
||||
updateFilter,
|
||||
setPage,
|
||||
setPageSize,
|
||||
toQueryString: getTableFilterQueryString,
|
||||
reset: resetFilter,
|
||||
} = useTableFilter({
|
||||
initial: {
|
||||
filter_by: '',
|
||||
sort_by: '',
|
||||
},
|
||||
paramMap: {
|
||||
pageSize: 'limit',
|
||||
},
|
||||
});
|
||||
|
||||
const { data: productionResults, isLoading: isLoadingProductionResults } =
|
||||
useSWR(
|
||||
projectFlockKandangId
|
||||
? `/reports/production-result/${projectFlockKandangId}${getTableFilterQueryString()}`
|
||||
: null,
|
||||
ProductionResultReportApi.getAllProductionResultFetcher,
|
||||
{
|
||||
keepPreviousData: true,
|
||||
}
|
||||
);
|
||||
|
||||
const [open, setOpen] = useState(false);
|
||||
|
||||
const [sorting, setSorting] = useState<SortingState>([]);
|
||||
|
||||
const productionResultColumns: ColumnDef<ProductionResult>[] = [
|
||||
{
|
||||
header: 'No',
|
||||
cell: (props) => props.row.index + 1,
|
||||
},
|
||||
{
|
||||
accessorKey: 'woa',
|
||||
header: 'WOA',
|
||||
},
|
||||
{
|
||||
accessorKey: 'bw',
|
||||
header: 'BW',
|
||||
cell: (props) => formatNumber(props.row.original.bw),
|
||||
},
|
||||
{
|
||||
accessorKey: 'std_bw',
|
||||
header: 'STD BW',
|
||||
cell: (props) => formatNumber(props.row.original.std_bw),
|
||||
},
|
||||
{
|
||||
accessorKey: 'uniformity',
|
||||
header: 'Uniformity',
|
||||
cell: (props) => formatNumber(props.row.original.uniformity),
|
||||
},
|
||||
{
|
||||
accessorKey: 'std_uniformity',
|
||||
header: 'STD Uniformity',
|
||||
},
|
||||
{
|
||||
accessorKey: 'dep_kum',
|
||||
header: 'Dep Kum',
|
||||
cell: (props) => formatNumber(props.row.original.dep_kum),
|
||||
},
|
||||
{
|
||||
accessorKey: 'dep_std',
|
||||
header: 'Dep STD',
|
||||
cell: (props) => formatNumber(props.row.original.dep_std),
|
||||
},
|
||||
// Butiran
|
||||
{
|
||||
header: 'Butiran',
|
||||
columns: [
|
||||
{
|
||||
accessorKey: 'butiran_utuh',
|
||||
header: 'Utuh',
|
||||
cell: (props) => formatNumber(props.row.original.butiran_utuh),
|
||||
},
|
||||
{
|
||||
accessorKey: 'butiran_putih',
|
||||
header: 'Putih',
|
||||
cell: (props) => formatNumber(props.row.original.butiran_putih),
|
||||
},
|
||||
{
|
||||
accessorKey: 'butiran_retak',
|
||||
header: 'Retak',
|
||||
cell: (props) => formatNumber(props.row.original.butiran_retak),
|
||||
},
|
||||
{
|
||||
accessorKey: 'butiran_pecah',
|
||||
header: 'Pecah',
|
||||
cell: (props) => formatNumber(props.row.original.butiran_pecah),
|
||||
},
|
||||
{
|
||||
accessorKey: 'butiran_jumlah',
|
||||
header: 'Jumlah (Butir)',
|
||||
cell: (props) => formatNumber(props.row.original.butiran_jumlah),
|
||||
},
|
||||
{
|
||||
accessorKey: 'total_butir',
|
||||
header: 'Total Butir',
|
||||
cell: (props) => formatNumber(props.row.original.total_butir),
|
||||
},
|
||||
],
|
||||
},
|
||||
// Kg
|
||||
{
|
||||
header: 'Kg',
|
||||
columns: [
|
||||
{
|
||||
accessorKey: 'kg_utuh',
|
||||
header: 'Utuh (Kg)',
|
||||
cell: (props) => formatNumber(props.row.original.kg_utuh),
|
||||
},
|
||||
{
|
||||
accessorKey: 'kg_putih',
|
||||
header: 'Putih (Kg)',
|
||||
cell: (props) => formatNumber(props.row.original.kg_putih),
|
||||
},
|
||||
{
|
||||
accessorKey: 'kg_retak',
|
||||
header: 'Retak (Kg)',
|
||||
cell: (props) => formatNumber(props.row.original.kg_retak),
|
||||
},
|
||||
{
|
||||
accessorKey: 'kg_pecah',
|
||||
header: 'Pecah (Kg)',
|
||||
cell: (props) => formatNumber(props.row.original.kg_pecah),
|
||||
},
|
||||
{
|
||||
accessorKey: 'kg_jumlah',
|
||||
header: 'Jumlah (Kg)',
|
||||
cell: (props) => formatNumber(props.row.original.kg_jumlah),
|
||||
},
|
||||
{
|
||||
accessorKey: 'total_kg',
|
||||
header: 'Total Kg',
|
||||
cell: (props) => formatNumber(props.row.original.total_kg),
|
||||
},
|
||||
],
|
||||
},
|
||||
// Persen
|
||||
{
|
||||
header: '%',
|
||||
columns: [
|
||||
{
|
||||
accessorKey: 'persen_utuh',
|
||||
header: 'Utuh',
|
||||
cell: (props) => formatNumber(props.row.original.persen_utuh),
|
||||
},
|
||||
{
|
||||
accessorKey: 'persen_putih',
|
||||
header: 'Putih',
|
||||
cell: (props) => formatNumber(props.row.original.persen_putih),
|
||||
},
|
||||
{
|
||||
accessorKey: 'persen_retak',
|
||||
header: 'Retak',
|
||||
cell: (props) => formatNumber(props.row.original.persen_retak),
|
||||
},
|
||||
{
|
||||
accessorKey: 'persen_pecah',
|
||||
header: 'Pecah',
|
||||
cell: (props) => formatNumber(props.row.original.persen_pecah),
|
||||
},
|
||||
],
|
||||
},
|
||||
// Produksi
|
||||
{
|
||||
header: 'Produksi',
|
||||
columns: [
|
||||
{
|
||||
accessorKey: 'hd',
|
||||
header: 'HD',
|
||||
cell: (props) => formatNumber(props.row.original.hd),
|
||||
},
|
||||
{
|
||||
accessorKey: 'hd_std',
|
||||
header: 'HD STD',
|
||||
cell: (props) => formatNumber(props.row.original.hd_std),
|
||||
},
|
||||
{
|
||||
accessorKey: 'fi',
|
||||
header: 'FI',
|
||||
cell: (props) => formatNumber(props.row.original.fi),
|
||||
},
|
||||
{
|
||||
accessorKey: 'fi_std',
|
||||
header: 'FI STD',
|
||||
cell: (props) => formatNumber(props.row.original.fi_std),
|
||||
},
|
||||
{
|
||||
accessorKey: 'em',
|
||||
header: 'EM',
|
||||
cell: (props) => formatNumber(props.row.original.em),
|
||||
},
|
||||
{
|
||||
accessorKey: 'em_std',
|
||||
header: 'EM STD',
|
||||
cell: (props) => formatNumber(props.row.original.em_std),
|
||||
},
|
||||
{
|
||||
accessorKey: 'ew',
|
||||
header: 'EW',
|
||||
cell: (props) => formatNumber(props.row.original.ew),
|
||||
},
|
||||
{
|
||||
accessorKey: 'ew_std',
|
||||
header: 'EW STD',
|
||||
cell: (props) => formatNumber(props.row.original.ew_std),
|
||||
},
|
||||
{
|
||||
accessorKey: 'fcr',
|
||||
header: 'FCR',
|
||||
cell: (props) => formatNumber(props.row.original.fcr),
|
||||
},
|
||||
{
|
||||
accessorKey: 'fcr_std',
|
||||
header: 'FCR STD',
|
||||
cell: (props) => formatNumber(props.row.original.fcr_std),
|
||||
},
|
||||
{
|
||||
accessorKey: 'hh',
|
||||
header: 'HH',
|
||||
cell: (props) => formatNumber(props.row.original.hh),
|
||||
},
|
||||
{
|
||||
accessorKey: 'hh_std',
|
||||
header: 'HH STD',
|
||||
cell: (props) => formatNumber(props.row.original.hh_std),
|
||||
},
|
||||
],
|
||||
},
|
||||
];
|
||||
|
||||
useEffect(() => {
|
||||
if (sorting.length === 1) {
|
||||
updateFilter('filter_by', sorting[0].id);
|
||||
updateFilter('sort_by', sorting[0].desc ? 'desc' : 'asc');
|
||||
} else {
|
||||
updateFilter('filter_by', '');
|
||||
updateFilter('sort_by', '');
|
||||
}
|
||||
}, [sorting]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!open) {
|
||||
setOpen(
|
||||
isResponseSuccess(productionResults)
|
||||
? productionResults.data.length > 0
|
||||
: false
|
||||
);
|
||||
}
|
||||
}, [productionResults, isResponseSuccess]);
|
||||
|
||||
return (
|
||||
<Card
|
||||
className={{
|
||||
wrapper: 'w-full',
|
||||
body: 'p-4 shadow',
|
||||
}}
|
||||
>
|
||||
<Collapse
|
||||
open={open}
|
||||
onOpenChange={setOpen}
|
||||
title={
|
||||
<div className='card-actions p-4 justify-between items-center w-full'>
|
||||
<div className='card-title'>{kandangName}</div>
|
||||
|
||||
<Icon
|
||||
icon='material-symbols:keyboard-arrow-down'
|
||||
width={24}
|
||||
height={24}
|
||||
className={cn('text-primary transition-transform', {
|
||||
'-rotate-180': open,
|
||||
})}
|
||||
/>
|
||||
</div>
|
||||
}
|
||||
className='w-full!'
|
||||
titleClassName='w-full p-0!'
|
||||
>
|
||||
<div className='w-full p-0'>
|
||||
{/* <div className='flex flex-col gap-2 mb-4'>
|
||||
<div className='w-full flex flex-col sm:flex-row justify-start items-end sm:items-center gap-4'>
|
||||
<DebouncedTextInput
|
||||
name='search'
|
||||
placeholder='Cari Record'
|
||||
value={tableFilterState.search}
|
||||
onChange={searchChangeHandler}
|
||||
className={{ wrapper: 'sm:max-w-3xs' }}
|
||||
/>
|
||||
</div>
|
||||
</div> */}
|
||||
|
||||
<Table<ProductionResult>
|
||||
data={
|
||||
isResponseSuccess(productionResults)
|
||||
? productionResults?.data
|
||||
: []
|
||||
}
|
||||
columns={productionResultColumns}
|
||||
pageSize={tableFilterState.pageSize}
|
||||
onPageSizeChange={setPageSize}
|
||||
rowOptions={[10, 20, 50, 100]}
|
||||
page={
|
||||
isResponseSuccess(productionResults)
|
||||
? productionResults?.meta?.page
|
||||
: 0
|
||||
}
|
||||
totalItems={
|
||||
isResponseSuccess(productionResults)
|
||||
? productionResults?.meta?.total_results
|
||||
: 0
|
||||
}
|
||||
onPageChange={setPage}
|
||||
isLoading={isLoadingProductionResults}
|
||||
sorting={sorting}
|
||||
setSorting={setSorting}
|
||||
renderFooter={false}
|
||||
className={{
|
||||
containerClassName: cn({
|
||||
'w-full mb-20':
|
||||
isResponseSuccess(productionResults) &&
|
||||
productionResults?.data?.length === 0,
|
||||
}),
|
||||
headerColumnClassName:
|
||||
'px-4 py-3 border-base-content/10 text-base-content/50',
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
</Collapse>
|
||||
</Card>
|
||||
);
|
||||
};
|
||||
|
||||
export default ProductionResultProjectFlockKandangTable;
|
||||
@@ -82,6 +82,10 @@ export const MAIN_DRAWER_LINKS: SidebarMenuItem[] = [
|
||||
text: 'Penjualan',
|
||||
link: '/report/marketing',
|
||||
},
|
||||
{
|
||||
text: 'Hasil Produksi',
|
||||
link: '/report/production-result',
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
|
||||
@@ -3,6 +3,7 @@ export const ROUTE_PERMISSIONS: Record<string, string[]> = {
|
||||
|
||||
// Dashboard
|
||||
'/dashboard/': ['lti.dashboard.list'],
|
||||
'/dashboard': ['lti.dashboard.list'],
|
||||
|
||||
// Production
|
||||
// Production - Project Flock
|
||||
@@ -75,12 +76,19 @@ export const ROUTE_PERMISSIONS: Record<string, string[]> = {
|
||||
'/expense/realization/edit/': ['lti.expense.update.realization'],
|
||||
|
||||
// Finance
|
||||
'/finance/': ['lti.finance.transaction.list'],
|
||||
'/finance/detail/': ['lti.finance.transaction.detail'],
|
||||
'/finance/add/': ['lti.finance.payments.create'],
|
||||
'/finance/detail/edit/': ['lti.finance.payments.update'],
|
||||
'/finance/add/initial-balance/': ['lti.finance.initial_balances.create'],
|
||||
'/finance/': ['lti.dashboard.list', 'lti.finance.transaction.list'],
|
||||
'/finance/detail/': ['lti.dashboard.list', 'lti.finance.transaction.detail'],
|
||||
'/finance/add/': ['lti.dashboard.list', 'lti.finance.payments.create'],
|
||||
'/finance/detail/edit/': [
|
||||
'lti.dashboard.list',
|
||||
'lti.finance.payments.update',
|
||||
],
|
||||
'/finance/add/initial-balance/': [
|
||||
'lti.dashboard.list',
|
||||
'lti.finance.initial_balances.create',
|
||||
],
|
||||
'/finance/detail/edit/initial-balance/': [
|
||||
'lti.dashboard.list',
|
||||
'lti.finance.initial_balances.update',
|
||||
],
|
||||
'/finance/add/injection/': ['lti.finance.injections.create'],
|
||||
@@ -95,6 +103,10 @@ export const ROUTE_PERMISSIONS: Record<string, string[]> = {
|
||||
'/report/expense/': ['lti.repport.expense.list'],
|
||||
'/report/marketing/': ['lti.repport.delivery.list'],
|
||||
|
||||
// TODO: change to real permission
|
||||
// '/report/production-result/': ['lti.repport.production_result.list'],
|
||||
'/report/production-result/': ['lti.repport.delivery.list'],
|
||||
|
||||
// Inventory
|
||||
'/inventory/adjustment/': ['lti.inventory.list'],
|
||||
'/inventory/adjustment/add/': ['lti.inventory.create'],
|
||||
@@ -178,14 +190,20 @@ export const ROUTE_PERMISSIONS: Record<string, string[]> = {
|
||||
'/master-data/flock/detail/': ['lti.master.flocks.detail'],
|
||||
'/master-data/flock/detail/edit/': ['lti.master.flocks.update'],
|
||||
|
||||
'/master-data/production-standard/': ['lti.master.production_standards.list'],
|
||||
'/master-data/production-standard/': [
|
||||
'lti.dashboard.list',
|
||||
'lti.master.production_standards.list',
|
||||
],
|
||||
'/master-data/production-standard/add/': [
|
||||
'lti.dashboard.list',
|
||||
'lti.master.production_standards.create',
|
||||
],
|
||||
'/master-data/production-standard/detail/': [
|
||||
'lti.dashboard.list',
|
||||
'lti.master.production_standards.detail',
|
||||
],
|
||||
'/master-data/production-standard/detail/edit/': [
|
||||
'lti.dashboard.list',
|
||||
'lti.master.production_standards.update',
|
||||
],
|
||||
};
|
||||
|
||||
@@ -15,6 +15,22 @@ export class BaseApiService<T, CreatePayloadGeneric, UpdatePayloadGeneric> {
|
||||
return await httpClientFetcher<BaseApiResponse<T[]>>(endpoint);
|
||||
}
|
||||
|
||||
async getAll(query?: Record<string, unknown>) {
|
||||
try {
|
||||
const getAllPath = this.basePath;
|
||||
const getAllRes = await httpClient<BaseApiResponse<T[]>>(getAllPath, {
|
||||
query,
|
||||
});
|
||||
|
||||
return getAllRes;
|
||||
} catch (error) {
|
||||
if (axios.isAxiosError<BaseApiResponse<T[]>>(error)) {
|
||||
return error.response?.data;
|
||||
}
|
||||
return undefined;
|
||||
}
|
||||
}
|
||||
|
||||
async getSingle(id: number) {
|
||||
try {
|
||||
const getSinglePath = `${this.basePath}/${id}`;
|
||||
|
||||
@@ -0,0 +1,166 @@
|
||||
import { sleep } from '@/lib/helper';
|
||||
import { BaseApiService } from '@/services/api/base';
|
||||
import { httpClientFetcher } from '@/services/http/client';
|
||||
import { BaseApiResponse } from '@/types/api/api-general';
|
||||
import { ProductionResult } from '@/types/api/report/production-result';
|
||||
|
||||
// TODO: delete this dummy data
|
||||
const PRODUCTION_RESULT_DUMMY_DATA: BaseApiResponse<ProductionResult[]> = {
|
||||
code: 200,
|
||||
status: 'success',
|
||||
message: 'Get Laporan Hasil Produksi successfully',
|
||||
meta: {
|
||||
page: 1,
|
||||
limit: 1,
|
||||
total_pages: 2,
|
||||
total_results: 2,
|
||||
},
|
||||
data: [
|
||||
{
|
||||
created_user: {
|
||||
id: 1,
|
||||
id_user: 1001,
|
||||
email: 'user@example.com',
|
||||
name: 'John Doe',
|
||||
},
|
||||
project_flock: {
|
||||
id: 1,
|
||||
name: 'PROJECT',
|
||||
category: 'LAYING',
|
||||
kandang: {
|
||||
id: 1,
|
||||
name: 'Cikaum',
|
||||
},
|
||||
},
|
||||
created_at: '2025-01-01T08:00:00Z',
|
||||
updated_at: '2025-01-02T10:30:00Z',
|
||||
|
||||
woa: 25,
|
||||
|
||||
bw: 62.5,
|
||||
std_bw: 60,
|
||||
uniformity: 88,
|
||||
std_uniformity: '90% up',
|
||||
|
||||
dep_kum: 3.2,
|
||||
dep_std: 2.5,
|
||||
|
||||
butiran_utuh: 850,
|
||||
butiran_putih: 50,
|
||||
butiran_retak: 70,
|
||||
butiran_pecah: 30,
|
||||
butiran_jumlah: 1000,
|
||||
total_butir: 1000,
|
||||
|
||||
kg_utuh: 52.3,
|
||||
kg_putih: 3.1,
|
||||
kg_retak: 4.2,
|
||||
kg_pecah: 1.9,
|
||||
kg_jumlah: 61.5,
|
||||
total_kg: 61.5,
|
||||
|
||||
persen_utuh: 85,
|
||||
persen_putih: 5,
|
||||
persen_retak: 7,
|
||||
persen_pecah: 3,
|
||||
|
||||
hd: 92,
|
||||
hd_std: 90,
|
||||
fi: 115,
|
||||
fi_std: 667,
|
||||
em: 85,
|
||||
em_std: 83,
|
||||
ew: 62,
|
||||
ew_std: 60,
|
||||
fcr: 2.1,
|
||||
fcr_std: 2.0,
|
||||
hh: 96,
|
||||
hh_std: 95,
|
||||
},
|
||||
{
|
||||
created_user: {
|
||||
id: 1,
|
||||
id_user: 1001,
|
||||
email: 'user@example.com',
|
||||
name: 'John Doe',
|
||||
},
|
||||
project_flock: {
|
||||
id: 1,
|
||||
name: 'PROJECT',
|
||||
category: 'LAYING',
|
||||
kandang: {
|
||||
id: 1,
|
||||
name: 'Cikaum',
|
||||
},
|
||||
},
|
||||
created_at: '2025-01-01T08:00:00Z',
|
||||
updated_at: '2025-01-02T10:30:00Z',
|
||||
|
||||
woa: 25,
|
||||
|
||||
bw: 62.5,
|
||||
std_bw: 60,
|
||||
uniformity: 88,
|
||||
std_uniformity: '90% up',
|
||||
|
||||
dep_kum: 3.2,
|
||||
dep_std: 2.5,
|
||||
|
||||
butiran_utuh: 850,
|
||||
butiran_putih: 50,
|
||||
butiran_retak: 70,
|
||||
butiran_pecah: 30,
|
||||
butiran_jumlah: 1000,
|
||||
total_butir: 1000,
|
||||
|
||||
kg_utuh: 52.3,
|
||||
kg_putih: 3.1,
|
||||
kg_retak: 4.2,
|
||||
kg_pecah: 1.9,
|
||||
kg_jumlah: 61.5,
|
||||
total_kg: 61.5,
|
||||
|
||||
persen_utuh: 85,
|
||||
persen_putih: 5,
|
||||
persen_retak: 7,
|
||||
persen_pecah: 3,
|
||||
|
||||
hd: 92,
|
||||
hd_std: 90,
|
||||
fi: 115,
|
||||
fi_std: 110,
|
||||
em: 85,
|
||||
em_std: 83,
|
||||
ew: 62,
|
||||
ew_std: 60,
|
||||
fcr: 2.1,
|
||||
fcr_std: 2.0,
|
||||
hh: 96,
|
||||
hh_std: 95,
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
export class ProductionResultReportApiService extends BaseApiService<
|
||||
ProductionResult,
|
||||
unknown,
|
||||
unknown
|
||||
> {
|
||||
constructor(basePath: string = '/reports/production-result') {
|
||||
super(basePath);
|
||||
}
|
||||
|
||||
async getAllProductionResultFetcher(
|
||||
endpoint: string
|
||||
): Promise<BaseApiResponse<ProductionResult[]>> {
|
||||
// return await httpClientFetcher<BaseApiResponse<ProductionResult[]>>(
|
||||
// endpoint
|
||||
// );
|
||||
|
||||
await sleep(1000);
|
||||
|
||||
return PRODUCTION_RESULT_DUMMY_DATA;
|
||||
}
|
||||
}
|
||||
|
||||
export const ProductionResultReportApi = new ProductionResultReportApiService();
|
||||
Vendored
+6
-7
@@ -6,6 +6,11 @@ import { Kandang } from '@/types/api/master-data/kandang';
|
||||
import { Product } from '@type/api/master-data/product';
|
||||
import { Customer } from '@type/api/master-data/customer';
|
||||
import { BaseMetadata } from '@/types/api/api-general';
|
||||
import { Kandang } from '@/types/api/master-data/kandang';
|
||||
import { Product } from '@type/api/master-data/product';
|
||||
import { Customer } from '@type/api/master-data/customer';
|
||||
import { BaseMetadata } from '@/types/api/api-general';
|
||||
import { ProjectFlock } from '@/types/api/production/project-flock';
|
||||
|
||||
export type BaseSales = {
|
||||
id: number;
|
||||
@@ -29,10 +34,6 @@ export type BaseClosingSales = {
|
||||
period: number;
|
||||
sales: BaseSales[];
|
||||
};
|
||||
import { Kandang } from '@/types/api/master-data/kandang';
|
||||
import { Product } from '@type/api/master-data/product';
|
||||
import { Customer } from '@type/api/master-data/customer';
|
||||
import { BaseMetadata } from '@/types/api/api-general';
|
||||
|
||||
export type BaseSales = {
|
||||
id: number;
|
||||
@@ -66,9 +67,6 @@ export type BaseClosing = {
|
||||
closing_date?: string;
|
||||
shed_label: string;
|
||||
shed_count: number;
|
||||
sales_paid_amount: number;
|
||||
sales_remaining_amount: number;
|
||||
sales_payment_status: string;
|
||||
project_status: 'Pengajuan' | 'Aktif' | 'Selesai';
|
||||
};
|
||||
|
||||
@@ -83,6 +81,7 @@ export type BaseClosingGeneralInformation = BaseClosing & {
|
||||
sales_payment_status: string;
|
||||
project_status: 'Pengajuan' | 'Aktif' | 'Selesai';
|
||||
closing_status: string;
|
||||
project_flock: ProjectFlock;
|
||||
};
|
||||
|
||||
export type ClosingGeneralInformation = BaseMetadata &
|
||||
|
||||
+14
-14
@@ -4,12 +4,23 @@ import { ProductWarehouse } from '@/types/api/inventory/product-warehouse';
|
||||
export type ProductionMetrics = {
|
||||
total_depletion_qty: number;
|
||||
cum_depletion_rate: number;
|
||||
daily_gain: number;
|
||||
avg_daily_gain: number;
|
||||
cum_intake: number;
|
||||
fcr_value: number;
|
||||
fcr_std?: number;
|
||||
total_chick_qty: number;
|
||||
cum_depletion: number;
|
||||
hand_day?: number;
|
||||
hand_house?: number;
|
||||
feed_intake?: number;
|
||||
egg_mesh?: number;
|
||||
egg_weight?: number;
|
||||
hand_day_std?: number;
|
||||
hand_house_std?: number;
|
||||
feed_intake_std?: number;
|
||||
egg_mesh_std?: number;
|
||||
egg_weight_std?: number;
|
||||
daily_gain?: number;
|
||||
avg_daily_gain?: number;
|
||||
cum_depletion?: number;
|
||||
};
|
||||
|
||||
export type BaseRecording = {
|
||||
@@ -20,12 +31,6 @@ export type BaseRecording = {
|
||||
project_flock_category?: 'GROWING' | 'LAYING';
|
||||
} & ProductionMetrics;
|
||||
|
||||
export type RecordingBW = {
|
||||
avg_weight: number;
|
||||
qty: number;
|
||||
total_weight: number;
|
||||
};
|
||||
|
||||
export type RecordingDepletion = {
|
||||
product_warehouse_id: number;
|
||||
qty: number;
|
||||
@@ -63,7 +68,6 @@ export type Recording = BaseMetadata &
|
||||
BaseRecording & {
|
||||
approval?: BaseApproval;
|
||||
created_user: User;
|
||||
body_weights?: RecordingBW[];
|
||||
depletions?: RecordingDepletion[];
|
||||
stocks?: RecordingStock[];
|
||||
eggs?: RecordingEgg[];
|
||||
@@ -77,10 +81,6 @@ export type NextDayRecording = {
|
||||
|
||||
export type CreateGrowingRecordingPayload = {
|
||||
project_flock_kandang_id: number;
|
||||
body_weights: {
|
||||
avg_weight: number;
|
||||
qty: number;
|
||||
}[];
|
||||
stocks?: {
|
||||
product_warehouse_id: number;
|
||||
qty: number;
|
||||
|
||||
+59
@@ -0,0 +1,59 @@
|
||||
import { BaseMetadata } from '@/types/api/api-general';
|
||||
import { ProjectFlock } from '@/types/api/production/project-flock';
|
||||
import { BaseKandang } from '@/types/api/master-data/kandang';
|
||||
|
||||
export type BaseProductionResult = {
|
||||
project_flock: Pick<ProjectFlock, 'id' | 'name' | 'category'> & {
|
||||
kandang: Pick<BaseKandang, 'id' | 'name'>;
|
||||
};
|
||||
|
||||
woa: number;
|
||||
|
||||
// BW
|
||||
bw: number;
|
||||
std_bw: number;
|
||||
uniformity: number;
|
||||
std_uniformity: string; // "90% up" - keeping as string based on "up" suffix potential
|
||||
|
||||
// Dep
|
||||
dep_kum: number;
|
||||
dep_std: number;
|
||||
|
||||
// Butiran
|
||||
butiran_utuh: number;
|
||||
butiran_putih: number;
|
||||
butiran_retak: number;
|
||||
butiran_pecah: number;
|
||||
butiran_jumlah: number;
|
||||
total_butir: number;
|
||||
|
||||
// Kg
|
||||
kg_utuh: number;
|
||||
kg_putih: number;
|
||||
kg_retak: number;
|
||||
kg_pecah: number;
|
||||
kg_jumlah: number;
|
||||
total_kg: number;
|
||||
|
||||
// %
|
||||
persen_utuh: number;
|
||||
persen_putih: number;
|
||||
persen_retak: number;
|
||||
persen_pecah: number;
|
||||
|
||||
// Produksi
|
||||
hd: number;
|
||||
hd_std: number;
|
||||
fi: number;
|
||||
fi_std: number;
|
||||
em: number;
|
||||
em_std: number;
|
||||
ew: number;
|
||||
ew_std: number;
|
||||
fcr: number;
|
||||
fcr_std: number;
|
||||
hh: number;
|
||||
hh_std: number;
|
||||
};
|
||||
|
||||
export type ProductionResult = BaseMetadata & BaseProductionResult;
|
||||
Reference in New Issue
Block a user