mirror of
https://gitlab.com/mbugroup/lti-web-client.git
synced 2026-05-20 13:32:00 +00:00
Merge branch 'development' of https://gitlab.com/mbugroup/lti-web-client into dev/randy
This commit is contained in:
@@ -33,6 +33,7 @@ const FileInput = ({
|
||||
isError,
|
||||
errorMessage,
|
||||
disabled = false,
|
||||
required = false,
|
||||
onChange,
|
||||
onBlur,
|
||||
readOnly = false,
|
||||
@@ -56,6 +57,13 @@ const FileInput = ({
|
||||
)}
|
||||
>
|
||||
{label}
|
||||
{required && (
|
||||
<>
|
||||
<span className='tooltip tooltip-error' data-tip='required'>
|
||||
<span className='text-error'> *</span>
|
||||
</span>
|
||||
</>
|
||||
)}
|
||||
</label>
|
||||
)}
|
||||
|
||||
|
||||
@@ -140,17 +140,17 @@ const ExpenseRequestContent = ({
|
||||
const confirmationModalDeleteClickHandler = async () => {
|
||||
setIsDeleteLoading(true);
|
||||
|
||||
try {
|
||||
await ExpenseApi.delete(initialValues?.id as number);
|
||||
const deleteResponse = await ExpenseApi.delete(initialValues?.id as number);
|
||||
|
||||
if (isResponseSuccess(deleteResponse)) {
|
||||
toast.success('Berhasil menghapus data biaya operasional!');
|
||||
router.push('/expense');
|
||||
} catch (error) {
|
||||
} else {
|
||||
toast.error('Gagal menghapus data biaya operasional!');
|
||||
} finally {
|
||||
}
|
||||
|
||||
deleteModal.closeModal();
|
||||
setIsDeleteLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
const confirmationModalCompleteClickHandler = async () => {
|
||||
|
||||
@@ -21,7 +21,7 @@ const ExpenseStatusBadge = ({ approval }: ExpenseStatusBadgeProps) => {
|
||||
|
||||
switch (latestApprovalStepNumber) {
|
||||
case 1:
|
||||
expenseStatusPillBadgeColor = 'yellow';
|
||||
expenseStatusPillBadgeColor = 'gray';
|
||||
break;
|
||||
|
||||
case 2:
|
||||
@@ -33,7 +33,7 @@ const ExpenseStatusBadge = ({ approval }: ExpenseStatusBadgeProps) => {
|
||||
break;
|
||||
|
||||
case 4:
|
||||
expenseStatusPillBadgeColor = 'red';
|
||||
expenseStatusPillBadgeColor = 'yellow';
|
||||
break;
|
||||
|
||||
case 5:
|
||||
|
||||
@@ -420,11 +420,19 @@ const ExpensesTable = () => {
|
||||
const confirmationModalDeleteClickHandler = async () => {
|
||||
setIsDeleteLoading(true);
|
||||
|
||||
await ExpenseApi.delete(selectedExpense?.id as number);
|
||||
refreshExpenses();
|
||||
const deleteResponse = await ExpenseApi.delete(
|
||||
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!');
|
||||
}
|
||||
|
||||
setIsDeleteLoading(false);
|
||||
};
|
||||
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import * as Yup from 'yup';
|
||||
import { Movement } from '@/types/api/inventory/movement';
|
||||
import { Movement, MovementDocument } from '@/types/api/inventory/movement';
|
||||
|
||||
type MovementFormSchemaType = {
|
||||
transfer_reason: string;
|
||||
@@ -29,7 +29,7 @@ type MovementFormSchemaType = {
|
||||
deliveries: {
|
||||
delivery_cost?: number | string;
|
||||
delivery_cost_per_item?: number | string;
|
||||
document?: File | string | null;
|
||||
document?: File | MovementDocument | null;
|
||||
document_path?: string | null;
|
||||
driver_name: string;
|
||||
vehicle_plate: string;
|
||||
@@ -61,7 +61,7 @@ export type ProductSchema = {
|
||||
export type DeliverySchema = {
|
||||
delivery_cost?: number | string;
|
||||
delivery_cost_per_item?: number | string;
|
||||
document?: File | string | null;
|
||||
document?: File | MovementDocument | null;
|
||||
document_path?: string | null;
|
||||
driver_name: string;
|
||||
vehicle_plate: string;
|
||||
@@ -129,13 +129,12 @@ const DeliveryObjectSchema: Yup.ObjectSchema<DeliverySchema> = Yup.object({
|
||||
}),
|
||||
document_path: Yup.string().optional(),
|
||||
document_index: Yup.number().optional(),
|
||||
document: Yup.mixed<File | string>()
|
||||
document: Yup.mixed<File | MovementDocument>()
|
||||
.nullable()
|
||||
.test('fileSize', 'Ukuran dokumen maksimal 2 MB', (value) => {
|
||||
if (!value) return true;
|
||||
if (typeof value === 'string') return true;
|
||||
if (value instanceof File) return value.size <= 2 * 1024 * 1024;
|
||||
return false;
|
||||
return true;
|
||||
}),
|
||||
driver_name: Yup.string().required('Nama sopir 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_per_item: d.shipping_cost_item ?? undefined,
|
||||
document_number: d.document_number ?? '',
|
||||
document: d.document_path ?? null,
|
||||
document: d.document ?? null,
|
||||
document_path: d.document_path ?? null,
|
||||
driver_name: d.driver_name ?? '',
|
||||
vehicle_plate: d.vehicle_plate ?? '',
|
||||
|
||||
@@ -35,6 +35,7 @@ import FileInput from '@/components/input/FileInput';
|
||||
import CheckboxInput from '@/components/input/CheckboxInput';
|
||||
import Badge from '@/components/Badge';
|
||||
import Card from '@/components/Card';
|
||||
import { S3_PUBLIC_BASE_URL } from '@/config/constant';
|
||||
|
||||
interface MovementFormProps {
|
||||
type?: 'add' | 'edit' | 'detail';
|
||||
@@ -55,16 +56,8 @@ const MovementForm = ({ type = 'add', initialValues }: MovementFormProps) => {
|
||||
|
||||
// ===== FORM HANDLERS =====
|
||||
const createMovementHandler = useCallback(
|
||||
async (payload: CreateMovementPayload, documents: File[] = []) => {
|
||||
const formData = new FormData();
|
||||
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
|
||||
);
|
||||
async (payload: CreateMovementPayload) => {
|
||||
const res = await MovementApi.createMovement(payload);
|
||||
if (isResponseError(res)) {
|
||||
setMovementFormErrorMessage(res.message);
|
||||
return;
|
||||
@@ -218,6 +211,7 @@ const MovementForm = ({ type = 'add', initialValues }: MovementFormProps) => {
|
||||
});
|
||||
|
||||
const payload: CreateMovementPayload = {
|
||||
data: {
|
||||
transfer_reason: values.transfer_reason,
|
||||
transfer_date: values.transfer_date,
|
||||
source_warehouse_id: values.source_warehouse_id,
|
||||
@@ -227,11 +221,13 @@ const MovementForm = ({ type = 'add', initialValues }: MovementFormProps) => {
|
||||
product_qty: parseInt(p.product_qty.toString()) || 0,
|
||||
})),
|
||||
deliveries: deliveriesPayload,
|
||||
},
|
||||
documents: documents.length > 0 ? documents : undefined,
|
||||
};
|
||||
|
||||
switch (type) {
|
||||
case 'add':
|
||||
await createMovementHandler(payload, documents);
|
||||
await createMovementHandler(payload);
|
||||
break;
|
||||
}
|
||||
},
|
||||
@@ -1537,31 +1533,58 @@ const MovementForm = ({ type = 'add', initialValues }: MovementFormProps) => {
|
||||
{type === 'detail' ? (
|
||||
<>
|
||||
<div className='flex flex-col items-start gap-2'>
|
||||
{delivery.document_path ? (
|
||||
<Button
|
||||
color='primary'
|
||||
className='w-full min-w-52 flex items-center justify-center gap-2'
|
||||
disabled={!delivery.document_path}
|
||||
href={delivery.document_path ?? undefined}
|
||||
href={`${S3_PUBLIC_BASE_URL}/${delivery.document_path.startsWith('/') ? delivery.document_path.slice(1) : delivery.document_path}`}
|
||||
target='_blank'
|
||||
rel='noopener noreferrer'
|
||||
>
|
||||
{delivery.document_path ? (
|
||||
<>
|
||||
<Icon
|
||||
icon='material-symbols:file-open-outline'
|
||||
width={20}
|
||||
height={20}
|
||||
/>
|
||||
Lihat Dokumen
|
||||
</>
|
||||
) : (
|
||||
'-'
|
||||
)}
|
||||
</Button>
|
||||
) : delivery.document &&
|
||||
delivery.document instanceof File === false ? (
|
||||
<Button
|
||||
color='primary'
|
||||
className='w-full min-w-52 flex items-center justify-center gap-2'
|
||||
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>
|
||||
</>
|
||||
) : (
|
||||
<FileInput
|
||||
accept='.pdf,.jpg,.jpeg,.png'
|
||||
name={`deliveries.${idx}.document`}
|
||||
onChange={(e) => {
|
||||
const file = e.target.files?.[0];
|
||||
|
||||
@@ -872,7 +872,7 @@ const RecordingTable = () => {
|
||||
'mb-20':
|
||||
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!',
|
||||
headerRowClassName: 'border-b border-b-gray-200',
|
||||
headerColumnClassName:
|
||||
|
||||
@@ -1,91 +1,93 @@
|
||||
import React from 'react';
|
||||
import React, { useMemo } from 'react';
|
||||
import Card from '@/components/Card';
|
||||
import UniformityBarChart from '@/components/pages/production/uniformity/chart/UniformityBarChart';
|
||||
import UniformityGaugeChart from '@/components/pages/production/uniformity/chart/UniformityGaugeChart';
|
||||
import UniformityBarChartSkeleton from '@/components/pages/production/uniformity/skeleton/UniformityBarChartSkeleton';
|
||||
import UniformityGaugeChartSkeleton from '@/components/pages/production/uniformity/skeleton/UniformityGaugeChartSkeleton';
|
||||
import {
|
||||
UniformityDetailItem,
|
||||
Uniformity,
|
||||
} from '@/types/api/production/uniformity';
|
||||
|
||||
interface BarChartData {
|
||||
name: string;
|
||||
uv: number;
|
||||
interface UniformityChartProps {
|
||||
uniformityData?: Uniformity | null;
|
||||
uniformityDetails?: UniformityDetailItem[];
|
||||
}
|
||||
|
||||
interface GaugeChartData {
|
||||
value: number;
|
||||
label: string;
|
||||
kandang?: string;
|
||||
week?: string;
|
||||
currentValue?: number;
|
||||
totalValue?: number;
|
||||
}
|
||||
|
||||
const UniformityChart = () => {
|
||||
// TODO: Replace with actual API call
|
||||
const barChartData: BarChartData[] = [
|
||||
{
|
||||
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,
|
||||
},
|
||||
const UniformityChart = ({
|
||||
uniformityData,
|
||||
uniformityDetails,
|
||||
}: UniformityChartProps) => {
|
||||
const defaultUniformityDetails: UniformityDetailItem[] = [
|
||||
{ id: 1, weight: 61, range: 'Ideal' },
|
||||
{ id: 2, weight: 62, range: 'Ideal' },
|
||||
{ id: 3, weight: 63, range: 'Ideal' },
|
||||
{ id: 4, weight: 64, range: 'Ideal' },
|
||||
{ id: 5, weight: 65, range: 'Ideal' },
|
||||
{ id: 6, weight: 66, range: 'Ideal' },
|
||||
{ id: 7, weight: 67, range: 'Ideal' },
|
||||
];
|
||||
|
||||
// TODO: Replace with actual API call
|
||||
// const gaugeChartData: GaugeChartData = {
|
||||
// value: 0,
|
||||
// label: '',
|
||||
// kandang: 'Kandang Cirangga',
|
||||
// week: 'Week 2',
|
||||
// currentValue: 512,
|
||||
// totalValue: 1024,
|
||||
// };
|
||||
const detailsToUse = uniformityDetails || defaultUniformityDetails;
|
||||
|
||||
const gaugeChartData: GaugeChartData = {
|
||||
value: 52,
|
||||
label: 'Uniformity',
|
||||
kandang: 'Kandang Cirangga',
|
||||
week: 'Week 2',
|
||||
currentValue: 512,
|
||||
totalValue: 1024,
|
||||
const barChartData = useMemo(() => {
|
||||
if (!uniformityData) {
|
||||
return [];
|
||||
}
|
||||
|
||||
if (!detailsToUse || detailsToUse.length === 0) {
|
||||
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 (
|
||||
<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'>
|
||||
{barChartData.length === 0 ? (
|
||||
{!uniformityData || barChartData.length === 0 ? (
|
||||
<UniformityBarChartSkeleton />
|
||||
) : (
|
||||
<UniformityBarChart data={barChartData} />
|
||||
)}
|
||||
</div>
|
||||
</Card>
|
||||
{gaugeChartData.value === 0 ? (
|
||||
{!uniformityData || !gaugeChartData ? (
|
||||
<Card
|
||||
variant='bordered'
|
||||
title='Weekly Performance ⓘ'
|
||||
@@ -128,7 +130,6 @@ const UniformityChart = () => {
|
||||
<UniformityGaugeChart
|
||||
value={gaugeChartData.value}
|
||||
label={gaugeChartData.label}
|
||||
kandang={gaugeChartData.kandang}
|
||||
week={gaugeChartData.week}
|
||||
currentValue={gaugeChartData.currentValue}
|
||||
totalValue={gaugeChartData.totalValue}
|
||||
|
||||
@@ -41,9 +41,7 @@ export default function UniformityPageWrapper({
|
||||
return (
|
||||
<>
|
||||
<div className='w-full p-4'>
|
||||
<UniformityTable
|
||||
refresh={() => !isOpen && router.push('/production/uniformity')}
|
||||
/>
|
||||
<UniformityTable />
|
||||
</div>
|
||||
|
||||
<Drawer
|
||||
|
||||
@@ -10,7 +10,11 @@ import Button from '@/components/Button';
|
||||
import UniformityChart from '@/components/pages/production/uniformity/UniformityChart';
|
||||
import { useTableFilter } from '@/services/hooks/useTableFilter';
|
||||
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 { type BaseApiResponse } from '@/types/api/api-general';
|
||||
import Table from '@/components/Table';
|
||||
@@ -45,62 +49,59 @@ import Dropdown from '@/components/Dropdown';
|
||||
import Menu from '@/components/menu/Menu';
|
||||
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 = ({
|
||||
uniformity,
|
||||
uniformityDetail,
|
||||
}: {
|
||||
uniformity?: Uniformity;
|
||||
uniformityDetail?: UniformityDetail;
|
||||
}) => {
|
||||
const data: UniformityPreviewData[] = [
|
||||
const data: DetailOptionType[] = [
|
||||
{
|
||||
id: 'tanggal',
|
||||
label: 'Tanggal',
|
||||
value: uniformity
|
||||
? formatDate(uniformity.applied_at, 'DD MMM YYYY')
|
||||
: uniformityDetail
|
||||
? formatDate(uniformityDetail.info_umum.tanggal, 'DD MMM YYYY')
|
||||
: '-',
|
||||
},
|
||||
{
|
||||
id: 'lokasi-farm',
|
||||
label: 'Lokasi Farm',
|
||||
value: uniformity?.location_name || '-',
|
||||
value:
|
||||
uniformity?.location_name ||
|
||||
uniformityDetail?.info_umum?.lokasi_farm ||
|
||||
'-',
|
||||
},
|
||||
{
|
||||
id: 'project-flock',
|
||||
label: 'Project Flock',
|
||||
value: uniformity?.flock_name || '-',
|
||||
value:
|
||||
uniformity?.flock_name ||
|
||||
uniformityDetail?.info_umum?.project_flock ||
|
||||
'-',
|
||||
},
|
||||
{
|
||||
id: 'kandang',
|
||||
label: 'Kandang',
|
||||
value: uniformity?.kandang_name || '-',
|
||||
value:
|
||||
uniformity?.kandang_name || uniformityDetail?.info_umum?.kandang || '-',
|
||||
},
|
||||
{
|
||||
id: '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',
|
||||
label: 'Status',
|
||||
value: uniformity?.status || '-',
|
||||
value: uniformity?.status || (uniformityDetail ? 'CREATED' : '-'),
|
||||
},
|
||||
];
|
||||
|
||||
const columns: ColumnDef<UniformityPreviewData>[] = [
|
||||
const columns: ColumnDef<DetailOptionType>[] = [
|
||||
{
|
||||
accessorKey: '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 searchParams = useSearchParams();
|
||||
const isSuccess = useUniformityStore((s) => s.isSuccess);
|
||||
@@ -355,6 +401,12 @@ const UniformityTable = ({ refresh }: { refresh?: () => void }) => {
|
||||
mutate: refreshUniformities,
|
||||
} = useSWR(uniformitySwrKey, UniformityApi.getAllFetcher);
|
||||
|
||||
useEffect(() => {
|
||||
if (isSuccess) {
|
||||
refreshUniformities();
|
||||
}
|
||||
}, [isSuccess, refreshUniformities]);
|
||||
|
||||
// ===== FILTER HANDLERS =====
|
||||
const handleFilterLocationChange = useCallback(
|
||||
(val: OptionType | OptionType[] | null) => {
|
||||
@@ -408,9 +460,15 @@ const UniformityTable = ({ refresh }: { refresh?: () => void }) => {
|
||||
const canApproveReject = useMemo(() => {
|
||||
return (
|
||||
selectedUniformities.length > 0 &&
|
||||
selectedUniformities.every(
|
||||
(u) => u.status === 'CREATED' || u.status === 'Pengajuan'
|
||||
)
|
||||
selectedUniformities.every((u) => {
|
||||
const approvalAction = u.latest_approval?.action;
|
||||
return (
|
||||
approvalAction === 'CREATED' ||
|
||||
approvalAction === 'Pengajuan' ||
|
||||
(!approvalAction &&
|
||||
(u.status === 'CREATED' || u.status === 'Pengajuan'))
|
||||
);
|
||||
})
|
||||
);
|
||||
}, [selectedUniformities]);
|
||||
|
||||
@@ -765,7 +823,9 @@ const UniformityTable = ({ refresh }: { refresh?: () => void }) => {
|
||||
accessorKey: 'status',
|
||||
header: 'Status',
|
||||
cell: (props) => {
|
||||
const status = props.row.original.status;
|
||||
const uniformity = props.row.original;
|
||||
const status =
|
||||
uniformity.latest_approval?.action ?? uniformity.status;
|
||||
return (
|
||||
<div className='w-full'>
|
||||
<Badge
|
||||
@@ -836,7 +896,7 @@ const UniformityTable = ({ refresh }: { refresh?: () => void }) => {
|
||||
<div className='my-4 divider'></div>
|
||||
|
||||
<section>
|
||||
<UniformityChart />
|
||||
<UniformityChartWrapper uniformitySwrKey={uniformitySwrKey} />
|
||||
</section>
|
||||
|
||||
<Card
|
||||
@@ -898,34 +958,7 @@ const UniformityTable = ({ refresh }: { refresh?: () => void }) => {
|
||||
<div className='flex flex-col gap-4 mt-4'>
|
||||
{createdUniformity ? (
|
||||
<UniformityConfirmationPreview
|
||||
uniformity={{
|
||||
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: '',
|
||||
}}
|
||||
uniformityDetail={createdUniformity}
|
||||
/>
|
||||
) : selectedRowIds.length === 1 ? (
|
||||
<UniformityConfirmationPreview
|
||||
|
||||
@@ -3,6 +3,7 @@ import {
|
||||
Bar,
|
||||
BarChart,
|
||||
CartesianGrid,
|
||||
Cell,
|
||||
Rectangle,
|
||||
ResponsiveContainer,
|
||||
Tooltip,
|
||||
@@ -25,6 +26,8 @@ interface CustomTooltipProps {
|
||||
interface BarChartData {
|
||||
name: string;
|
||||
uv: number;
|
||||
isIdeal?: boolean;
|
||||
idealCount?: number;
|
||||
}
|
||||
|
||||
interface UniformityBarChartProps {
|
||||
@@ -33,7 +36,25 @@ interface UniformityBarChartProps {
|
||||
|
||||
function CustomTooltip({ payload, label, active }: CustomTooltipProps) {
|
||||
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);
|
||||
|
||||
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 (
|
||||
<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>
|
||||
@@ -105,9 +126,6 @@ const UniformityBarChart: React.FC<UniformityBarChartProps> = ({ data }) => {
|
||||
<Bar
|
||||
name='Birds'
|
||||
dataKey='uv'
|
||||
fill='#FFFFFF'
|
||||
stroke='#DDD'
|
||||
strokeWidth={2}
|
||||
radius={[25, 25, 0, 0]}
|
||||
activeBar={
|
||||
<Rectangle
|
||||
@@ -117,7 +135,16 @@ const UniformityBarChart: React.FC<UniformityBarChartProps> = ({ data }) => {
|
||||
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>
|
||||
</ResponsiveContainer>
|
||||
);
|
||||
|
||||
@@ -1,25 +1,29 @@
|
||||
import React from 'react';
|
||||
import React, { useState } from 'react';
|
||||
import { Cell, Pie, PieChart, ResponsiveContainer } from 'recharts';
|
||||
import Card from '@/components/Card';
|
||||
import { Icon } from '@iconify/react';
|
||||
import { formatNumber } from '@/lib/helper';
|
||||
import { Icon } from '@iconify/react';
|
||||
|
||||
interface UniformityGaugeChartProps {
|
||||
value: number;
|
||||
label: string;
|
||||
kandang?: string;
|
||||
week?: string;
|
||||
currentValue?: number;
|
||||
totalValue?: number;
|
||||
onWeekChange?: (direction: 'prev' | 'next') => void;
|
||||
hasPrevWeek?: boolean;
|
||||
hasNextWeek?: boolean;
|
||||
}
|
||||
|
||||
const UniformityGaugeChart: React.FC<UniformityGaugeChartProps> = ({
|
||||
value,
|
||||
label,
|
||||
kandang,
|
||||
week,
|
||||
currentValue,
|
||||
totalValue,
|
||||
onWeekChange,
|
||||
hasPrevWeek = false,
|
||||
hasNextWeek = false,
|
||||
}) => {
|
||||
const numberOfSegments = 50;
|
||||
const filledSegments = Math.round((value / 100) * numberOfSegments);
|
||||
@@ -34,7 +38,7 @@ const UniformityGaugeChart: React.FC<UniformityGaugeChartProps> = ({
|
||||
const inactiveColor = '#f0f0f0';
|
||||
|
||||
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='relative w-full h-full flex flex-col items-center justify-end'>
|
||||
<ResponsiveContainer width='100%' height='100%'>
|
||||
@@ -73,20 +77,24 @@ const UniformityGaugeChart: React.FC<UniformityGaugeChartProps> = ({
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className='flex items-center justify-center gap-2 w-full'>
|
||||
<button
|
||||
onClick={() => onWeekChange?.('prev')}
|
||||
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'
|
||||
>
|
||||
<Icon icon='heroicons:chevron-left' width={20} height={20} />
|
||||
</button>
|
||||
<Card
|
||||
variant='bordered'
|
||||
className={{
|
||||
wrapper: 'w-full',
|
||||
wrapper: 'max-w-xs',
|
||||
}}
|
||||
>
|
||||
<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:calendar-date-range' width={24} height={24} />
|
||||
</div>
|
||||
<div className='grid grid-cols-1 min-w-0'>
|
||||
<div className='flex items-center space-x-2 text-[#18181B80] text-sm mb-1'>
|
||||
<span className='font-medium truncate'>{kandang}</span>
|
||||
<span className='shrink-0'>•</span>
|
||||
<section className='flex items-center justify-center gap-4'>
|
||||
<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>
|
||||
@@ -96,11 +104,22 @@ const UniformityGaugeChart: React.FC<UniformityGaugeChartProps> = ({
|
||||
{formatNumber(currentValue ?? 0)}
|
||||
</span>
|
||||
<span className='mx-1 text-gray-400 text-base'>From</span>
|
||||
<span className='break-all'>{formatNumber(totalValue ?? 0)}</span>
|
||||
<span className='break-all'>
|
||||
{formatNumber(totalValue ?? 0)}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
</Card>
|
||||
<button
|
||||
onClick={() => onWeekChange?.('next')}
|
||||
disabled={!hasNextWeek}
|
||||
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='Next week'
|
||||
>
|
||||
<Icon icon='heroicons:chevron-right' width={20} height={20} />
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -119,11 +119,13 @@ const UniformityDetail: React.FC<UniformityDetailProps> = ({
|
||||
const statusValue = latest_approval?.action ?? '-';
|
||||
|
||||
const valueMap: Record<string, string> = {
|
||||
tanggal: formatDate(info_umum.tanggal, 'DD MMMM YYYY'),
|
||||
'lokasi-farm': info_umum.lokasi_farm,
|
||||
'project-flock': info_umum.project_flock,
|
||||
kandang: info_umum.kandang,
|
||||
'document-name': info_umum.file_name,
|
||||
tanggal: info_umum?.tanggal
|
||||
? formatDate(info_umum.tanggal, 'DD MMMM YYYY')
|
||||
: '-',
|
||||
'lokasi-farm': info_umum?.lokasi_farm ?? '-',
|
||||
'project-flock': info_umum?.project_flock ?? '-',
|
||||
kandang: info_umum?.kandang ?? '-',
|
||||
'document-name': info_umum?.file_name ?? '-',
|
||||
'approval-status': statusValue,
|
||||
};
|
||||
|
||||
|
||||
@@ -229,9 +229,10 @@ const UniformityDetailsPreview = ({
|
||||
{/* Form Section */}
|
||||
<div className='divider mt-3.5'></div>
|
||||
<section className='w-full px-6'>
|
||||
{uniformity_details && uniformity_details.length > 0 ? (
|
||||
{info_umum || sampling || result ? (
|
||||
<div className='flex flex-col gap-4'>
|
||||
{/* Sampling and Range */}
|
||||
{sampling && (
|
||||
<div className=''>
|
||||
<p className='text-sm font-medium mb-5'>Sampling and Range</p>
|
||||
<Table<DetailOptionType>
|
||||
@@ -244,7 +245,10 @@ const UniformityDetailsPreview = ({
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Result */}
|
||||
{result && (
|
||||
<div className=''>
|
||||
<p className='text-sm font-medium mb-5'>Result</p>
|
||||
<Table<DetailOptionType>
|
||||
@@ -257,8 +261,9 @@ const UniformityDetailsPreview = ({
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Body Weight Details Button */}
|
||||
{!uniformity_details || uniformity_details.length === 0 ? (
|
||||
<div className='mt-4'>
|
||||
<Button
|
||||
type='button'
|
||||
@@ -269,9 +274,7 @@ const UniformityDetailsPreview = ({
|
||||
{isLoading ? 'Loading...' : 'Show Body Weight Details'}
|
||||
</Button>
|
||||
</div>
|
||||
{/*{!uniformity_details || uniformity_details.length === 0 ? (
|
||||
<></>
|
||||
) : null}*/}
|
||||
) : null}
|
||||
|
||||
{/* Body Weight Details */}
|
||||
{uniformity_details && uniformity_details.length > 0 && (
|
||||
|
||||
@@ -97,12 +97,16 @@ const UniformityForm = ({
|
||||
setInputValue: setLocationSelectInputValue,
|
||||
options: locationOptions,
|
||||
isLoadingOptions: isLoadingLocations,
|
||||
} = useSelect(LocationApi.basePath, 'id', 'name', 'search');
|
||||
} = useSelect(LocationApi.basePath, 'id', 'name', 'search', {
|
||||
page: '1',
|
||||
limit: '100',
|
||||
});
|
||||
|
||||
// ===== FETCH PROJECT FLOCKS DATA =====
|
||||
const projectFlocksUrl = useMemo(() => {
|
||||
const params = new URLSearchParams({
|
||||
search: projectFlockSearchValue || '',
|
||||
page: '1',
|
||||
limit: '100',
|
||||
});
|
||||
if (selectedLocation) {
|
||||
@@ -141,6 +145,7 @@ const UniformityForm = ({
|
||||
const approvedProjectFlockKandangsUrl = useMemo(() => {
|
||||
const params = new URLSearchParams({
|
||||
step_name: 'Disetujui',
|
||||
page: '1',
|
||||
limit: '100',
|
||||
});
|
||||
return `${ProjectFlockKandangApi.basePath}?${params.toString()}`;
|
||||
|
||||
+1
-1
@@ -28,7 +28,7 @@ const UniformityGaugeChartSkeleton: React.FC<
|
||||
const inactiveColor = '#f0f0f0';
|
||||
|
||||
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='relative w-full h-full flex flex-col items-center justify-end min-w-0'>
|
||||
<ResponsiveContainer width='100%' height={256}>
|
||||
|
||||
@@ -52,9 +52,14 @@ const PurchaseOrderAcceptApprovalForm = ({
|
||||
const searchParams = useSearchParams();
|
||||
const [purchaseOrderFormErrorMessage, setPurchaseOrderFormErrorMessage] =
|
||||
useState('');
|
||||
const [key, setKey] = useState(0);
|
||||
|
||||
const isRejected = initialValues?.latest_approval?.action === 'REJECTED';
|
||||
|
||||
useEffect(() => {
|
||||
setKey((prev) => prev + 1);
|
||||
}, [initialValues?.id]);
|
||||
|
||||
// ===== UTILITY FUNCTIONS =====
|
||||
const isRepeaterInputError = (
|
||||
idx: number,
|
||||
@@ -164,6 +169,7 @@ const PurchaseOrderAcceptApprovalForm = ({
|
||||
validationSchema: PurchaseRequestAcceptApprovalFormSchema,
|
||||
validateOnChange: true,
|
||||
validateOnBlur: true,
|
||||
enableReinitialize: false,
|
||||
onSubmit: async (values) => {
|
||||
const payload: CreateAcceptApprovalRequestPayload = {
|
||||
action: 'APPROVED',
|
||||
@@ -238,7 +244,12 @@ const PurchaseOrderAcceptApprovalForm = ({
|
||||
travel_number: item.travel_number || '',
|
||||
travel_document_path: item.travel_document_path || '',
|
||||
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,
|
||||
received_qty: item.total_qty || '',
|
||||
transport_per_item: item.transport_per_item || '',
|
||||
@@ -246,7 +257,7 @@ const PurchaseOrderAcceptApprovalForm = ({
|
||||
});
|
||||
formik.setFieldValue('items', updatedItems);
|
||||
}
|
||||
}, [purchaseItems, initialValues]);
|
||||
}, [purchaseItems, initialValues, key]);
|
||||
|
||||
useEffect(() => {
|
||||
if (
|
||||
@@ -336,7 +347,11 @@ const PurchaseOrderAcceptApprovalForm = ({
|
||||
};
|
||||
|
||||
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'>
|
||||
<h2 className='text-lg font-semibold mb-4'>
|
||||
{type === 'add'
|
||||
@@ -674,6 +689,16 @@ const PurchaseOrderAcceptApprovalForm = ({
|
||||
accept='.pdf,.jpg,.jpeg,.png'
|
||||
onChange={(e) => {
|
||||
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);
|
||||
}}
|
||||
onBlur={formik.handleBlur}
|
||||
@@ -699,7 +724,9 @@ const PurchaseOrderAcceptApprovalForm = ({
|
||||
color='warning'
|
||||
className='px-4'
|
||||
onClick={() => {
|
||||
if (type === 'add') {
|
||||
formik.resetForm();
|
||||
}
|
||||
setPurchaseOrderFormErrorMessage('');
|
||||
onCancel?.();
|
||||
onModalClose?.();
|
||||
|
||||
@@ -312,7 +312,8 @@ export const PurchaseRequestStaffApprovalFormInitialValues: PurchaseRequestStaff
|
||||
};
|
||||
|
||||
export const PurchaseRequestStaffApprovalFormDefaultValues = (
|
||||
purchase?: Purchase
|
||||
purchase?: Purchase,
|
||||
type?: 'add' | 'edit'
|
||||
): PurchaseRequestStaffApprovalFormSchemaType => {
|
||||
return {
|
||||
action: 'APPROVED',
|
||||
@@ -331,8 +332,18 @@ export const PurchaseRequestStaffApprovalFormDefaultValues = (
|
||||
label: item.warehouse?.name || '',
|
||||
},
|
||||
qty: item.sub_qty || item.qty || 0,
|
||||
price: item.price,
|
||||
total_price: item.total_price,
|
||||
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!')
|
||||
.typeError('Item pembelian wajib diisi!'),
|
||||
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!')
|
||||
.min(1, 'Minimal upload 1 dokumen surat jalan!')
|
||||
.typeError('Dokumen surat jalan wajib diupload!'),
|
||||
|
||||
@@ -294,9 +294,9 @@ const PurchaseOrderStaffApprovalForm = ({
|
||||
// ===== FORM CONFIGURATION =====
|
||||
const formikInitialValues = useMemo(() => {
|
||||
return initialValues
|
||||
? PurchaseRequestStaffApprovalFormDefaultValues(initialValues)
|
||||
? PurchaseRequestStaffApprovalFormDefaultValues(initialValues, type)
|
||||
: PurchaseRequestStaffApprovalFormInitialValues;
|
||||
}, [initialValues]);
|
||||
}, [initialValues, type]);
|
||||
|
||||
const formik = useFormik({
|
||||
initialValues: formikInitialValues,
|
||||
@@ -485,9 +485,18 @@ const PurchaseOrderStaffApprovalForm = ({
|
||||
},
|
||||
warehouse_id: purchaseItem.warehouse_id || 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:
|
||||
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;
|
||||
});
|
||||
@@ -1140,6 +1149,7 @@ const PurchaseOrderStaffApprovalForm = ({
|
||||
color='warning'
|
||||
className='px-4'
|
||||
onClick={() => {
|
||||
formik.setValues(formikInitialValues);
|
||||
formik.resetForm();
|
||||
setPurchaseOrderFormErrorMessage('');
|
||||
onCancel?.();
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { BaseApiService } from '@/services/api/base';
|
||||
import { BaseApiResponse } from '@/types/api/api-general';
|
||||
import {
|
||||
CreateProductWarehousePayload,
|
||||
ProductWarehouse,
|
||||
@@ -20,11 +21,38 @@ export const ProductWarehouseApi = new BaseApiService<
|
||||
UpdateProductWarehousePayload
|
||||
>('/inventory/product-warehouses');
|
||||
|
||||
export const MovementApi = new BaseApiService<
|
||||
export class MovementApiService extends BaseApiService<
|
||||
Movement,
|
||||
CreateMovementPayload,
|
||||
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<
|
||||
InventoryAdjustment,
|
||||
|
||||
+15
-1
@@ -14,6 +14,14 @@ type MovementWarehouse = {
|
||||
};
|
||||
};
|
||||
|
||||
export type MovementDocument = {
|
||||
id: number;
|
||||
path: string;
|
||||
name: string;
|
||||
ext: string;
|
||||
size: number;
|
||||
};
|
||||
|
||||
export type BaseMovement = {
|
||||
id: number;
|
||||
transfer_reason: string;
|
||||
@@ -39,6 +47,7 @@ export type BaseMovement = {
|
||||
document_path: string;
|
||||
shipping_cost_item: number;
|
||||
shipping_cost_total: number;
|
||||
document?: MovementDocument;
|
||||
items: {
|
||||
id: number;
|
||||
stock_transfer_detail_id: number;
|
||||
@@ -49,7 +58,7 @@ export type BaseMovement = {
|
||||
|
||||
export type Movement = BaseMetadata & BaseMovement;
|
||||
|
||||
export type CreateMovementPayload = {
|
||||
export type CreateMovementPayloadData = {
|
||||
transfer_reason: string;
|
||||
transfer_date: string;
|
||||
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;
|
||||
brand: string;
|
||||
sku: string;
|
||||
ProductPrice: number;
|
||||
SellingPrice?: number;
|
||||
product_price: number;
|
||||
selling_price?: number;
|
||||
tax?: number;
|
||||
|
||||
+1
-3
@@ -1,7 +1,4 @@
|
||||
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';
|
||||
|
||||
// ==================== GET ALL RESPONSE ====================
|
||||
@@ -11,6 +8,7 @@ export type Uniformity = BaseMetadata & {
|
||||
location_name: string;
|
||||
flock_name: string;
|
||||
kandang_name: string;
|
||||
file_name: string;
|
||||
applied_at: string;
|
||||
week: number;
|
||||
status: string;
|
||||
|
||||
Vendored
+2
@@ -10,6 +10,8 @@ export type PurchaseItemProduct = {
|
||||
id: number;
|
||||
name: string;
|
||||
flags?: string[];
|
||||
ProductPrice?: number;
|
||||
SellingPrice?: number;
|
||||
uom?: {
|
||||
name: string;
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user