Merge branch 'development' into feat/FE/daily-checklist

This commit is contained in:
ValdiANS
2026-01-08 10:04:32 +07:00
44 changed files with 966 additions and 552 deletions
+46
View File
@@ -0,0 +1,46 @@
import Alert from '@/components/Alert';
import Button from '@/components/Button';
import { Icon } from '@iconify/react';
/**
* Alert Unique Error List
* @param formErrorList - Array of error messages
* @param onClose - Function to close the alert
*/
const AlertErrorList = ({
formErrorList,
onClose,
}: {
formErrorList: string[];
onClose: () => void;
}) => {
return (
<Alert color='error' className='flex flex-col gap-2 px-4 m-4'>
<div className='flex justify-between items-center gap-2 w-full'>
<div className='flex items-center gap-2'>
<Icon icon='material-symbols:error-outline' width={24} height={24} />
<span className='font-semibold'>
Terdapat {formErrorList.length} error pada form:
</span>
</div>
<Button
onClick={onClose}
variant='link'
className='ml-auto p-0 w-fit text-white'
color='none'
>
<Icon icon='material-symbols:close' width={24} height={24} />
</Button>
</div>
<ul className='list-disc list-inside pl-8 space-y-1 w-full'>
{formErrorList.map((error, index) => (
<li key={index} className='text-sm'>
{error}
</li>
))}
</ul>
</Alert>
);
};
export default AlertErrorList;
+8
View File
@@ -33,6 +33,7 @@ const FileInput = ({
isError, isError,
errorMessage, errorMessage,
disabled = false, disabled = false,
required = false,
onChange, onChange,
onBlur, onBlur,
readOnly = false, readOnly = false,
@@ -56,6 +57,13 @@ const FileInput = ({
)} )}
> >
{label} {label}
{required && (
<>
<span className='tooltip tooltip-error' data-tip='required'>
<span className='text-error'> *</span>
</span>
</>
)}
</label> </label>
)} )}
+1 -1
View File
@@ -309,7 +309,7 @@ const useApprovalSteps = ({
moduleId: string; moduleId: string;
params?: { params?: {
page?: number; page?: number;
limit: number; limit: number | string;
search?: string; search?: string;
group_step_number?: boolean; group_step_number?: boolean;
}; };
@@ -45,7 +45,12 @@ const ClosingDetail: React.FC<ClosingDetailProps> = ({
{ {
id: 'perhitunganSapronak', id: 'perhitunganSapronak',
label: 'Perhitungan Sapronak', label: 'Perhitungan Sapronak',
content: <ClosingSapronakCalculationTabContent projectFlockId={id} />, content: (
<ClosingSapronakCalculationTabContent
closingGeneralInformation={initialValue}
projectFlockId={id}
/>
),
}, },
{ {
id: 'penjualan', id: 'penjualan',
@@ -1,21 +1,25 @@
'use client'; 'use client';
import ClosingIncomingSapronaksTable from '@/components/pages/closing/ClosingIncomingSapronaksTable';
import ClosingOutgoingSapronaksTable from '@/components/pages/closing/ClosingOutgoingSapronaksTable';
import ClosingSapronakCalculationTable from '@/components/pages/closing/ClosingSapronakCalculationTable'; import ClosingSapronakCalculationTable from '@/components/pages/closing/ClosingSapronakCalculationTable';
import { ClosingGeneralInformation } from '@/types/api/closing';
interface ClosingSapronakCalculationTabContentProps { interface ClosingSapronakCalculationTabContentProps {
projectFlockId?: number; projectFlockId?: number;
closingGeneralInformation?: ClosingGeneralInformation;
} }
const ClosingSapronakCalculationTabContent = ({ const ClosingSapronakCalculationTabContent = ({
projectFlockId, projectFlockId,
closingGeneralInformation,
}: ClosingSapronakCalculationTabContentProps) => { }: ClosingSapronakCalculationTabContentProps) => {
return ( return (
<div className='flex flex-col gap-4'> <div className='flex flex-col gap-4'>
{projectFlockId && ( {projectFlockId && (
<> <>
<ClosingSapronakCalculationTable projectFlockId={projectFlockId} /> <ClosingSapronakCalculationTable
closingGeneralInformation={closingGeneralInformation}
projectFlockId={projectFlockId}
/>
</> </>
)} )}
</div> </div>
@@ -13,15 +13,16 @@ import { useMemo } from 'react';
import useSWR from 'swr'; import useSWR from 'swr';
import { ClosingApi } from '@/services/api/closing'; import { ClosingApi } from '@/services/api/closing';
import { isResponseSuccess } from '@/lib/api-helper'; import { isResponseSuccess } from '@/lib/api-helper';
import { ClosingGeneralInformation } from '@/types/api/closing';
interface ClosingSapronakCalculationTableProps { interface ClosingSapronakCalculationTableProps {
type?: 'detail';
projectFlockId: number; projectFlockId: number;
closingGeneralInformation?: ClosingGeneralInformation;
} }
const ClosingSapronakCalculationTable = ({ const ClosingSapronakCalculationTable = ({
type,
projectFlockId, projectFlockId,
closingGeneralInformation,
}: ClosingSapronakCalculationTableProps) => { }: ClosingSapronakCalculationTableProps) => {
const { data: sapronakCalculation, isLoading } = useSWR( const { data: sapronakCalculation, isLoading } = useSWR(
`/closing/sapronak-calculation/${projectFlockId}`, `/closing/sapronak-calculation/${projectFlockId}`,
@@ -182,8 +183,13 @@ const ClosingSapronakCalculationTable = ({
return ( return (
<div className='flex flex-col gap-4'> <div className='flex flex-col gap-4'>
{/* Table DOC jika kategori Project Flock Growing */}
<Card <Card
title='DOC' title={
closingGeneralInformation?.project_category === 'GROWING'
? 'DOC'
: 'Pullet'
}
collapsible collapsible
defaultCollapsed={false} defaultCollapsed={false}
className={{ className={{
@@ -194,10 +200,16 @@ const ClosingSapronakCalculationTable = ({
<Table<RowSapronakCalculation> <Table<RowSapronakCalculation>
data={ data={
isResponseSuccess(sapronakCalculation) isResponseSuccess(sapronakCalculation)
? (sapronakCalculation.data?.doc?.rows ?? []) ? ((closingGeneralInformation?.project_category === 'GROWING'
? sapronakCalculation.data?.doc?.rows
: sapronakCalculation.data?.pullet?.rows) ?? [])
: [] : []
} }
columns={docColumns} columns={
closingGeneralInformation?.project_category === 'GROWING'
? docColumns
: pulletColumns
}
className={{ className={{
containerClassName: 'my-4', containerClassName: 'my-4',
}} }}
@@ -250,29 +262,6 @@ const ClosingSapronakCalculationTable = ({
renderFooter={isResponseSuccess(sapronakCalculation)} renderFooter={isResponseSuccess(sapronakCalculation)}
/> />
</Card> </Card>
<Card
title='Pullet'
variant='bordered'
collapsible
defaultCollapsed={true}
className={{
wrapper: 'w-full',
}}
>
<Table<RowSapronakCalculation>
data={
isResponseSuccess(sapronakCalculation)
? (sapronakCalculation.data?.pullet?.rows ?? [])
: []
}
columns={pulletColumns}
className={{
containerClassName: 'my-4',
}}
renderFooter={isResponseSuccess(sapronakCalculation)}
/>
</Card>
</div> </div>
); );
}; };
@@ -140,17 +140,17 @@ const ExpenseRequestContent = ({
const confirmationModalDeleteClickHandler = async () => { const confirmationModalDeleteClickHandler = async () => {
setIsDeleteLoading(true); setIsDeleteLoading(true);
try { const deleteResponse = await ExpenseApi.delete(initialValues?.id as number);
await ExpenseApi.delete(initialValues?.id as number);
if (isResponseSuccess(deleteResponse)) {
toast.success('Berhasil menghapus data biaya operasional!'); toast.success('Berhasil menghapus data biaya operasional!');
router.push('/expense'); router.push('/expense');
} catch (error) { } else {
toast.error('Gagal menghapus data biaya operasional!'); toast.error('Gagal menghapus data biaya operasional!');
} finally {
deleteModal.closeModal();
setIsDeleteLoading(false);
} }
deleteModal.closeModal();
setIsDeleteLoading(false);
}; };
const confirmationModalCompleteClickHandler = async () => { const confirmationModalCompleteClickHandler = async () => {
@@ -21,7 +21,7 @@ const ExpenseStatusBadge = ({ approval }: ExpenseStatusBadgeProps) => {
switch (latestApprovalStepNumber) { switch (latestApprovalStepNumber) {
case 1: case 1:
expenseStatusPillBadgeColor = 'yellow'; expenseStatusPillBadgeColor = 'gray';
break; break;
case 2: case 2:
@@ -33,7 +33,7 @@ const ExpenseStatusBadge = ({ approval }: ExpenseStatusBadgeProps) => {
break; break;
case 4: case 4:
expenseStatusPillBadgeColor = 'red'; expenseStatusPillBadgeColor = 'yellow';
break; break;
case 5: case 5:
+12 -4
View File
@@ -420,11 +420,19 @@ const ExpensesTable = () => {
const confirmationModalDeleteClickHandler = async () => { const confirmationModalDeleteClickHandler = async () => {
setIsDeleteLoading(true); setIsDeleteLoading(true);
await ExpenseApi.delete(selectedExpense?.id as number); const deleteResponse = await ExpenseApi.delete(
refreshExpenses(); selectedExpense?.id as number
);
if (isResponseSuccess(deleteResponse)) {
refreshExpenses();
deleteModal.closeModal();
toast.success('Berhasil menghapus biaya operasional!');
} else {
deleteModal.closeModal();
toast.error('Gagal menghapus biaya operasional!');
}
deleteModal.closeModal();
toast.success('Berhasil menghapus biaya operasional!');
setIsDeleteLoading(false); setIsDeleteLoading(false);
}; };
@@ -125,6 +125,7 @@ const InventoryAdjustmentForm = ({
const warehouseUrl = `${WarehouseApi.basePath}?${new URLSearchParams({ const warehouseUrl = `${WarehouseApi.basePath}?${new URLSearchParams({
search: '', search: '',
limit: '100',
}).toString()}`; }).toString()}`;
const { data: warehouses, isLoading: isLoadingWarehouses } = useSWR( const { data: warehouses, isLoading: isLoadingWarehouses } = useSWR(
warehouseUrl, warehouseUrl,
@@ -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,31 +1533,58 @@ 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}
/>
<span className='truncate max-w-[200px]'>
{delivery.document.name}
</span>
</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>
</> </>
) : ( ) : (
<FileInput <FileInput
accept='.pdf,.jpg,.jpeg,.png'
name={`deliveries.${idx}.document`} name={`deliveries.${idx}.document`}
onChange={(e) => { onChange={(e) => {
const file = e.target.files?.[0]; const file = e.target.files?.[0];
@@ -682,7 +682,7 @@ const MarketingTable = () => {
<Modal <Modal
ref={productsModal.ref} ref={productsModal.ref}
className={{ className={{
modalBox: 'max-w-2/5 z-100', modalBox: 'xs:max-w-2/5 z-100',
}} }}
closeOnBackdrop closeOnBackdrop
> >
@@ -724,6 +724,7 @@ const MarketingTable = () => {
}, },
]} ]}
className={{ className={{
containerClassName: 'p-6',
tableWrapperClassName: 'overflow-x-auto min-h-full!', 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',
@@ -124,7 +124,10 @@ const MarketingDetail = ({
return ( return (
<> <>
<div className='flex flex-col w-full gap-4'> <div className='flex flex-col w-full gap-4'>
<FormHeader title='Detail Sales Order' backUrl='/marketing' /> <FormHeader
title={`Detail ${Number(initialValues?.latest_approval?.step_number) > 2 ? 'Delivery Order' : 'Sales Order'}`}
backUrl='/marketing'
/>
{!isLoadingApproval && approvals && ( {!isLoadingApproval && approvals && (
<ApprovalSteps approvals={approvals} /> <ApprovalSteps approvals={approvals} />
)} )}
@@ -202,8 +205,23 @@ const MarketingDetail = ({
No. Sales Order No. Sales Order
</td> </td>
<td>:</td> <td>:</td>
<td width='50%'>{initialValues?.so_number}</td> <td width='50%' className='font-mono'>
{initialValues?.so_number}
</td>
</tr> </tr>
{Number(initialValues?.latest_approval?.step_number) > 2 && (
<tr>
<td width='45%' className='font-semibold'>
No. Delivery Order
</td>
<td>:</td>
<td width='50%' className='font-mono'>
{initialValues?.delivery_order
?.map((item) => item.do_number)
.join(', ')}
</td>
</tr>
)}
<tr> <tr>
<td className='font-semibold'>Nama Pelanggan</td> <td className='font-semibold'>Nama Pelanggan</td>
<td>:</td> <td>:</td>
@@ -230,12 +248,27 @@ const MarketingDetail = ({
<td>{initialValues?.notes ?? '-'}</td> <td>{initialValues?.notes ?? '-'}</td>
</tr> </tr>
<tr> <tr>
<td className='font-semibold'>Dokumen</td> <td className='font-semibold'>Dokumen Penjualan</td>
<td>:</td> <td>:</td>
<td> <td>
<SalesOrderExport data={initialValues} /> <SalesOrderExport data={initialValues} />
</td> </td>
</tr> </tr>
{Number(initialValues?.latest_approval?.step_number) > 2 && (
<tr>
<td className='font-semibold'>Dokumen Pengiriman</td>
<td>:</td>
<td className='flex flex-wrap gap-2'>
{initialValues?.delivery_order?.map((item, index) => (
<DeliveryOrderExport
key={index}
data={initialValues}
deliveryOrder={item}
/>
))}
</td>
</tr>
)}
</tbody> </tbody>
</table> </table>
</div> </div>
@@ -48,6 +48,8 @@ import DeliveryOrderProductForm from '@/components/pages/marketing/form/repeater
import { SalesOrderProductFormValues } from '@/components/pages/marketing/form/repeater/sales-order/SalesOrderProduct.schema'; import { SalesOrderProductFormValues } from '@/components/pages/marketing/form/repeater/sales-order/SalesOrderProduct.schema';
import { DeliveryOrderProductFormValues } from '@/components/pages/marketing/form/repeater/delivery-order/DeliverOrderProduct.schema'; import { DeliveryOrderProductFormValues } from '@/components/pages/marketing/form/repeater/delivery-order/DeliverOrderProduct.schema';
import RequirePermission from '@/components/helper/RequirePermission'; import RequirePermission from '@/components/helper/RequirePermission';
import { getUniqueFormikErrors } from '@/lib/formik-helper';
import AlertErrorList from '@/components/helper/form/FormErrors';
const MemoizedSalesOrderProductTable = memo(SalesOrderProductTable); const MemoizedSalesOrderProductTable = memo(SalesOrderProductTable);
const MemoizedSalesOrderProductForm = memo(SalesOrderProductForm); const MemoizedSalesOrderProductForm = memo(SalesOrderProductForm);
@@ -217,6 +219,7 @@ const MarketingForm = ({
const [deliveryFormState, setDeliveryFormState] = useState<'add' | 'edit'>( const [deliveryFormState, setDeliveryFormState] = useState<'add' | 'edit'>(
'add' 'add'
); );
const [formErrorList, setFormErrorList] = useState<string[]>([]);
const [deliveryOrderValues, setDeliveryOrderValues] = useState< const [deliveryOrderValues, setDeliveryOrderValues] = useState<
DeliveryOrderProductFormValues[] DeliveryOrderProductFormValues[]
>( >(
@@ -558,11 +561,28 @@ const MarketingForm = ({
); );
}, [memoSalesOrder]); }, [memoSalesOrder]);
const handleValidateForm = async () => {
const errors = await formik.validateForm();
if (Object.keys(errors).length > 0) {
// Parse and display errors
const errorMessages = getUniqueFormikErrors(errors);
setFormErrorList(errorMessages);
return; // Stop submission
}
};
const handleFormSubmit = (e: React.FormEvent<HTMLFormElement>) => {
e.preventDefault();
handleValidateForm();
formik.handleSubmit();
};
return ( return (
<> <>
<form <form
className='flex flex-col gap-4' className='flex flex-col gap-4'
onSubmit={formik.handleSubmit} onSubmit={handleFormSubmit}
onReset={formik.handleReset} onReset={formik.handleReset}
> >
<FormHeader <FormHeader
@@ -666,6 +686,14 @@ const MarketingForm = ({
</div> </div>
</div> </div>
{/* Error List Alert */}
{formErrorList.length > 0 && (
<AlertErrorList
formErrorList={formErrorList}
onClose={() => setFormErrorList([])}
/>
)}
{/* Form Actions */} {/* Form Actions */}
<div className='flex flex-row items-start justify-center gap-2 mt-4'> <div className='flex flex-row items-start justify-center gap-2 mt-4'>
<Button type='reset' color='warning' disabled={formik.isSubmitting}> <Button type='reset' color='warning' disabled={formik.isSubmitting}>
@@ -673,7 +701,7 @@ const MarketingForm = ({
</Button> </Button>
<Button <Button
type='submit' type='submit'
disabled={!formik.isValid || formik.isSubmitting} disabled={formik.isSubmitting}
isLoading={formik.isSubmitting} isLoading={formik.isSubmitting}
> >
Submit Submit
@@ -15,6 +15,9 @@ import { BaseSalesOrder } from '@/types/api/marketing/marketing';
import Badge from '@/components/Badge'; import Badge from '@/components/Badge';
import { SalesProductToFieldValues } from '@/components/pages/marketing/form/MarketingForm'; import { SalesProductToFieldValues } from '@/components/pages/marketing/form/MarketingForm';
import * as Yup from 'yup'; import * as Yup from 'yup';
import { isResponseSuccess } from '@/lib/api-helper';
import { getUniqueFormikErrors } from '@/lib/formik-helper';
import AlertErrorList from '@/components/helper/form/FormErrors';
const DeliveryOrderProductForm = ({ const DeliveryOrderProductForm = ({
formState, formState,
@@ -39,6 +42,8 @@ const DeliveryOrderProductForm = ({
null null
); );
const [currentInput, setCurrentInput] = useState<string>(''); const [currentInput, setCurrentInput] = useState<string>('');
const [formErrorList, setFormErrorList] = useState<string[]>([]);
const salesOrder = salesOrders.find( const salesOrder = salesOrders.find(
(item) => item.id === initialValues?.marketing_product_id (item) => item.id === initialValues?.marketing_product_id
); );
@@ -163,15 +168,27 @@ const DeliveryOrderProductForm = ({
} }
}, [initialValues]); }, [initialValues]);
const handleValidateForm = () => {
formik.validateForm();
const formErrorList = getUniqueFormikErrors(formik.errors);
setFormErrorList(formErrorList);
if (formErrorList.length > 0) {
return;
}
};
const handleFormSubmit = (e: React.FormEvent<HTMLFormElement>) => {
e.preventDefault();
handleBlurField(currentInput);
handleValidateForm();
formik.handleSubmit(e);
};
return ( return (
<> <>
<form <form
className='size-full' className='size-full'
onSubmit={(e) => { onSubmit={handleFormSubmit}
e.preventDefault();
handleBlurField(currentInput);
formik.handleSubmit(e);
}}
onReset={handleResetForm} onReset={handleResetForm}
> >
{formikErrorMessage && ( {formikErrorMessage && (
@@ -208,7 +225,7 @@ const DeliveryOrderProductForm = ({
...formik.values, ...formik.values,
marketing_product_id: undefined, marketing_product_id: undefined,
marketing_product: null, marketing_product: null,
qty: formik.values.qty || '', qty: '',
unit_price: '', unit_price: '',
total_price: '', total_price: '',
avg_weight: '', avg_weight: '',
@@ -222,7 +239,7 @@ const DeliveryOrderProductForm = ({
...formik.values, ...formik.values,
marketing_product_id: selected.value as number, marketing_product_id: selected.value as number,
marketing_product: SalesProductToFieldValues(so), marketing_product: SalesProductToFieldValues(so),
qty: formik.values.qty || so.qty, qty: so.qty,
unit_price: so.unit_price, unit_price: so.unit_price,
total_price: so.total_price, total_price: so.total_price,
avg_weight: so.avg_weight, avg_weight: so.avg_weight,
@@ -298,8 +315,18 @@ const DeliveryOrderProductForm = ({
isError={Boolean(formik.errors.qty)} isError={Boolean(formik.errors.qty)}
errorMessage={formik.errors.qty} errorMessage={formik.errors.qty}
placeholder='Masukan Kuantitas' placeholder='Masukan Kuantitas'
bottomLabel={
formik.values.marketing_product_id
? 'Stok dijual: ' +
salesOrders?.find(
(item) => item.id === formik.values.marketing_product_id
)?.qty
: ''
}
/> />
</div>
<div className='divider my-6'></div>
<div className='grid sm:grid-cols-2 gap-4'>
<NumberInput <NumberInput
required required
label='Avg. Bobot (Kg)' label='Avg. Bobot (Kg)'
@@ -361,6 +388,13 @@ const DeliveryOrderProductForm = ({
/> />
</div> </div>
{formErrorList.length > 0 && (
<AlertErrorList
formErrorList={formErrorList}
onClose={() => setFormErrorList([])}
/>
)}
<div className='flex flex-row justify-end gap-3 mt-4'> <div className='flex flex-row justify-end gap-3 mt-4'>
<Button type='reset' color='warning'> <Button type='reset' color='warning'>
Reset Reset
@@ -368,7 +402,7 @@ const DeliveryOrderProductForm = ({
<Button <Button
type='submit' type='submit'
isLoading={formik.isSubmitting} isLoading={formik.isSubmitting}
disabled={!formik.isValid || formik.isSubmitting} disabled={formik.isSubmitting}
> >
Submit Submit
</Button> </Button>
@@ -25,15 +25,19 @@ export const SalesOrderProductSchema: Yup.ObjectSchema<SalesOrderProductSchemaTy
id: Yup.number(), id: Yup.number(),
vehicle_number: Yup.string().required('Nomor Kendaraan wajib diisi!'), vehicle_number: Yup.string().required('Nomor Kendaraan wajib diisi!'),
kandang: Yup.object({ kandang: Yup.object({
value: Yup.number().min(1).required(), value: Yup.number()
label: Yup.string().required(), .min(1, 'Kandang wajib diisi!')
.required('Kandang wajib diisi!'),
label: Yup.string().required('Kandang wajib diisi!'),
}).nullable(), }).nullable(),
kandang_id: Yup.number() kandang_id: Yup.number()
.min(1, 'Kandang wajib diisi!') .min(1, 'Kandang wajib diisi!')
.required('Kandang wajib diisi!'), .required('Kandang wajib diisi!'),
product_warehouse: Yup.object({ product_warehouse: Yup.object({
value: Yup.number().min(1).required(), value: Yup.number()
label: Yup.string().required(), .min(1, 'Produk wajib diisi!')
.required('Produk wajib diisi!'),
label: Yup.string().required('Produk wajib diisi!'),
}).nullable(), }).nullable(),
product_warehouse_id: Yup.number() product_warehouse_id: Yup.number()
.min(1, 'Produk wajib diisi!') .min(1, 'Produk wajib diisi!')
@@ -17,9 +17,15 @@ import { ProductWarehouseApi } from '@/services/api/inventory';
import NumberInput from '@/components/input/NumberInput'; import NumberInput from '@/components/input/NumberInput';
import Button from '@/components/Button'; import Button from '@/components/Button';
import { isResponseSuccess } from '@/lib/api-helper'; import { isResponseSuccess } from '@/lib/api-helper';
import { formatVechicleNumber } from '@/lib/helper'; import {
formatCurrency,
formatNumber,
formatVechicleNumber,
} from '@/lib/helper';
import PatternInput from '@/components/input/PatternInput'; import PatternInput from '@/components/input/PatternInput';
import Alert from '@/components/Alert'; import Alert from '@/components/Alert';
import { getUniqueFormikErrors } from '@/lib/formik-helper';
import AlertErrorList from '@/components/helper/form/FormErrors';
const SalesOrderProductForm = ({ const SalesOrderProductForm = ({
initialValues, initialValues,
@@ -33,7 +39,9 @@ const SalesOrderProductForm = ({
}) => { }) => {
const [formErrorMessage, setFormErrorMessage] = useState(''); const [formErrorMessage, setFormErrorMessage] = useState('');
const [currentInput, setCurrentInput] = useState<string>(''); const [currentInput, setCurrentInput] = useState<string>('');
const [formErrorList, setFormErrorList] = useState<string[]>([]);
// ============ Formik ============
const formik = useFormik<SalesOrderProductFormValues>({ const formik = useFormik<SalesOrderProductFormValues>({
enableReinitialize: true, enableReinitialize: true,
initialValues: { initialValues: {
@@ -58,6 +66,7 @@ const SalesOrderProductForm = ({
isInitialValid: false, isInitialValid: false,
}); });
// ===== Options =====
const { const {
options: kandangSourceOptions, options: kandangSourceOptions,
isLoadingOptions: isLoadingKandangSourceOptions, isLoadingOptions: isLoadingKandangSourceOptions,
@@ -86,12 +95,13 @@ const SalesOrderProductForm = ({
); );
}, [warehouseSourceOptions, exisitingValues]); }, [warehouseSourceOptions, exisitingValues]);
// ===== Handler =====
const kandangChangeHandler = (val: OptionType | OptionType[] | null) => { const kandangChangeHandler = (val: OptionType | OptionType[] | null) => {
formik.setFieldValue('kandang', val as OptionType); formik.setFieldValue('kandang', val as OptionType);
formik.setFieldValue('kandang_id', (val as OptionType)?.value); formik.setFieldValue('kandang_id', (val as OptionType)?.value);
formik.setFieldValue('product_warehouse_id', null); formik.setFieldValue('product_warehouse_id', null);
formik.setFieldValue('product_warehouse', null); formik.setFieldValue('product_warehouse', null);
formik.setFieldValue('qty', null); formik.setFieldValue('qty', '');
}; };
const warehouseChangeHandler = (val: OptionType | OptionType[] | null) => { const warehouseChangeHandler = (val: OptionType | OptionType[] | null) => {
@@ -106,7 +116,7 @@ const SalesOrderProductForm = ({
formik.setFieldValue('qty', productWarehouse?.quantity); formik.setFieldValue('qty', productWarehouse?.quantity);
handleBlurField('qty'); handleBlurField('qty');
} else { } else {
formik.setFieldValue('qty', null); formik.setFieldValue('qty', '');
} }
}; };
@@ -162,15 +172,29 @@ const SalesOrderProductForm = ({
} }
}; };
const handleValidateForm = async () => {
const errors = await formik.validateForm();
if (Object.keys(errors).length > 0) {
// Parse and display errors
const errorMessages = getUniqueFormikErrors(errors);
setFormErrorList(errorMessages);
return; // Stop submission
}
};
const handleFormSubmit = (e: React.FormEvent<HTMLFormElement>) => {
e.preventDefault();
handleBlurField(currentInput);
handleValidateForm();
formik.handleSubmit(e);
};
return ( return (
<> <>
<form <form
className='size-full' className='size-full'
onSubmit={(e) => { onSubmit={handleFormSubmit}
e.preventDefault();
handleBlurField(currentInput);
formik.handleSubmit(e);
}}
onReset={handleResetForm} onReset={handleResetForm}
> >
{formErrorMessage && ( {formErrorMessage && (
@@ -248,7 +272,24 @@ const SalesOrderProductForm = ({
isError={formik.touched.qty && Boolean(formik.errors.qty)} isError={formik.touched.qty && Boolean(formik.errors.qty)}
errorMessage={formik.errors.qty} errorMessage={formik.errors.qty}
placeholder='Masukan Kuantitas' placeholder='Masukan Kuantitas'
bottomLabel={
isResponseSuccess(warehouseSourceRawData) &&
formik.values.product_warehouse_id
? `Stok tersedia: ${formatNumber(
warehouseSourceRawData?.data?.find(
(item) => item.id === formik.values.product_warehouse_id
)?.quantity ?? 0
)} ${
warehouseSourceRawData?.data?.find(
(item) => item.id === formik.values.product_warehouse_id
)?.product?.uom?.name ?? ''
}`
: ''
}
/> />
</div>
<div className='divider my-6'></div>
<div className='grid sm:grid-cols-2 gap-4 z-200'>
<NumberInput <NumberInput
required required
label='Avg. Bobot (Kg)' label='Avg. Bobot (Kg)'
@@ -314,6 +355,15 @@ const SalesOrderProductForm = ({
placeholder='Masukan Total Penjualan' placeholder='Masukan Total Penjualan'
/> />
</div> </div>
{/* Error List Alert */}
{formErrorList.length > 0 && (
<AlertErrorList
formErrorList={formErrorList}
onClose={() => setFormErrorList([])}
/>
)}
<div className='flex flex-row justify-end gap-3 mt-4'> <div className='flex flex-row justify-end gap-3 mt-4'>
<Button type='reset' color='warning' onClick={handleResetForm}> <Button type='reset' color='warning' onClick={handleResetForm}>
Reset Reset
@@ -321,7 +371,7 @@ const SalesOrderProductForm = ({
<Button <Button
type='submit' type='submit'
isLoading={formik.isSubmitting} isLoading={formik.isSubmitting}
disabled={!formik.isValid || formik.isSubmitting} disabled={formik.isSubmitting}
> >
Submit Submit
</Button> </Button>
@@ -18,6 +18,7 @@ import { Icon } from '@iconify/react';
import Badge from '@/components/Badge'; import Badge from '@/components/Badge';
import { CHICKINS_APPROVAL_LINE } from '@/config/approval-line'; import { CHICKINS_APPROVAL_LINE } from '@/config/approval-line';
import RequirePermission from '@/components/helper/RequirePermission'; import RequirePermission from '@/components/helper/RequirePermission';
import { BaseApproval } from '@/types/api/api-general';
const ChickinFormKandang = ({ const ChickinFormKandang = ({
formType = 'add', formType = 'add',
initialValues, initialValues,
@@ -33,11 +34,16 @@ const ChickinFormKandang = ({
approvals, approvals,
isLoading: approvalsLoading, isLoading: approvalsLoading,
refresh: refreshApprovals, refresh: refreshApprovals,
rawDataApprovals,
} = useApprovalSteps({ } = useApprovalSteps({
latestApproval: initialValues?.approval, latestApproval: initialValues?.chickin_approval,
approvalLines: CHICKINS_APPROVAL_LINE, approvalLines: CHICKINS_APPROVAL_LINE,
moduleName: 'CHICKINS', moduleName: 'CHICKINS',
moduleId: initialValues?.id.toString() ?? '', moduleId: initialValues?.id.toString() ?? '',
params: {
limit: 'limit',
group_step_number: false,
},
}); });
const afterSubmitFormChickin = () => { const afterSubmitFormChickin = () => {
@@ -180,6 +186,7 @@ const ChickinFormKandang = ({
</div> </div>
{openChickin && ( {openChickin && (
<ChickinLogsView <ChickinLogsView
rawDataApprovals={rawDataApprovals as BaseApproval[]}
initialValues={initialValues} initialValues={initialValues}
afterSubmit={afterSubmitFormChickin} afterSubmit={afterSubmitFormChickin}
/> />
@@ -8,6 +8,7 @@ import PillBadge from '@/components/PillBadge';
import { isResponseError, isResponseSuccess } from '@/lib/api-helper'; import { isResponseError, isResponseSuccess } from '@/lib/api-helper';
import { formatDate, formatNumber } from '@/lib/helper'; import { formatDate, formatNumber } from '@/lib/helper';
import { ChickinApi } from '@/services/api/production/chickin'; import { ChickinApi } from '@/services/api/production/chickin';
import { BaseApproval } from '@/types/api/api-general';
import { ProjectFlockKandang } from '@/types/api/production/project-flock-kandang'; import { ProjectFlockKandang } from '@/types/api/production/project-flock-kandang';
import { Icon } from '@iconify/react'; import { Icon } from '@iconify/react';
import { useState } from 'react'; import { useState } from 'react';
@@ -16,9 +17,11 @@ import toast from 'react-hot-toast';
const ChickinLogsView = ({ const ChickinLogsView = ({
initialValues, initialValues,
afterSubmit, afterSubmit,
rawDataApprovals,
}: { }: {
initialValues: ProjectFlockKandang; initialValues: ProjectFlockKandang;
afterSubmit?: () => void; afterSubmit?: () => void;
rawDataApprovals: BaseApproval[];
}) => { }) => {
const confirmModal = useModal(); const confirmModal = useModal();
const [isApproveLoading, setIsApproveLoading] = useState(false); const [isApproveLoading, setIsApproveLoading] = useState(false);
@@ -60,8 +63,15 @@ const ChickinLogsView = ({
</div> </div>
) : ( ) : (
(initialValues?.chickins || []).map((chickin, index) => { (initialValues?.chickins || []).map((chickin, index) => {
const isApproved = chickin.usage_qty !== 0; const latestApproval = rawDataApprovals[0];
const isPending = chickin.pending_usage_qty !== 0; const isApproved =
index == (initialValues?.chickins || []).length - 1
? latestApproval?.step_number === 2
: true;
const isPending =
index == (initialValues?.chickins || []).length - 1
? latestApproval?.step_number === 1
: false;
const quantity = isApproved const quantity = isApproved
? chickin.usage_qty ? chickin.usage_qty
: isPending : isPending
@@ -81,7 +91,7 @@ const ChickinLogsView = ({
{/* Header with Status Badge */} {/* Header with Status Badge */}
<div className='flex flex-row justify-between items-center'> <div className='flex flex-row justify-between items-center'>
<div className='text-lg font-semibold'> <div className='text-lg font-semibold'>
Chick In #{index + 1} Chick In #{index + 1} - {latestApproval?.step_number}
</div> </div>
<PillBadge <PillBadge
content={ content={
@@ -146,18 +156,19 @@ const ChickinLogsView = ({
}) })
)} )}
{initialValues?.approval?.step_number <= 2 && ( {initialValues.chickin_approval &&
<RequirePermission permissions='lti.production.chickins.approve'> initialValues?.chickin_approval?.step_number < 2 && (
<Button <RequirePermission permissions='lti.production.chickins.approve'>
color='success' <Button
onClick={handleClickApprove} color='success'
className='w-full' onClick={handleClickApprove}
> className='w-full'
<Icon width={24} height={24} icon='material-symbols:check' /> >
Approve Semua Chick In <Icon width={24} height={24} icon='material-symbols:check' />
</Button> Approve Semua Chick In
</RequirePermission> </Button>
)} </RequirePermission>
)}
{chickinErrorMessage && ( {chickinErrorMessage && (
<div className='w-full' onClick={() => setChickinErrorMessage('')}> <div className='w-full' onClick={() => setChickinErrorMessage('')}>
@@ -308,7 +308,10 @@ const ProjectFlockTable = ({ refresh }: { refresh?: () => void }) => {
<Button <Button
color='primary' color='primary'
className='w-full sm:w-fit' className='w-full sm:w-fit'
href='/production/project-flock/add' onClick={() => {
setRowSelection({});
router.push('/production/project-flock/add');
}}
> >
<Icon icon='ic:round-plus' width={24} height={24} /> <Icon icon='ic:round-plus' width={24} height={24} />
Tambah Tambah
@@ -64,9 +64,9 @@ export const ProjectFlockBudgetsSchema: Yup.ObjectSchema<ProjectFlockBudgetsSche
.min(1, 'Harga minimal 1!') .min(1, 'Harga minimal 1!')
.required('Harga wajib diisi!'), .required('Harga wajib diisi!'),
total_price: Yup.number() total_price: Yup.number()
.typeError('Harga harus berupa angka!') .typeError('Total Harga harus berupa angka!')
.min(1, 'Harga minimal 1!') .min(1, 'Total Harga minimal 1!')
.required('Harga wajib diisi!'), .required('Total Harga wajib diisi!'),
}); });
export const ProjectFlockFormSchema: Yup.ObjectSchema<ProjectFlockFormSchemaType> = export const ProjectFlockFormSchema: Yup.ObjectSchema<ProjectFlockFormSchemaType> =
@@ -6,6 +6,8 @@ import SelectInput, {
useSelect, useSelect,
} from '@/components/input/SelectInput'; } from '@/components/input/SelectInput';
import { isResponseError, isResponseSuccess } from '@/lib/api-helper'; import { isResponseError, isResponseSuccess } from '@/lib/api-helper';
import { getUniqueFormikErrors } from '@/lib/formik-helper';
import AlertErrorList from '@/components/helper/form/FormErrors';
import { import {
AreaApi, AreaApi,
FcrApi, FcrApi,
@@ -38,11 +40,6 @@ import { BaseApiResponse } from '@/types/api/api-general';
import { FLOCK_CATEGORY_OPTIONS } from '@/config/constant'; import { FLOCK_CATEGORY_OPTIONS } from '@/config/constant';
import { useModal } from '@/components/Modal'; import { useModal } from '@/components/Modal';
import ConfirmationModal from '@/components/modal/ConfirmationModal'; import ConfirmationModal from '@/components/modal/ConfirmationModal';
import ApprovalSteps, {
useApprovalSteps,
} from '@/components/pages/ApprovalSteps';
import { PROJECT_FLOCK_APPROVAL_LINE } from '@/config/approval-line';
import ConfirmationModalWithNotes from '@/components/modal/ConfirmationModalWithNotes';
import NumberInput from '@/components/input/NumberInput'; import NumberInput from '@/components/input/NumberInput';
import Card from '@/components/Card'; import Card from '@/components/Card';
import ProjectFlockKandangTable from '@/components/pages/production/project-flock/form/ProjectFlockKandangTable'; import ProjectFlockKandangTable from '@/components/pages/production/project-flock/form/ProjectFlockKandangTable';
@@ -69,8 +66,10 @@ const ProjectFlockForm = ({
const [projectFlockFormErrorMessage, setProjectFlockFormErrorMessage] = const [projectFlockFormErrorMessage, setProjectFlockFormErrorMessage] =
useState(''); useState('');
const [formErrorList, setFormErrorList] = useState<string[]>([]);
const [selectedArea, setSelectedArea] = useState(''); const [selectedArea, setSelectedArea] = useState('');
const [selectedLocation, setSelectedLocation] = useState(''); const [selectedLocation, setSelectedLocation] = useState('');
const [selectedCategory, setSelectedCategory] = useState('');
const [disabledLocation, setDisabledLocation] = useState( const [disabledLocation, setDisabledLocation] = useState(
initialValues?.location?.id ? false : true initialValues?.location?.id ? false : true
); );
@@ -90,18 +89,8 @@ const ProjectFlockForm = ({
const setIsValid = useUiStore((s) => s.setIsValid); const setIsValid = useUiStore((s) => s.setIsValid);
const deleteModal = useModal(); const deleteModal = useModal();
const confirmModal = useModal();
const [isDeleteLoading, setIsDeleteLoading] = useState(false); const [isDeleteLoading, setIsDeleteLoading] = useState(false);
const [isApproveLoading, setIsApproveLoading] = useState(false);
const [isApprovedDisabled, setIsApprovedDisabled] = useState(
initialValues?.approval?.step_name == 'Pengajuan' ? false : true
);
const [isRejectedDisabled, setIsRejectedDisabled] =
useState(!isApprovedDisabled);
const [approvalAction, setApprovalAction] = useState<'APPROVED' | 'REJECTED'>(
!isApprovedDisabled ? 'APPROVED' : 'REJECTED'
);
const [rowSelection, setRowSelection] = useState<Record<string, boolean>>( const [rowSelection, setRowSelection] = useState<Record<string, boolean>>(
() => () =>
@@ -140,11 +129,15 @@ const ProjectFlockForm = ({
const { const {
options: optionsProductionStandards, options: optionsProductionStandards,
isLoadingOptions: isLoadingProductionStandards, isLoadingOptions: isLoadingProductionStandards,
} = useSelect(ProductionStandardApi.basePath, 'id', 'name'); } = useSelect(ProductionStandardApi.basePath, 'id', 'name', '', {
search: '',
project_category: selectedCategory,
});
const kandangUrl = `${KandangApi.basePath}?${new URLSearchParams({ const kandangUrl = `${KandangApi.basePath}?${new URLSearchParams({
search: '', search: '',
location_id: selectedLocation == '' ? '0' : selectedLocation, location_id: selectedLocation == '' ? '0' : selectedLocation,
limit: 'limit',
}).toString()}`; }).toString()}`;
const { const {
data: kandang, data: kandang,
@@ -163,17 +156,6 @@ const ProjectFlockForm = ({
isLoadingOptions: isLoadingNonstocks, isLoadingOptions: isLoadingNonstocks,
} = useSelect<Nonstock>(NonstockApi.basePath, 'id', 'name'); } = useSelect<Nonstock>(NonstockApi.basePath, 'id', 'name');
const {
approvals,
isLoading: approvalsLoading,
refresh: refreshApprovals,
} = useApprovalSteps({
latestApproval: initialValues?.approval,
approvalLines: PROJECT_FLOCK_APPROVAL_LINE,
moduleName: 'PROJECT_FLOCKS',
moduleId: initialValues?.id.toString() ?? '',
});
useEffect(() => { useEffect(() => {
if (isResponseSuccess(kandang)) { if (isResponseSuccess(kandang)) {
if (selectedLocation) { if (selectedLocation) {
@@ -263,9 +245,19 @@ const ProjectFlockForm = ({
}; };
const categoryChangeHandler = (val: OptionType | OptionType[] | null) => { const categoryChangeHandler = (val: OptionType | OptionType[] | null) => {
formik.setFieldValue('category', (val as OptionType)?.value); // Reset production standard when category is changed
formik.setFieldValue('production_standard_id', '');
formik.setFieldValue('production_standard', '');
formik.setFieldValue('category_option', val); formik.setFieldValue('category_option', val);
if (val == null) { formik.setFieldValue('category', val ? (val as OptionType)?.value : '');
setSelectedCategory((val as OptionType)?.value as string);
if (Boolean(val)) {
formik.setFieldTouched('category', false);
formik.setFieldError('category', '');
} else {
formik.setFieldTouched('category', true); formik.setFieldTouched('category', true);
} }
}; };
@@ -404,8 +396,6 @@ const ProjectFlockForm = ({
validationSchema: validationSchema:
formType == 'add' ? ProjectFlockFormSchema : UpdateProjectFlockFormSchema, formType == 'add' ? ProjectFlockFormSchema : UpdateProjectFlockFormSchema,
validateOnBlur: true, validateOnBlur: true,
// validateOnChange: true,
// validateOnMount: true,
onSubmit: async (values) => { onSubmit: async (values) => {
setProjectFlockFormErrorMessage(''); setProjectFlockFormErrorMessage('');
const payload: CreateProjectFlockPayload = { const payload: CreateProjectFlockPayload = {
@@ -522,19 +512,6 @@ const ProjectFlockForm = ({
return unsub; return unsub;
}, []); }, []);
useEffect(() => {
if (initialValues?.approval?.step_name) {
const pengajuanRejected =
initialValues.approval.step_number == 1 &&
initialValues.approval.action == 'REJECTED';
const approvedDisabled =
initialValues.approval.step_number !== 1 || pengajuanRejected;
setIsApprovedDisabled(approvedDisabled);
setIsRejectedDisabled(!approvedDisabled || pengajuanRejected);
setApprovalAction(!approvedDisabled ? 'APPROVED' : 'REJECTED');
}
}, [initialValues]);
// Actions handler // Actions handler
const confirmationModalDeleteClickHandler = async () => { const confirmationModalDeleteClickHandler = async () => {
setIsDeleteLoading(true); setIsDeleteLoading(true);
@@ -588,29 +565,6 @@ const ProjectFlockForm = ({
} }
}; };
const confirmApprovalHandler = async (
notes: string,
approvalAction: 'REJECTED' | 'APPROVED'
) => {
if (initialValues?.id === undefined) return;
setIsApproveLoading(true);
const approvalRes =
approvalAction == 'APPROVED'
? await ProjectFlockApi.approve(initialValues?.id, notes)
: await ProjectFlockApi.reject(initialValues?.id, notes);
if (isResponseSuccess(approvalRes)) {
refreshProjectFlocks?.();
toast.success(approvalRes.message as string);
}
if (isResponseError(approvalRes)) {
toast.error(approvalRes?.message as string);
}
refreshApprovals();
confirmModal.closeModal();
setIsApproveLoading(false);
};
const handleBudgetChange = ( const handleBudgetChange = (
index: number, index: number,
fieldName: 'qty' | 'price' | 'total_price', fieldName: 'qty' | 'price' | 'total_price',
@@ -688,6 +642,17 @@ const ProjectFlockForm = ({
return !isNonstockAlreadyInBudgets; return !isNonstockAlreadyInBudgets;
}); });
const handleValidateForm = async () => {
const errors = await formik.validateForm();
if (Object.keys(errors).length > 0) {
// Parse and display errors
const errorMessages = getUniqueFormikErrors(errors);
setFormErrorList(errorMessages);
return; // Stop submission
}
};
return ( return (
<> <>
<section className='w-full'> <section className='w-full'>
@@ -744,50 +709,14 @@ const ProjectFlockForm = ({
</div> </div>
</div> </div>
)} )}
{approvals && !approvalsLoading && formType == 'detail' && (
<ApprovalSteps approvals={approvals} />
)}
{formType == 'detail' && (
<div className='w-full flex flex-col sm:flex-row gap-2 py-4'>
<RequirePermission permissions='lti.production.project_flocks.approve'>
<Button
variant='outline'
color='success'
onClick={() => {
if (initialValues?.id) {
setApprovalAction('APPROVED');
confirmModal.openModal();
}
}}
disabled={!initialValues?.id || isApprovedDisabled}
className='w-full sm:w-fit'
>
<Icon icon='material-symbols:check' width={24} height={24} />
Approve
</Button>
</RequirePermission>
<RequirePermission permissions='lti.production.project_flocks.approve'>
<Button
variant='outline'
color='error'
onClick={() => {
if (initialValues?.id) {
setApprovalAction('REJECTED');
confirmModal.openModal();
}
}}
disabled={!initialValues?.id || isRejectedDisabled}
className='w-full sm:w-fit'
>
<Icon icon='mdi:times' width={24} height={24} />
Reject
</Button>
</RequirePermission>
</div>
)}
<form <form
className='w-auto h-auto' className='w-auto h-auto'
onSubmit={formik.handleSubmit} onSubmit={(e) => {
e.preventDefault();
handleValidateForm();
formik.handleSubmit(e);
}}
onReset={formik.handleReset} onReset={formik.handleReset}
> >
{/* Form Informasi Umum */} {/* Form Informasi Umum */}
@@ -872,23 +801,6 @@ const ProjectFlockForm = ({
isClearable isClearable
isDisabled={formType != 'add'} isDisabled={formType != 'add'}
/> />
<SelectInput
required
label='Standar Produksi'
value={formik.values.production_standard as OptionType}
onChange={(val) => {
optionChangeHandler(val, 'production_standard');
}}
options={optionsProductionStandards}
isLoading={isLoadingProductionStandards}
isError={
formik.touched.production_standard &&
Boolean(formik.errors.production_standard)
}
errorMessage={formik.errors.production_standard as string}
isClearable
isDisabled={formType != 'add'}
/>
<SelectInput <SelectInput
required required
label='Kategori' label='Kategori'
@@ -902,6 +814,23 @@ const ProjectFlockForm = ({
isClearable isClearable
isDisabled={formType != 'add'} isDisabled={formType != 'add'}
/> />
<SelectInput
required
label='Standar Produksi'
value={formik.values.production_standard as OptionType}
onChange={(val) => {
optionChangeHandler(val, 'production_standard');
}}
options={optionsProductionStandards}
isLoading={isLoadingProductionStandards}
isError={
formik.touched.production_standard_id &&
Boolean(formik.errors.production_standard_id)
}
errorMessage={formik.errors.production_standard_id as string}
isClearable
isDisabled={formType != 'add'}
/>
<NumberInput <NumberInput
name='period' name='period'
label='Periode' label='Periode'
@@ -1153,15 +1082,15 @@ const ProjectFlockForm = ({
</div> </div>
</div> </div>
{/* Error List Alert */}
{formErrorList.length > 0 && (
<AlertErrorList
formErrorList={formErrorList}
onClose={() => setFormErrorList([])}
/>
)}
<div className='flex flex-row justify-center gap-2 flex-wrap my-6 px-4'> <div className='flex flex-row justify-center gap-2 flex-wrap my-6 px-4'>
{/* <div className='w-120'>
<div className='text-primary text-sm'>
{JSON.stringify(formik.values)}
</div>
<div className='text-error text-sm'>
{JSON.stringify(formik.errors)}
</div>
</div> */}
{formType !== 'detail' && ( {formType !== 'detail' && (
<RequirePermission <RequirePermission
permissions={ permissions={
@@ -1174,7 +1103,7 @@ const ProjectFlockForm = ({
type='submit' type='submit'
color='primary' color='primary'
isLoading={formik.isSubmitting} isLoading={formik.isSubmitting}
disabled={!formik.isValid || formik.isSubmitting} disabled={formik.isSubmitting}
className='px-4 w-full' className='px-4 w-full'
> >
<Icon icon='mdi:plus' width={24} height={24} /> <Icon icon='mdi:plus' width={24} height={24} />
@@ -1200,27 +1129,6 @@ const ProjectFlockForm = ({
onClick: confirmationModalDeleteClickHandler, onClick: confirmationModalDeleteClickHandler,
}} }}
/> />
<ConfirmationModalWithNotes
ref={confirmModal.ref}
type={approvalAction == 'APPROVED' ? 'success' : 'error'}
text={`Apakah anda yakin ingin ${
approvalAction == 'APPROVED' ? 'approve' : 'reject'
} Project Flock berikut? (${initialValues?.flock_name} - ${
initialValues?.area?.name
})?`}
secondaryButton={{
text: 'Tidak',
}}
primaryButton={{
text: 'Ya',
color: approvalAction == 'APPROVED' ? 'success' : 'error',
isLoading: isApproveLoading,
onClick: (notes) => {
confirmApprovalHandler(notes, approvalAction);
},
}}
/>
</> </>
); );
}; };
@@ -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,62 +49,59 @@ 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,
uniformityDetail,
}: { }: {
uniformity?: Uniformity; uniformity?: Uniformity;
uniformityDetail?: UniformityDetail;
}) => { }) => {
const data: UniformityPreviewData[] = [ const data: DetailOptionType[] = [
{ {
id: 'tanggal', id: 'tanggal',
label: 'Tanggal', label: 'Tanggal',
value: uniformity value: uniformity
? formatDate(uniformity.applied_at, 'DD MMM YYYY') ? formatDate(uniformity.applied_at, 'DD MMM YYYY')
: '-', : uniformityDetail
? formatDate(uniformityDetail.info_umum.tanggal, 'DD MMM YYYY')
: '-',
}, },
{ {
id: 'lokasi-farm', id: 'lokasi-farm',
label: 'Lokasi Farm', label: 'Lokasi Farm',
value: uniformity?.location_name || '-', value:
uniformity?.location_name ||
uniformityDetail?.info_umum?.lokasi_farm ||
'-',
}, },
{ {
id: 'project-flock', id: 'project-flock',
label: 'Project Flock', label: 'Project Flock',
value: uniformity?.flock_name || '-', value:
uniformity?.flock_name ||
uniformityDetail?.info_umum?.project_flock ||
'-',
}, },
{ {
id: 'kandang', id: 'kandang',
label: 'Kandang', label: 'Kandang',
value: uniformity?.kandang_name || '-', value:
uniformity?.kandang_name || uniformityDetail?.info_umum?.kandang || '-',
}, },
{ {
id: 'file-uniformity', id: 'file-uniformity',
label: 'File Uniformity', label: 'File Uniformity',
value: '-', // File name tidak tersedia di GET ALL response value:
uniformity?.file_name || uniformityDetail?.info_umum?.file_name || '-',
}, },
{ {
id: 'status', id: 'status',
label: 'Status', label: 'Status',
value: uniformity?.status || '-', value: uniformity?.status || (uniformityDetail ? 'CREATED' : '-'),
}, },
]; ];
const columns: ColumnDef<UniformityPreviewData>[] = [ const columns: ColumnDef<DetailOptionType>[] = [
{ {
accessorKey: 'label', accessorKey: 'label',
header: 'Label', header: 'Label',
@@ -148,7 +149,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 +401,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) => {
@@ -408,9 +460,15 @@ const UniformityTable = ({ refresh }: { refresh?: () => void }) => {
const canApproveReject = useMemo(() => { const canApproveReject = useMemo(() => {
return ( return (
selectedUniformities.length > 0 && selectedUniformities.length > 0 &&
selectedUniformities.every( selectedUniformities.every((u) => {
(u) => u.status === 'CREATED' || u.status === 'Pengajuan' const approvalAction = u.latest_approval?.action;
) return (
approvalAction === 'CREATED' ||
approvalAction === 'Pengajuan' ||
(!approvalAction &&
(u.status === 'CREATED' || u.status === 'Pengajuan'))
);
})
); );
}, [selectedUniformities]); }, [selectedUniformities]);
@@ -765,7 +823,9 @@ const UniformityTable = ({ refresh }: { refresh?: () => void }) => {
accessorKey: 'status', accessorKey: 'status',
header: 'Status', header: 'Status',
cell: (props) => { cell: (props) => {
const status = props.row.original.status; const uniformity = props.row.original;
const status =
uniformity.latest_approval?.action ?? uniformity.status;
return ( return (
<div className='w-full'> <div className='w-full'>
<Badge <Badge
@@ -836,7 +896,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
@@ -898,34 +958,7 @@ const UniformityTable = ({ refresh }: { refresh?: () => void }) => {
<div className='flex flex-col gap-4 mt-4'> <div className='flex flex-col gap-4 mt-4'>
{createdUniformity ? ( {createdUniformity ? (
<UniformityConfirmationPreview <UniformityConfirmationPreview
uniformity={{ uniformityDetail={createdUniformity}
id: createdUniformity.id,
location_name: createdUniformity.info_umum.lokasi_farm,
flock_name: createdUniformity.info_umum.project_flock,
kandang_name: createdUniformity.info_umum.kandang,
applied_at: createdUniformity.info_umum.tanggal,
week: 0,
status: 'Pengajuan',
uniformity: createdUniformity.result.uniformity,
cv: createdUniformity.result.cv,
chick_qty_of_weight:
createdUniformity.sampling.chick_qty_of_weight,
uniform_qty: createdUniformity.result.uniform_qty,
mean_up: createdUniformity.sampling.mean_up,
mean_down: createdUniformity.sampling.mean_down,
standard_mean_weight: null,
standard_uniformity: null,
created_at: '',
created_by: 0,
project_flock_kandang_id: 0,
created_user: {
id: 0,
id_user: 0,
email: '',
name: '',
},
updated_at: '',
}}
/> />
) : selectedRowIds.length === 1 ? ( ) : selectedRowIds.length === 1 ? (
<UniformityConfirmationPreview <UniformityConfirmationPreview
@@ -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'
@@ -674,6 +689,16 @@ const PurchaseOrderAcceptApprovalForm = ({
accept='.pdf,.jpg,.jpeg,.png' accept='.pdf,.jpg,.jpeg,.png'
onChange={(e) => { onChange={(e) => {
const files = Array.from(e.target.files || []); const files = Array.from(e.target.files || []);
const invalidFiles = files.filter(
(file) => file.size > 2 * 1024 * 1024
);
if (invalidFiles.length > 0) {
toast.error('Ukuran dokumen maksimal 2 MB!');
e.target.value = '';
return;
}
formik.setFieldValue('travel_documents', files); formik.setFieldValue('travel_documents', files);
}} }}
onBlur={formik.handleBlur} onBlur={formik.handleBlur}
@@ -699,7 +724,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?.();
@@ -312,7 +312,8 @@ export const PurchaseRequestStaffApprovalFormInitialValues: PurchaseRequestStaff
}; };
export const PurchaseRequestStaffApprovalFormDefaultValues = ( export const PurchaseRequestStaffApprovalFormDefaultValues = (
purchase?: Purchase purchase?: Purchase,
type?: 'add' | 'edit'
): PurchaseRequestStaffApprovalFormSchemaType => { ): PurchaseRequestStaffApprovalFormSchemaType => {
return { return {
action: 'APPROVED', action: 'APPROVED',
@@ -331,8 +332,18 @@ export const PurchaseRequestStaffApprovalFormDefaultValues = (
label: item.warehouse?.name || '', label: item.warehouse?.name || '',
}, },
qty: item.sub_qty || item.qty || 0, qty: item.sub_qty || item.qty || 0,
price: item.price, price:
total_price: item.total_price, type === 'add'
? 'ProductPrice' in item.product
? item.product.ProductPrice || item.price || ''
: item.price
: item.price,
total_price:
type === 'add'
? ('ProductPrice' in item.product
? item.product.ProductPrice || item.price || 0
: item.price) * (item.sub_qty || item.qty || 0)
: item.total_price,
})) }))
: [ : [
{ {
@@ -381,7 +392,15 @@ export const PurchaseRequestAcceptApprovalFormSchema: Yup.ObjectSchema<PurchaseR
.required('Item pembelian wajib diisi!') .required('Item pembelian wajib diisi!')
.typeError('Item pembelian wajib diisi!'), .typeError('Item pembelian wajib diisi!'),
travel_documents: Yup.array() travel_documents: Yup.array()
.of(Yup.mixed<File>().required()) .of(
Yup.mixed<File>()
.required('Dokumen surat jalan wajib diupload!')
.test('fileSize', 'Ukuran dokumen maksimal 2 MB', (value) => {
if (!value) return true;
if (value instanceof File) return value.size <= 2 * 1024 * 1024;
return true;
})
)
.required('Dokumen surat jalan wajib diupload!') .required('Dokumen surat jalan wajib diupload!')
.min(1, 'Minimal upload 1 dokumen surat jalan!') .min(1, 'Minimal upload 1 dokumen surat jalan!')
.typeError('Dokumen surat jalan wajib diupload!'), .typeError('Dokumen surat jalan wajib diupload!'),
@@ -294,9 +294,9 @@ const PurchaseOrderStaffApprovalForm = ({
// ===== FORM CONFIGURATION ===== // ===== FORM CONFIGURATION =====
const formikInitialValues = useMemo(() => { const formikInitialValues = useMemo(() => {
return initialValues return initialValues
? PurchaseRequestStaffApprovalFormDefaultValues(initialValues) ? PurchaseRequestStaffApprovalFormDefaultValues(initialValues, type)
: PurchaseRequestStaffApprovalFormInitialValues; : PurchaseRequestStaffApprovalFormInitialValues;
}, [initialValues]); }, [initialValues, type]);
const formik = useFormik({ const formik = useFormik({
initialValues: formikInitialValues, initialValues: formikInitialValues,
@@ -485,9 +485,18 @@ const PurchaseOrderStaffApprovalForm = ({
}, },
warehouse_id: purchaseItem.warehouse_id || 0, warehouse_id: purchaseItem.warehouse_id || 0,
qty: originalItem?.qty || purchaseItem.quantity || 0, qty: originalItem?.qty || purchaseItem.quantity || 0,
price: type === 'edit' && originalItem ? originalItem.price : '', price:
type === 'edit' && originalItem
? originalItem.price
: originalItem?.product && 'ProductPrice' in originalItem.product
? originalItem.product.ProductPrice || ''
: '',
total_price: total_price:
type === 'edit' && originalItem ? originalItem.total_price : '', type === 'edit' && originalItem
? originalItem.total_price
: (originalItem?.product && 'ProductPrice' in originalItem.product
? originalItem.product.ProductPrice || 0
: 0) * (originalItem?.qty || purchaseItem.quantity || 0),
}; };
return itemData; return itemData;
}); });
@@ -1140,6 +1149,7 @@ const PurchaseOrderStaffApprovalForm = ({
color='warning' color='warning'
className='px-4' className='px-4'
onClick={() => { onClick={() => {
formik.setValues(formikInitialValues);
formik.resetForm(); formik.resetForm();
setPurchaseOrderFormErrorMessage(''); setPurchaseOrderFormErrorMessage('');
onCancel?.(); onCancel?.();
+71
View File
@@ -0,0 +1,71 @@
import { FormikErrors } from 'formik';
export type ErrorMessage = {
key: string;
message: string;
};
/**
* Parse Formik errors object into a flat array of error messages
* @param errors - Formik errors object
* @param parentKey - Parent key for nested objects (used internally for recursion)
* @returns Array of error messages
*/
export function parseFormikErrors<T>(
errors: FormikErrors<T>,
parentKey: string = ''
): ErrorMessage[] {
const errorList: ErrorMessage[] = [];
Object.keys(errors).forEach((key) => {
const value = errors[key as keyof typeof errors];
const fullKey = parentKey ? `${parentKey}.${key}` : key;
if (typeof value === 'string') {
// Direct error message
errorList.push({ key: fullKey, message: value });
} else if (Array.isArray(value)) {
// Array of errors
value.forEach((item, index) => {
if (typeof item === 'string') {
errorList.push({ key: `${fullKey}[${index}]`, message: item });
} else if (item && typeof item === 'object') {
// Nested object in array
const nestedErrors = parseFormikErrors(
item as FormikErrors<unknown>,
`${fullKey}[${index}]`
);
errorList.push(...nestedErrors);
}
});
} else if (value && typeof value === 'object') {
// Nested object
const nestedErrors = parseFormikErrors(
value as FormikErrors<unknown>,
fullKey
);
errorList.push(...nestedErrors);
}
});
return errorList;
}
/**
* Get unique error messages from Formik errors
* @param errors - Formik errors object
* @returns Array of unique error messages
*/
export function getUniqueFormikErrors<T>(errors: FormikErrors<T>): string[] {
const errorList = parseFormikErrors(errors);
return Array.from(new Set(errorList.map((e) => e.message)));
}
/**
* Get all error messages from Formik errors
* @param errors - Formik errors object
* @returns Array of error messages
*/
export function getAllFormikErrors<T>(errors: FormikErrors<T>): ErrorMessage[] {
return parseFormikErrors(errors);
}
+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,
+2 -2
View File
@@ -13,7 +13,7 @@ export class MarketingReportApiService extends BaseApiService<
unknown, unknown,
unknown unknown
> { > {
constructor(basePath: string = '/reports/marketings/daily-marketing') { constructor(basePath: string = '/reports/marketing') {
super(basePath); super(basePath);
} }
@@ -71,5 +71,5 @@ export class MarketingReportApiService extends BaseApiService<
} }
export const MarketingReportApi = new MarketingReportApiService( export const MarketingReportApi = new MarketingReportApiService(
'/reports/marketings/daily-marketing' '/reports/marketing'
); );
+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[];
};
+2
View File
@@ -10,6 +10,8 @@ export type BaseInventoryProduct = {
name: string; name: string;
brand: string; brand: string;
sku: string; sku: string;
ProductPrice: number;
SellingPrice?: number;
product_price: number; product_price: number;
selling_price?: number; selling_price?: number;
tax?: number; tax?: number;
+3 -10
View File
@@ -10,9 +10,10 @@ export type BaseProjectFlockKandang = {
kandang_id: number; kandang_id: number;
kandang: Kandang; kandang: Kandang;
project_flock: ProjectFlock; project_flock: ProjectFlock;
available_qtys?: AvailableQty[];
chickins?: Chickin[];
approval: BaseApproval; approval: BaseApproval;
chickins?: Chickin[];
available_qtys?: AvailableQty[];
chickin_approval?: BaseApproval;
}; };
export type AvailableQty = { export type AvailableQty = {
@@ -56,14 +57,6 @@ export type ClosingExpense = {
reference_number: string; reference_number: string;
}; };
// "flag_name": "PAKAN",
// "product_warehouse_id": 14,
// "product_id": 8,
// "product_name": "281 SPECIAL STARTER",
// "product_category": "Bahan Baku",
// "uom": "Kilogram",
// "quantity": 1100
export type StockItem = { export type StockItem = {
flag_name: string; flag_name: string;
product_warehouse_id: number; product_warehouse_id: number;
+1 -3
View File
@@ -1,7 +1,4 @@
import { BaseMetadata } from '@/types/api/api-general'; import { BaseMetadata } from '@/types/api/api-general';
import { Location } from '@/types/api/location/location';
import { ProjectFlock } from '@/types/api/project-flock/project-flock';
import { Kandang } from '@/types/api/kandang/kandang';
import { BaseApproval } from '@/types/api/approval/approval'; import { BaseApproval } from '@/types/api/approval/approval';
// ==================== GET ALL RESPONSE ==================== // ==================== GET ALL RESPONSE ====================
@@ -11,6 +8,7 @@ export type Uniformity = BaseMetadata & {
location_name: string; location_name: string;
flock_name: string; flock_name: string;
kandang_name: string; kandang_name: string;
file_name: string;
applied_at: string; applied_at: string;
week: number; week: number;
status: string; status: string;
+2
View File
@@ -10,6 +10,8 @@ export type PurchaseItemProduct = {
id: number; id: number;
name: string; name: string;
flags?: string[]; flags?: string[];
ProductPrice?: number;
SellingPrice?: number;
uom?: { uom?: {
name: string; name: string;
}; };