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:
rstubryan
2025-12-31 10:03:37 +07:00
14 changed files with 1308 additions and 577 deletions
+11
View File
@@ -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;
@@ -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;
+4
View File
@@ -82,6 +82,10 @@ export const MAIN_DRAWER_LINKS: SidebarMenuItem[] = [
text: 'Penjualan',
link: '/report/marketing',
},
{
text: 'Hasil Produksi',
link: '/report/production-result',
},
],
},
{
+24 -6
View File
@@ -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',
],
};
+16
View File
@@ -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();
+6 -7
View File
@@ -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
View File
@@ -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
View File
@@ -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;