mirror of
https://gitlab.com/mbugroup/lti-web-client.git
synced 2026-05-25 15:55:48 +00:00
Merge branch 'development' into feat/FE/daily-checklist
This commit is contained in:
@@ -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;
|
||||||
@@ -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>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
|
|||||||
@@ -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:
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
+43
-9
@@ -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>
|
||||||
|
|||||||
+8
-4
@@ -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()}`;
|
||||||
|
|||||||
+1
-1
@@ -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?.();
|
||||||
|
|||||||
@@ -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);
|
||||||
|
}
|
||||||
@@ -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,
|
||||||
|
|||||||
@@ -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
@@ -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[];
|
||||||
|
};
|
||||||
|
|||||||
Vendored
+2
@@ -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
@@ -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
@@ -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;
|
||||||
|
|||||||
Vendored
+2
@@ -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;
|
||||||
};
|
};
|
||||||
|
|||||||
Reference in New Issue
Block a user