Merge remote-tracking branch 'origin/development' into staging

This commit is contained in:
M1 AIR
2026-01-06 19:03:21 +07:00
27 changed files with 713 additions and 582 deletions
@@ -96,11 +96,6 @@ const ClosingProductionDataTabContent = ({
value={formatNumber(purchase.feed_used)} value={formatNumber(purchase.feed_used)}
unit='Kg' unit='Kg'
/> />
<DataRow
label='Pakan Terpakai per Ekor'
value={formatNumber(purchase.feed_used_per_head)}
unit='Kg'
/>
</div> </div>
</section> </section>
@@ -124,14 +119,12 @@ const ClosingProductionDataTabContent = ({
/> />
<DataRow <DataRow
label='Bobot Rata-Rata' label='Bobot Rata-Rata'
value={formatNumber(sales.chicken.average_weight)} value={formatNumber(sales.chicken.avg_weight)}
unit='Kg/Ekor' unit='Kg/Ekor'
/> />
<DataRow <DataRow
label='Harga Jual Rata-Rata' label='Harga Jual Rata-Rata'
value={formatNumber( value={formatNumber(sales.chicken.avg_selling_price)}
sales.chicken.chicken_average_selling_price
)}
unit='Rupiah' unit='Rupiah'
/> />
</div> </div>
@@ -148,17 +141,17 @@ const ClosingProductionDataTabContent = ({
/> />
<DataRow <DataRow
label='Telur (Kg)' label='Telur (Kg)'
value={formatNumber(sales.egg.egg_mass_kg)} value={formatNumber(sales.egg.egg_mass)}
unit='Kg' unit='Kg'
/> />
<DataRow <DataRow
label='Berat Telur Rata-Rata' label='Berat Telur Rata-Rata'
value={formatNumber(sales.egg.average_egg_weight_kg)} value={formatNumber(sales.egg.avg_egg_weight)}
unit='Kg' unit='Kg'
/> />
<DataRow <DataRow
label='Harga Jual Telur Rata-Rata' label='Harga Jual Telur Rata-Rata'
value={formatNumber(sales.egg.egg_average_selling_price)} value={formatNumber(sales.egg.avg_selling_price)}
unit='Rupiah' unit='Rupiah'
/> />
</div> </div>
@@ -191,17 +184,37 @@ const ClosingProductionDataTabContent = ({
/> />
<DataRow <DataRow
label='Mortalitas Std' label='Mortalitas Std'
value={formatNumber(performance.mortality_std)} value={formatNumber(performance.mor_std)}
unitClassName='hidden' unitClassName='hidden'
/> />
<DataRow <DataRow
label='Mortalitas Act' label='Mortalitas Act'
value={formatNumber(performance.mortality_act)} value={formatNumber(performance.mor_act)}
unitClassName='hidden' unitClassName='hidden'
/> />
<DataRow <DataRow
label='DEFF Mortalitas' label='DEFF Mortalitas'
value={formatNumber(performance.deff_mortality)} value={formatNumber(performance.mor_diff)}
unitClassName='hidden'
/>
<DataRow
label='AWG Std'
value={formatNumber(performance.awg_std)}
unit='Gr/Hari'
/>
<DataRow
label='AWG Act'
value={formatNumber(performance.awg_act)}
unit='Gr/Hari'
/>
<DataRow
label='Feed Intake Std'
value={formatNumber(performance.feed_intake_std)}
unitClassName='hidden'
/>
<DataRow
label='Feed Intake Act'
value={formatNumber(performance.feed_intake)}
unitClassName='hidden' unitClassName='hidden'
/> />
<DataRow <DataRow
@@ -216,14 +229,70 @@ const ClosingProductionDataTabContent = ({
/> />
<DataRow <DataRow
label='DEFF FCR' label='DEFF FCR'
value={formatNumber(performance.deff_fcr)} value={formatNumber(performance.fcr_diff)}
unitClassName='hidden' unitClassName='hidden'
/> />
<DataRow
label='AWG' {/* Laying Specific Fields */}
value={formatNumber(performance.awg)} {performance.hen_day_act !== undefined && (
unit='Gr/Hari' <>
/> <DataRow
label='Hen Day Std'
value={formatNumber(performance.hen_day_std!)}
unit='%'
/>
<DataRow
label='Hen Day Act'
value={formatNumber(performance.hen_day_act)}
unit='%'
/>
</>
)}
{performance.egg_mass !== undefined && (
<>
<DataRow
label='Egg Mass Std'
value={formatNumber(performance.egg_mass_std!)}
unit='Kg'
/>
<DataRow
label='Egg Mass Act'
value={formatNumber(performance.egg_mass)}
unit='Kg'
/>
</>
)}
{performance.egg_weight !== undefined && (
<>
<DataRow
label='Egg Weight Std'
value={formatNumber(performance.egg_weight_std!)}
unit='Gr'
/>
<DataRow
label='Egg Weight Act'
value={formatNumber(performance.egg_weight)}
unit='Gr'
/>
</>
)}
{performance.hen_housed_act !== undefined && (
<>
<DataRow
label='Hen Housed Std'
value={formatNumber(performance.hen_housed_std!)}
unit='%'
/>
<DataRow
label='Hen Housed Act'
value={formatNumber(performance.hen_housed_act)}
unit='%'
/>
</>
)}
</div> </div>
</section> </section>
</div> </div>
+26 -4
View File
@@ -9,6 +9,7 @@ import Table from '@/components/Table';
import { import {
FINANCE_INITIAL_BALANCE_STATUS, FINANCE_INITIAL_BALANCE_STATUS,
FINANCE_TRANSACTION_STATUS, FINANCE_TRANSACTION_STATUS,
FINANCE_INJECTION_STATUS,
} from '@/config/constant'; } from '@/config/constant';
import { formatCurrency, formatDate, formatTitleCase } from '@/lib/helper'; import { formatCurrency, formatDate, formatTitleCase } from '@/lib/helper';
import { FinanceApi } from '@/services/api/finance'; import { FinanceApi } from '@/services/api/finance';
@@ -33,7 +34,7 @@ const FinanceDetail = ({ finance }: { finance: Finance }) => {
}, },
{ {
label: 'Pihak', label: 'Pihak',
value: finance.party.name, value: finance.party.id ? finance.party.name : '-',
}, },
{ {
label: 'Tanggal', label: 'Tanggal',
@@ -51,7 +52,7 @@ const FinanceDetail = ({ finance }: { finance: Finance }) => {
const informasiTransfer = [ const informasiTransfer = [
{ {
label: 'No. Referensi', label: 'No. Referensi',
value: finance.reference_number, value: finance.reference_number ?? '-',
}, },
{ {
label: 'Nomor Rekening', label: 'Nomor Rekening',
@@ -69,7 +70,16 @@ const FinanceDetail = ({ finance }: { finance: Finance }) => {
label: 'Sisa', label: 'Sisa',
value: formatCurrency(finance.income_amount), value: formatCurrency(finance.income_amount),
}, },
]; ].filter((item) => {
// Hide party account number row if transaction type is INJECTION
if (
FINANCE_INJECTION_STATUS.includes(finance.transaction_type) &&
item.label === `Rekening ${formatTitleCase(finance.party.type)}`
) {
return false;
}
return true;
});
const confirmationModalDeleteClickHandler = async () => { const confirmationModalDeleteClickHandler = async () => {
setIsDeleteLoading(true); setIsDeleteLoading(true);
@@ -162,7 +172,19 @@ const FinanceDetail = ({ finance }: { finance: Finance }) => {
</Button> </Button>
</RequirePermission> </RequirePermission>
)} )}
<RequirePermission permissions='lti.finance.transaction.delete'> {FINANCE_INJECTION_STATUS.includes(finance.transaction_type) && (
<RequirePermission permissions='lti.finance.injections.update'>
<Button
color='warning'
className='min-w-24'
href={`/finance/detail/edit/injection?financeId=${finance.id}`}
>
<Icon icon='mdi:pencil-outline' />
Edit
</Button>
</RequirePermission>
)}
<RequirePermission permissions='lti.finance.transactions.delete'>
<Button <Button
color='error' color='error'
className='min-w-24' className='min-w-24'
@@ -49,7 +49,14 @@ const RowOptionsMenu = ({
}) => { }) => {
return ( return (
<RowOptionsMenuWrapper type={type}> <RowOptionsMenuWrapper type={type}>
<RequirePermission permissions='lti.finance.transaction.detail'> <RequirePermission
permissions={[
'lti.finance.transactions.detail',
'lti.finance.initial_balances.detail',
'lti.finance.injections.detail',
'lti.finance.payments.detail',
]}
>
<Button <Button
href={`/finance/detail?financeId=${props.row.original.id}`} href={`/finance/detail?financeId=${props.row.original.id}`}
variant='ghost' variant='ghost'
@@ -109,7 +116,7 @@ const RowOptionsMenu = ({
</RequirePermission> </RequirePermission>
)} )}
<RequirePermission permissions='lti.finance.transaction.delete'> <RequirePermission permissions='lti.finance.transactions.delete'>
<Button <Button
onClick={deleteClickHandler} onClick={deleteClickHandler}
variant='ghost' variant='ghost'
@@ -1,5 +1,5 @@
import * as Yup from 'yup'; import * as Yup from 'yup';
import { Movement } from '@/types/api/inventory/movement'; import { Movement, MovementDocument } from '@/types/api/inventory/movement';
type MovementFormSchemaType = { type MovementFormSchemaType = {
transfer_reason: string; transfer_reason: string;
@@ -29,7 +29,7 @@ type MovementFormSchemaType = {
deliveries: { deliveries: {
delivery_cost?: number | string; delivery_cost?: number | string;
delivery_cost_per_item?: number | string; delivery_cost_per_item?: number | string;
document?: File | string | null; document?: File | MovementDocument | null;
document_path?: string | null; document_path?: string | null;
driver_name: string; driver_name: string;
vehicle_plate: string; vehicle_plate: string;
@@ -61,7 +61,7 @@ export type ProductSchema = {
export type DeliverySchema = { export type DeliverySchema = {
delivery_cost?: number | string; delivery_cost?: number | string;
delivery_cost_per_item?: number | string; delivery_cost_per_item?: number | string;
document?: File | string | null; document?: File | MovementDocument | null;
document_path?: string | null; document_path?: string | null;
driver_name: string; driver_name: string;
vehicle_plate: string; vehicle_plate: string;
@@ -129,13 +129,12 @@ const DeliveryObjectSchema: Yup.ObjectSchema<DeliverySchema> = Yup.object({
}), }),
document_path: Yup.string().optional(), document_path: Yup.string().optional(),
document_index: Yup.number().optional(), document_index: Yup.number().optional(),
document: Yup.mixed<File | string>() document: Yup.mixed<File | MovementDocument>()
.nullable() .nullable()
.test('fileSize', 'Ukuran dokumen maksimal 2 MB', (value) => { .test('fileSize', 'Ukuran dokumen maksimal 2 MB', (value) => {
if (!value) return true; if (!value) return true;
if (typeof value === 'string') return true;
if (value instanceof File) return value.size <= 2 * 1024 * 1024; if (value instanceof File) return value.size <= 2 * 1024 * 1024;
return false; return true;
}), }),
driver_name: Yup.string().required('Nama sopir wajib diisi!'), driver_name: Yup.string().required('Nama sopir wajib diisi!'),
vehicle_plate: Yup.string().required('Plat nomor wajib diisi!'), vehicle_plate: Yup.string().required('Plat nomor wajib diisi!'),
@@ -241,7 +240,7 @@ export const getMovementFormInitialValues = (
delivery_cost: d.shipping_cost_total ?? undefined, delivery_cost: d.shipping_cost_total ?? undefined,
delivery_cost_per_item: d.shipping_cost_item ?? undefined, delivery_cost_per_item: d.shipping_cost_item ?? undefined,
document_number: d.document_number ?? '', document_number: d.document_number ?? '',
document: d.document_path ?? null, document: d.document ?? null,
document_path: d.document_path ?? null, document_path: d.document_path ?? null,
driver_name: d.driver_name ?? '', driver_name: d.driver_name ?? '',
vehicle_plate: d.vehicle_plate ?? '', vehicle_plate: d.vehicle_plate ?? '',
@@ -35,6 +35,7 @@ import FileInput from '@/components/input/FileInput';
import CheckboxInput from '@/components/input/CheckboxInput'; import CheckboxInput from '@/components/input/CheckboxInput';
import Badge from '@/components/Badge'; import Badge from '@/components/Badge';
import Card from '@/components/Card'; import Card from '@/components/Card';
import { S3_PUBLIC_BASE_URL } from '@/config/constant';
interface MovementFormProps { interface MovementFormProps {
type?: 'add' | 'edit' | 'detail'; type?: 'add' | 'edit' | 'detail';
@@ -55,16 +56,8 @@ const MovementForm = ({ type = 'add', initialValues }: MovementFormProps) => {
// ===== FORM HANDLERS ===== // ===== FORM HANDLERS =====
const createMovementHandler = useCallback( const createMovementHandler = useCallback(
async (payload: CreateMovementPayload, documents: File[] = []) => { async (payload: CreateMovementPayload) => {
const formData = new FormData(); const res = await MovementApi.createMovement(payload);
formData.append('data', JSON.stringify(payload));
documents.forEach((file, index) => {
formData.append(`documents[${index}]`, file);
});
const res = await MovementApi.create(
formData as unknown as CreateMovementPayload
);
if (isResponseError(res)) { if (isResponseError(res)) {
setMovementFormErrorMessage(res.message); setMovementFormErrorMessage(res.message);
return; return;
@@ -218,20 +211,23 @@ const MovementForm = ({ type = 'add', initialValues }: MovementFormProps) => {
}); });
const payload: CreateMovementPayload = { const payload: CreateMovementPayload = {
transfer_reason: values.transfer_reason, data: {
transfer_date: values.transfer_date, transfer_reason: values.transfer_reason,
source_warehouse_id: values.source_warehouse_id, transfer_date: values.transfer_date,
destination_warehouse_id: values.destination_warehouse_id, source_warehouse_id: values.source_warehouse_id,
products: values.products.map((p) => ({ destination_warehouse_id: values.destination_warehouse_id,
product_id: p.product_id, products: values.products.map((p) => ({
product_qty: parseInt(p.product_qty.toString()) || 0, product_id: p.product_id,
})), product_qty: parseInt(p.product_qty.toString()) || 0,
deliveries: deliveriesPayload, })),
deliveries: deliveriesPayload,
},
documents: documents.length > 0 ? documents : undefined,
}; };
switch (type) { switch (type) {
case 'add': case 'add':
await createMovementHandler(payload, documents); await createMovementHandler(payload);
break; break;
} }
}, },
@@ -1537,27 +1533,51 @@ const MovementForm = ({ type = 'add', initialValues }: MovementFormProps) => {
{type === 'detail' ? ( {type === 'detail' ? (
<> <>
<div className='flex flex-col items-start gap-2'> <div className='flex flex-col items-start gap-2'>
<Button {delivery.document_path ? (
color='primary' <Button
className='w-full min-w-52 flex items-center justify-center gap-2' color='primary'
disabled={!delivery.document_path} className='w-full min-w-52 flex items-center justify-center gap-2'
href={delivery.document_path ?? undefined} href={`${S3_PUBLIC_BASE_URL}/${delivery.document_path.startsWith('/') ? delivery.document_path.slice(1) : delivery.document_path}`}
target='_blank' target='_blank'
rel='noopener noreferrer' rel='noopener noreferrer'
> >
{delivery.document_path ? ( <Icon
<> icon='material-symbols:file-open-outline'
<Icon width={20}
icon='material-symbols:file-open-outline' height={20}
width={20} />
height={20} Lihat Dokumen
/> </Button>
Lihat Dokumen ) : delivery.document &&
</> delivery.document instanceof File === false ? (
) : ( <Button
'-' color='primary'
)} className='w-full min-w-52 flex items-center justify-center gap-2'
</Button> href={`${S3_PUBLIC_BASE_URL}/${delivery.document.path.startsWith('/') ? delivery.document.path.slice(1) : delivery.document.path}`}
target='_blank'
rel='noopener noreferrer'
>
<Icon
icon='material-symbols:file-open-outline'
width={20}
height={20}
/>
{delivery.document.name}
</Button>
) : (
<Button
color='neutral'
className='w-full min-w-52 flex items-center justify-center gap-2 cursor-not-allowed'
disabled
>
<Icon
icon='material-symbols:description'
width={20}
height={20}
/>
Tidak ada dokumen
</Button>
)}
</div> </div>
</> </>
) : ( ) : (
@@ -77,10 +77,6 @@ const MarketingDetail = ({
confirmationModal.openModal(); confirmationModal.openModal();
}; };
const deliveryClickHandler = () => {
deliveryModal.openModal();
};
const deleteClickHandler = () => { const deleteClickHandler = () => {
deleteModal.openModal(); deleteModal.openModal();
}; };
@@ -135,7 +131,7 @@ const MarketingDetail = ({
<div className='flex-row flex gap-3'> <div className='flex-row flex gap-3'>
{initialValues?.latest_approval?.step_number == 1 && ( {initialValues?.latest_approval?.step_number == 1 && (
<> <>
{/* <RequirePermission permissions='lti.marketing.sales_order.approve'> <RequirePermission permissions='lti.marketing.sales_order.approve'>
<Button <Button
color='success' color='success'
onClick={approveClickHandler} onClick={approveClickHandler}
@@ -147,20 +143,9 @@ const MarketingDetail = ({
<Icon icon='mdi:check' width={24} height={24} /> <Icon icon='mdi:check' width={24} height={24} />
Approve Approve
</Button> </Button>
</RequirePermission> */} </RequirePermission>
<Button
color='success'
onClick={approveClickHandler}
disabled={
initialValues?.latest_approval?.step_number == 1 &&
initialValues?.latest_approval?.action == 'REJECTED'
}
>
<Icon icon='mdi:check' width={24} height={24} />
Approve
</Button>
{/* <RequirePermission permissions='lti.marketing.sales_order.approve'> <RequirePermission permissions='lti.marketing.sales_order.approve'>
<Button <Button
color='error' color='error'
onClick={rejectClickHandler} onClick={rejectClickHandler}
@@ -172,23 +157,12 @@ const MarketingDetail = ({
<Icon icon='mdi:close' width={24} height={24} /> <Icon icon='mdi:close' width={24} height={24} />
Reject Reject
</Button> </Button>
</RequirePermission> */} </RequirePermission>
<Button
color='error'
onClick={rejectClickHandler}
disabled={
initialValues?.latest_approval?.step_number == 1 &&
initialValues?.latest_approval?.action == 'REJECTED'
}
>
<Icon icon='mdi:close' width={24} height={24} />
Reject
</Button>
</> </>
)} )}
{initialValues?.latest_approval?.step_number != 1 && ( {initialValues?.latest_approval?.step_number != 1 && (
<> <>
{/* <RequirePermission <RequirePermission
permissions={ permissions={
initialValues?.latest_approval?.step_number == 3 initialValues?.latest_approval?.step_number == 3
? 'lti.marketing.delivery_order.update' ? 'lti.marketing.delivery_order.update'
@@ -209,21 +183,7 @@ const MarketingDetail = ({
: 'Tambah '} : 'Tambah '}
Delivery Order Delivery Order
</Button> </Button>
</RequirePermission> */} </RequirePermission>
<Button
color='success'
href={
initialValues?.latest_approval?.step_number == 3
? `/marketing/detail/delivery-orders/edit?marketingId=${initialValues?.id}`
: `/marketing/add/delivery-orders?marketingId=${initialValues?.id}`
}
>
<Icon icon='mdi:truck' width={24} height={24} />
{initialValues?.latest_approval?.step_number == 3
? 'Edit '
: 'Tambah '}
Delivery Order
</Button>
</> </>
)} )}
</div> </div>
@@ -466,7 +426,7 @@ const MarketingDetail = ({
<div className='flex flex-row gap-3'> <div className='flex flex-row gap-3'>
{initialValues?.latest_approval?.step_number != 3 && ( {initialValues?.latest_approval?.step_number != 3 && (
<> <>
{/* <RequirePermission permissions='lti.marketing.sales_order.update'> <RequirePermission permissions='lti.marketing.sales_order.update'>
<Button <Button
color='warning' color='warning'
type='button' type='button'
@@ -475,27 +435,15 @@ const MarketingDetail = ({
<Icon icon='mdi:pencil' width={24} height={24} /> <Icon icon='mdi:pencil' width={24} height={24} />
Edit Edit
</Button> </Button>
</RequirePermission> */} </RequirePermission>
<Button
color='warning'
type='button'
href={`/marketing/detail/${initialValues?.latest_approval?.step_number == 3 ? 'delivery-orders' : 'sales-orders'}/edit?marketingId=${initialValues?.id}`}
>
<Icon icon='mdi:pencil' width={24} height={24} />
Edit
</Button>
</> </>
)} )}
{/* <RequirePermission permissions='lti.marketing.sales_order.delete'> <RequirePermission permissions='lti.marketing.sales_order.delete'>
<Button color='error' onClick={deleteClickHandler}> <Button color='error' onClick={deleteClickHandler}>
<Icon icon='mdi:delete' width={24} height={24} /> <Icon icon='mdi:delete' width={24} height={24} />
Hapus Hapus
</Button> </Button>
</RequirePermission> */} </RequirePermission>
<Button color='error' onClick={deleteClickHandler}>
<Icon icon='mdi:delete' width={24} height={24} />
Hapus
</Button>
</div> </div>
</div> </div>
<ConfirmationModal <ConfirmationModal
@@ -635,12 +635,6 @@ const MarketingForm = ({
wrapper: 'bg-white w-full', wrapper: 'bg-white w-full',
}} }}
> >
{/* <div className='text-blue-500'>
{JSON.stringify(formik.values)}
</div>
<div className='text-red-500'>
{JSON.stringify(formik.errors)}
</div> */}
<MemoizedDeliveryOrderProductTable <MemoizedDeliveryOrderProductTable
formType={formType} formType={formType}
data={deliveryOrderValues} data={deliveryOrderValues}
@@ -690,7 +684,7 @@ const MarketingForm = ({
{/* Actions button */} {/* Actions button */}
{formType == 'edit' && ( {formType == 'edit' && (
<div className='flex flex-row justify-start'> <div className='flex flex-row justify-start'>
{/* <RequirePermission permissions='lti.marketing.sales_order.delete'> <RequirePermission permissions='lti.marketing.sales_order.delete'>
<Button <Button
type='button' type='button'
color='error' color='error'
@@ -700,16 +694,7 @@ const MarketingForm = ({
<Icon icon='mdi:trash' width={24} height={24} /> <Icon icon='mdi:trash' width={24} height={24} />
Hapus Hapus
</Button> </Button>
</RequirePermission> */} </RequirePermission>
<Button
type='button'
color='error'
onClick={handleDelete}
isLoading={isLoading}
>
<Icon icon='mdi:trash' width={24} height={24} />
Hapus
</Button>
</div> </div>
)} )}
@@ -11,7 +11,7 @@ import SelectInput, {
useSelect, useSelect,
} from '@/components/input/SelectInput'; } from '@/components/input/SelectInput';
import { Kandang } from '@/types/api/master-data/kandang'; import { Kandang } from '@/types/api/master-data/kandang';
import { KandangApi, WarehouseApi } from '@/services/api/master-data'; import { WarehouseApi } from '@/services/api/master-data';
import { ProductWarehouse } from '@/types/api/inventory/product-warehouse'; import { ProductWarehouse } from '@/types/api/inventory/product-warehouse';
import { ProductWarehouseApi } from '@/services/api/inventory'; import { ProductWarehouseApi } from '@/services/api/inventory';
import NumberInput from '@/components/input/NumberInput'; import NumberInput from '@/components/input/NumberInput';
@@ -180,9 +180,6 @@ const SalesOrderProductForm = ({
</Alert> </Alert>
</div> </div>
)} )}
{/* <small className='block text-rose-500'>
{JSON.stringify(formik.errors)}
</small> */}
<div className='grid sm:grid-cols-2 gap-4 z-200'> <div className='grid sm:grid-cols-2 gap-4 z-200'>
<PatternInput <PatternInput
name='vehicle_number' name='vehicle_number'
@@ -32,38 +32,6 @@ const DeliveryOrderProductTable = ({
const columns = useMemo(() => { const columns = useMemo(() => {
const cols = [ const cols = [
// {
// id: 'select',
// header: ({
// table,
// }: {
// table: TanStack.Table<DeliveryOrderProductFormValues>;
// }) => (
// <div className='w-full flex flex-row justify-center'>
// <CheckboxInput
// name='allRow'
// checked={table.getIsAllRowsSelected()}
// indeterminate={table.getIsSomeRowsSelected()}
// onChange={table.getToggleAllRowsSelectedHandler()}
// />
// </div>
// ),
// cell: ({
// row,
// }: {
// row: TanStack.Row<DeliveryOrderProductFormValues>;
// }) => (
// <div>
// <CheckboxInput
// name='row'
// checked={row.getIsSelected()}
// disabled={!row.getCanSelect()}
// indeterminate={row.getIsSomeSelected()}
// onChange={row.getToggleSelectedHandler()}
// />
// </div>
// ),
// },
{ {
accessorFn: (row: DeliveryOrderProductFormValues) => row.do_number, accessorFn: (row: DeliveryOrderProductFormValues) => row.do_number,
header: 'No. Pengiriman', header: 'No. Pengiriman',
@@ -188,18 +156,6 @@ const DeliveryOrderProductTable = ({
</Button> </Button>
)} )}
{!props.row.original.qty && '-'} {!props.row.original.qty && '-'}
{/* {formType == 'add_deliver' && (
<Button
color='error'
className='p-1'
onClick={() =>
onDeleteRef.current(props.row.original.id as number)
}
type='button'
>
<Icon icon='mdi:trash' width={16} height={16} />
</Button>
)} */}
</> </>
</div> </div>
), ),
@@ -248,22 +204,6 @@ const DeliveryOrderProductTable = ({
<Icon icon='mdi:plus' width={16} height={16} /> <Icon icon='mdi:plus' width={16} height={16} />
Tambah Pengiriman Tambah Pengiriman
</Button> </Button>
{/* {selectedRowIds.length > 0 && (
<Button
type='button'
variant='outline'
color='error'
className='justify-start w-fit py-1 text-sm'
onClick={onBulkDelete}
>
<Icon icon='mdi:trash' width={16} height={16} />
Hapus
{selectedRowIds.length > 0
? ` (${selectedRowIds.length})`
: ''}{' '}
Pengiriman
</Button>
)} */}
</div> </div>
</> </>
); );
@@ -872,7 +872,7 @@ const RecordingTable = () => {
'mb-20': 'mb-20':
isResponseSuccess(recordings) && recordings?.data?.length === 0, isResponseSuccess(recordings) && recordings?.data?.length === 0,
}), }),
tableWrapperClassName: 'overflow-x-auto min-h-full overflow-visible!', tableWrapperClassName: 'overflow-x-auto min-h-full!',
tableClassName: 'font-inter w-full table-auto min-h-full!', tableClassName: 'font-inter w-full table-auto min-h-full!',
headerRowClassName: 'border-b border-b-gray-200', headerRowClassName: 'border-b border-b-gray-200',
headerColumnClassName: headerColumnClassName:
@@ -1,91 +1,93 @@
import React from 'react'; import React, { useMemo } from 'react';
import Card from '@/components/Card'; import Card from '@/components/Card';
import UniformityBarChart from '@/components/pages/production/uniformity/chart/UniformityBarChart'; import UniformityBarChart from '@/components/pages/production/uniformity/chart/UniformityBarChart';
import UniformityGaugeChart from '@/components/pages/production/uniformity/chart/UniformityGaugeChart'; import UniformityGaugeChart from '@/components/pages/production/uniformity/chart/UniformityGaugeChart';
import UniformityBarChartSkeleton from '@/components/pages/production/uniformity/skeleton/UniformityBarChartSkeleton'; import UniformityBarChartSkeleton from '@/components/pages/production/uniformity/skeleton/UniformityBarChartSkeleton';
import UniformityGaugeChartSkeleton from '@/components/pages/production/uniformity/skeleton/UniformityGaugeChartSkeleton'; import UniformityGaugeChartSkeleton from '@/components/pages/production/uniformity/skeleton/UniformityGaugeChartSkeleton';
import {
UniformityDetailItem,
Uniformity,
} from '@/types/api/production/uniformity';
interface BarChartData { interface UniformityChartProps {
name: string; uniformityData?: Uniformity | null;
uv: number; uniformityDetails?: UniformityDetailItem[];
} }
interface GaugeChartData { const UniformityChart = ({
value: number; uniformityData,
label: string; uniformityDetails,
kandang?: string; }: UniformityChartProps) => {
week?: string; const defaultUniformityDetails: UniformityDetailItem[] = [
currentValue?: number; { id: 1, weight: 61, range: 'Ideal' },
totalValue?: number; { id: 2, weight: 62, range: 'Ideal' },
} { id: 3, weight: 63, range: 'Ideal' },
{ id: 4, weight: 64, range: 'Ideal' },
const UniformityChart = () => { { id: 5, weight: 65, range: 'Ideal' },
// TODO: Replace with actual API call { id: 6, weight: 66, range: 'Ideal' },
const barChartData: BarChartData[] = [ { id: 7, weight: 67, range: 'Ideal' },
{
name: '48-52',
uv: 80,
},
{
name: '52-56',
uv: 120,
},
{
name: '56-60',
uv: 160,
},
{
name: '60-64',
uv: 200,
},
{
name: '64-68',
uv: 160,
},
{
name: '68-72',
uv: 120,
},
{
name: '72-76',
uv: 80,
},
{
name: '76-80',
uv: 120,
},
{
name: '84-88',
uv: 160,
},
{
name: '88-92',
uv: 200,
},
{
name: '92-96',
uv: 160,
},
]; ];
// TODO: Replace with actual API call const detailsToUse = uniformityDetails || defaultUniformityDetails;
// const gaugeChartData: GaugeChartData = {
// value: 0,
// label: '',
// kandang: 'Kandang Cirangga',
// week: 'Week 2',
// currentValue: 512,
// totalValue: 1024,
// };
const gaugeChartData: GaugeChartData = { const barChartData = useMemo(() => {
value: 52, if (!uniformityData) {
label: 'Uniformity', return [];
kandang: 'Kandang Cirangga', }
week: 'Week 2',
currentValue: 512, if (!detailsToUse || detailsToUse.length === 0) {
totalValue: 1024, return [];
}; }
const weights = detailsToUse.map((d) => d.weight);
const minWeight = Math.floor(Math.min(...weights) / 5) * 5;
const maxWeight = Math.ceil(Math.max(...weights) / 5) * 5;
const rangeSize = maxWeight - minWeight < 11 ? 4 : 5;
const ranges: string[] = [];
for (let start = minWeight; start <= maxWeight; start += rangeSize) {
const end = start + rangeSize;
ranges.push(`${start}-${end}`);
}
const totalIdealCount = detailsToUse.filter(
(d) => d.range === 'Ideal'
).length;
return ranges.map((range) => {
const [minStr, maxStr] = range.split('-').map(Number);
const min = minStr;
const max = maxStr;
const birdsInRange = detailsToUse.filter(
(d) => d.weight >= min && d.weight < max
).length;
const hasIdeal = detailsToUse.some(
(d) => d.range === 'Ideal' && d.weight >= min && d.weight < max
);
return {
name: range,
uv: birdsInRange,
isIdeal: hasIdeal,
idealCount: hasIdeal ? totalIdealCount : undefined,
};
});
}, [uniformityData, detailsToUse]);
const gaugeChartData = useMemo(() => {
if (!uniformityData) return undefined;
return {
value: uniformityData.uniformity,
label: 'Uniformity',
week: `Week ${uniformityData.week}`,
currentValue: uniformityData.uniform_qty,
totalValue: uniformityData.chick_qty_of_weight,
};
}, [uniformityData]);
return ( return (
<section className='w-full grid grid-cols-1 xl:grid-cols-2 2xl:grid-cols-4 gap-4'> <section className='w-full grid grid-cols-1 xl:grid-cols-2 2xl:grid-cols-4 gap-4'>
@@ -98,14 +100,14 @@ const UniformityChart = () => {
}} }}
> >
<div className='w-full h-full flex items-center justify-center'> <div className='w-full h-full flex items-center justify-center'>
{barChartData.length === 0 ? ( {!uniformityData || barChartData.length === 0 ? (
<UniformityBarChartSkeleton /> <UniformityBarChartSkeleton />
) : ( ) : (
<UniformityBarChart data={barChartData} /> <UniformityBarChart data={barChartData} />
)} )}
</div> </div>
</Card> </Card>
{gaugeChartData.value === 0 ? ( {!uniformityData || !gaugeChartData ? (
<Card <Card
variant='bordered' variant='bordered'
title='Weekly Performance ⓘ' title='Weekly Performance ⓘ'
@@ -128,7 +130,6 @@ const UniformityChart = () => {
<UniformityGaugeChart <UniformityGaugeChart
value={gaugeChartData.value} value={gaugeChartData.value}
label={gaugeChartData.label} label={gaugeChartData.label}
kandang={gaugeChartData.kandang}
week={gaugeChartData.week} week={gaugeChartData.week}
currentValue={gaugeChartData.currentValue} currentValue={gaugeChartData.currentValue}
totalValue={gaugeChartData.totalValue} totalValue={gaugeChartData.totalValue}
@@ -41,9 +41,7 @@ export default function UniformityPageWrapper({
return ( return (
<> <>
<div className='w-full p-4'> <div className='w-full p-4'>
<UniformityTable <UniformityTable />
refresh={() => !isOpen && router.push('/production/uniformity')}
/>
</div> </div>
<Drawer <Drawer
@@ -10,7 +10,11 @@ import Button from '@/components/Button';
import UniformityChart from '@/components/pages/production/uniformity/UniformityChart'; import UniformityChart from '@/components/pages/production/uniformity/UniformityChart';
import { useTableFilter } from '@/services/hooks/useTableFilter'; import { useTableFilter } from '@/services/hooks/useTableFilter';
import { UniformityApi } from '@/services/api/uniformity'; import { UniformityApi } from '@/services/api/uniformity';
import { type Uniformity } from '@/types/api/production/uniformity'; import {
DetailOptionType,
type Uniformity,
type UniformityDetail,
} from '@/types/api/production/uniformity';
import { isResponseSuccess } from '@/lib/api-helper'; import { isResponseSuccess } from '@/lib/api-helper';
import { type BaseApiResponse } from '@/types/api/api-general'; import { type BaseApiResponse } from '@/types/api/api-general';
import Table from '@/components/Table'; import Table from '@/components/Table';
@@ -45,27 +49,12 @@ import Dropdown from '@/components/Dropdown';
import Menu from '@/components/menu/Menu'; import Menu from '@/components/menu/Menu';
import MenuItem from '@/components/menu/MenuItem'; import MenuItem from '@/components/menu/MenuItem';
const isUniformityLocked = (uniformity: Uniformity): boolean => {
// Uniformity data is never locked - checkbox is always enabled
return false;
};
const canApproveRejectUniformity = (uniformity: Uniformity): boolean => {
return uniformity.status === 'CREATED' || uniformity.status === 'Pengajuan';
};
interface UniformityPreviewData {
id: string;
label: string;
value: string;
}
const UniformityConfirmationPreview = ({ const UniformityConfirmationPreview = ({
uniformity, uniformity,
}: { }: {
uniformity?: Uniformity; uniformity?: Uniformity;
}) => { }) => {
const data: UniformityPreviewData[] = [ const data: DetailOptionType[] = [
{ {
id: 'tanggal', id: 'tanggal',
label: 'Tanggal', label: 'Tanggal',
@@ -91,7 +80,7 @@ const UniformityConfirmationPreview = ({
{ {
id: 'file-uniformity', id: 'file-uniformity',
label: 'File Uniformity', label: 'File Uniformity',
value: '-', // File name tidak tersedia di GET ALL response value: '-',
}, },
{ {
id: 'status', id: 'status',
@@ -100,7 +89,7 @@ const UniformityConfirmationPreview = ({
}, },
]; ];
const columns: ColumnDef<UniformityPreviewData>[] = [ const columns: ColumnDef<DetailOptionType>[] = [
{ {
accessorKey: 'label', accessorKey: 'label',
header: 'Label', header: 'Label',
@@ -148,7 +137,52 @@ const UniformityConfirmationPreview = ({
); );
}; };
const UniformityTable = ({ refresh }: { refresh?: () => void }) => { const UniformityChartWrapper = ({
uniformitySwrKey,
}: {
uniformitySwrKey: string;
}) => {
const { data: uniformities } = useSWR(
uniformitySwrKey,
UniformityApi.getAllFetcher
);
const uniformityData = useMemo(() => {
if (isResponseSuccess(uniformities) && uniformities?.data?.length > 0) {
return uniformities.data[0];
}
return null;
}, [uniformities]);
const shouldFetchDetails = !!uniformityData;
const uniformityDetailSwrKey = useMemo(() => {
if (!uniformityData) return null;
return `${UniformityApi.basePath}/${uniformityData.id}?with_details=true`;
}, [uniformityData]);
const { data: uniformityDetailResponse } = useSWR(
uniformityDetailSwrKey,
shouldFetchDetails ? UniformityApi.getAllFetcher : null
);
const uniformityDetails = useMemo(() => {
if (shouldFetchDetails && isResponseSuccess(uniformityDetailResponse)) {
const detailData =
uniformityDetailResponse.data as unknown as UniformityDetail;
return detailData.uniformity_details;
}
return undefined;
}, [shouldFetchDetails, uniformityDetailResponse]);
return (
<UniformityChart
uniformityData={uniformityData}
uniformityDetails={uniformityDetails}
/>
);
};
const UniformityTable = () => {
const router = useRouter(); const router = useRouter();
const searchParams = useSearchParams(); const searchParams = useSearchParams();
const isSuccess = useUniformityStore((s) => s.isSuccess); const isSuccess = useUniformityStore((s) => s.isSuccess);
@@ -355,6 +389,12 @@ const UniformityTable = ({ refresh }: { refresh?: () => void }) => {
mutate: refreshUniformities, mutate: refreshUniformities,
} = useSWR(uniformitySwrKey, UniformityApi.getAllFetcher); } = useSWR(uniformitySwrKey, UniformityApi.getAllFetcher);
useEffect(() => {
if (isSuccess) {
refreshUniformities();
}
}, [isSuccess, refreshUniformities]);
// ===== FILTER HANDLERS ===== // ===== FILTER HANDLERS =====
const handleFilterLocationChange = useCallback( const handleFilterLocationChange = useCallback(
(val: OptionType | OptionType[] | null) => { (val: OptionType | OptionType[] | null) => {
@@ -836,7 +876,7 @@ const UniformityTable = ({ refresh }: { refresh?: () => void }) => {
<div className='my-4 divider'></div> <div className='my-4 divider'></div>
<section> <section>
<UniformityChart /> <UniformityChartWrapper uniformitySwrKey={uniformitySwrKey} />
</section> </section>
<Card <Card
@@ -3,6 +3,7 @@ import {
Bar, Bar,
BarChart, BarChart,
CartesianGrid, CartesianGrid,
Cell,
Rectangle, Rectangle,
ResponsiveContainer, ResponsiveContainer,
Tooltip, Tooltip,
@@ -25,6 +26,8 @@ interface CustomTooltipProps {
interface BarChartData { interface BarChartData {
name: string; name: string;
uv: number; uv: number;
isIdeal?: boolean;
idealCount?: number;
} }
interface UniformityBarChartProps { interface UniformityBarChartProps {
@@ -33,7 +36,25 @@ interface UniformityBarChartProps {
function CustomTooltip({ payload, label, active }: CustomTooltipProps) { function CustomTooltip({ payload, label, active }: CustomTooltipProps) {
if (active && payload && payload.length && label !== undefined) { if (active && payload && payload.length && label !== undefined) {
const data = payload[0] as unknown as { payload: BarChartData };
const chartData = data.payload as BarChartData;
const labelStr = String(label); const labelStr = String(label);
if (chartData.isIdeal && chartData.idealCount !== undefined) {
return (
<div className='bg-[#18181B] p-2.5 shadow-sm text-white rounded-2xl rounded-bl-none'>
<p className='m-0 font-bold text-white/50'>Uniformity 2025</p>
<div className='flex items-center gap-2 mt-2 justify-between'>
<div className='flex items-center gap-2'>
<div className='w-5 h-5 bg-[#0069E0] rounded-md'></div>
{chartData.idealCount} of Birds
</div>
<span>{labelStr}</span>
</div>
</div>
);
}
return ( return (
<div className='bg-[#18181B] p-2.5 shadow-sm text-white rounded-2xl rounded-bl-none'> <div className='bg-[#18181B] p-2.5 shadow-sm text-white rounded-2xl rounded-bl-none'>
<p className='m-0 font-bold text-white/50'>Uniformity 2025</p> <p className='m-0 font-bold text-white/50'>Uniformity 2025</p>
@@ -105,9 +126,6 @@ const UniformityBarChart: React.FC<UniformityBarChartProps> = ({ data }) => {
<Bar <Bar
name='Birds' name='Birds'
dataKey='uv' dataKey='uv'
fill='#FFFFFF'
stroke='#DDD'
strokeWidth={2}
radius={[25, 25, 0, 0]} radius={[25, 25, 0, 0]}
activeBar={ activeBar={
<Rectangle <Rectangle
@@ -117,7 +135,16 @@ const UniformityBarChart: React.FC<UniformityBarChartProps> = ({ data }) => {
radius={[25, 25, 0, 0]} radius={[25, 25, 0, 0]}
/> />
} }
/> >
{data.map((entry, index) => (
<Cell
key={`cell-${index}`}
fill={entry.isIdeal ? 'url(#activeBarGradient)' : '#FFFFFF'}
stroke={entry.isIdeal ? '#18181B' : '#DDD'}
strokeWidth={entry.isIdeal ? 0 : 2}
/>
))}
</Bar>
</BarChart> </BarChart>
</ResponsiveContainer> </ResponsiveContainer>
); );
@@ -1,25 +1,29 @@
import React from 'react'; import React, { useState } from 'react';
import { Cell, Pie, PieChart, ResponsiveContainer } from 'recharts'; import { Cell, Pie, PieChart, ResponsiveContainer } from 'recharts';
import Card from '@/components/Card'; import Card from '@/components/Card';
import { Icon } from '@iconify/react';
import { formatNumber } from '@/lib/helper'; import { formatNumber } from '@/lib/helper';
import { Icon } from '@iconify/react';
interface UniformityGaugeChartProps { interface UniformityGaugeChartProps {
value: number; value: number;
label: string; label: string;
kandang?: string;
week?: string; week?: string;
currentValue?: number; currentValue?: number;
totalValue?: number; totalValue?: number;
onWeekChange?: (direction: 'prev' | 'next') => void;
hasPrevWeek?: boolean;
hasNextWeek?: boolean;
} }
const UniformityGaugeChart: React.FC<UniformityGaugeChartProps> = ({ const UniformityGaugeChart: React.FC<UniformityGaugeChartProps> = ({
value, value,
label, label,
kandang,
week, week,
currentValue, currentValue,
totalValue, totalValue,
onWeekChange,
hasPrevWeek = false,
hasNextWeek = false,
}) => { }) => {
const numberOfSegments = 50; const numberOfSegments = 50;
const filledSegments = Math.round((value / 100) * numberOfSegments); const filledSegments = Math.round((value / 100) * numberOfSegments);
@@ -34,7 +38,7 @@ const UniformityGaugeChart: React.FC<UniformityGaugeChartProps> = ({
const inactiveColor = '#f0f0f0'; const inactiveColor = '#f0f0f0';
return ( return (
<div className='flex flex-col w-full'> <div className='flex flex-col w-full items-center'>
<div className='h-64 w-full relative flex justify-center'> <div className='h-64 w-full relative flex justify-center'>
<div className='relative w-full h-full flex flex-col items-center justify-end'> <div className='relative w-full h-full flex flex-col items-center justify-end'>
<ResponsiveContainer width='100%' height='100%'> <ResponsiveContainer width='100%' height='100%'>
@@ -73,34 +77,49 @@ const UniformityGaugeChart: React.FC<UniformityGaugeChartProps> = ({
</div> </div>
</div> </div>
</div> </div>
<Card <div className='flex items-center justify-center gap-2 w-full'>
variant='bordered' <button
className={{ onClick={() => onWeekChange?.('prev')}
wrapper: 'w-full', disabled={!hasPrevWeek}
}} className='p-2 rounded-lg border border-gray-200 bg-white hover:bg-gray-50 disabled:opacity-30 disabled:cursor-not-allowed transition-colors shadow-sm'
> aria-label='Previous week'
<section className='flex items-center gap-4'> >
<div className='w-12 h-12 bg-base-200 rounded-lg flex items-center justify-center border border-gray-200 shrink-0'> <Icon icon='heroicons:chevron-left' width={20} height={20} />
<Icon icon='heroicons:calendar-date-range' width={24} height={24} /> </button>
</div> <Card
<div className='grid grid-cols-1 min-w-0'> variant='bordered'
<div className='flex items-center space-x-2 text-[#18181B80] text-sm mb-1'> className={{
<span className='font-medium truncate'>{kandang}</span> wrapper: 'max-w-xs',
<span className='shrink-0'></span> }}
<span className='text-[#0069E0] font-semibold truncate'> >
{week} <section className='flex items-center justify-center gap-4'>
</span> <div className='grid grid-cols-1 min-w-0 text-center'>
<div className='flex items-center justify-center space-x-2 text-[#18181B80] text-sm mb-1'>
<span className='text-[#0069E0] font-semibold truncate'>
{week}
</span>
</div>
<div className='text-xl font-bold text-[#18181B80]'>
<span className='text-[#0069E0] break-all'>
{formatNumber(currentValue ?? 0)}
</span>
<span className='mx-1 text-gray-400 text-base'>From</span>
<span className='break-all'>
{formatNumber(totalValue ?? 0)}
</span>
</div>
</div> </div>
<div className='text-xl font-bold text-[#18181B80]'> </section>
<span className='text-[#0069E0] break-all'> </Card>
{formatNumber(currentValue ?? 0)} <button
</span> onClick={() => onWeekChange?.('next')}
<span className='mx-1 text-gray-400 text-base'>From</span> disabled={!hasNextWeek}
<span className='break-all'>{formatNumber(totalValue ?? 0)}</span> className='p-2 rounded-lg border border-gray-200 bg-white hover:bg-gray-50 disabled:opacity-30 disabled:cursor-not-allowed transition-colors shadow-sm'
</div> aria-label='Next week'
</div> >
</section> <Icon icon='heroicons:chevron-right' width={20} height={20} />
</Card> </button>
</div>
</div> </div>
); );
}; };
@@ -119,11 +119,13 @@ const UniformityDetail: React.FC<UniformityDetailProps> = ({
const statusValue = latest_approval?.action ?? '-'; const statusValue = latest_approval?.action ?? '-';
const valueMap: Record<string, string> = { const valueMap: Record<string, string> = {
tanggal: formatDate(info_umum.tanggal, 'DD MMMM YYYY'), tanggal: info_umum?.tanggal
'lokasi-farm': info_umum.lokasi_farm, ? formatDate(info_umum.tanggal, 'DD MMMM YYYY')
'project-flock': info_umum.project_flock, : '-',
kandang: info_umum.kandang, 'lokasi-farm': info_umum?.lokasi_farm ?? '-',
'document-name': info_umum.file_name, 'project-flock': info_umum?.project_flock ?? '-',
kandang: info_umum?.kandang ?? '-',
'document-name': info_umum?.file_name ?? '-',
'approval-status': statusValue, 'approval-status': statusValue,
}; };
@@ -229,49 +229,52 @@ const UniformityDetailsPreview = ({
{/* Form Section */} {/* Form Section */}
<div className='divider mt-3.5'></div> <div className='divider mt-3.5'></div>
<section className='w-full px-6'> <section className='w-full px-6'>
{uniformity_details && uniformity_details.length > 0 ? ( {info_umum || sampling || result ? (
<div className='flex flex-col gap-4'> <div className='flex flex-col gap-4'>
{/* Sampling and Range */} {/* Sampling and Range */}
<div className=''> {sampling && (
<p className='text-sm font-medium mb-5'>Sampling and Range</p> <div className=''>
<Table<DetailOptionType> <p className='text-sm font-medium mb-5'>Sampling and Range</p>
data={samplingTableData} <Table<DetailOptionType>
columns={columnsSampling} data={samplingTableData}
pageSize={4} columns={columnsSampling}
className={{ pageSize={4}
containerClassName: 'mb-0', className={{
paginationClassName: 'hidden', containerClassName: 'mb-0',
}} paginationClassName: 'hidden',
/> }}
</div> />
{/* Result */} </div>
<div className=''> )}
<p className='text-sm font-medium mb-5'>Result</p>
<Table<DetailOptionType>
data={resultTableData}
columns={resultColumns}
pageSize={4}
className={{
containerClassName: 'mb-0',
paginationClassName: 'hidden',
}}
/>
</div>
{/* Body Weight Details Button */} {/* Result */}
<div className='mt-4'> {result && (
<Button <div className=''>
type='button' <p className='text-sm font-medium mb-5'>Result</p>
onClick={fetchWeightData} <Table<DetailOptionType>
disabled={isLoading} data={resultTableData}
className='w-full' columns={resultColumns}
> pageSize={4}
{isLoading ? 'Loading...' : 'Show Body Weight Details'} className={{
</Button> containerClassName: 'mb-0',
</div> paginationClassName: 'hidden',
{/*{!uniformity_details || uniformity_details.length === 0 ? ( }}
<></> />
) : null}*/} </div>
)}
{!uniformity_details || uniformity_details.length === 0 ? (
<div className='mt-4'>
<Button
type='button'
onClick={fetchWeightData}
disabled={isLoading}
className='w-full'
>
{isLoading ? 'Loading...' : 'Show Body Weight Details'}
</Button>
</div>
) : null}
{/* Body Weight Details */} {/* Body Weight Details */}
{uniformity_details && uniformity_details.length > 0 && ( {uniformity_details && uniformity_details.length > 0 && (
@@ -97,12 +97,16 @@ const UniformityForm = ({
setInputValue: setLocationSelectInputValue, setInputValue: setLocationSelectInputValue,
options: locationOptions, options: locationOptions,
isLoadingOptions: isLoadingLocations, isLoadingOptions: isLoadingLocations,
} = useSelect(LocationApi.basePath, 'id', 'name', 'search'); } = useSelect(LocationApi.basePath, 'id', 'name', 'search', {
page: '1',
limit: '100',
});
// ===== FETCH PROJECT FLOCKS DATA ===== // ===== FETCH PROJECT FLOCKS DATA =====
const projectFlocksUrl = useMemo(() => { const projectFlocksUrl = useMemo(() => {
const params = new URLSearchParams({ const params = new URLSearchParams({
search: projectFlockSearchValue || '', search: projectFlockSearchValue || '',
page: '1',
limit: '100', limit: '100',
}); });
if (selectedLocation) { if (selectedLocation) {
@@ -141,6 +145,7 @@ const UniformityForm = ({
const approvedProjectFlockKandangsUrl = useMemo(() => { const approvedProjectFlockKandangsUrl = useMemo(() => {
const params = new URLSearchParams({ const params = new URLSearchParams({
step_name: 'Disetujui', step_name: 'Disetujui',
page: '1',
limit: '100', limit: '100',
}); });
return `${ProjectFlockKandangApi.basePath}?${params.toString()}`; return `${ProjectFlockKandangApi.basePath}?${params.toString()}`;
@@ -28,7 +28,7 @@ const UniformityGaugeChartSkeleton: React.FC<
const inactiveColor = '#f0f0f0'; const inactiveColor = '#f0f0f0';
return ( return (
<div className='flex flex-col w-full'> <div className='flex flex-col w-full items-center'>
<div className='h-64 w-full relative flex justify-center min-h-[256px]'> <div className='h-64 w-full relative flex justify-center min-h-[256px]'>
<div className='relative w-full h-full flex flex-col items-center justify-end min-w-0'> <div className='relative w-full h-full flex flex-col items-center justify-end min-w-0'>
<ResponsiveContainer width='100%' height={256}> <ResponsiveContainer width='100%' height={256}>
@@ -52,9 +52,14 @@ const PurchaseOrderAcceptApprovalForm = ({
const searchParams = useSearchParams(); const searchParams = useSearchParams();
const [purchaseOrderFormErrorMessage, setPurchaseOrderFormErrorMessage] = const [purchaseOrderFormErrorMessage, setPurchaseOrderFormErrorMessage] =
useState(''); useState('');
const [key, setKey] = useState(0);
const isRejected = initialValues?.latest_approval?.action === 'REJECTED'; const isRejected = initialValues?.latest_approval?.action === 'REJECTED';
useEffect(() => {
setKey((prev) => prev + 1);
}, [initialValues?.id]);
// ===== UTILITY FUNCTIONS ===== // ===== UTILITY FUNCTIONS =====
const isRepeaterInputError = ( const isRepeaterInputError = (
idx: number, idx: number,
@@ -164,6 +169,7 @@ const PurchaseOrderAcceptApprovalForm = ({
validationSchema: PurchaseRequestAcceptApprovalFormSchema, validationSchema: PurchaseRequestAcceptApprovalFormSchema,
validateOnChange: true, validateOnChange: true,
validateOnBlur: true, validateOnBlur: true,
enableReinitialize: false,
onSubmit: async (values) => { onSubmit: async (values) => {
const payload: CreateAcceptApprovalRequestPayload = { const payload: CreateAcceptApprovalRequestPayload = {
action: 'APPROVED', action: 'APPROVED',
@@ -238,7 +244,12 @@ const PurchaseOrderAcceptApprovalForm = ({
travel_number: item.travel_number || '', travel_number: item.travel_number || '',
travel_document_path: item.travel_document_path || '', travel_document_path: item.travel_document_path || '',
vehicle_number: item.vehicle_number || '', vehicle_number: item.vehicle_number || '',
expedition_vendor: null, expedition_vendor: item.expedition_vendor
? {
value: item.expedition_vendor.id,
label: item.expedition_vendor.name,
}
: null,
expedition_vendor_id: item.expedition_vendor_id || 0, expedition_vendor_id: item.expedition_vendor_id || 0,
received_qty: item.total_qty || '', received_qty: item.total_qty || '',
transport_per_item: item.transport_per_item || '', transport_per_item: item.transport_per_item || '',
@@ -246,7 +257,7 @@ const PurchaseOrderAcceptApprovalForm = ({
}); });
formik.setFieldValue('items', updatedItems); formik.setFieldValue('items', updatedItems);
} }
}, [purchaseItems, initialValues]); }, [purchaseItems, initialValues, key]);
useEffect(() => { useEffect(() => {
if ( if (
@@ -336,7 +347,11 @@ const PurchaseOrderAcceptApprovalForm = ({
}; };
return ( return (
<form onSubmit={formik.handleSubmit} className='w-full flex flex-col gap-6'> <form
key={key}
onSubmit={formik.handleSubmit}
className='w-full flex flex-col gap-6'
>
<div className='w-full'> <div className='w-full'>
<h2 className='text-lg font-semibold mb-4'> <h2 className='text-lg font-semibold mb-4'>
{type === 'add' {type === 'add'
@@ -699,7 +714,9 @@ const PurchaseOrderAcceptApprovalForm = ({
color='warning' color='warning'
className='px-4' className='px-4'
onClick={() => { onClick={() => {
formik.resetForm(); if (type === 'add') {
formik.resetForm();
}
setPurchaseOrderFormErrorMessage(''); setPurchaseOrderFormErrorMessage('');
onCancel?.(); onCancel?.();
onModalClose?.(); onModalClose?.();
@@ -24,6 +24,7 @@ import {
import { ProjectFlockKandang } from '@/types/api/production/project-flock-kandang'; import { ProjectFlockKandang } from '@/types/api/production/project-flock-kandang';
import { isResponseError } from '@/lib/api-helper'; import { isResponseError } from '@/lib/api-helper';
import Pagination from '@/components/Pagination'; import Pagination from '@/components/Pagination';
import { ProductionResultReportApi } from '@/services/api/report/production-result';
const ProductionResultContent = () => { const ProductionResultContent = () => {
const [projectFlockKandangs, setProjectFlockKandangs] = useState< const [projectFlockKandangs, setProjectFlockKandangs] = useState<
@@ -145,8 +146,11 @@ const ProductionResultContent = () => {
const exportToExcelHandler = async () => { const exportToExcelHandler = async () => {
setIsLoadingExportingToExcel(true); setIsLoadingExportingToExcel(true);
// TODO: Implement export functionality in API service first if needed
toast.error('Fitur export belum tersedia'); await ProductionResultReportApi.exportProductionResultToExcel(
projectFlockKandangs
);
setIsLoadingExportingToExcel(false); setIsLoadingExportingToExcel(false);
}; };
@@ -319,7 +323,13 @@ const ProductionResultContent = () => {
align='end' align='end'
direction='bottom' direction='bottom'
trigger={ trigger={
<Button> <Button
disabled={
!selectedArea ||
!selectedLocation ||
!selectedProjectFlock
}
>
Export{' '} Export{' '}
<Icon <Icon
icon='heroicons-outline:download' icon='heroicons-outline:download'
@@ -352,7 +352,7 @@ const ProductionResultProjectFlockKandangTable = ({
productionResults?.data?.length === 0, productionResults?.data?.length === 0,
}), }),
headerColumnClassName: headerColumnClassName:
'px-4 py-3 border-base-content/10 text-base-content/50', 'px-4 py-3 border-x border-base-content/10 text-base-content/50',
}} }}
/> />
</div> </div>
+8 -6
View File
@@ -75,8 +75,13 @@ export const ROUTE_PERMISSIONS: Record<string, string[]> = {
'/expense/realization/edit/': ['lti.expense.update.realization'], '/expense/realization/edit/': ['lti.expense.update.realization'],
// Finance // Finance
'/finance/': ['lti.finance.transaction.list'], '/finance/': ['lti.finance.transactions.list'],
'/finance/detail/': ['lti.finance.transaction.detail'], '/finance/detail/': [
'lti.finance.transactions.detail',
'lti.finance.initial_balances.detail',
'lti.finance.injections.detail',
'lti.finance.payments.detail',
],
'/finance/add/': ['lti.finance.payments.create'], '/finance/add/': ['lti.finance.payments.create'],
'/finance/detail/edit/': ['lti.finance.payments.update'], '/finance/detail/edit/': ['lti.finance.payments.update'],
'/finance/add/initial-balance/': ['lti.finance.initial_balances.create'], '/finance/add/initial-balance/': ['lti.finance.initial_balances.create'],
@@ -94,10 +99,7 @@ export const ROUTE_PERMISSIONS: Record<string, string[]> = {
'/report/logistic-stock/': ['lti.repport.purchasesupplier.list'], '/report/logistic-stock/': ['lti.repport.purchasesupplier.list'],
'/report/expense/': ['lti.repport.expense.list'], '/report/expense/': ['lti.repport.expense.list'],
'/report/marketing/': ['lti.repport.delivery.list'], '/report/marketing/': ['lti.repport.delivery.list'],
'/report/production-result/': ['lti.repport.production_result.list'],
// TODO: change to real permission
// '/report/production-result/': ['lti.repport.production_result.list'],
'/report/production-result/': ['lti.repport.delivery.list'],
// Inventory // Inventory
'/inventory/adjustment/': ['lti.inventory.list'], '/inventory/adjustment/': ['lti.inventory.list'],
+30 -2
View File
@@ -1,4 +1,5 @@
import { BaseApiService } from '@/services/api/base'; import { BaseApiService } from '@/services/api/base';
import { BaseApiResponse } from '@/types/api/api-general';
import { import {
CreateProductWarehousePayload, CreateProductWarehousePayload,
ProductWarehouse, ProductWarehouse,
@@ -20,11 +21,38 @@ export const ProductWarehouseApi = new BaseApiService<
UpdateProductWarehousePayload UpdateProductWarehousePayload
>('/inventory/product-warehouses'); >('/inventory/product-warehouses');
export const MovementApi = new BaseApiService< export class MovementApiService extends BaseApiService<
Movement, Movement,
CreateMovementPayload, CreateMovementPayload,
unknown unknown
>('/inventory/transfers'); > {
constructor(basePath: string) {
super(basePath);
}
async createMovement(
payload: CreateMovementPayload
): Promise<BaseApiResponse<Movement> | undefined> {
const formData = new FormData();
// Append data as JSON string
formData.append('data', JSON.stringify(payload.data));
// Append documents if any
if (payload.documents && payload.documents.length > 0) {
payload.documents.forEach((file) => {
formData.append('documents', file);
});
}
return await this.customRequest<BaseApiResponse<Movement>>('', {
method: 'POST',
payload: formData as unknown as Record<string, unknown>,
});
}
}
export const MovementApi = new MovementApiService('/inventory/transfers');
export const InventoryAdjustmentApi = new BaseApiService< export const InventoryAdjustmentApi = new BaseApiService<
InventoryAdjustment, InventoryAdjustment,
+115 -145
View File
@@ -1,145 +1,12 @@
import { sleep } from '@/lib/helper'; import * as XLSX from 'xlsx';
import toast from 'react-hot-toast';
import { formatDate } from '@/lib/helper';
import { isResponseSuccess } from '@/lib/api-helper';
import { BaseApiService } from '@/services/api/base'; import { BaseApiService } from '@/services/api/base';
import { httpClientFetcher } from '@/services/http/client'; import { httpClient, httpClientFetcher } from '@/services/http/client';
import { BaseApiResponse } from '@/types/api/api-general'; import { BaseApiResponse } from '@/types/api/api-general';
import { ProductionResult } from '@/types/api/report/production-result'; import { ProductionResult } from '@/types/api/report/production-result';
import { BaseProjectFlockKandang } from '@/types/api/production/project-flock-kandang';
// 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< export class ProductionResultReportApiService extends BaseApiService<
ProductionResult, ProductionResult,
@@ -153,14 +20,117 @@ export class ProductionResultReportApiService extends BaseApiService<
async getAllProductionResultFetcher( async getAllProductionResultFetcher(
endpoint: string endpoint: string
): Promise<BaseApiResponse<ProductionResult[]>> { ): Promise<BaseApiResponse<ProductionResult[]>> {
// return await httpClientFetcher<BaseApiResponse<ProductionResult[]>>( return await httpClientFetcher<BaseApiResponse<ProductionResult[]>>(
// endpoint endpoint
// ); );
}
await sleep(1000); async exportProductionResultToExcel(
projectFlockKandangs: BaseProjectFlockKandang[] | null
) {
try {
const mappedProductionResults: {
projectFlockKandang: BaseProjectFlockKandang;
productionResult: ProductionResult[] | null;
}[] = [];
return PRODUCTION_RESULT_DUMMY_DATA; projectFlockKandangs?.forEach(async (projectFlockKandang) => {
const getProductionResultPath = `${this.basePath}/${projectFlockKandang.id}?page=1&limit=99999999`;
const getProductionResultRes = await httpClient<
BaseApiResponse<ProductionResult[]>
>(getProductionResultPath);
mappedProductionResults.push({
projectFlockKandang,
productionResult: isResponseSuccess(getProductionResultRes)
? getProductionResultRes.data
: null,
});
});
const rows = mappedProductionResults;
if (!rows || rows.length === 0) {
toast.error('Tidak ada data untuk diexport.');
return;
}
// Group by Project Flock Kandang Name
const groupedData: Record<
string,
Record<string, string | number | undefined>[]
> = {};
rows.forEach((row) => {
const kandangName = row.projectFlockKandang.kandang.name || 'Unknown';
if (!groupedData[kandangName]) {
groupedData[kandangName] = [];
}
row.productionResult?.forEach((productionResult) => {
groupedData[kandangName].push({
woa: productionResult.woa,
bw: productionResult.bw,
std_bw: productionResult.std_bw,
uniformity: productionResult.uniformity,
std_uniformity: productionResult.std_uniformity,
dep_kum: productionResult.dep_kum,
dep_std: productionResult.dep_std,
butiran_utuh: productionResult.butiran_utuh,
butiran_putih: productionResult.butiran_putih,
butiran_retak: productionResult.butiran_retak,
butiran_pecah: productionResult.butiran_pecah,
butiran_jumlah: productionResult.butiran_jumlah,
total_butir: productionResult.total_butir,
kg_utuh: productionResult.kg_utuh,
kg_putih: productionResult.kg_putih,
kg_retak: productionResult.kg_retak,
kg_pecah: productionResult.kg_pecah,
kg_jumlah: productionResult.kg_jumlah,
total_kg: productionResult.total_kg,
persen_utuh: productionResult.persen_utuh,
persen_putih: productionResult.persen_putih,
persen_retak: productionResult.persen_retak,
persen_pecah: productionResult.persen_pecah,
hd: productionResult.hd,
hd_std: productionResult.hd_std,
fi: productionResult.fi,
fi_std: productionResult.fi_std,
em: productionResult.em,
em_std: productionResult.em_std,
ew: productionResult.ew,
ew_std: productionResult.ew_std,
fcr: productionResult.fcr,
fcr_std: productionResult.fcr_std,
hh: productionResult.hh,
hh_std: productionResult.hh_std,
project_flock_name: productionResult.project_flock.name,
project_flock_category: productionResult.project_flock.category,
kandang_name: productionResult.project_flock.kandang.name,
created_at: formatDate(productionResult.created_at, 'YYYY-MM-DD'),
updated_at: formatDate(productionResult.updated_at, 'YYYY-MM-DD'),
});
});
});
const wb = XLSX.utils.book_new();
Object.keys(groupedData).forEach((sheetName) => {
const ws = XLSX.utils.json_to_sheet(groupedData[sheetName]);
// Sheet names cannot exceed 31 chars
const safeSheetName = sheetName.substring(0, 31);
XLSX.utils.book_append_sheet(wb, ws, safeSheetName);
});
const productionResultExcelFileName = `laporan-hasil-produksi-${formatDate(Date.now(), 'YYYY-MM-DD')}-${rows[0].projectFlockKandang.project_flock.flock_name}.xlsx`;
XLSX.writeFile(wb, productionResultExcelFileName);
} catch (error) {
console.error(error);
toast.error('Gagal melakukan export laporan hasil produksi! Coba lagi.');
}
} }
} }
export const ProductionResultReportApi = new ProductionResultReportApiService(); export const ProductionResultReportApi = new ProductionResultReportApiService(
'/reports/production-result'
);
+22 -14
View File
@@ -112,34 +112,42 @@ export type ClosingProductionData = {
final_population: number; final_population: number;
feed_in: number; feed_in: number;
feed_used: number; feed_used: number;
feed_used_per_head: number;
}; };
sales: { sales: {
chicken: { chicken: {
sales_population: number; sales_population: number;
sales_weight: number; sales_weight: number;
average_weight: number; avg_weight: number;
chicken_average_selling_price: number; avg_selling_price: number;
}; };
egg?: { egg?: {
egg_pieces: number; egg_pieces: number;
egg_mass_kg: number; egg_mass: number;
average_egg_weight_kg: number; avg_egg_weight: number;
egg_average_selling_price: number; avg_selling_price: number;
}; };
}; };
performance: { performance: {
depletion: number; depletion: number;
age_day: number; age_day: number;
mortality_std: number; mor_std: number;
mortality_act: number; mor_act: number;
deff_mortality: number; mor_diff: number;
fcr_std: number; awg_act: number;
awg_std: number;
feed_intake: number;
feed_intake_std: number;
fcr_act: number; fcr_act: number;
deff_fcr: number; fcr_std: number;
awg: number; fcr_diff: number;
hen_day_act?: number;
hen_day_std?: number;
egg_mass?: number;
egg_mass_std?: number;
egg_weight?: number;
egg_weight_std?: number;
hen_housed_act?: number;
hen_housed_std?: number;
}; };
}; };
+15 -1
View File
@@ -14,6 +14,14 @@ type MovementWarehouse = {
}; };
}; };
export type MovementDocument = {
id: number;
path: string;
name: string;
ext: string;
size: number;
};
export type BaseMovement = { export type BaseMovement = {
id: number; id: number;
transfer_reason: string; transfer_reason: string;
@@ -39,6 +47,7 @@ export type BaseMovement = {
document_path: string; document_path: string;
shipping_cost_item: number; shipping_cost_item: number;
shipping_cost_total: number; shipping_cost_total: number;
document?: MovementDocument;
items: { items: {
id: number; id: number;
stock_transfer_detail_id: number; stock_transfer_detail_id: number;
@@ -49,7 +58,7 @@ export type BaseMovement = {
export type Movement = BaseMetadata & BaseMovement; export type Movement = BaseMetadata & BaseMovement;
export type CreateMovementPayload = { export type CreateMovementPayloadData = {
transfer_reason: string; transfer_reason: string;
transfer_date: string; transfer_date: string;
source_warehouse_id: number; source_warehouse_id: number;
@@ -71,3 +80,8 @@ export type CreateMovementPayload = {
}[]; }[];
}[]; }[];
}; };
export type CreateMovementPayload = {
data: CreateMovementPayloadData;
documents?: File[];
};