mirror of
https://gitlab.com/mbugroup/lti-web-client.git
synced 2026-05-24 15:25:46 +00:00
Merge branch 'development' of https://gitlab.com/mbugroup/lti-web-client into feat/closing-finance-kandang
This commit is contained in:
@@ -1,6 +1,7 @@
|
|||||||
'use client';
|
'use client';
|
||||||
|
|
||||||
import { ChangeEventHandler, useEffect, useState } from 'react';
|
import { ChangeEventHandler, useEffect, useState } from 'react';
|
||||||
|
import { useSearchParams } from 'next/navigation';
|
||||||
import useSWR from 'swr';
|
import useSWR from 'swr';
|
||||||
import { ColumnDef, SortingState } from '@tanstack/react-table';
|
import { ColumnDef, SortingState } from '@tanstack/react-table';
|
||||||
|
|
||||||
@@ -23,6 +24,9 @@ interface ClosingIncomingSapronaksTableProps {
|
|||||||
const ClosingIncomingSapronaksTable = ({
|
const ClosingIncomingSapronaksTable = ({
|
||||||
projectFlockId,
|
projectFlockId,
|
||||||
}: ClosingIncomingSapronaksTableProps) => {
|
}: ClosingIncomingSapronaksTableProps) => {
|
||||||
|
const searchParams = useSearchParams();
|
||||||
|
const kandangId = searchParams.get('kandangId');
|
||||||
|
|
||||||
const {
|
const {
|
||||||
state: tableFilterState,
|
state: tableFilterState,
|
||||||
updateFilter,
|
updateFilter,
|
||||||
@@ -43,7 +47,7 @@ const ClosingIncomingSapronaksTable = ({
|
|||||||
|
|
||||||
const { data: incomingSapronaks, isLoading: isLoadingIncomingSapronaks } =
|
const { data: incomingSapronaks, isLoading: isLoadingIncomingSapronaks } =
|
||||||
useSWR(
|
useSWR(
|
||||||
`${ClosingApi.basePath}/${projectFlockId}/sapronak${getTableFilterQueryString()}&type=incoming`,
|
`${ClosingApi.basePath}/${projectFlockId}/sapronak${getTableFilterQueryString()}&type=incoming&kandang_id=${kandangId ? `${kandangId}` : ''}`,
|
||||||
ClosingApi.getAllIncomingSapronakFetcher,
|
ClosingApi.getAllIncomingSapronakFetcher,
|
||||||
{
|
{
|
||||||
keepPreviousData: true,
|
keepPreviousData: true,
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
'use client';
|
'use client';
|
||||||
|
|
||||||
import { ChangeEventHandler, useEffect, useState } from 'react';
|
import { ChangeEventHandler, useEffect, useState } from 'react';
|
||||||
|
import { useSearchParams } from 'next/navigation';
|
||||||
import useSWR from 'swr';
|
import useSWR from 'swr';
|
||||||
import { ColumnDef, SortingState } from '@tanstack/react-table';
|
import { ColumnDef, SortingState } from '@tanstack/react-table';
|
||||||
|
|
||||||
@@ -23,6 +24,9 @@ interface ClosingOutgoingSapronaksTableProps {
|
|||||||
const ClosingOutgoingSapronaksTable = ({
|
const ClosingOutgoingSapronaksTable = ({
|
||||||
projectFlockId,
|
projectFlockId,
|
||||||
}: ClosingOutgoingSapronaksTableProps) => {
|
}: ClosingOutgoingSapronaksTableProps) => {
|
||||||
|
const searchParams = useSearchParams();
|
||||||
|
const kandangId = searchParams.get('kandangId');
|
||||||
|
|
||||||
const {
|
const {
|
||||||
state: tableFilterState,
|
state: tableFilterState,
|
||||||
updateFilter,
|
updateFilter,
|
||||||
@@ -43,7 +47,7 @@ const ClosingOutgoingSapronaksTable = ({
|
|||||||
|
|
||||||
const { data: outgoingSapronaks, isLoading: isLoadingOutgoingSapronaks } =
|
const { data: outgoingSapronaks, isLoading: isLoadingOutgoingSapronaks } =
|
||||||
useSWR(
|
useSWR(
|
||||||
`${ClosingApi.basePath}/${projectFlockId}/sapronak${getTableFilterQueryString()}&type=outgoing`,
|
`${ClosingApi.basePath}/${projectFlockId}/sapronak${getTableFilterQueryString()}&type=outgoing&kandang_id=${kandangId ? `${kandangId}` : ''}`,
|
||||||
ClosingApi.getAllOutgoingSapronakFetcher,
|
ClosingApi.getAllOutgoingSapronakFetcher,
|
||||||
{
|
{
|
||||||
keepPreviousData: true,
|
keepPreviousData: true,
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
'use client';
|
'use client';
|
||||||
|
|
||||||
|
import { useSearchParams } from 'next/navigation';
|
||||||
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';
|
||||||
@@ -12,9 +13,12 @@ interface ClosingProductionDataTabContentProps {
|
|||||||
const ClosingProductionDataTabContent = ({
|
const ClosingProductionDataTabContent = ({
|
||||||
projectFlockId,
|
projectFlockId,
|
||||||
}: ClosingProductionDataTabContentProps) => {
|
}: ClosingProductionDataTabContentProps) => {
|
||||||
|
const searchParams = useSearchParams();
|
||||||
|
const kandangId = searchParams.get('kandangId');
|
||||||
|
|
||||||
const { data: productionData, isLoading } = useSWR(
|
const { data: productionData, isLoading } = useSWR(
|
||||||
`${ClosingApi.basePath}/${projectFlockId}/production-data`,
|
`${ClosingApi.basePath}/${projectFlockId}/production-data?kandang_id=${kandangId ? `${kandangId}` : ''}`,
|
||||||
() => ClosingApi.getProductionData(projectFlockId)
|
() => ClosingApi.getProductionData(projectFlockId, Number(kandangId))
|
||||||
);
|
);
|
||||||
|
|
||||||
if (isLoading) {
|
if (isLoading) {
|
||||||
|
|||||||
@@ -11,6 +11,13 @@ import {
|
|||||||
type MarketingSchemaType = {
|
type MarketingSchemaType = {
|
||||||
customer_id: number | undefined;
|
customer_id: number | undefined;
|
||||||
sales_person_id: number | undefined;
|
sales_person_id: number | undefined;
|
||||||
|
sales_person:
|
||||||
|
| {
|
||||||
|
value: number;
|
||||||
|
label: string;
|
||||||
|
}
|
||||||
|
| undefined
|
||||||
|
| null;
|
||||||
customer:
|
customer:
|
||||||
| {
|
| {
|
||||||
value: number;
|
value: number;
|
||||||
@@ -33,7 +40,11 @@ type DeliveryOrderSchemaType = {
|
|||||||
export const SalesOrderSchema: Yup.ObjectSchema<SalesOrderSchemaType> =
|
export const SalesOrderSchema: Yup.ObjectSchema<SalesOrderSchemaType> =
|
||||||
Yup.object({
|
Yup.object({
|
||||||
customer_id: Yup.number().required('Customer wajib diisi!'),
|
customer_id: Yup.number().required('Customer wajib diisi!'),
|
||||||
sales_person_id: Yup.number().required('Sales Person wajib diisi!'),
|
sales_person_id: Yup.number().required('Sales wajib diisi!'),
|
||||||
|
sales_person: Yup.object({
|
||||||
|
value: Yup.number().required(),
|
||||||
|
label: Yup.string().required(),
|
||||||
|
}).nullable(),
|
||||||
customer: Yup.object({
|
customer: Yup.object({
|
||||||
value: Yup.number().required(),
|
value: Yup.number().required(),
|
||||||
label: Yup.string().required(),
|
label: Yup.string().required(),
|
||||||
|
|||||||
@@ -50,6 +50,8 @@ import { DeliveryOrderProductFormValues } from '@/components/pages/marketing/for
|
|||||||
import RequirePermission from '@/components/helper/RequirePermission';
|
import RequirePermission from '@/components/helper/RequirePermission';
|
||||||
import AlertErrorList from '@/components/helper/form/FormErrors';
|
import AlertErrorList from '@/components/helper/form/FormErrors';
|
||||||
import { useFormikErrorList } from '@/services/hooks/useFormikErrorList';
|
import { useFormikErrorList } from '@/services/hooks/useFormikErrorList';
|
||||||
|
import { CreatedUser } from '@/types/api/api-general';
|
||||||
|
import { UserApi } from '@/services/api/user';
|
||||||
|
|
||||||
const MemoizedSalesOrderProductTable = memo(SalesOrderProductTable);
|
const MemoizedSalesOrderProductTable = memo(SalesOrderProductTable);
|
||||||
const MemoizedSalesOrderProductForm = memo(SalesOrderProductForm);
|
const MemoizedSalesOrderProductForm = memo(SalesOrderProductForm);
|
||||||
@@ -244,7 +246,15 @@ const MarketingForm = ({
|
|||||||
const {
|
const {
|
||||||
options: customerOptions,
|
options: customerOptions,
|
||||||
isLoadingOptions: isLoadingCustomerOptions,
|
isLoadingOptions: isLoadingCustomerOptions,
|
||||||
|
setInputValue: setInputCustomerValue,
|
||||||
|
loadMore: loadMoreCustomer,
|
||||||
} = useSelect<Customer>(CustomerApi.basePath, 'id', 'name');
|
} = useSelect<Customer>(CustomerApi.basePath, 'id', 'name');
|
||||||
|
const {
|
||||||
|
options: salesOptions,
|
||||||
|
isLoadingOptions: isLoadingSalesOptions,
|
||||||
|
setInputValue: setInputSalesValue,
|
||||||
|
loadMore: loadMoreSales,
|
||||||
|
} = useSelect<CreatedUser>(UserApi.basePath, 'id', 'name');
|
||||||
|
|
||||||
// ================== SETUP FORMIK ==================
|
// ================== SETUP FORMIK ==================
|
||||||
const formikInitialValues = useMemo<
|
const formikInitialValues = useMemo<
|
||||||
@@ -255,6 +265,12 @@ const MarketingForm = ({
|
|||||||
notes: initialValues?.notes || undefined,
|
notes: initialValues?.notes || undefined,
|
||||||
customer_id: initialValues?.customer?.id || undefined,
|
customer_id: initialValues?.customer?.id || undefined,
|
||||||
sales_person_id: initialValues?.sales_person?.id || 1,
|
sales_person_id: initialValues?.sales_person?.id || 1,
|
||||||
|
sales_person: initialValues?.sales_person
|
||||||
|
? {
|
||||||
|
value: initialValues.sales_person.id,
|
||||||
|
label: initialValues.sales_person.name,
|
||||||
|
}
|
||||||
|
: null,
|
||||||
customer: initialValues?.customer
|
customer: initialValues?.customer
|
||||||
? {
|
? {
|
||||||
value: initialValues.customer.id,
|
value: initialValues.customer.id,
|
||||||
@@ -443,6 +459,13 @@ const MarketingForm = ({
|
|||||||
},
|
},
|
||||||
[]
|
[]
|
||||||
);
|
);
|
||||||
|
const handleChangeSalesPerson = useCallback(
|
||||||
|
(val: OptionType | OptionType[] | null) => {
|
||||||
|
formik.setFieldValue('sales_person_id', (val as OptionType)?.value);
|
||||||
|
formik.setFieldValue('sales_person', val as OptionType);
|
||||||
|
},
|
||||||
|
[]
|
||||||
|
);
|
||||||
const handleDelete = useCallback(() => {
|
const handleDelete = useCallback(() => {
|
||||||
deleteModal.openModal();
|
deleteModal.openModal();
|
||||||
}, [deleteModal]);
|
}, [deleteModal]);
|
||||||
@@ -580,6 +603,7 @@ const MarketingForm = ({
|
|||||||
className={{
|
className={{
|
||||||
wrapper: 'bg-white w-full',
|
wrapper: 'bg-white w-full',
|
||||||
}}
|
}}
|
||||||
|
variant='bordered'
|
||||||
>
|
>
|
||||||
<div className='grid sm:grid-cols-2 gap-3 mt-3'>
|
<div className='grid sm:grid-cols-2 gap-3 mt-3'>
|
||||||
<SelectInput
|
<SelectInput
|
||||||
@@ -588,6 +612,8 @@ const MarketingForm = ({
|
|||||||
isLoading={isLoadingCustomerOptions}
|
isLoading={isLoadingCustomerOptions}
|
||||||
value={formik.values.customer}
|
value={formik.values.customer}
|
||||||
onChange={handleChangeCustomer}
|
onChange={handleChangeCustomer}
|
||||||
|
onInputChange={setInputCustomerValue}
|
||||||
|
onMenuScrollToBottom={loadMoreCustomer}
|
||||||
isError={
|
isError={
|
||||||
formik.touched.customer_id && Boolean(formik.errors.customer_id)
|
formik.touched.customer_id && Boolean(formik.errors.customer_id)
|
||||||
}
|
}
|
||||||
@@ -617,6 +643,7 @@ const MarketingForm = ({
|
|||||||
className={{
|
className={{
|
||||||
wrapper: 'bg-white w-full',
|
wrapper: 'bg-white w-full',
|
||||||
}}
|
}}
|
||||||
|
variant='bordered'
|
||||||
>
|
>
|
||||||
<MemoizedSalesOrderProductTable
|
<MemoizedSalesOrderProductTable
|
||||||
formType={formType}
|
formType={formType}
|
||||||
@@ -651,19 +678,42 @@ const MarketingForm = ({
|
|||||||
|
|
||||||
{/* Input Notes */}
|
{/* Input Notes */}
|
||||||
<div className='grid sm:grid-cols-2 gap-3'>
|
<div className='grid sm:grid-cols-2 gap-3'>
|
||||||
<DebouncedTextArea
|
<div className='flex flex-col h-full items-end gap-3'>
|
||||||
required
|
<SelectInput
|
||||||
name='notes'
|
label='Sales'
|
||||||
label='Catatan'
|
options={salesOptions}
|
||||||
rows={3}
|
isLoading={isLoadingSalesOptions}
|
||||||
placeholder='Masukan catatan penjualan'
|
value={formik.values.sales_person}
|
||||||
value={formik.values.notes}
|
onChange={handleChangeSalesPerson}
|
||||||
onChange={formik.handleChange}
|
onInputChange={setInputSalesValue}
|
||||||
isError={formik.touched.notes && Boolean(formik.errors.notes)}
|
onMenuScrollToBottom={loadMoreSales}
|
||||||
errorMessage={formik.errors.notes}
|
isError={
|
||||||
disabled={formType === 'add_deliver' || formType === 'edit_deliver'}
|
formik.touched.sales_person_id &&
|
||||||
/>
|
Boolean(formik.errors.sales_person_id)
|
||||||
<div className='flex flex-col h-full justify-between items-end py-6'>
|
}
|
||||||
|
errorMessage={formik.errors.sales_person_id}
|
||||||
|
isClearable
|
||||||
|
placeholder='Pilih Sales'
|
||||||
|
isDisabled={
|
||||||
|
formType === 'add_deliver' || formType === 'edit_deliver'
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
<DebouncedTextArea
|
||||||
|
required
|
||||||
|
name='notes'
|
||||||
|
label='Catatan'
|
||||||
|
rows={3}
|
||||||
|
placeholder='Masukan catatan penjualan'
|
||||||
|
value={formik.values.notes}
|
||||||
|
onChange={formik.handleChange}
|
||||||
|
isError={formik.touched.notes && Boolean(formik.errors.notes)}
|
||||||
|
errorMessage={formik.errors.notes}
|
||||||
|
disabled={
|
||||||
|
formType === 'add_deliver' || formType === 'edit_deliver'
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div className='flex flex-col h-full justify-end items-end'>
|
||||||
<span>Total Penjualan</span>
|
<span>Total Penjualan</span>
|
||||||
<span className='text-lg font-semibold'>
|
<span className='text-lg font-semibold'>
|
||||||
{formatCurrency(grandTotal)}{' '}
|
{formatCurrency(grandTotal)}{' '}
|
||||||
|
|||||||
+99
-35
@@ -18,6 +18,11 @@ import * as Yup from 'yup';
|
|||||||
import { isResponseSuccess } from '@/lib/api-helper';
|
import { isResponseSuccess } from '@/lib/api-helper';
|
||||||
import AlertErrorList from '@/components/helper/form/FormErrors';
|
import AlertErrorList from '@/components/helper/form/FormErrors';
|
||||||
import { useFormikErrorList } from '@/services/hooks/useFormikErrorList';
|
import { useFormikErrorList } from '@/services/hooks/useFormikErrorList';
|
||||||
|
import useSWR from 'swr';
|
||||||
|
import { ProductApi } from '@/services/api/master-data';
|
||||||
|
|
||||||
|
const roundWeight = (value: number) => Number(value.toFixed(2));
|
||||||
|
const roundPrice = (value: number) => Math.round(value);
|
||||||
|
|
||||||
const DeliveryOrderProductForm = ({
|
const DeliveryOrderProductForm = ({
|
||||||
formState,
|
formState,
|
||||||
@@ -43,6 +48,17 @@ const DeliveryOrderProductForm = ({
|
|||||||
);
|
);
|
||||||
const [currentInput, setCurrentInput] = useState<string>('');
|
const [currentInput, setCurrentInput] = useState<string>('');
|
||||||
|
|
||||||
|
// ============ Fetch Data ============
|
||||||
|
const { data: productData } = useSWR(
|
||||||
|
selectedProduct?.value
|
||||||
|
? ProductApi.basePath + '/' + selectedProduct?.value
|
||||||
|
: null,
|
||||||
|
() =>
|
||||||
|
selectedProduct?.value
|
||||||
|
? ProductApi.getSingle(Number(selectedProduct?.value))
|
||||||
|
: undefined
|
||||||
|
);
|
||||||
|
|
||||||
const salesOrder = salesOrders.find(
|
const salesOrder = salesOrders.find(
|
||||||
(item) => item.id === initialValues?.marketing_product_id
|
(item) => item.id === initialValues?.marketing_product_id
|
||||||
);
|
);
|
||||||
@@ -113,22 +129,60 @@ const DeliveryOrderProductForm = ({
|
|||||||
|
|
||||||
const handleBlurField = (field: string) => {
|
const handleBlurField = (field: string) => {
|
||||||
setCurrentInput(field);
|
setCurrentInput(field);
|
||||||
const { qty, unit_price, total_price, avg_weight, total_weight } =
|
|
||||||
formik.values;
|
|
||||||
|
|
||||||
if (field === 'unit_price' || field === 'total_price' || field === 'qty') {
|
const qty = Number(formik.values.qty || 0);
|
||||||
if (qty && unit_price && (field === 'unit_price' || field === 'qty')) {
|
const avgWeight = Number(formik.values.avg_weight || 0);
|
||||||
formik.setFieldValue('total_price', Number(qty) * Number(unit_price));
|
const totalWeight = Number(formik.values.total_weight || 0);
|
||||||
} else if (qty && total_price && field === 'total_price') {
|
const unitPrice = Number(formik.values.unit_price || 0);
|
||||||
formik.setFieldValue('unit_price', Number(total_price) / Number(qty));
|
const totalPrice = Number(formik.values.total_price || 0);
|
||||||
|
|
||||||
|
if (qty <= 0) return;
|
||||||
|
|
||||||
|
switch (field) {
|
||||||
|
// ===== SOURCE FIELDS =====
|
||||||
|
case 'qty': {
|
||||||
|
if (avgWeight > 0) {
|
||||||
|
formik.setFieldValue('total_weight', roundWeight(qty * avgWeight));
|
||||||
|
}
|
||||||
|
|
||||||
|
if (unitPrice > 0) {
|
||||||
|
formik.setFieldValue('total_price', roundPrice(qty * unitPrice));
|
||||||
|
}
|
||||||
|
break;
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
if (field === 'avg_weight' || field === 'total_weight' || field === 'qty') {
|
case 'avg_weight': {
|
||||||
if (qty && avg_weight && (field === 'avg_weight' || field === 'qty')) {
|
if (avgWeight > 0) {
|
||||||
formik.setFieldValue('total_weight', Number(qty) * Number(avg_weight));
|
const tw = roundWeight(qty * avgWeight);
|
||||||
} else if (qty && total_weight && field === 'total_weight') {
|
formik.setFieldValue('total_weight', tw);
|
||||||
formik.setFieldValue('avg_weight', Number(total_weight) / Number(qty));
|
|
||||||
|
if (unitPrice > 0) {
|
||||||
|
formik.setFieldValue('total_price', roundPrice(qty * unitPrice));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
case 'unit_price': {
|
||||||
|
if (unitPrice > 0) {
|
||||||
|
formik.setFieldValue('total_price', roundPrice(qty * unitPrice));
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ===== TOTAL EDITABLE =====
|
||||||
|
case 'total_weight': {
|
||||||
|
if (totalWeight > 0) {
|
||||||
|
formik.setFieldValue('avg_weight', roundWeight(totalWeight / qty));
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
case 'total_price': {
|
||||||
|
if (totalPrice > 0) {
|
||||||
|
formik.setFieldValue('unit_price', roundPrice(totalPrice / qty));
|
||||||
|
}
|
||||||
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
@@ -183,7 +237,7 @@ const DeliveryOrderProductForm = ({
|
|||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
<div className='grid sm:grid-cols-2 gap-4'>
|
<div className='grid sm:grid-cols-3 gap-4'>
|
||||||
<SelectInput
|
<SelectInput
|
||||||
options={options}
|
options={options}
|
||||||
label='Produk'
|
label='Produk'
|
||||||
@@ -287,7 +341,9 @@ const DeliveryOrderProductForm = ({
|
|||||||
isError={Boolean(formik.errors.vehicle_number)}
|
isError={Boolean(formik.errors.vehicle_number)}
|
||||||
errorMessage={formik.errors.vehicle_number}
|
errorMessage={formik.errors.vehicle_number}
|
||||||
/>
|
/>
|
||||||
|
</div>
|
||||||
|
<div className='divider my-6'></div>
|
||||||
|
<div className='grid sm:grid-cols-3 gap-4'>
|
||||||
<NumberInput
|
<NumberInput
|
||||||
required
|
required
|
||||||
label='Kuantitas'
|
label='Kuantitas'
|
||||||
@@ -301,33 +357,28 @@ 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'
|
||||||
|
endAdornment={
|
||||||
|
<div className='flex items-center gap-2'>
|
||||||
|
<span className='text-sm text-gray-500'>
|
||||||
|
{isResponseSuccess(productData)
|
||||||
|
? productData?.data?.uom.name
|
||||||
|
: ''}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
bottomLabel={
|
bottomLabel={
|
||||||
formik.values.marketing_product_id
|
formik.values.marketing_product_id
|
||||||
? 'Stok dijual: ' +
|
? 'Stok dijual: ' +
|
||||||
salesOrders?.find(
|
salesOrders?.find(
|
||||||
(item) => item.id === formik.values.marketing_product_id
|
(item) => item.id === formik.values.marketing_product_id
|
||||||
)?.qty
|
)?.qty +
|
||||||
|
' ' +
|
||||||
|
(isResponseSuccess(productData)
|
||||||
|
? productData?.data?.uom.name
|
||||||
|
: '')
|
||||||
: ''
|
: ''
|
||||||
}
|
}
|
||||||
/>
|
/>
|
||||||
</div>
|
|
||||||
<div className='divider my-6'></div>
|
|
||||||
<div className='grid sm:grid-cols-2 gap-4'>
|
|
||||||
<NumberInput
|
|
||||||
required
|
|
||||||
label='Avg. Bobot (Kg)'
|
|
||||||
name='avg_weight'
|
|
||||||
value={formik.values.avg_weight}
|
|
||||||
onChange={(e) => {
|
|
||||||
formik.handleChange(e);
|
|
||||||
setCurrentInput(e.target.name);
|
|
||||||
}}
|
|
||||||
onBlur={() => handleBlurField('avg_weight')}
|
|
||||||
isError={Boolean(formik.errors.avg_weight)}
|
|
||||||
errorMessage={formik.errors.avg_weight}
|
|
||||||
placeholder='Masukan Bobot Rata-rata'
|
|
||||||
/>
|
|
||||||
|
|
||||||
<NumberInput
|
<NumberInput
|
||||||
required
|
required
|
||||||
label='Harga Satuan (Rp)'
|
label='Harga Satuan (Rp)'
|
||||||
@@ -342,7 +393,20 @@ const DeliveryOrderProductForm = ({
|
|||||||
errorMessage={formik.errors.unit_price}
|
errorMessage={formik.errors.unit_price}
|
||||||
placeholder='Masukan Harga Satuan'
|
placeholder='Masukan Harga Satuan'
|
||||||
/>
|
/>
|
||||||
|
<NumberInput
|
||||||
|
required
|
||||||
|
label='Avg. Bobot (Kg)'
|
||||||
|
name='avg_weight'
|
||||||
|
value={formik.values.avg_weight}
|
||||||
|
onChange={(e) => {
|
||||||
|
formik.handleChange(e);
|
||||||
|
setCurrentInput(e.target.name);
|
||||||
|
}}
|
||||||
|
onBlur={() => handleBlurField('avg_weight')}
|
||||||
|
isError={Boolean(formik.errors.avg_weight)}
|
||||||
|
errorMessage={formik.errors.avg_weight}
|
||||||
|
placeholder='Masukan Bobot Rata-rata'
|
||||||
|
/>
|
||||||
<NumberInput
|
<NumberInput
|
||||||
required
|
required
|
||||||
label='Total Bobot (Kg)'
|
label='Total Bobot (Kg)'
|
||||||
|
|||||||
+111
-50
@@ -11,7 +11,7 @@ import SelectInput, {
|
|||||||
useSelect,
|
useSelect,
|
||||||
} from '@/components/input/SelectInput';
|
} from '@/components/input/SelectInput';
|
||||||
import { Kandang } from '@/types/api/master-data/kandang';
|
import { Kandang } from '@/types/api/master-data/kandang';
|
||||||
import { WarehouseApi } from '@/services/api/master-data';
|
import { ProductApi, UomApi, WarehouseApi } from '@/services/api/master-data';
|
||||||
import { ProductWarehouse } from '@/types/api/inventory/product-warehouse';
|
import { ProductWarehouse } from '@/types/api/inventory/product-warehouse';
|
||||||
import { ProductWarehouseApi } from '@/services/api/inventory';
|
import { ProductWarehouseApi } from '@/services/api/inventory';
|
||||||
import NumberInput from '@/components/input/NumberInput';
|
import NumberInput from '@/components/input/NumberInput';
|
||||||
@@ -26,6 +26,10 @@ import PatternInput from '@/components/input/PatternInput';
|
|||||||
import Alert from '@/components/Alert';
|
import Alert from '@/components/Alert';
|
||||||
import AlertErrorList from '@/components/helper/form/FormErrors';
|
import AlertErrorList from '@/components/helper/form/FormErrors';
|
||||||
import { useFormikErrorList } from '@/services/hooks/useFormikErrorList';
|
import { useFormikErrorList } from '@/services/hooks/useFormikErrorList';
|
||||||
|
import useSWR from 'swr';
|
||||||
|
|
||||||
|
const roundWeight = (value: number) => Number(value.toFixed(2));
|
||||||
|
const roundPrice = (value: number) => Math.round(value);
|
||||||
|
|
||||||
const SalesOrderProductForm = ({
|
const SalesOrderProductForm = ({
|
||||||
initialValues,
|
initialValues,
|
||||||
@@ -39,6 +43,19 @@ const SalesOrderProductForm = ({
|
|||||||
}) => {
|
}) => {
|
||||||
const [formErrorMessage, setFormErrorMessage] = useState('');
|
const [formErrorMessage, setFormErrorMessage] = useState('');
|
||||||
const [currentInput, setCurrentInput] = useState<string>('');
|
const [currentInput, setCurrentInput] = useState<string>('');
|
||||||
|
const [selectedProductWarehouse, setSelectedProductWarehouse] =
|
||||||
|
useState<ProductWarehouse | null>(null);
|
||||||
|
|
||||||
|
// ============ Fetch Data ============
|
||||||
|
const { data: productData } = useSWR(
|
||||||
|
selectedProductWarehouse?.product_id
|
||||||
|
? ProductApi.basePath + '/' + selectedProductWarehouse?.product_id
|
||||||
|
: null,
|
||||||
|
() =>
|
||||||
|
selectedProductWarehouse?.product_id
|
||||||
|
? ProductApi.getSingle(selectedProductWarehouse?.product_id)
|
||||||
|
: undefined
|
||||||
|
);
|
||||||
|
|
||||||
// ============ Formik ============
|
// ============ Formik ============
|
||||||
const formik = useFormik<SalesOrderProductFormValues>({
|
const formik = useFormik<SalesOrderProductFormValues>({
|
||||||
@@ -69,17 +86,21 @@ const SalesOrderProductForm = ({
|
|||||||
const {
|
const {
|
||||||
options: kandangSourceOptions,
|
options: kandangSourceOptions,
|
||||||
isLoadingOptions: isLoadingKandangSourceOptions,
|
isLoadingOptions: isLoadingKandangSourceOptions,
|
||||||
|
setInputValue: setKandangInputValue,
|
||||||
|
loadMore: loadMoreKandang,
|
||||||
} = useSelect<Kandang>(WarehouseApi.basePath, 'id', 'name');
|
} = useSelect<Kandang>(WarehouseApi.basePath, 'id', 'name');
|
||||||
|
|
||||||
const {
|
const {
|
||||||
options: warehouseSourceOptions,
|
options: warehouseSourceOptions,
|
||||||
rawData: warehouseSourceRawData,
|
rawData: warehouseSourceRawData,
|
||||||
isLoadingOptions: isLoadingWarehouseSourceOptions,
|
isLoadingOptions: isLoadingWarehouseSourceOptions,
|
||||||
|
setInputValue: setWarehouseInputValue,
|
||||||
|
loadMore: loadMoreWarehouse,
|
||||||
} = useSelect<ProductWarehouse>(
|
} = useSelect<ProductWarehouse>(
|
||||||
ProductWarehouseApi.basePath,
|
ProductWarehouseApi.basePath,
|
||||||
'id',
|
'id',
|
||||||
'product.name',
|
'product.name',
|
||||||
'search',
|
'',
|
||||||
{
|
{
|
||||||
warehouse_id: formik.values.kandang_id?.toString() ?? '',
|
warehouse_id: formik.values.kandang_id?.toString() ?? '',
|
||||||
}
|
}
|
||||||
@@ -112,6 +133,7 @@ const SalesOrderProductForm = ({
|
|||||||
const productWarehouse = warehouseSourceRawData?.data.find(
|
const productWarehouse = warehouseSourceRawData?.data.find(
|
||||||
(item: ProductWarehouse) => item.id === newId
|
(item: ProductWarehouse) => item.id === newId
|
||||||
);
|
);
|
||||||
|
setSelectedProductWarehouse(productWarehouse || null);
|
||||||
formik.setFieldValue('qty', productWarehouse?.quantity);
|
formik.setFieldValue('qty', productWarehouse?.quantity);
|
||||||
handleBlurField('qty');
|
handleBlurField('qty');
|
||||||
} else {
|
} else {
|
||||||
@@ -139,34 +161,60 @@ const SalesOrderProductForm = ({
|
|||||||
|
|
||||||
const handleBlurField = (field: string) => {
|
const handleBlurField = (field: string) => {
|
||||||
setCurrentInput(field);
|
setCurrentInput(field);
|
||||||
const { qty, unit_price, total_price, avg_weight, total_weight } =
|
|
||||||
formik.values;
|
|
||||||
|
|
||||||
if (field === 'unit_price' || field === 'total_price' || field === 'qty') {
|
const qty = Number(formik.values.qty || 0);
|
||||||
if (qty && unit_price && (field === 'unit_price' || field === 'qty')) {
|
const avgWeight = Number(formik.values.avg_weight || 0);
|
||||||
formik.setFieldValue(
|
const totalWeight = Number(formik.values.total_weight || 0);
|
||||||
'total_price',
|
const unitPrice = Number(formik.values.unit_price || 0);
|
||||||
(qty as number) * (unit_price as number)
|
const totalPrice = Number(formik.values.total_price || 0);
|
||||||
);
|
|
||||||
} else if (qty && total_price && field === 'total_price') {
|
if (qty <= 0) return;
|
||||||
formik.setFieldValue(
|
|
||||||
'unit_price',
|
switch (field) {
|
||||||
(total_price as number) / (qty as number)
|
// ===== SOURCE FIELDS =====
|
||||||
);
|
case 'qty': {
|
||||||
|
if (avgWeight > 0) {
|
||||||
|
formik.setFieldValue('total_weight', roundWeight(qty * avgWeight));
|
||||||
|
}
|
||||||
|
|
||||||
|
if (unitPrice > 0) {
|
||||||
|
formik.setFieldValue('total_price', roundPrice(qty * unitPrice));
|
||||||
|
}
|
||||||
|
break;
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
if (field === 'avg_weight' || field === 'total_weight' || field === 'qty') {
|
case 'avg_weight': {
|
||||||
if (qty && avg_weight && (field === 'avg_weight' || field === 'qty')) {
|
if (avgWeight > 0) {
|
||||||
formik.setFieldValue(
|
const tw = roundWeight(qty * avgWeight);
|
||||||
'total_weight',
|
formik.setFieldValue('total_weight', tw);
|
||||||
(qty as number) * (avg_weight as number)
|
|
||||||
);
|
if (unitPrice > 0) {
|
||||||
} else if (qty && total_weight && field === 'total_weight') {
|
formik.setFieldValue('total_price', roundPrice(qty * unitPrice));
|
||||||
formik.setFieldValue(
|
}
|
||||||
'avg_weight',
|
}
|
||||||
(total_weight as number) / (qty as number)
|
break;
|
||||||
);
|
}
|
||||||
|
|
||||||
|
case 'unit_price': {
|
||||||
|
if (unitPrice > 0) {
|
||||||
|
formik.setFieldValue('total_price', roundPrice(qty * unitPrice));
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ===== TOTAL EDITABLE =====
|
||||||
|
case 'total_weight': {
|
||||||
|
if (totalWeight > 0) {
|
||||||
|
formik.setFieldValue('avg_weight', roundWeight(totalWeight / qty));
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
case 'total_price': {
|
||||||
|
if (totalPrice > 0) {
|
||||||
|
formik.setFieldValue('unit_price', roundPrice(totalPrice / qty));
|
||||||
|
}
|
||||||
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
@@ -188,7 +236,7 @@ const SalesOrderProductForm = ({
|
|||||||
</Alert>
|
</Alert>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
<div className='grid sm:grid-cols-2 gap-4 z-200'>
|
<div className='grid sm:grid-cols-3 gap-4 z-200'>
|
||||||
<PatternInput
|
<PatternInput
|
||||||
name='vehicle_number'
|
name='vehicle_number'
|
||||||
label='No. Polisi'
|
label='No. Polisi'
|
||||||
@@ -215,6 +263,8 @@ const SalesOrderProductForm = ({
|
|||||||
value={formik.values.kandang}
|
value={formik.values.kandang}
|
||||||
onChange={kandangChangeHandler}
|
onChange={kandangChangeHandler}
|
||||||
isClearable
|
isClearable
|
||||||
|
onInputChange={setKandangInputValue}
|
||||||
|
onMenuScrollToBottom={loadMoreKandang}
|
||||||
isError={
|
isError={
|
||||||
formik.touched.kandang_id && Boolean(formik.errors.kandang_id)
|
formik.touched.kandang_id && Boolean(formik.errors.kandang_id)
|
||||||
}
|
}
|
||||||
@@ -228,6 +278,8 @@ const SalesOrderProductForm = ({
|
|||||||
isLoading={isLoadingWarehouseSourceOptions}
|
isLoading={isLoadingWarehouseSourceOptions}
|
||||||
value={formik.values.product_warehouse}
|
value={formik.values.product_warehouse}
|
||||||
onChange={warehouseChangeHandler}
|
onChange={warehouseChangeHandler}
|
||||||
|
onInputChange={setWarehouseInputValue}
|
||||||
|
onMenuScrollToBottom={loadMoreWarehouse}
|
||||||
isClearable
|
isClearable
|
||||||
placeholder={
|
placeholder={
|
||||||
formik.values.kandang_id
|
formik.values.kandang_id
|
||||||
@@ -243,6 +295,9 @@ const SalesOrderProductForm = ({
|
|||||||
}
|
}
|
||||||
errorMessage={formik.errors.product_warehouse_id}
|
errorMessage={formik.errors.product_warehouse_id}
|
||||||
/>
|
/>
|
||||||
|
</div>
|
||||||
|
<div className='divider my-6'></div>
|
||||||
|
<div className='grid sm:grid-cols-3 gap-4 z-200'>
|
||||||
<NumberInput
|
<NumberInput
|
||||||
required
|
required
|
||||||
label='Kuantitas'
|
label='Kuantitas'
|
||||||
@@ -256,6 +311,15 @@ 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'
|
||||||
|
endAdornment={
|
||||||
|
<div className='flex items-center gap-2'>
|
||||||
|
<span className='text-sm text-gray-500'>
|
||||||
|
{isResponseSuccess(productData)
|
||||||
|
? productData?.data?.uom.name
|
||||||
|
: ''}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
bottomLabel={
|
bottomLabel={
|
||||||
isResponseSuccess(warehouseSourceRawData) &&
|
isResponseSuccess(warehouseSourceRawData) &&
|
||||||
formik.values.product_warehouse_id
|
formik.values.product_warehouse_id
|
||||||
@@ -264,32 +328,13 @@ const SalesOrderProductForm = ({
|
|||||||
(item) => item.id === formik.values.product_warehouse_id
|
(item) => item.id === formik.values.product_warehouse_id
|
||||||
)?.quantity ?? 0
|
)?.quantity ?? 0
|
||||||
)} ${
|
)} ${
|
||||||
warehouseSourceRawData?.data?.find(
|
isResponseSuccess(productData)
|
||||||
(item) => item.id === formik.values.product_warehouse_id
|
? productData?.data?.uom.name
|
||||||
)?.product?.uom?.name ?? ''
|
: ''
|
||||||
}`
|
}`
|
||||||
: ''
|
: ''
|
||||||
}
|
}
|
||||||
/>
|
/>
|
||||||
</div>
|
|
||||||
<div className='divider my-6'></div>
|
|
||||||
<div className='grid sm:grid-cols-2 gap-4 z-200'>
|
|
||||||
<NumberInput
|
|
||||||
required
|
|
||||||
label='Avg. Bobot (Kg)'
|
|
||||||
name='avg_weight'
|
|
||||||
value={formik.values.avg_weight}
|
|
||||||
onChange={(e) => {
|
|
||||||
formik.handleChange(e);
|
|
||||||
setCurrentInput(e.target.name);
|
|
||||||
}}
|
|
||||||
onBlur={() => handleBlurField('avg_weight')}
|
|
||||||
isError={
|
|
||||||
formik.touched.avg_weight && Boolean(formik.errors.avg_weight)
|
|
||||||
}
|
|
||||||
errorMessage={formik.errors.avg_weight}
|
|
||||||
placeholder='Masukan Bobot Rata-rata'
|
|
||||||
/>
|
|
||||||
<NumberInput
|
<NumberInput
|
||||||
required
|
required
|
||||||
label='Harga Satuan (Rp)'
|
label='Harga Satuan (Rp)'
|
||||||
@@ -306,6 +351,22 @@ const SalesOrderProductForm = ({
|
|||||||
errorMessage={formik.errors.unit_price}
|
errorMessage={formik.errors.unit_price}
|
||||||
placeholder='Masukan Harga Satuan'
|
placeholder='Masukan Harga Satuan'
|
||||||
/>
|
/>
|
||||||
|
<NumberInput
|
||||||
|
required
|
||||||
|
label='Avg. Bobot (Kg)'
|
||||||
|
name='avg_weight'
|
||||||
|
value={formik.values.avg_weight}
|
||||||
|
onChange={(e) => {
|
||||||
|
formik.handleChange(e);
|
||||||
|
setCurrentInput(e.target.name);
|
||||||
|
}}
|
||||||
|
onBlur={() => handleBlurField('avg_weight')}
|
||||||
|
isError={
|
||||||
|
formik.touched.avg_weight && Boolean(formik.errors.avg_weight)
|
||||||
|
}
|
||||||
|
errorMessage={formik.errors.avg_weight}
|
||||||
|
placeholder='Masukan Bobot Rata-rata'
|
||||||
|
/>
|
||||||
<NumberInput
|
<NumberInput
|
||||||
required
|
required
|
||||||
label='Total Bobot (Kg)'
|
label='Total Bobot (Kg)'
|
||||||
|
|||||||
@@ -20,7 +20,7 @@ import RequirePermission from '@/components/helper/RequirePermission';
|
|||||||
import { Area } from '@/types/api/master-data/area';
|
import { Area } from '@/types/api/master-data/area';
|
||||||
import { AreaApi } from '@/services/api/master-data';
|
import { AreaApi } from '@/services/api/master-data';
|
||||||
import { cn } from '@/lib/helper';
|
import { cn } from '@/lib/helper';
|
||||||
import { isResponseSuccess } from '@/lib/api-helper';
|
import { isResponseError, isResponseSuccess } from '@/lib/api-helper';
|
||||||
import { useTableFilter } from '@/services/hooks/useTableFilter';
|
import { useTableFilter } from '@/services/hooks/useTableFilter';
|
||||||
import { ROWS_OPTIONS } from '@/config/constant';
|
import { ROWS_OPTIONS } from '@/config/constant';
|
||||||
|
|
||||||
@@ -164,7 +164,14 @@ const AreasTable = () => {
|
|||||||
const confirmationModalDeleteClickHandler = async () => {
|
const confirmationModalDeleteClickHandler = async () => {
|
||||||
setIsDeleteLoading(true);
|
setIsDeleteLoading(true);
|
||||||
|
|
||||||
await AreaApi.delete(selectedArea?.id as number);
|
const deleteResponse = await AreaApi.delete(selectedArea?.id as number);
|
||||||
|
|
||||||
|
if (isResponseError(deleteResponse)) {
|
||||||
|
toast.error(deleteResponse.message);
|
||||||
|
setIsDeleteLoading(false);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
refreshAreas();
|
refreshAreas();
|
||||||
|
|
||||||
deleteModal.closeModal();
|
deleteModal.closeModal();
|
||||||
|
|||||||
@@ -20,7 +20,7 @@ import RequirePermission from '@/components/helper/RequirePermission';
|
|||||||
import { Bank } from '@/types/api/master-data/bank';
|
import { Bank } from '@/types/api/master-data/bank';
|
||||||
import { BankApi } from '@/services/api/master-data';
|
import { BankApi } from '@/services/api/master-data';
|
||||||
import { cn } from '@/lib/helper';
|
import { cn } from '@/lib/helper';
|
||||||
import { isResponseSuccess } from '@/lib/api-helper';
|
import { isResponseError, isResponseSuccess } from '@/lib/api-helper';
|
||||||
import { useTableFilter } from '@/services/hooks/useTableFilter';
|
import { useTableFilter } from '@/services/hooks/useTableFilter';
|
||||||
import { ROWS_OPTIONS } from '@/config/constant';
|
import { ROWS_OPTIONS } from '@/config/constant';
|
||||||
|
|
||||||
@@ -177,7 +177,14 @@ const BanksTable = () => {
|
|||||||
const confirmationModalDeleteClickHandler = async () => {
|
const confirmationModalDeleteClickHandler = async () => {
|
||||||
setIsDeleteLoading(true);
|
setIsDeleteLoading(true);
|
||||||
|
|
||||||
await BankApi.delete(selectedBank?.id as number);
|
const deleteResponse = await BankApi.delete(selectedBank?.id as number);
|
||||||
|
|
||||||
|
if (isResponseError(deleteResponse)) {
|
||||||
|
toast.error(deleteResponse.message);
|
||||||
|
setIsDeleteLoading(false);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
refreshBanks();
|
refreshBanks();
|
||||||
|
|
||||||
deleteModal.closeModal();
|
deleteModal.closeModal();
|
||||||
|
|||||||
@@ -11,7 +11,7 @@ import RowDropdownOptions from '@/components/table/RowDropdownOptions';
|
|||||||
import RowOptionsMenuWrapper from '@/components/table/RowOptionsMenuWrapper';
|
import RowOptionsMenuWrapper from '@/components/table/RowOptionsMenuWrapper';
|
||||||
import RequirePermission from '@/components/helper/RequirePermission';
|
import RequirePermission from '@/components/helper/RequirePermission';
|
||||||
import { ROWS_OPTIONS } from '@/config/constant';
|
import { ROWS_OPTIONS } from '@/config/constant';
|
||||||
import { isResponseSuccess } from '@/lib/api-helper';
|
import { isResponseError, isResponseSuccess } from '@/lib/api-helper';
|
||||||
import { cn } from '@/lib/helper';
|
import { cn } from '@/lib/helper';
|
||||||
import { CustomerApi } from '@/services/api/master-data';
|
import { CustomerApi } from '@/services/api/master-data';
|
||||||
import { useTableFilter } from '@/services/hooks/useTableFilter';
|
import { useTableFilter } from '@/services/hooks/useTableFilter';
|
||||||
@@ -186,7 +186,16 @@ const CustomersTable = () => {
|
|||||||
const confirmationModalDeleteClickHandler = async () => {
|
const confirmationModalDeleteClickHandler = async () => {
|
||||||
setIsDeleteLoading(true);
|
setIsDeleteLoading(true);
|
||||||
|
|
||||||
await CustomerApi.delete(selectedCustomer?.id as number);
|
const deleteResponse = await CustomerApi.delete(
|
||||||
|
selectedCustomer?.id as number
|
||||||
|
);
|
||||||
|
|
||||||
|
if (isResponseError(deleteResponse)) {
|
||||||
|
toast.error(deleteResponse.message);
|
||||||
|
setIsDeleteLoading(false);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
refreshCustomers();
|
refreshCustomers();
|
||||||
|
|
||||||
deleteModal.closeModal();
|
deleteModal.closeModal();
|
||||||
|
|||||||
@@ -20,7 +20,7 @@ import RequirePermission from '@/components/helper/RequirePermission';
|
|||||||
import { Fcr } from '@/types/api/master-data/fcr';
|
import { Fcr } from '@/types/api/master-data/fcr';
|
||||||
import { FcrApi } from '@/services/api/master-data';
|
import { FcrApi } from '@/services/api/master-data';
|
||||||
import { cn } from '@/lib/helper';
|
import { cn } from '@/lib/helper';
|
||||||
import { isResponseSuccess } from '@/lib/api-helper';
|
import { isResponseError, isResponseSuccess } from '@/lib/api-helper';
|
||||||
import { useTableFilter } from '@/services/hooks/useTableFilter';
|
import { useTableFilter } from '@/services/hooks/useTableFilter';
|
||||||
import { ROWS_OPTIONS } from '@/config/constant';
|
import { ROWS_OPTIONS } from '@/config/constant';
|
||||||
|
|
||||||
@@ -164,7 +164,14 @@ const FcrsTable = () => {
|
|||||||
const confirmationModalDeleteClickHandler = async () => {
|
const confirmationModalDeleteClickHandler = async () => {
|
||||||
setIsDeleteLoading(true);
|
setIsDeleteLoading(true);
|
||||||
|
|
||||||
await FcrApi.delete(selectedFcr?.id as number);
|
const deleteResponse = await FcrApi.delete(selectedFcr?.id as number);
|
||||||
|
|
||||||
|
if (isResponseError(deleteResponse)) {
|
||||||
|
toast.error(deleteResponse.message);
|
||||||
|
setIsDeleteLoading(false);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
refreshFcrs();
|
refreshFcrs();
|
||||||
|
|
||||||
deleteModal.closeModal();
|
deleteModal.closeModal();
|
||||||
|
|||||||
@@ -19,7 +19,7 @@ import DebouncedTextInput from '@/components/input/DebouncedTextInput';
|
|||||||
import SelectInput, { OptionType } from '@/components/input/SelectInput';
|
import SelectInput, { OptionType } from '@/components/input/SelectInput';
|
||||||
import { ROWS_OPTIONS } from '@/config/constant';
|
import { ROWS_OPTIONS } from '@/config/constant';
|
||||||
import Table from '@/components/Table';
|
import Table from '@/components/Table';
|
||||||
import { isResponseSuccess } from '@/lib/api-helper';
|
import { isResponseError, isResponseSuccess } from '@/lib/api-helper';
|
||||||
import ConfirmationModal from '@/components/modal/ConfirmationModal';
|
import ConfirmationModal from '@/components/modal/ConfirmationModal';
|
||||||
|
|
||||||
const RowsOptions = ({
|
const RowsOptions = ({
|
||||||
@@ -182,7 +182,14 @@ const FlockTable = () => {
|
|||||||
const confirmationModalDeleteClickHandler = async () => {
|
const confirmationModalDeleteClickHandler = async () => {
|
||||||
setIsDeleteLoading(true);
|
setIsDeleteLoading(true);
|
||||||
|
|
||||||
await FlockApi.delete(selectedFlock?.id as number);
|
const deleteResponse = await FlockApi.delete(selectedFlock?.id as number);
|
||||||
|
|
||||||
|
if (isResponseError(deleteResponse)) {
|
||||||
|
toast.error(deleteResponse.message);
|
||||||
|
setIsDeleteLoading(false);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
refreshFlocks();
|
refreshFlocks();
|
||||||
|
|
||||||
deleteModal.closeModal();
|
deleteModal.closeModal();
|
||||||
|
|||||||
@@ -25,7 +25,7 @@ import RequirePermission from '@/components/helper/RequirePermission';
|
|||||||
import { Kandang } from '@/types/api/master-data/kandang';
|
import { Kandang } from '@/types/api/master-data/kandang';
|
||||||
import { KandangApi } from '@/services/api/master-data';
|
import { KandangApi } from '@/services/api/master-data';
|
||||||
import { cn, formatNumber } from '@/lib/helper';
|
import { cn, formatNumber } from '@/lib/helper';
|
||||||
import { isResponseSuccess } from '@/lib/api-helper';
|
import { isResponseError, isResponseSuccess } from '@/lib/api-helper';
|
||||||
import { useTableFilter } from '@/services/hooks/useTableFilter';
|
import { useTableFilter } from '@/services/hooks/useTableFilter';
|
||||||
import { ROWS_OPTIONS } from '@/config/constant';
|
import { ROWS_OPTIONS } from '@/config/constant';
|
||||||
|
|
||||||
@@ -199,7 +199,16 @@ const KandangsTable = () => {
|
|||||||
const confirmationModalDeleteClickHandler = async () => {
|
const confirmationModalDeleteClickHandler = async () => {
|
||||||
setIsDeleteLoading(true);
|
setIsDeleteLoading(true);
|
||||||
|
|
||||||
await KandangApi.delete(selectedKandang?.id as number);
|
const deleteResponse = await KandangApi.delete(
|
||||||
|
selectedKandang?.id as number
|
||||||
|
);
|
||||||
|
|
||||||
|
if (isResponseError(deleteResponse)) {
|
||||||
|
toast.error(deleteResponse.message);
|
||||||
|
setIsDeleteLoading(false);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
refreshKandangs();
|
refreshKandangs();
|
||||||
|
|
||||||
deleteModal.closeModal();
|
deleteModal.closeModal();
|
||||||
|
|||||||
@@ -215,7 +215,7 @@ const KandangForm = ({ type = 'add', initialValues }: KandangFormProps) => {
|
|||||||
required
|
required
|
||||||
label='Nama'
|
label='Nama'
|
||||||
name='name'
|
name='name'
|
||||||
placeholder='Masukkan nama lokasi'
|
placeholder='Masukkan nama kandang'
|
||||||
value={formik.values.name}
|
value={formik.values.name}
|
||||||
onChange={formik.handleChange}
|
onChange={formik.handleChange}
|
||||||
onBlur={formik.handleBlur}
|
onBlur={formik.handleBlur}
|
||||||
|
|||||||
@@ -25,7 +25,7 @@ import RequirePermission from '@/components/helper/RequirePermission';
|
|||||||
import { Location } from '@/types/api/master-data/location';
|
import { Location } from '@/types/api/master-data/location';
|
||||||
import { LocationApi } from '@/services/api/master-data';
|
import { LocationApi } from '@/services/api/master-data';
|
||||||
import { cn } from '@/lib/helper';
|
import { cn } from '@/lib/helper';
|
||||||
import { isResponseSuccess } from '@/lib/api-helper';
|
import { isResponseError, isResponseSuccess } from '@/lib/api-helper';
|
||||||
import { useTableFilter } from '@/services/hooks/useTableFilter';
|
import { useTableFilter } from '@/services/hooks/useTableFilter';
|
||||||
import { ROWS_OPTIONS } from '@/config/constant';
|
import { ROWS_OPTIONS } from '@/config/constant';
|
||||||
|
|
||||||
@@ -186,7 +186,16 @@ const LocationsTable = () => {
|
|||||||
const confirmationModalDeleteClickHandler = async () => {
|
const confirmationModalDeleteClickHandler = async () => {
|
||||||
setIsDeleteLoading(true);
|
setIsDeleteLoading(true);
|
||||||
|
|
||||||
await LocationApi.delete(selectedLocation?.id as number);
|
const deleteResponse = await LocationApi.delete(
|
||||||
|
selectedLocation?.id as number
|
||||||
|
);
|
||||||
|
|
||||||
|
if (isResponseError(deleteResponse)) {
|
||||||
|
toast.error(deleteResponse.message);
|
||||||
|
setIsDeleteLoading(false);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
refreshLocations();
|
refreshLocations();
|
||||||
|
|
||||||
deleteModal.closeModal();
|
deleteModal.closeModal();
|
||||||
|
|||||||
@@ -25,7 +25,7 @@ import RequirePermission from '@/components/helper/RequirePermission';
|
|||||||
import { Nonstock } from '@/types/api/master-data/nonstock';
|
import { Nonstock } from '@/types/api/master-data/nonstock';
|
||||||
import { NonstockApi } from '@/services/api/master-data';
|
import { NonstockApi } from '@/services/api/master-data';
|
||||||
import { cn } from '@/lib/helper';
|
import { cn } from '@/lib/helper';
|
||||||
import { isResponseSuccess } from '@/lib/api-helper';
|
import { isResponseError, isResponseSuccess } from '@/lib/api-helper';
|
||||||
import { useTableFilter } from '@/services/hooks/useTableFilter';
|
import { useTableFilter } from '@/services/hooks/useTableFilter';
|
||||||
import { ROWS_OPTIONS } from '@/config/constant';
|
import { ROWS_OPTIONS } from '@/config/constant';
|
||||||
|
|
||||||
@@ -198,7 +198,16 @@ const NonstocksTable = () => {
|
|||||||
const confirmationModalDeleteClickHandler = async () => {
|
const confirmationModalDeleteClickHandler = async () => {
|
||||||
setIsDeleteLoading(true);
|
setIsDeleteLoading(true);
|
||||||
|
|
||||||
await NonstockApi.delete(selectedNonstock?.id as number);
|
const deleteResponse = await NonstockApi.delete(
|
||||||
|
selectedNonstock?.id as number
|
||||||
|
);
|
||||||
|
|
||||||
|
if (isResponseError(deleteResponse)) {
|
||||||
|
toast.error(deleteResponse.message);
|
||||||
|
setIsDeleteLoading(false);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
refreshNonstocks();
|
refreshNonstocks();
|
||||||
|
|
||||||
deleteModal.closeModal();
|
deleteModal.closeModal();
|
||||||
|
|||||||
@@ -83,7 +83,7 @@ const NonstockForm = ({ type = 'add', initialValues }: NonstockFormProps) => {
|
|||||||
const formikInitialValues = useMemo<NonstockFormValues>(() => {
|
const formikInitialValues = useMemo<NonstockFormValues>(() => {
|
||||||
return {
|
return {
|
||||||
name: initialValues?.name ?? '',
|
name: initialValues?.name ?? '',
|
||||||
uomId: initialValues?.uom_id ?? 0,
|
uomId: initialValues?.uom?.id ?? 0,
|
||||||
uom: initialValues?.uom
|
uom: initialValues?.uom
|
||||||
? {
|
? {
|
||||||
value: initialValues?.uom?.id,
|
value: initialValues?.uom?.id,
|
||||||
@@ -229,7 +229,7 @@ const NonstockForm = ({ type = 'add', initialValues }: NonstockFormProps) => {
|
|||||||
required
|
required
|
||||||
label='Nama'
|
label='Nama'
|
||||||
name='name'
|
name='name'
|
||||||
placeholder='Masukkan nama lokasi'
|
placeholder='Masukkan nama nonstock'
|
||||||
value={formik.values.name}
|
value={formik.values.name}
|
||||||
onChange={formik.handleChange}
|
onChange={formik.handleChange}
|
||||||
onBlur={formik.handleBlur}
|
onBlur={formik.handleBlur}
|
||||||
|
|||||||
@@ -20,7 +20,7 @@ import RequirePermission from '@/components/helper/RequirePermission';
|
|||||||
import { ProductCategory } from '@/types/api/master-data/product-category';
|
import { ProductCategory } from '@/types/api/master-data/product-category';
|
||||||
import { ProductCategoryApi } from '@/services/api/master-data';
|
import { ProductCategoryApi } from '@/services/api/master-data';
|
||||||
import { cn } from '@/lib/helper';
|
import { cn } from '@/lib/helper';
|
||||||
import { isResponseSuccess } from '@/lib/api-helper';
|
import { isResponseError, isResponseSuccess } from '@/lib/api-helper';
|
||||||
import { useTableFilter } from '@/services/hooks/useTableFilter';
|
import { useTableFilter } from '@/services/hooks/useTableFilter';
|
||||||
import { ROWS_OPTIONS } from '@/config/constant';
|
import { ROWS_OPTIONS } from '@/config/constant';
|
||||||
|
|
||||||
@@ -170,7 +170,16 @@ const ProductCategoryTable = () => {
|
|||||||
const confirmationModalDeleteClickHandler = async () => {
|
const confirmationModalDeleteClickHandler = async () => {
|
||||||
setIsDeleteLoading(true);
|
setIsDeleteLoading(true);
|
||||||
|
|
||||||
await ProductCategoryApi.delete(selectedProductCategory?.id as number);
|
const deleteResponse = await ProductCategoryApi.delete(
|
||||||
|
selectedProductCategory?.id as number
|
||||||
|
);
|
||||||
|
|
||||||
|
if (isResponseError(deleteResponse)) {
|
||||||
|
toast.error(deleteResponse.message);
|
||||||
|
setIsDeleteLoading(false);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
refreshProductCategories();
|
refreshProductCategories();
|
||||||
|
|
||||||
deleteModal.closeModal();
|
deleteModal.closeModal();
|
||||||
|
|||||||
@@ -25,7 +25,7 @@ import RequirePermission from '@/components/helper/RequirePermission';
|
|||||||
import { Product } from '@/types/api/master-data/product';
|
import { Product } from '@/types/api/master-data/product';
|
||||||
import { ProductApi } from '@/services/api/master-data';
|
import { ProductApi } from '@/services/api/master-data';
|
||||||
import { cn } from '@/lib/helper';
|
import { cn } from '@/lib/helper';
|
||||||
import { isResponseSuccess } from '@/lib/api-helper';
|
import { isResponseError, isResponseSuccess } from '@/lib/api-helper';
|
||||||
import { useTableFilter } from '@/services/hooks/useTableFilter';
|
import { useTableFilter } from '@/services/hooks/useTableFilter';
|
||||||
import { ROWS_OPTIONS } from '@/config/constant';
|
import { ROWS_OPTIONS } from '@/config/constant';
|
||||||
|
|
||||||
@@ -230,8 +230,19 @@ const ProductsTable = () => {
|
|||||||
|
|
||||||
const confirmationModalDeleteClickHandler = async () => {
|
const confirmationModalDeleteClickHandler = async () => {
|
||||||
setIsDeleteLoading(true);
|
setIsDeleteLoading(true);
|
||||||
await ProductApi.delete(selectedProduct?.id as number);
|
|
||||||
|
const deleteResponse = await ProductApi.delete(
|
||||||
|
selectedProduct?.id as number
|
||||||
|
);
|
||||||
|
|
||||||
|
if (isResponseError(deleteResponse)) {
|
||||||
|
toast.error(deleteResponse.message);
|
||||||
|
setIsDeleteLoading(false);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
refreshProducts();
|
refreshProducts();
|
||||||
|
|
||||||
deleteModal.closeModal();
|
deleteModal.closeModal();
|
||||||
toast.success('Successfully delete Product!');
|
toast.success('Successfully delete Product!');
|
||||||
setIsDeleteLoading(false);
|
setIsDeleteLoading(false);
|
||||||
|
|||||||
@@ -3,7 +3,7 @@ import * as Yup from 'yup';
|
|||||||
type ProductFormSchemaType = {
|
type ProductFormSchemaType = {
|
||||||
name: string;
|
name: string;
|
||||||
brand: string;
|
brand: string;
|
||||||
sku: string;
|
sku?: string;
|
||||||
uom?: {
|
uom?: {
|
||||||
value: number;
|
value: number;
|
||||||
label: string;
|
label: string;
|
||||||
@@ -15,10 +15,16 @@ type ProductFormSchemaType = {
|
|||||||
} | null;
|
} | null;
|
||||||
product_category_id: number;
|
product_category_id: number;
|
||||||
product_price: number | string;
|
product_price: number | string;
|
||||||
selling_price: number | string;
|
selling_price?: number | string;
|
||||||
tax: number | string;
|
tax?: number | string;
|
||||||
expiry_period: number | string;
|
expiry_period?: number | string;
|
||||||
supplier_ids: number[];
|
suppliers: {
|
||||||
|
supplier: {
|
||||||
|
value: number;
|
||||||
|
label: string;
|
||||||
|
} | null;
|
||||||
|
price: number;
|
||||||
|
}[];
|
||||||
flags: string[];
|
flags: string[];
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -26,7 +32,7 @@ export const ProductFormSchema: Yup.ObjectSchema<ProductFormSchemaType> =
|
|||||||
Yup.object({
|
Yup.object({
|
||||||
name: Yup.string().required('Nama wajib diisi!'),
|
name: Yup.string().required('Nama wajib diisi!'),
|
||||||
brand: Yup.string().required('Merek wajib diisi!'),
|
brand: Yup.string().required('Merek wajib diisi!'),
|
||||||
sku: Yup.string().required('SKU wajib diisi!'),
|
sku: Yup.string(),
|
||||||
|
|
||||||
uom: Yup.object({
|
uom: Yup.object({
|
||||||
value: Yup.number()
|
value: Yup.number()
|
||||||
@@ -58,24 +64,34 @@ export const ProductFormSchema: Yup.ObjectSchema<ProductFormSchemaType> =
|
|||||||
.min(1, 'Harga produk tidak boleh kurang dari 1!'),
|
.min(1, 'Harga produk tidak boleh kurang dari 1!'),
|
||||||
|
|
||||||
selling_price: Yup.number()
|
selling_price: Yup.number()
|
||||||
.required('Harga jual wajib diisi!')
|
.typeError('Harga hanya boleh angka!')
|
||||||
.typeError('Harga jual wajib diisi!')
|
|
||||||
.min(1, 'Harga jual tidak boleh kurang dari 1!'),
|
.min(1, 'Harga jual tidak boleh kurang dari 1!'),
|
||||||
|
|
||||||
tax: Yup.number()
|
tax: Yup.number()
|
||||||
.required('Pajak wajib diisi!')
|
.typeError('Pajak hanya boleh angka!')
|
||||||
.typeError('Pajak wajib diisi!')
|
|
||||||
.min(0, 'Pajak tidak boleh kurang dari 0!')
|
.min(0, 'Pajak tidak boleh kurang dari 0!')
|
||||||
.max(100, 'Pajak tidak boleh lebih dari 100%!'),
|
.max(100, 'Pajak tidak boleh lebih dari 100%!'),
|
||||||
|
|
||||||
expiry_period: Yup.number()
|
expiry_period: Yup.number()
|
||||||
.required('Periode kadaluarsa wajib diisi!')
|
.typeError('Periode kadaluarsa hanya boleh angka!')
|
||||||
.typeError('Periode kadaluarsa wajib diisi!')
|
|
||||||
.min(1, 'Periode kadaluarsa tidak boleh kurang dari 1 hari!'),
|
.min(1, 'Periode kadaluarsa tidak boleh kurang dari 1 hari!'),
|
||||||
|
|
||||||
supplier_ids: Yup.array()
|
suppliers: Yup.array()
|
||||||
.of(Yup.number().required().typeError('Supplier tidak valid!'))
|
.of(
|
||||||
.min(1, 'Minimal harus ada 1 supplier!')
|
Yup.object({
|
||||||
|
supplier: Yup.object({
|
||||||
|
value: Yup.number()
|
||||||
|
.min(1, 'Supplier wajib dipilih!')
|
||||||
|
.required('Supplier wajib dipilih!')
|
||||||
|
.typeError('Supplier wajib dipilih!'),
|
||||||
|
label: Yup.string().required('Supplier wajib dipilih!'),
|
||||||
|
}).required('Supplier wajib dipilih!'),
|
||||||
|
price: Yup.number()
|
||||||
|
.min(1, 'Harga tidak boleh kurang dari 1!')
|
||||||
|
.required('Harga wajib diisi!')
|
||||||
|
.typeError('Harga wajib diisi!'),
|
||||||
|
})
|
||||||
|
)
|
||||||
.required('Supplier wajib diisi!'),
|
.required('Supplier wajib diisi!'),
|
||||||
|
|
||||||
flags: Yup.array()
|
flags: Yup.array()
|
||||||
|
|||||||
@@ -41,6 +41,8 @@ import { cn } from '@/lib/helper';
|
|||||||
import { PRODUCT_FLAG_OPTIONS } from '@/config/constant';
|
import { PRODUCT_FLAG_OPTIONS } from '@/config/constant';
|
||||||
import { useFormikErrorList } from '@/services/hooks/useFormikErrorList';
|
import { useFormikErrorList } from '@/services/hooks/useFormikErrorList';
|
||||||
import { Supplier } from '@/types/api/master-data/supplier';
|
import { Supplier } from '@/types/api/master-data/supplier';
|
||||||
|
import Card from '@/components/Card';
|
||||||
|
import { removeArrayItemAndSync } from '@/lib/utils/formik';
|
||||||
|
|
||||||
interface ProductFormProps {
|
interface ProductFormProps {
|
||||||
type?: 'add' | 'edit' | 'detail';
|
type?: 'add' | 'edit' | 'detail';
|
||||||
@@ -101,7 +103,15 @@ const ProductForm = ({ type = 'add', initialValues }: ProductFormProps) => {
|
|||||||
selling_price: initialValues?.selling_price ?? '',
|
selling_price: initialValues?.selling_price ?? '',
|
||||||
tax: initialValues?.tax ?? '',
|
tax: initialValues?.tax ?? '',
|
||||||
expiry_period: initialValues?.expiry_period ?? '',
|
expiry_period: initialValues?.expiry_period ?? '',
|
||||||
supplier_ids: initialValues?.suppliers?.map((s) => s.id) ?? [],
|
suppliers: initialValues?.suppliers
|
||||||
|
? initialValues.suppliers.map((supplier) => ({
|
||||||
|
supplier: {
|
||||||
|
value: supplier.id,
|
||||||
|
label: supplier.name,
|
||||||
|
},
|
||||||
|
price: supplier.price,
|
||||||
|
}))
|
||||||
|
: [],
|
||||||
flags: initialValues?.flags ?? [],
|
flags: initialValues?.flags ?? [],
|
||||||
}),
|
}),
|
||||||
[initialValues]
|
[initialValues]
|
||||||
@@ -120,12 +130,17 @@ const ProductForm = ({ type = 'add', initialValues }: ProductFormProps) => {
|
|||||||
uom_id: values.uom_id,
|
uom_id: values.uom_id,
|
||||||
product_category_id: values.product_category_id,
|
product_category_id: values.product_category_id,
|
||||||
product_price: parseInt(values.product_price.toString()) || 0,
|
product_price: parseInt(values.product_price.toString()) || 0,
|
||||||
selling_price: parseInt(values.selling_price.toString()) || 0,
|
selling_price: values.selling_price
|
||||||
tax: parseInt(values.tax.toString()) || 0,
|
? parseInt(values.selling_price.toString()) || 0
|
||||||
expiry_period: parseInt(values.expiry_period.toString()) || 0,
|
: undefined,
|
||||||
supplier_ids: values.supplier_ids.filter(
|
tax: values.tax ? parseInt(values.tax.toString()) || 0 : undefined,
|
||||||
(id): id is number => typeof id === 'number'
|
expiry_period: values.expiry_period
|
||||||
),
|
? parseInt(values.expiry_period.toString()) || 0
|
||||||
|
: undefined,
|
||||||
|
suppliers: values.suppliers.map((s) => ({
|
||||||
|
supplier_id: s.supplier?.value as number,
|
||||||
|
price: parseInt(s.price.toString()) || 0,
|
||||||
|
})),
|
||||||
flags: values.flags.filter((f): f is string => typeof f === 'string'),
|
flags: values.flags.filter((f): f is string => typeof f === 'string'),
|
||||||
};
|
};
|
||||||
switch (type) {
|
switch (type) {
|
||||||
@@ -179,13 +194,29 @@ const ProductForm = ({ type = 'add', initialValues }: ProductFormProps) => {
|
|||||||
category: 'SAPRONAK',
|
category: 'SAPRONAK',
|
||||||
});
|
});
|
||||||
|
|
||||||
const supplierChangeHandler = (val: OptionType | OptionType[] | null) => {
|
const filteredSupplierOptions = useMemo(() => {
|
||||||
const arr = Array.isArray(val) ? val : val ? [val] : [];
|
return supplierOptions.filter((opt) => {
|
||||||
formik.setFieldTouched('supplier_ids', true);
|
return !formik.values.suppliers.some(
|
||||||
formik.setFieldValue(
|
(s) => s.supplier?.value === opt.value
|
||||||
'supplier_ids',
|
);
|
||||||
arr.map((v) => (v as OptionType).value)
|
});
|
||||||
);
|
}, [supplierOptions, formik.values.suppliers]);
|
||||||
|
|
||||||
|
const addSupplierHandler = () => {
|
||||||
|
formik.setFieldValue('suppliers', [
|
||||||
|
...formik.values.suppliers,
|
||||||
|
{
|
||||||
|
supplier_id: '',
|
||||||
|
price: formik.values.product_price,
|
||||||
|
},
|
||||||
|
]);
|
||||||
|
};
|
||||||
|
|
||||||
|
const deleteSupplierItemHandler = (idx: number) => {
|
||||||
|
const path = 'suppliers';
|
||||||
|
|
||||||
|
// trims values, errors, and touched at idx
|
||||||
|
removeArrayItemAndSync(formik, path, idx);
|
||||||
};
|
};
|
||||||
|
|
||||||
const deleteProductClickHandler = () => {
|
const deleteProductClickHandler = () => {
|
||||||
@@ -201,6 +232,19 @@ const ProductForm = ({ type = 'add', initialValues }: ProductFormProps) => {
|
|||||||
router.push('/master-data/product');
|
router.push('/master-data/product');
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const isSupplierRepeaterError = (
|
||||||
|
column: 'supplier' | 'price',
|
||||||
|
supplierIdx: number
|
||||||
|
) => {
|
||||||
|
return (
|
||||||
|
formik.touched.suppliers?.[supplierIdx]?.[column] &&
|
||||||
|
Boolean(
|
||||||
|
formik.errors.suppliers?.[supplierIdx] instanceof Object &&
|
||||||
|
formik.errors.suppliers?.[supplierIdx]?.[column]
|
||||||
|
)
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
formikSetValues(formikInitialValues);
|
formikSetValues(formikInitialValues);
|
||||||
}, [formikSetValues, formikInitialValues]);
|
}, [formikSetValues, formikInitialValues]);
|
||||||
@@ -271,7 +315,6 @@ const ProductForm = ({ type = 'add', initialValues }: ProductFormProps) => {
|
|||||||
readOnly={type === 'detail'}
|
readOnly={type === 'detail'}
|
||||||
/>
|
/>
|
||||||
<TextInput
|
<TextInput
|
||||||
required
|
|
||||||
label='SKU'
|
label='SKU'
|
||||||
name='sku'
|
name='sku'
|
||||||
placeholder='Masukkan SKU...'
|
placeholder='Masukkan SKU...'
|
||||||
@@ -344,7 +387,6 @@ const ProductForm = ({ type = 'add', initialValues }: ProductFormProps) => {
|
|||||||
readOnly={type === 'detail'}
|
readOnly={type === 'detail'}
|
||||||
/>
|
/>
|
||||||
<NumberInput
|
<NumberInput
|
||||||
required
|
|
||||||
label='Harga Jual'
|
label='Harga Jual'
|
||||||
name='selling_price'
|
name='selling_price'
|
||||||
placeholder='Masukkan harga jual...'
|
placeholder='Masukkan harga jual...'
|
||||||
@@ -366,7 +408,6 @@ const ProductForm = ({ type = 'add', initialValues }: ProductFormProps) => {
|
|||||||
</div>
|
</div>
|
||||||
<div className='grid sm:grid-cols-2 gap-4'>
|
<div className='grid sm:grid-cols-2 gap-4'>
|
||||||
<NumberInput
|
<NumberInput
|
||||||
required
|
|
||||||
label='Pajak (%)'
|
label='Pajak (%)'
|
||||||
name='tax'
|
name='tax'
|
||||||
placeholder='Masukkan pajak...'
|
placeholder='Masukkan pajak...'
|
||||||
@@ -383,7 +424,6 @@ const ProductForm = ({ type = 'add', initialValues }: ProductFormProps) => {
|
|||||||
readOnly={type === 'detail'}
|
readOnly={type === 'detail'}
|
||||||
/>
|
/>
|
||||||
<NumberInput
|
<NumberInput
|
||||||
required
|
|
||||||
label='Periode Kadaluarsa (hari)'
|
label='Periode Kadaluarsa (hari)'
|
||||||
name='expiry_period'
|
name='expiry_period'
|
||||||
placeholder='Masukkan periode kadaluarsa...'
|
placeholder='Masukkan periode kadaluarsa...'
|
||||||
@@ -403,28 +443,7 @@ const ProductForm = ({ type = 'add', initialValues }: ProductFormProps) => {
|
|||||||
readOnly={type === 'detail'}
|
readOnly={type === 'detail'}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div className='grid sm:grid-cols-2 gap-4'>
|
<div className='grid sm:grid-cols-1 gap-4'>
|
||||||
<SelectInput
|
|
||||||
required
|
|
||||||
label='Supplier'
|
|
||||||
placeholder='Pilih supplier...'
|
|
||||||
isMulti
|
|
||||||
value={supplierOptions.filter((opt) =>
|
|
||||||
(formik.values.supplier_ids || []).includes(opt.value)
|
|
||||||
)}
|
|
||||||
onChange={supplierChangeHandler}
|
|
||||||
options={supplierOptions}
|
|
||||||
onInputChange={setSupplierSelectInputValue}
|
|
||||||
onMenuScrollToBottom={loadMoreSuppliers}
|
|
||||||
isLoading={isLoadingSuppliers}
|
|
||||||
isError={
|
|
||||||
formik.touched.supplier_ids &&
|
|
||||||
Boolean(formik.errors.supplier_ids)
|
|
||||||
}
|
|
||||||
errorMessage={formik.errors.supplier_ids as string}
|
|
||||||
isDisabled={type === 'detail'}
|
|
||||||
isClearable
|
|
||||||
/>
|
|
||||||
<SelectInput
|
<SelectInput
|
||||||
required
|
required
|
||||||
label='Flags'
|
label='Flags'
|
||||||
@@ -447,6 +466,129 @@ const ProductForm = ({ type = 'add', initialValues }: ProductFormProps) => {
|
|||||||
isClearable
|
isClearable
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<div className='grid sm:grid-cols-1 gap-4'>
|
||||||
|
{type !== 'detail' && formik.values.suppliers.length === 0 && (
|
||||||
|
<Button
|
||||||
|
type='button'
|
||||||
|
color='success'
|
||||||
|
onClick={addSupplierHandler}
|
||||||
|
className='w-fit mx-auto'
|
||||||
|
>
|
||||||
|
<Icon icon='ic:round-plus' width={24} height={24} /> Tambah
|
||||||
|
Supplier
|
||||||
|
</Button>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{formik.values.suppliers.length > 0 && (
|
||||||
|
<Card
|
||||||
|
className={{
|
||||||
|
wrapper: 'w-full',
|
||||||
|
body: 'p-4 shadow',
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<div className='mb-4 text-center'>
|
||||||
|
<h4 className='font-bold text-xl'>Supplier</h4>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className='overflow-x-auto'>
|
||||||
|
<table className='table'>
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th className='after:content-["*"] after:text-red-500 after:ml-0.5'>
|
||||||
|
Supplier
|
||||||
|
</th>
|
||||||
|
<th className='after:content-["*"] after:text-red-500 after:ml-0.5'>
|
||||||
|
Harga
|
||||||
|
</th>
|
||||||
|
<th>Aksi</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
|
||||||
|
<tbody>
|
||||||
|
{formik.values.suppliers.map((supplier, idx) => (
|
||||||
|
<tr key={idx}>
|
||||||
|
<td className='p-2 w-full max-w-1/2'>
|
||||||
|
<SelectInput
|
||||||
|
placeholder='Pilih Supplier'
|
||||||
|
options={filteredSupplierOptions}
|
||||||
|
onInputChange={setSupplierSelectInputValue}
|
||||||
|
onMenuScrollToBottom={loadMoreSuppliers}
|
||||||
|
isLoading={isLoadingSuppliers}
|
||||||
|
value={formik.values.suppliers[idx].supplier}
|
||||||
|
onChange={(val) => {
|
||||||
|
formik.setFieldValue(
|
||||||
|
`suppliers.${idx}.supplier`,
|
||||||
|
val
|
||||||
|
);
|
||||||
|
}}
|
||||||
|
isError={isSupplierRepeaterError(
|
||||||
|
'supplier',
|
||||||
|
idx
|
||||||
|
)}
|
||||||
|
isClearable
|
||||||
|
isDisabled={type === 'detail'}
|
||||||
|
className={{
|
||||||
|
wrapper: 'min-w-48 w-full',
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</td>
|
||||||
|
<td className='p-2 w-full max-w-1/2'>
|
||||||
|
<NumberInput
|
||||||
|
required
|
||||||
|
name={`suppliers.${idx}.price`}
|
||||||
|
placeholder='Masukkan harga...'
|
||||||
|
value={formik.values.suppliers[idx].price}
|
||||||
|
onChange={formik.handleChange}
|
||||||
|
onBlur={formik.handleBlur}
|
||||||
|
decimalScale={2}
|
||||||
|
allowNegative={false}
|
||||||
|
thousandSeparator=','
|
||||||
|
decimalSeparator='.'
|
||||||
|
inputPrefix='Rp '
|
||||||
|
isError={isSupplierRepeaterError('price', idx)}
|
||||||
|
readOnly={type === 'detail'}
|
||||||
|
className={{
|
||||||
|
wrapper: 'min-w-48 w-full',
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</td>
|
||||||
|
{type !== 'detail' && (
|
||||||
|
<td>
|
||||||
|
<Button
|
||||||
|
type='button'
|
||||||
|
color='error'
|
||||||
|
onClick={() => deleteSupplierItemHandler(idx)}
|
||||||
|
>
|
||||||
|
<Icon
|
||||||
|
icon='material-symbols:delete-outline-rounded'
|
||||||
|
width={24}
|
||||||
|
height={24}
|
||||||
|
/>
|
||||||
|
</Button>
|
||||||
|
</td>
|
||||||
|
)}
|
||||||
|
</tr>
|
||||||
|
))}
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{type !== 'detail' && (
|
||||||
|
<div className='w-full flex flex-row justify-center'>
|
||||||
|
<Button
|
||||||
|
type='button'
|
||||||
|
color='success'
|
||||||
|
onClick={addSupplierHandler}
|
||||||
|
>
|
||||||
|
<Icon icon='ic:round-plus' width={24} height={24} />{' '}
|
||||||
|
Tambah Supplier
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</Card>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className='flex flex-row justify-between gap-2 flex-wrap'>
|
<div className='flex flex-row justify-between gap-2 flex-wrap'>
|
||||||
{type !== 'add' && (
|
{type !== 'add' && (
|
||||||
|
|||||||
@@ -7,7 +7,7 @@ import { ProductionStandard } from '@/types/api/master-data/production-standard'
|
|||||||
import { Icon } from '@iconify/react';
|
import { Icon } from '@iconify/react';
|
||||||
import useSWR from 'swr';
|
import useSWR from 'swr';
|
||||||
import { ProductionStandardApi } from '@/services/api/master-data';
|
import { ProductionStandardApi } from '@/services/api/master-data';
|
||||||
import { isResponseSuccess } from '@/lib/api-helper';
|
import { isResponseError, isResponseSuccess } from '@/lib/api-helper';
|
||||||
import RowOptionsMenuWrapper from '@/components/table/RowOptionsMenuWrapper';
|
import RowOptionsMenuWrapper from '@/components/table/RowOptionsMenuWrapper';
|
||||||
import { CellContext } from '@tanstack/react-table';
|
import { CellContext } from '@tanstack/react-table';
|
||||||
import { useModal } from '@/components/Modal';
|
import { useModal } from '@/components/Modal';
|
||||||
@@ -94,9 +94,16 @@ const ProductionStandardTable = () => {
|
|||||||
const confirmationModalDeleteClickHandler = async () => {
|
const confirmationModalDeleteClickHandler = async () => {
|
||||||
setIsDeleteLoading(true);
|
setIsDeleteLoading(true);
|
||||||
|
|
||||||
await ProductionStandardApi.delete(
|
const deleteResponse = await ProductionStandardApi.delete(
|
||||||
selectedProductionStandard?.id as number
|
selectedProductionStandard?.id as number
|
||||||
);
|
);
|
||||||
|
|
||||||
|
if (isResponseError(deleteResponse)) {
|
||||||
|
toast.error(deleteResponse.message);
|
||||||
|
setIsDeleteLoading(false);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
refreshProductionStandards();
|
refreshProductionStandards();
|
||||||
|
|
||||||
deleteModal.closeModal();
|
deleteModal.closeModal();
|
||||||
|
|||||||
+15
-19
@@ -2,34 +2,30 @@ import * as Yup from 'yup';
|
|||||||
|
|
||||||
// Schema for LAYING category (production_standard_details is required)
|
// Schema for LAYING category (production_standard_details is required)
|
||||||
const LayingRepeaterFormSchema = Yup.object({
|
const LayingRepeaterFormSchema = Yup.object({
|
||||||
week: Yup.number().required('Minggu wajib diisi!'),
|
week: Yup.number().required('Wajib diisi!'),
|
||||||
production_standard_uniformity_details: Yup.object({
|
production_standard_uniformity_details: Yup.object({
|
||||||
target_mean_bw: Yup.number().required('Berat rata-rata wajib diisi!'),
|
target_mean_bw: Yup.number().required('Wajib diisi!'),
|
||||||
max_depletion: Yup.number().required('Maksimal depletion wajib diisi!'),
|
max_depletion: Yup.number().required('Wajib diisi!'),
|
||||||
min_uniformity: Yup.number().required('Minimal uniformitas wajib diisi!'),
|
min_uniformity: Yup.number().required('Wajib diisi!'),
|
||||||
feed_intake: Yup.number().required('Pengambilan makanan wajib diisi!'),
|
feed_intake: Yup.number().required('Wajib diisi!'),
|
||||||
}),
|
}),
|
||||||
production_standard_details: Yup.object({
|
production_standard_details: Yup.object({
|
||||||
target_hen_day_production: Yup.number().required(
|
target_hen_day_production: Yup.number().required('Wajib diisi!'),
|
||||||
'Produksi telur per hari wajib diisi!'
|
target_hen_house_production: Yup.number().required('Wajib diisi!'),
|
||||||
),
|
target_egg_weight: Yup.number().required('Wajib diisi!'),
|
||||||
target_hen_house_production: Yup.number().required(
|
target_egg_mass: Yup.number().required('Wajib diisi!'),
|
||||||
'Produksi telur per kandang wajib diisi!'
|
standard_fcr: Yup.number().required('Wajib diisi!'),
|
||||||
),
|
|
||||||
target_egg_weight: Yup.number().required('Berat telur wajib diisi!'),
|
|
||||||
target_egg_mass: Yup.number().required('Massa telur wajib diisi!'),
|
|
||||||
standard_fcr: Yup.number().required('FCR wajib diisi!'),
|
|
||||||
}).required(),
|
}).required(),
|
||||||
});
|
});
|
||||||
|
|
||||||
// Schema for GROWING category (production_standard_details is optional)
|
// Schema for GROWING category (production_standard_details is optional)
|
||||||
const GrowingRepeaterFormSchema = Yup.object({
|
const GrowingRepeaterFormSchema = Yup.object({
|
||||||
week: Yup.number().required('Minggu wajib diisi!'),
|
week: Yup.number().required('Wajib diisi!'),
|
||||||
production_standard_uniformity_details: Yup.object({
|
production_standard_uniformity_details: Yup.object({
|
||||||
target_mean_bw: Yup.number().required('Berat rata-rata wajib diisi!'),
|
target_mean_bw: Yup.number().required('Wajib diisi!'),
|
||||||
max_depletion: Yup.number().required('Maksimal depletion wajib diisi!'),
|
max_depletion: Yup.number().required('Wajib diisi!'),
|
||||||
min_uniformity: Yup.number().required('Minimal uniformitas wajib diisi!'),
|
min_uniformity: Yup.number().required('Wajib diisi!'),
|
||||||
feed_intake: Yup.number().required('Pengambilan makanan wajib diisi!'),
|
feed_intake: Yup.number().required('Wajib diisi!'),
|
||||||
}),
|
}),
|
||||||
production_standard_details: Yup.object({
|
production_standard_details: Yup.object({
|
||||||
target_hen_day_production: Yup.number().optional(),
|
target_hen_day_production: Yup.number().optional(),
|
||||||
|
|||||||
+86
-45
@@ -344,7 +344,7 @@ const ProductionStandardForm = ({
|
|||||||
const columns = useMemo<ColumnDef<TableRowsType>[]>(() => {
|
const columns = useMemo<ColumnDef<TableRowsType>[]>(() => {
|
||||||
const baseColumns: ColumnDef<TableRowsType>[] = [
|
const baseColumns: ColumnDef<TableRowsType>[] = [
|
||||||
{
|
{
|
||||||
header: 'Minggu',
|
header: 'Week',
|
||||||
accessorKey: 'week',
|
accessorKey: 'week',
|
||||||
enableSorting: false,
|
enableSorting: false,
|
||||||
},
|
},
|
||||||
@@ -358,30 +358,40 @@ const ProductionStandardForm = ({
|
|||||||
header: 'Hen Day',
|
header: 'Hen Day',
|
||||||
accessorFn: (row) =>
|
accessorFn: (row) =>
|
||||||
row.production_standard_details?.target_hen_day_production,
|
row.production_standard_details?.target_hen_day_production,
|
||||||
|
cell: ({ row }) =>
|
||||||
|
`${row.original.production_standard_details?.target_hen_day_production}%`,
|
||||||
enableSorting: false,
|
enableSorting: false,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
header: 'Hen House',
|
header: 'Hen House',
|
||||||
accessorFn: (row) =>
|
accessorFn: (row) =>
|
||||||
row.production_standard_details?.target_hen_house_production,
|
row.production_standard_details?.target_hen_house_production,
|
||||||
|
cell: ({ row }) =>
|
||||||
|
`${row.original.production_standard_details?.target_hen_house_production} pc`,
|
||||||
enableSorting: false,
|
enableSorting: false,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
header: 'Egg Weight',
|
header: 'Egg Weight',
|
||||||
accessorFn: (row) =>
|
accessorFn: (row) =>
|
||||||
row.production_standard_details?.target_egg_weight,
|
row.production_standard_details?.target_egg_weight,
|
||||||
|
cell: ({ row }) =>
|
||||||
|
`${row.original.production_standard_details?.target_egg_weight} g`,
|
||||||
enableSorting: false,
|
enableSorting: false,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
header: 'Egg Mass',
|
header: 'Egg Mass',
|
||||||
accessorFn: (row) =>
|
accessorFn: (row) =>
|
||||||
row.production_standard_details?.target_egg_mass,
|
row.production_standard_details?.target_egg_mass,
|
||||||
|
cell: ({ row }) =>
|
||||||
|
`${row.original.production_standard_details?.target_egg_mass} g`,
|
||||||
enableSorting: false,
|
enableSorting: false,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
header: 'FCR',
|
header: 'FCR',
|
||||||
accessorFn: (row) =>
|
accessorFn: (row) =>
|
||||||
row.production_standard_details?.standard_fcr,
|
row.production_standard_details?.standard_fcr,
|
||||||
|
cell: ({ row }) =>
|
||||||
|
`${row.original.production_standard_details?.standard_fcr} g`,
|
||||||
enableSorting: false,
|
enableSorting: false,
|
||||||
},
|
},
|
||||||
]
|
]
|
||||||
@@ -393,24 +403,32 @@ const ProductionStandardForm = ({
|
|||||||
header: 'Mean BW',
|
header: 'Mean BW',
|
||||||
accessorFn: (row) =>
|
accessorFn: (row) =>
|
||||||
row.production_standard_uniformity_details?.target_mean_bw,
|
row.production_standard_uniformity_details?.target_mean_bw,
|
||||||
|
cell: ({ row }) =>
|
||||||
|
`${row.original.production_standard_uniformity_details?.target_mean_bw} g`,
|
||||||
enableSorting: false,
|
enableSorting: false,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
header: 'Max Depletion',
|
header: 'Max Depletion',
|
||||||
accessorFn: (row) =>
|
accessorFn: (row) =>
|
||||||
row.production_standard_uniformity_details?.max_depletion,
|
row.production_standard_uniformity_details?.max_depletion,
|
||||||
|
cell: ({ row }) =>
|
||||||
|
`${row.original.production_standard_uniformity_details?.max_depletion}%`,
|
||||||
enableSorting: false,
|
enableSorting: false,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
header: 'Min Uniformity',
|
header: 'Min Uniformity',
|
||||||
accessorFn: (row) =>
|
accessorFn: (row) =>
|
||||||
row.production_standard_uniformity_details?.min_uniformity,
|
row.production_standard_uniformity_details?.min_uniformity,
|
||||||
|
cell: ({ row }) =>
|
||||||
|
`${row.original.production_standard_uniformity_details?.min_uniformity}%`,
|
||||||
enableSorting: false,
|
enableSorting: false,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
header: 'Feed Intake',
|
header: 'Feed Intake',
|
||||||
accessorFn: (row) =>
|
accessorFn: (row) =>
|
||||||
row.production_standard_uniformity_details?.feed_intake,
|
row.production_standard_uniformity_details?.feed_intake,
|
||||||
|
cell: ({ row }) =>
|
||||||
|
`${row.original.production_standard_uniformity_details?.feed_intake} g`,
|
||||||
enableSorting: false,
|
enableSorting: false,
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
@@ -728,7 +746,52 @@ const ProductionStandardForm = ({
|
|||||||
};
|
};
|
||||||
|
|
||||||
// ===== Formik Error List =====
|
// ===== Formik Error List =====
|
||||||
const { formErrorList, close, handleFormSubmit } = useFormikErrorList(formik);
|
const { formErrorList, close, handleFormSubmit } = useFormikErrorList(
|
||||||
|
formik,
|
||||||
|
{
|
||||||
|
onBeforeSubmit: (e) => {
|
||||||
|
e.preventDefault();
|
||||||
|
|
||||||
|
// For GROWING category, clear production_standard_details errors and set default values
|
||||||
|
if (formik.values.project_category === 'GROWING') {
|
||||||
|
// Set default values for production_standard_details
|
||||||
|
formik.values.details?.forEach((detail) => {
|
||||||
|
detail.production_standard_details = {
|
||||||
|
target_hen_day_production: 0,
|
||||||
|
target_hen_house_production: 0,
|
||||||
|
target_egg_weight: 0,
|
||||||
|
target_egg_mass: 0,
|
||||||
|
standard_fcr: 0,
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
|
// Clear any errors related to production_standard_details
|
||||||
|
const currentErrors = { ...formik.errors };
|
||||||
|
if (currentErrors.details && Array.isArray(currentErrors.details)) {
|
||||||
|
const cleanedDetails = currentErrors.details
|
||||||
|
.map((detailError) => {
|
||||||
|
if (detailError && typeof detailError === 'object') {
|
||||||
|
const { production_standard_details, ...rest } = detailError;
|
||||||
|
return Object.keys(rest).length > 0 ? rest : undefined;
|
||||||
|
}
|
||||||
|
return detailError;
|
||||||
|
})
|
||||||
|
.filter(
|
||||||
|
(error): error is Exclude<typeof error, undefined> =>
|
||||||
|
error !== undefined
|
||||||
|
);
|
||||||
|
|
||||||
|
currentErrors.details = (
|
||||||
|
cleanedDetails.length > 0 ? cleanedDetails : undefined
|
||||||
|
) as typeof currentErrors.details;
|
||||||
|
}
|
||||||
|
formik.setErrors(currentErrors);
|
||||||
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
|
},
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
@@ -821,19 +884,20 @@ const ProductionStandardForm = ({
|
|||||||
key={`row-${row.index}`}
|
key={`row-${row.index}`}
|
||||||
className='sticky bottom-0 bg-base-100 shadow-lg'
|
className='sticky bottom-0 bg-base-100 shadow-lg'
|
||||||
>
|
>
|
||||||
<td colSpan={colSpan} className='p-6'>
|
<td colSpan={colSpan} className='p-2'>
|
||||||
<form
|
<form
|
||||||
className='h-full w-full flex flex-col justify-end'
|
className='h-full w-full flex flex-col justify-end'
|
||||||
onSubmit={repeaterFormik.handleSubmit}
|
onSubmit={repeaterFormik.handleSubmit}
|
||||||
onReset={repeaterFormik.handleReset}
|
onReset={repeaterFormik.handleReset}
|
||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
className={cn(
|
className='grid gap-2 items-start w-full'
|
||||||
'grid gap-4 items-start',
|
style={{
|
||||||
formik.values.project_category === 'LAYING'
|
gridTemplateColumns:
|
||||||
? 'grid-cols-10'
|
formik.values.project_category === 'LAYING'
|
||||||
: 'grid-cols-5'
|
? 'repeat(10, minmax(auto, 1fr)) minmax(auto, auto)'
|
||||||
)}
|
: 'repeat(4, minmax(auto, 1fr)) minmax(auto, auto)',
|
||||||
|
}}
|
||||||
>
|
>
|
||||||
<NumberInput
|
<NumberInput
|
||||||
name='week'
|
name='week'
|
||||||
@@ -862,7 +926,7 @@ const ProductionStandardForm = ({
|
|||||||
}
|
}
|
||||||
onChange={repeaterFormik.handleChange}
|
onChange={repeaterFormik.handleChange}
|
||||||
onBlur={repeaterFormik.handleBlur}
|
onBlur={repeaterFormik.handleBlur}
|
||||||
endAdornment={<Icon icon='mdi:percent' />}
|
bottomLabel='Persen (%)'
|
||||||
errorMessage={getProductionDetailsError(
|
errorMessage={getProductionDetailsError(
|
||||||
repeaterFormik.errors
|
repeaterFormik.errors
|
||||||
.production_standard_details,
|
.production_standard_details,
|
||||||
@@ -894,11 +958,7 @@ const ProductionStandardForm = ({
|
|||||||
}
|
}
|
||||||
onChange={repeaterFormik.handleChange}
|
onChange={repeaterFormik.handleChange}
|
||||||
onBlur={repeaterFormik.handleBlur}
|
onBlur={repeaterFormik.handleBlur}
|
||||||
endAdornment={
|
bottomLabel='Butir (pc)'
|
||||||
<div className='w-full h-full flex items-center justify-center'>
|
|
||||||
Butir
|
|
||||||
</div>
|
|
||||||
}
|
|
||||||
errorMessage={getProductionDetailsError(
|
errorMessage={getProductionDetailsError(
|
||||||
repeaterFormik.errors
|
repeaterFormik.errors
|
||||||
.production_standard_details,
|
.production_standard_details,
|
||||||
@@ -930,11 +990,7 @@ const ProductionStandardForm = ({
|
|||||||
}
|
}
|
||||||
onChange={repeaterFormik.handleChange}
|
onChange={repeaterFormik.handleChange}
|
||||||
onBlur={repeaterFormik.handleBlur}
|
onBlur={repeaterFormik.handleBlur}
|
||||||
endAdornment={
|
bottomLabel='Gram (g)'
|
||||||
<div className='w-full h-full flex items-center justify-center'>
|
|
||||||
gr
|
|
||||||
</div>
|
|
||||||
}
|
|
||||||
errorMessage={getProductionDetailsError(
|
errorMessage={getProductionDetailsError(
|
||||||
repeaterFormik.errors
|
repeaterFormik.errors
|
||||||
.production_standard_details,
|
.production_standard_details,
|
||||||
@@ -959,17 +1015,13 @@ const ProductionStandardForm = ({
|
|||||||
name='production_standard_details.target_egg_mass'
|
name='production_standard_details.target_egg_mass'
|
||||||
label='Egg Mass'
|
label='Egg Mass'
|
||||||
placeholder='1'
|
placeholder='1'
|
||||||
|
bottomLabel='Gram (g)'
|
||||||
value={
|
value={
|
||||||
repeaterFormik.values
|
repeaterFormik.values
|
||||||
.production_standard_details?.target_egg_mass
|
.production_standard_details?.target_egg_mass
|
||||||
}
|
}
|
||||||
onChange={repeaterFormik.handleChange}
|
onChange={repeaterFormik.handleChange}
|
||||||
onBlur={repeaterFormik.handleBlur}
|
onBlur={repeaterFormik.handleBlur}
|
||||||
endAdornment={
|
|
||||||
<div className='w-full h-full flex items-center justify-center'>
|
|
||||||
gr
|
|
||||||
</div>
|
|
||||||
}
|
|
||||||
errorMessage={getProductionDetailsError(
|
errorMessage={getProductionDetailsError(
|
||||||
repeaterFormik.errors
|
repeaterFormik.errors
|
||||||
.production_standard_details,
|
.production_standard_details,
|
||||||
@@ -1000,11 +1052,7 @@ const ProductionStandardForm = ({
|
|||||||
}
|
}
|
||||||
onChange={repeaterFormik.handleChange}
|
onChange={repeaterFormik.handleChange}
|
||||||
onBlur={repeaterFormik.handleBlur}
|
onBlur={repeaterFormik.handleBlur}
|
||||||
endAdornment={
|
bottomLabel='Gram (g)'
|
||||||
<div className='w-full h-full flex items-center justify-center'>
|
|
||||||
gr
|
|
||||||
</div>
|
|
||||||
}
|
|
||||||
errorMessage={getProductionDetailsError(
|
errorMessage={getProductionDetailsError(
|
||||||
repeaterFormik.errors
|
repeaterFormik.errors
|
||||||
.production_standard_details,
|
.production_standard_details,
|
||||||
@@ -1038,11 +1086,7 @@ const ProductionStandardForm = ({
|
|||||||
}
|
}
|
||||||
onChange={repeaterFormik.handleChange}
|
onChange={repeaterFormik.handleChange}
|
||||||
onBlur={repeaterFormik.handleBlur}
|
onBlur={repeaterFormik.handleBlur}
|
||||||
endAdornment={
|
bottomLabel='Gram (g)'
|
||||||
<div className='w-full h-full flex items-center justify-center'>
|
|
||||||
gr
|
|
||||||
</div>
|
|
||||||
}
|
|
||||||
errorMessage={
|
errorMessage={
|
||||||
repeaterFormik.errors
|
repeaterFormik.errors
|
||||||
.production_standard_uniformity_details
|
.production_standard_uniformity_details
|
||||||
@@ -1072,7 +1116,7 @@ const ProductionStandardForm = ({
|
|||||||
}
|
}
|
||||||
onChange={repeaterFormik.handleChange}
|
onChange={repeaterFormik.handleChange}
|
||||||
onBlur={repeaterFormik.handleBlur}
|
onBlur={repeaterFormik.handleBlur}
|
||||||
endAdornment={<Icon icon='mdi:percent' />}
|
bottomLabel='Persen (%)'
|
||||||
errorMessage={
|
errorMessage={
|
||||||
repeaterFormik.errors
|
repeaterFormik.errors
|
||||||
.production_standard_uniformity_details
|
.production_standard_uniformity_details
|
||||||
@@ -1102,7 +1146,7 @@ const ProductionStandardForm = ({
|
|||||||
}
|
}
|
||||||
onChange={repeaterFormik.handleChange}
|
onChange={repeaterFormik.handleChange}
|
||||||
onBlur={repeaterFormik.handleBlur}
|
onBlur={repeaterFormik.handleBlur}
|
||||||
endAdornment={<Icon icon='mdi:percent' />}
|
bottomLabel='Persen (%)'
|
||||||
errorMessage={
|
errorMessage={
|
||||||
repeaterFormik.errors
|
repeaterFormik.errors
|
||||||
.production_standard_uniformity_details
|
.production_standard_uniformity_details
|
||||||
@@ -1132,11 +1176,8 @@ const ProductionStandardForm = ({
|
|||||||
}
|
}
|
||||||
onChange={repeaterFormik.handleChange}
|
onChange={repeaterFormik.handleChange}
|
||||||
onBlur={repeaterFormik.handleBlur}
|
onBlur={repeaterFormik.handleBlur}
|
||||||
endAdornment={
|
bottomLabel='Gram/Ekor (g)'
|
||||||
<div className='w-full h-full flex items-center justify-center'>
|
endAdornment
|
||||||
gr/ekor
|
|
||||||
</div>
|
|
||||||
}
|
|
||||||
errorMessage={
|
errorMessage={
|
||||||
repeaterFormik.errors
|
repeaterFormik.errors
|
||||||
.production_standard_uniformity_details
|
.production_standard_uniformity_details
|
||||||
@@ -1162,7 +1203,7 @@ const ProductionStandardForm = ({
|
|||||||
type='button'
|
type='button'
|
||||||
color='error'
|
color='error'
|
||||||
variant='outline'
|
variant='outline'
|
||||||
className='min-w-24'
|
className='min-w-xs'
|
||||||
onClick={handleCancelEdit}
|
onClick={handleCancelEdit}
|
||||||
>
|
>
|
||||||
<Icon icon='mdi:close' /> Batal
|
<Icon icon='mdi:close' /> Batal
|
||||||
@@ -1178,7 +1219,7 @@ const ProductionStandardForm = ({
|
|||||||
<Button
|
<Button
|
||||||
type='submit'
|
type='submit'
|
||||||
color={editMode ? 'warning' : 'success'}
|
color={editMode ? 'warning' : 'success'}
|
||||||
className='min-w-24'
|
className='min-w-xs'
|
||||||
disabled={
|
disabled={
|
||||||
isAddingRow ||
|
isAddingRow ||
|
||||||
formik.values.project_category === ''
|
formik.values.project_category === ''
|
||||||
@@ -1195,7 +1236,7 @@ const ProductionStandardForm = ({
|
|||||||
variant='outline'
|
variant='outline'
|
||||||
color='primary'
|
color='primary'
|
||||||
onClick={toggleTableHeight}
|
onClick={toggleTableHeight}
|
||||||
className='absolute bottom-6 right-6'
|
className='absolute bottom-2 right-2'
|
||||||
>
|
>
|
||||||
<Icon
|
<Icon
|
||||||
icon={
|
icon={
|
||||||
|
|||||||
@@ -11,7 +11,7 @@ import RowDropdownOptions from '@/components/table/RowDropdownOptions';
|
|||||||
import RowOptionsMenuWrapper from '@/components/table/RowOptionsMenuWrapper';
|
import RowOptionsMenuWrapper from '@/components/table/RowOptionsMenuWrapper';
|
||||||
import RequirePermission from '@/components/helper/RequirePermission';
|
import RequirePermission from '@/components/helper/RequirePermission';
|
||||||
import { ROWS_OPTIONS } from '@/config/constant';
|
import { ROWS_OPTIONS } from '@/config/constant';
|
||||||
import { isResponseSuccess } from '@/lib/api-helper';
|
import { isResponseError, isResponseSuccess } from '@/lib/api-helper';
|
||||||
import { cn } from '@/lib/helper';
|
import { cn } from '@/lib/helper';
|
||||||
import { SupplierApi } from '@/services/api/master-data';
|
import { SupplierApi } from '@/services/api/master-data';
|
||||||
import { useTableFilter } from '@/services/hooks/useTableFilter';
|
import { useTableFilter } from '@/services/hooks/useTableFilter';
|
||||||
@@ -205,7 +205,16 @@ const SuppliersTable = () => {
|
|||||||
const confirmationModalDeleteClickHandler = async () => {
|
const confirmationModalDeleteClickHandler = async () => {
|
||||||
setIsDeleteLoading(true);
|
setIsDeleteLoading(true);
|
||||||
|
|
||||||
await SupplierApi.delete(selectedSupplier?.id as number);
|
const deleteResponse = await SupplierApi.delete(
|
||||||
|
selectedSupplier?.id as number
|
||||||
|
);
|
||||||
|
|
||||||
|
if (isResponseError(deleteResponse)) {
|
||||||
|
toast.error(deleteResponse.message);
|
||||||
|
setIsDeleteLoading(false);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
refreshSuppliers();
|
refreshSuppliers();
|
||||||
|
|
||||||
deleteModal.closeModal();
|
deleteModal.closeModal();
|
||||||
|
|||||||
@@ -20,7 +20,7 @@ import RequirePermission from '@/components/helper/RequirePermission';
|
|||||||
import { Uom } from '@/types/api/master-data/uom';
|
import { Uom } from '@/types/api/master-data/uom';
|
||||||
import { UomApi } from '@/services/api/master-data';
|
import { UomApi } from '@/services/api/master-data';
|
||||||
import { cn } from '@/lib/helper';
|
import { cn } from '@/lib/helper';
|
||||||
import { isResponseSuccess } from '@/lib/api-helper';
|
import { isResponseError, isResponseSuccess } from '@/lib/api-helper';
|
||||||
import { useTableFilter } from '@/services/hooks/useTableFilter';
|
import { useTableFilter } from '@/services/hooks/useTableFilter';
|
||||||
import { ROWS_OPTIONS } from '@/config/constant';
|
import { ROWS_OPTIONS } from '@/config/constant';
|
||||||
|
|
||||||
@@ -164,7 +164,14 @@ const UomsTable = () => {
|
|||||||
const confirmationModalDeleteClickHandler = async () => {
|
const confirmationModalDeleteClickHandler = async () => {
|
||||||
setIsDeleteLoading(true);
|
setIsDeleteLoading(true);
|
||||||
|
|
||||||
await UomApi.delete(selectedUom?.id as number);
|
const deleteResponse = await UomApi.delete(selectedUom?.id as number);
|
||||||
|
|
||||||
|
if (isResponseError(deleteResponse)) {
|
||||||
|
toast.error(deleteResponse.message);
|
||||||
|
setIsDeleteLoading(false);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
refreshUoms();
|
refreshUoms();
|
||||||
|
|
||||||
deleteModal.closeModal();
|
deleteModal.closeModal();
|
||||||
|
|||||||
@@ -25,7 +25,7 @@ import RequirePermission from '@/components/helper/RequirePermission';
|
|||||||
import { Warehouse } from '@/types/api/master-data/warehouse';
|
import { Warehouse } from '@/types/api/master-data/warehouse';
|
||||||
import { WarehouseApi } from '@/services/api/master-data';
|
import { WarehouseApi } from '@/services/api/master-data';
|
||||||
import { cn } from '@/lib/helper';
|
import { cn } from '@/lib/helper';
|
||||||
import { isResponseSuccess } from '@/lib/api-helper';
|
import { isResponseError, isResponseSuccess } from '@/lib/api-helper';
|
||||||
import { useTableFilter } from '@/services/hooks/useTableFilter';
|
import { useTableFilter } from '@/services/hooks/useTableFilter';
|
||||||
import { ROWS_OPTIONS } from '@/config/constant';
|
import { ROWS_OPTIONS } from '@/config/constant';
|
||||||
|
|
||||||
@@ -220,7 +220,16 @@ const WarehousesTable = () => {
|
|||||||
const confirmationModalDeleteClickHandler = async () => {
|
const confirmationModalDeleteClickHandler = async () => {
|
||||||
setIsDeleteLoading(true);
|
setIsDeleteLoading(true);
|
||||||
|
|
||||||
await WarehouseApi.delete(selectedWarehouse?.id as number);
|
const deleteResponse = await WarehouseApi.delete(
|
||||||
|
selectedWarehouse?.id as number
|
||||||
|
);
|
||||||
|
|
||||||
|
if (isResponseError(deleteResponse)) {
|
||||||
|
toast.error(deleteResponse.message);
|
||||||
|
setIsDeleteLoading(false);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
refreshWarehouses();
|
refreshWarehouses();
|
||||||
|
|
||||||
deleteModal.closeModal();
|
deleteModal.closeModal();
|
||||||
|
|||||||
@@ -330,7 +330,7 @@ const WarehouseForm = ({ type = 'add', initialValues }: WarehouseFormProps) => {
|
|||||||
required
|
required
|
||||||
label='Nama'
|
label='Nama'
|
||||||
name='name'
|
name='name'
|
||||||
placeholder='Masukkan nama lokasi'
|
placeholder='Masukkan nama warehouse'
|
||||||
value={formik.values.name}
|
value={formik.values.name}
|
||||||
onChange={formik.handleChange}
|
onChange={formik.handleChange}
|
||||||
onBlur={formik.handleBlur}
|
onBlur={formik.handleBlur}
|
||||||
|
|||||||
@@ -102,34 +102,47 @@ const ProjectFlockForm = ({
|
|||||||
);
|
);
|
||||||
|
|
||||||
// Fetch Data
|
// Fetch Data
|
||||||
const { isLoadingOptions: isLoadingFlocks, options: optionsFlock } =
|
const {
|
||||||
useSelect(FlockApi.basePath, 'id', 'name');
|
setInputValue: setInputValueFlock,
|
||||||
|
isLoadingOptions: isLoadingFlocks,
|
||||||
|
options: optionsFlock,
|
||||||
|
loadMore: loadMoreFlock,
|
||||||
|
} = useSelect(FlockApi.basePath, 'id', 'name', '', {
|
||||||
|
project_category: selectedCategory,
|
||||||
|
});
|
||||||
|
|
||||||
const { options: optionsArea, isLoadingOptions: isLoadingAreas } = useSelect(
|
const {
|
||||||
AreaApi.basePath,
|
setInputValue: setInputValueArea,
|
||||||
'id',
|
options: optionsArea,
|
||||||
'name'
|
isLoadingOptions: isLoadingAreas,
|
||||||
);
|
loadMore: loadMoreArea,
|
||||||
|
} = useSelect(AreaApi.basePath, 'id', 'name');
|
||||||
|
|
||||||
const { options: optionsLocation, isLoadingOptions: isLoadingLocations } =
|
const {
|
||||||
useSelect(LocationApi.basePath, 'id', 'name', '', {
|
options: optionsLocation,
|
||||||
area_id:
|
isLoadingOptions: isLoadingLocations,
|
||||||
selectedArea != ''
|
setInputValue: setInputValueLocation,
|
||||||
? selectedArea
|
loadMore: loadMoreLocation,
|
||||||
: ((initialValues?.area?.id ?? '') as string),
|
} = useSelect(LocationApi.basePath, 'id', 'name', '', {
|
||||||
});
|
area_id:
|
||||||
|
selectedArea != ''
|
||||||
|
? selectedArea
|
||||||
|
: ((initialValues?.area?.id ?? '') as string),
|
||||||
|
});
|
||||||
|
|
||||||
const { options: optionsFcr, isLoadingOptions: isLoadingFcrs } = useSelect(
|
const {
|
||||||
FcrApi.basePath,
|
options: optionsFcr,
|
||||||
'id',
|
isLoadingOptions: isLoadingFcrs,
|
||||||
'name'
|
setInputValue: setInputValueFcr,
|
||||||
);
|
loadMore: loadMoreFcr,
|
||||||
|
} = useSelect(FcrApi.basePath, 'id', 'name');
|
||||||
|
|
||||||
const {
|
const {
|
||||||
options: optionsProductionStandards,
|
options: optionsProductionStandards,
|
||||||
isLoadingOptions: isLoadingProductionStandards,
|
isLoadingOptions: isLoadingProductionStandards,
|
||||||
|
setInputValue: setInputValueProductionStandard,
|
||||||
|
loadMore: loadMoreProductionStandard,
|
||||||
} = useSelect(ProductionStandardApi.basePath, 'id', 'name', '', {
|
} = useSelect(ProductionStandardApi.basePath, 'id', 'name', '', {
|
||||||
search: '',
|
|
||||||
project_category: selectedCategory,
|
project_category: selectedCategory,
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -153,6 +166,8 @@ const ProjectFlockForm = ({
|
|||||||
options: optionsNonstock,
|
options: optionsNonstock,
|
||||||
rawData: nonstocks,
|
rawData: nonstocks,
|
||||||
isLoadingOptions: isLoadingNonstocks,
|
isLoadingOptions: isLoadingNonstocks,
|
||||||
|
setInputValue: setInputValueNonstock,
|
||||||
|
loadMore: loadMoreNonstock,
|
||||||
} = useSelect<Nonstock>(NonstockApi.basePath, 'id', 'name');
|
} = useSelect<Nonstock>(NonstockApi.basePath, 'id', 'name');
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
@@ -722,6 +737,8 @@ const ProjectFlockForm = ({
|
|||||||
formik.touched.area_id && Boolean(formik.errors.area_id)
|
formik.touched.area_id && Boolean(formik.errors.area_id)
|
||||||
}
|
}
|
||||||
errorMessage={formik.errors.area_id as string}
|
errorMessage={formik.errors.area_id as string}
|
||||||
|
onInputChange={setInputValueArea}
|
||||||
|
onMenuScrollToBottom={loadMoreArea}
|
||||||
isClearable
|
isClearable
|
||||||
isDisabled={formType != 'add'}
|
isDisabled={formType != 'add'}
|
||||||
/>
|
/>
|
||||||
@@ -740,6 +757,8 @@ const ProjectFlockForm = ({
|
|||||||
formik.touched.location_id &&
|
formik.touched.location_id &&
|
||||||
Boolean(formik.errors.location_id)
|
Boolean(formik.errors.location_id)
|
||||||
}
|
}
|
||||||
|
onInputChange={setInputValueLocation}
|
||||||
|
onMenuScrollToBottom={loadMoreLocation}
|
||||||
errorMessage={formik.errors.location_id as string}
|
errorMessage={formik.errors.location_id as string}
|
||||||
isClearable
|
isClearable
|
||||||
isDisabled={formType != 'add' || disabledLocation}
|
isDisabled={formType != 'add' || disabledLocation}
|
||||||
@@ -766,6 +785,8 @@ const ProjectFlockForm = ({
|
|||||||
);
|
);
|
||||||
}}
|
}}
|
||||||
options={optionsFlock}
|
options={optionsFlock}
|
||||||
|
onInputChange={setInputValueFlock}
|
||||||
|
onMenuScrollToBottom={loadMoreFlock}
|
||||||
isLoading={isLoadingFlocks}
|
isLoading={isLoadingFlocks}
|
||||||
isError={
|
isError={
|
||||||
formik.touched.flock_name && Boolean(formik.errors.flock_name)
|
formik.touched.flock_name && Boolean(formik.errors.flock_name)
|
||||||
@@ -781,6 +802,8 @@ const ProjectFlockForm = ({
|
|||||||
onChange={(val) => {
|
onChange={(val) => {
|
||||||
optionChangeHandler(val, 'fcr');
|
optionChangeHandler(val, 'fcr');
|
||||||
}}
|
}}
|
||||||
|
onInputChange={setInputValueFcr}
|
||||||
|
onMenuScrollToBottom={loadMoreFcr}
|
||||||
options={optionsFcr}
|
options={optionsFcr}
|
||||||
isLoading={isLoadingFcrs}
|
isLoading={isLoadingFcrs}
|
||||||
isError={formik.touched.fcr_id && Boolean(formik.errors.fcr_id)}
|
isError={formik.touched.fcr_id && Boolean(formik.errors.fcr_id)}
|
||||||
@@ -808,6 +831,8 @@ const ProjectFlockForm = ({
|
|||||||
onChange={(val) => {
|
onChange={(val) => {
|
||||||
optionChangeHandler(val, 'production_standard');
|
optionChangeHandler(val, 'production_standard');
|
||||||
}}
|
}}
|
||||||
|
onInputChange={setInputValueProductionStandard}
|
||||||
|
onMenuScrollToBottom={loadMoreProductionStandard}
|
||||||
options={optionsProductionStandards}
|
options={optionsProductionStandards}
|
||||||
isLoading={isLoadingProductionStandards}
|
isLoading={isLoadingProductionStandards}
|
||||||
isError={
|
isError={
|
||||||
@@ -892,6 +917,8 @@ const ProjectFlockForm = ({
|
|||||||
isLoading={isLoadingNonstocks}
|
isLoading={isLoadingNonstocks}
|
||||||
placeholder='Pilih barang non stock'
|
placeholder='Pilih barang non stock'
|
||||||
value={formik.values.project_budgets[index].nonstock}
|
value={formik.values.project_budgets[index].nonstock}
|
||||||
|
onInputChange={setInputValueNonstock}
|
||||||
|
onMenuScrollToBottom={loadMoreNonstock}
|
||||||
onChange={(val) => {
|
onChange={(val) => {
|
||||||
const updatedBudgets = [
|
const updatedBudgets = [
|
||||||
...formik.values.project_budgets,
|
...formik.values.project_budgets,
|
||||||
|
|||||||
@@ -563,7 +563,7 @@ const RecordingForm = ({ type = 'add', initialValues }: RecordingFormProps) => {
|
|||||||
todayRecordings.forEach((recording) => {
|
todayRecordings.forEach((recording) => {
|
||||||
const recordingDate = recording.record_datetime?.split('T')[0];
|
const recordingDate = recording.record_datetime?.split('T')[0];
|
||||||
if (recordingDate === today) {
|
if (recordingDate === today) {
|
||||||
recordedIds.add(recording.project_flock.project_flock_kandang_id);
|
recordedIds.add(recording.project_flock?.project_flock_kandang_id);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -18,6 +18,47 @@ Font.register({
|
|||||||
src: 'helvetica',
|
src: 'helvetica',
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// Status color mappings (same as in DebtSupplierTab)
|
||||||
|
const dueStatusColors: Record<
|
||||||
|
string,
|
||||||
|
{ bg: string; text: string; border: string }
|
||||||
|
> = {
|
||||||
|
'Sudah Jatuh Tempo': { bg: '#FEE2E2', text: '#991B1B', border: '#F87171' }, // error/red
|
||||||
|
'Belum Jatuh Tempo': { bg: '#D1FAE5', text: '#065F46', border: '#34D399' }, // success/green
|
||||||
|
'Mendekati Jatuh Tempo': {
|
||||||
|
bg: '#FEF3C7',
|
||||||
|
text: '#92400E',
|
||||||
|
border: '#FBBF24',
|
||||||
|
}, // warning/yellow
|
||||||
|
};
|
||||||
|
|
||||||
|
const paymentStatusColors: Record<
|
||||||
|
string,
|
||||||
|
{ bg: string; text: string; border: string }
|
||||||
|
> = {
|
||||||
|
'Belum Lunas': { bg: '#FEF3C7', text: '#92400E', border: '#FBBF24' }, // warning/yellow
|
||||||
|
Lunas: { bg: '#DBEAFE', text: '#1E40AF', border: '#60A5FA' }, // primary/blue
|
||||||
|
Pembayaran: { bg: '#D1FAE5', text: '#065F46', border: '#34D399' }, // success/green
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get badge style for PDF rendering
|
||||||
|
* @param statusText - The status text
|
||||||
|
* @param type - Type of status: 'due' or 'payment'
|
||||||
|
* @returns Style object with background and text colors
|
||||||
|
*/
|
||||||
|
const getPDFBadgeStyle = (
|
||||||
|
statusText: string,
|
||||||
|
type: 'due' | 'payment' = 'payment'
|
||||||
|
) => {
|
||||||
|
const colors =
|
||||||
|
type === 'due'
|
||||||
|
? dueStatusColors[statusText]
|
||||||
|
: paymentStatusColors[statusText];
|
||||||
|
|
||||||
|
return colors || { bg: '#F3F4F6', text: '#374151', border: '#D1D5DB' }; // neutral fallback
|
||||||
|
};
|
||||||
|
|
||||||
const pdfStyles = StyleSheet.create({
|
const pdfStyles = StyleSheet.create({
|
||||||
page: {
|
page: {
|
||||||
fontSize: 10,
|
fontSize: 10,
|
||||||
@@ -136,10 +177,40 @@ const pdfStyles = StyleSheet.create({
|
|||||||
backgroundColor: '#F0F0F0',
|
backgroundColor: '#F0F0F0',
|
||||||
fontWeight: 'bold',
|
fontWeight: 'bold',
|
||||||
},
|
},
|
||||||
|
badge: {
|
||||||
|
paddingVertical: 2,
|
||||||
|
paddingHorizontal: 4,
|
||||||
|
borderRadius: 12,
|
||||||
|
fontSize: 5,
|
||||||
|
fontWeight: 'bold',
|
||||||
|
borderWidth: 1,
|
||||||
|
textAlign: 'center',
|
||||||
|
whiteSpace: 'nowrap',
|
||||||
|
},
|
||||||
|
parameterBadge: {
|
||||||
|
backgroundColor: '#F5F5F5',
|
||||||
|
color: '#333333',
|
||||||
|
padding: 4,
|
||||||
|
borderRadius: 4,
|
||||||
|
fontSize: 8,
|
||||||
|
marginRight: 8,
|
||||||
|
marginBottom: 4,
|
||||||
|
},
|
||||||
|
parameterContainer: {
|
||||||
|
flexDirection: 'row',
|
||||||
|
flexWrap: 'wrap',
|
||||||
|
marginBottom: 8,
|
||||||
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
interface DebtSupplierExportPDFParams {
|
interface DebtSupplierExportPDFParams {
|
||||||
data: DebtSupplier[];
|
data: DebtSupplier[];
|
||||||
|
params?: {
|
||||||
|
supplier_name?: string;
|
||||||
|
start_date?: string;
|
||||||
|
end_date?: string;
|
||||||
|
filter_by?: string;
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
const createPDFDocument = (params: DebtSupplierExportPDFParams) => {
|
const createPDFDocument = (params: DebtSupplierExportPDFParams) => {
|
||||||
@@ -157,9 +228,50 @@ const createPDFDocument = (params: DebtSupplierExportPDFParams) => {
|
|||||||
<Text style={pdfStyles.mainTitle}>
|
<Text style={pdfStyles.mainTitle}>
|
||||||
Laporan > Rekapitulasi Hutang ke Supplier
|
Laporan > Rekapitulasi Hutang ke Supplier
|
||||||
</Text>
|
</Text>
|
||||||
|
<View style={pdfStyles.parameterContainer}>
|
||||||
|
<View style={pdfStyles.parameterBadge}>
|
||||||
|
<Text>
|
||||||
|
Periode:{' '}
|
||||||
|
{params.params?.start_date
|
||||||
|
? formatDate(params.params.start_date, 'DD MMM YYYY')
|
||||||
|
: '-'}{' '}
|
||||||
|
s.d{' '}
|
||||||
|
{params.params?.end_date
|
||||||
|
? formatDate(params.params.end_date, 'DD MMM YYYY')
|
||||||
|
: '-'}
|
||||||
|
</Text>
|
||||||
|
</View>
|
||||||
|
{params.params?.filter_by && (
|
||||||
|
<View style={pdfStyles.parameterBadge}>
|
||||||
|
<Text>
|
||||||
|
Filter Tanggal:{' '}
|
||||||
|
{params.params.filter_by === 'po_date'
|
||||||
|
? 'Tanggal PO'
|
||||||
|
: params.params.filter_by === 'received_date'
|
||||||
|
? 'Tanggal Terima'
|
||||||
|
: params.params.filter_by === 'due_date'
|
||||||
|
? 'Tanggal Jatuh Tempo'
|
||||||
|
: params.params.filter_by}
|
||||||
|
</Text>
|
||||||
|
</View>
|
||||||
|
)}
|
||||||
|
<View style={pdfStyles.parameterBadge}>
|
||||||
|
<Text>
|
||||||
|
Supplier: {params.params?.supplier_name || 'Semua Supplier'}
|
||||||
|
</Text>
|
||||||
|
</View>
|
||||||
|
<View style={pdfStyles.parameterBadge}>
|
||||||
|
<Text>
|
||||||
|
Dicetak: {formatDate(new Date(), 'DD MMM YYYY HH:mm')}
|
||||||
|
</Text>
|
||||||
|
</View>
|
||||||
|
</View>
|
||||||
<Text style={pdfStyles.supplierTitle}>
|
<Text style={pdfStyles.supplierTitle}>
|
||||||
{supplierReport.supplier.name}
|
{supplierReport.supplier.name}
|
||||||
</Text>
|
</Text>
|
||||||
|
<Text style={pdfStyles.supplierInfo}>
|
||||||
|
{supplierReport.supplier.category}
|
||||||
|
</Text>
|
||||||
</View>
|
</View>
|
||||||
|
|
||||||
{/* Table */}
|
{/* Table */}
|
||||||
@@ -193,7 +305,7 @@ const createPDFDocument = (params: DebtSupplierExportPDFParams) => {
|
|||||||
<View style={[pdfStyles.tableCellHeader, { flex: 1 }]}>
|
<View style={[pdfStyles.tableCellHeader, { flex: 1 }]}>
|
||||||
<Text>Jatuh Tempo</Text>
|
<Text>Jatuh Tempo</Text>
|
||||||
</View>
|
</View>
|
||||||
<View style={[pdfStyles.tableCellHeader, { flex: 1 }]}>
|
<View style={[pdfStyles.tableCellHeader, { flex: 2 }]}>
|
||||||
<Text>Status Jatuh Tempo</Text>
|
<Text>Status Jatuh Tempo</Text>
|
||||||
</View>
|
</View>
|
||||||
<View style={[pdfStyles.tableCellHeaderRight, { flex: 1.5 }]}>
|
<View style={[pdfStyles.tableCellHeaderRight, { flex: 1.5 }]}>
|
||||||
@@ -205,7 +317,7 @@ const createPDFDocument = (params: DebtSupplierExportPDFParams) => {
|
|||||||
<View style={[pdfStyles.tableCellHeaderRight, { flex: 1.5 }]}>
|
<View style={[pdfStyles.tableCellHeaderRight, { flex: 1.5 }]}>
|
||||||
<Text>Sisa Saldo Hutang (Rp)</Text>
|
<Text>Sisa Saldo Hutang (Rp)</Text>
|
||||||
</View>
|
</View>
|
||||||
<View style={[pdfStyles.tableCellHeader, { flex: 1 }]}>
|
<View style={[pdfStyles.tableCellHeader, { flex: 1.2 }]}>
|
||||||
<Text>Status</Text>
|
<Text>Status</Text>
|
||||||
</View>
|
</View>
|
||||||
<View style={[pdfStyles.tableCellHeader, { flex: 1 }]}>
|
<View style={[pdfStyles.tableCellHeader, { flex: 1 }]}>
|
||||||
@@ -216,40 +328,40 @@ const createPDFDocument = (params: DebtSupplierExportPDFParams) => {
|
|||||||
{/* Initial Balance Row */}
|
{/* Initial Balance Row */}
|
||||||
<View style={[pdfStyles.tableRow, pdfStyles.tableBorderBottom]}>
|
<View style={[pdfStyles.tableRow, pdfStyles.tableBorderBottom]}>
|
||||||
<View style={[pdfStyles.tableCellNo, { flex: 0.5 }]}>
|
<View style={[pdfStyles.tableCellNo, { flex: 0.5 }]}>
|
||||||
<Text></Text>
|
<Text></Text> {/* NO */}
|
||||||
</View>
|
</View>
|
||||||
<View style={[pdfStyles.tableCell, { flex: 1.5 }]}>
|
<View style={[pdfStyles.tableCell, { flex: 1.5 }]}>
|
||||||
<Text></Text>
|
<Text></Text> {/* No. PR */}
|
||||||
</View>
|
</View>
|
||||||
<View style={[pdfStyles.tableCell, { flex: 1.5 }]}>
|
<View style={[pdfStyles.tableCell, { flex: 1.5 }]}>
|
||||||
<Text></Text>
|
<Text></Text> {/* No. PO */}
|
||||||
</View>
|
</View>
|
||||||
<View style={[pdfStyles.tableCellCenter, { flex: 1 }]}>
|
<View style={[pdfStyles.tableCellCenter, { flex: 1 }]}>
|
||||||
<Text></Text>
|
<Text></Text> {/* Tgl Terima/Bayar */}
|
||||||
</View>
|
</View>
|
||||||
<View style={[pdfStyles.tableCellCenter, { flex: 1 }]}>
|
<View style={[pdfStyles.tableCellCenter, { flex: 1 }]}>
|
||||||
<Text></Text>
|
<Text></Text> {/* Tgl PO */}
|
||||||
</View>
|
</View>
|
||||||
<View style={[pdfStyles.tableCellCenter, { flex: 0.6 }]}>
|
<View style={[pdfStyles.tableCellCenter, { flex: 0.6 }]}>
|
||||||
<Text></Text>
|
<Text></Text> {/* Aging */}
|
||||||
</View>
|
</View>
|
||||||
<View style={[pdfStyles.tableCell, { flex: 1 }]}>
|
<View style={[pdfStyles.tableCell, { flex: 1 }]}>
|
||||||
<Text></Text>
|
<Text></Text> {/* Area */}
|
||||||
</View>
|
</View>
|
||||||
<View style={[pdfStyles.tableCell, { flex: 1 }]}>
|
<View style={[pdfStyles.tableCell, { flex: 1 }]}>
|
||||||
<Text></Text>
|
<Text></Text> {/* Gudang */}
|
||||||
</View>
|
</View>
|
||||||
<View style={[pdfStyles.tableCellCenter, { flex: 1 }]}>
|
<View style={[pdfStyles.tableCellCenter, { flex: 1 }]}>
|
||||||
<Text></Text>
|
<Text></Text> {/* Jatuh Tempo */}
|
||||||
</View>
|
</View>
|
||||||
<View style={[pdfStyles.tableCell, { flex: 1 }]}>
|
<View style={[pdfStyles.tableCell, { flex: 2 }]}>
|
||||||
<Text></Text>
|
<Text></Text> {/* Status Jatuh Tempo */}
|
||||||
</View>
|
</View>
|
||||||
<View style={[pdfStyles.tableCellRight, { flex: 1.5 }]}>
|
<View style={[pdfStyles.tableCellRight, { flex: 1.5 }]}>
|
||||||
<Text></Text>
|
<Text></Text> {/* Nominal Pembelian (Rp) */}
|
||||||
</View>
|
</View>
|
||||||
<View style={[pdfStyles.tableCellRight, { flex: 1.5 }]}>
|
<View style={[pdfStyles.tableCellRight, { flex: 1.5 }]}>
|
||||||
<Text></Text>
|
<Text></Text> {/* Pembayaran (Rp) */}
|
||||||
</View>
|
</View>
|
||||||
<View
|
<View
|
||||||
style={[
|
style={[
|
||||||
@@ -261,14 +373,16 @@ const createPDFDocument = (params: DebtSupplierExportPDFParams) => {
|
|||||||
]}
|
]}
|
||||||
>
|
>
|
||||||
<Text>
|
<Text>
|
||||||
|
{' '}
|
||||||
|
{/* Sisa Saldo Hutang (Rp) */}
|
||||||
{formatCurrency(supplierReport.initial_balance || 0)}
|
{formatCurrency(supplierReport.initial_balance || 0)}
|
||||||
</Text>
|
</Text>
|
||||||
</View>
|
</View>
|
||||||
<View style={[pdfStyles.tableCell, { flex: 1 }]}>
|
<View style={[pdfStyles.tableCell, { flex: 1.2 }]}>
|
||||||
<Text></Text>
|
<Text></Text> {/* Status */}
|
||||||
</View>
|
</View>
|
||||||
<View style={[pdfStyles.tableCell, { flex: 1 }]}>
|
<View style={[pdfStyles.tableCell, { flex: 1 }]}>
|
||||||
<Text></Text>
|
<Text></Text> {/* No. Perjalanan */}
|
||||||
</View>
|
</View>
|
||||||
</View>
|
</View>
|
||||||
|
|
||||||
@@ -328,8 +442,32 @@ const createPDFDocument = (params: DebtSupplierExportPDFParams) => {
|
|||||||
: '-'}
|
: '-'}
|
||||||
</Text>
|
</Text>
|
||||||
</View>
|
</View>
|
||||||
<View style={[pdfStyles.tableCell, { flex: 1 }]}>
|
<View style={[pdfStyles.tableCell, { flex: 2 }]}>
|
||||||
<Text>{item.due_status || '-'}</Text>
|
{item.due_status && item.due_status !== '-' ? (
|
||||||
|
<View
|
||||||
|
style={[
|
||||||
|
pdfStyles.badge,
|
||||||
|
{
|
||||||
|
backgroundColor: getPDFBadgeStyle(
|
||||||
|
item.due_status,
|
||||||
|
'due'
|
||||||
|
).bg,
|
||||||
|
borderColor: getPDFBadgeStyle(item.due_status, 'due')
|
||||||
|
.border,
|
||||||
|
},
|
||||||
|
]}
|
||||||
|
>
|
||||||
|
<Text
|
||||||
|
style={{
|
||||||
|
color: getPDFBadgeStyle(item.due_status, 'due').text,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{item.due_status}
|
||||||
|
</Text>
|
||||||
|
</View>
|
||||||
|
) : (
|
||||||
|
<Text>-</Text>
|
||||||
|
)}
|
||||||
</View>
|
</View>
|
||||||
<View
|
<View
|
||||||
style={[
|
style={[
|
||||||
@@ -361,8 +499,32 @@ const createPDFDocument = (params: DebtSupplierExportPDFParams) => {
|
|||||||
>
|
>
|
||||||
<Text>{formatCurrency(item.balance)}</Text>
|
<Text>{formatCurrency(item.balance)}</Text>
|
||||||
</View>
|
</View>
|
||||||
<View style={[pdfStyles.tableCell, { flex: 1 }]}>
|
<View style={[pdfStyles.tableCell, { flex: 1.2 }]}>
|
||||||
<Text>{item.status || '-'}</Text>
|
{item.status && item.status !== '-' ? (
|
||||||
|
<View
|
||||||
|
style={[
|
||||||
|
pdfStyles.badge,
|
||||||
|
{
|
||||||
|
backgroundColor: getPDFBadgeStyle(
|
||||||
|
item.status,
|
||||||
|
'payment'
|
||||||
|
).bg,
|
||||||
|
borderColor: getPDFBadgeStyle(item.status, 'payment')
|
||||||
|
.border,
|
||||||
|
},
|
||||||
|
]}
|
||||||
|
>
|
||||||
|
<Text
|
||||||
|
style={{
|
||||||
|
color: getPDFBadgeStyle(item.status, 'payment').text,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{item.status}
|
||||||
|
</Text>
|
||||||
|
</View>
|
||||||
|
) : (
|
||||||
|
<Text>-</Text>
|
||||||
|
)}
|
||||||
</View>
|
</View>
|
||||||
<View style={[pdfStyles.tableCell, { flex: 1 }]}>
|
<View style={[pdfStyles.tableCell, { flex: 1 }]}>
|
||||||
<Text>{item.travel_number || '-'}</Text>
|
<Text>{item.travel_number || '-'}</Text>
|
||||||
@@ -400,7 +562,7 @@ const createPDFDocument = (params: DebtSupplierExportPDFParams) => {
|
|||||||
<View style={[pdfStyles.tableCell, { flex: 1 }]}>
|
<View style={[pdfStyles.tableCell, { flex: 1 }]}>
|
||||||
<Text></Text>
|
<Text></Text>
|
||||||
</View>
|
</View>
|
||||||
<View style={[pdfStyles.tableCell, { flex: 1 }]}>
|
<View style={[pdfStyles.tableCell, { flex: 2 }]}>
|
||||||
<Text></Text>
|
<Text></Text>
|
||||||
</View>
|
</View>
|
||||||
<View
|
<View
|
||||||
@@ -445,7 +607,7 @@ const createPDFDocument = (params: DebtSupplierExportPDFParams) => {
|
|||||||
>
|
>
|
||||||
<Text>{formatCurrency(supplierReport.total.debt_price)}</Text>
|
<Text>{formatCurrency(supplierReport.total.debt_price)}</Text>
|
||||||
</View>
|
</View>
|
||||||
<View style={[pdfStyles.tableCell, { flex: 1 }]}>
|
<View style={[pdfStyles.tableCell, { flex: 1.2 }]}>
|
||||||
<Text></Text>
|
<Text></Text>
|
||||||
</View>
|
</View>
|
||||||
<View style={[pdfStyles.tableCellLast, { flex: 1 }]}>
|
<View style={[pdfStyles.tableCellLast, { flex: 1 }]}>
|
||||||
|
|||||||
@@ -64,7 +64,7 @@ export const generateDebtSupplierExcel = (
|
|||||||
'Status Jatuh Tempo': item.due_status || '',
|
'Status Jatuh Tempo': item.due_status || '',
|
||||||
'Nominal Pembelian (Rp)': item.total_price || 0,
|
'Nominal Pembelian (Rp)': item.total_price || 0,
|
||||||
'Pembayaran (Rp)': item.payment_price || 0,
|
'Pembayaran (Rp)': item.payment_price || 0,
|
||||||
'Sisa Saldo Hutang (Rp)': item.debt_price || 0,
|
'Sisa Saldo Hutang (Rp)': item.balance || 0,
|
||||||
Status: item.status || '',
|
Status: item.status || '',
|
||||||
'Nomor Perjalanan': item.travel_number || '',
|
'Nomor Perjalanan': item.travel_number || '',
|
||||||
})),
|
})),
|
||||||
@@ -94,18 +94,18 @@ export const generateDebtSupplierExcel = (
|
|||||||
|
|
||||||
const colWidths = [
|
const colWidths = [
|
||||||
{ wch: 5 }, // No
|
{ wch: 5 }, // No
|
||||||
{ wch: 15 }, // Nomor PR
|
{ wch: 10 }, // Nomor PR
|
||||||
{ wch: 15 }, // Nomor PO
|
{ wch: 10 }, // Nomor PO
|
||||||
{ wch: 15 }, // Tanggal Terima/Bayar
|
{ wch: 20 }, // Tanggal Terima/Bayar
|
||||||
{ wch: 15 }, // Tanggal PO
|
{ wch: 10 }, // Tanggal PO
|
||||||
{ wch: 12 }, // Aging
|
{ wch: 10 }, // Aging
|
||||||
{ wch: 15 }, // Area
|
{ wch: 15 }, // Area
|
||||||
{ wch: 15 }, // Gudang
|
{ wch: 15 }, // Gudang
|
||||||
{ wch: 18 }, // Jatuh Tempo
|
{ wch: 12 }, // Jatuh Tempo
|
||||||
{ wch: 18 }, // Status Jatuh Tempo
|
{ wch: 20 }, // Status Jatuh Tempo
|
||||||
{ wch: 15 }, // Nominal Pembelian (Rp)
|
{ wch: 20 }, // Nominal Pembelian (Rp)
|
||||||
{ wch: 15 }, // Pembayaran (Rp)
|
{ wch: 15 }, // Pembayaran (Rp)
|
||||||
{ wch: 15 }, // Sisa Saldo Hutang (Rp)
|
{ wch: 20 }, // Sisa Saldo Hutang (Rp)
|
||||||
{ wch: 12 }, // Status
|
{ wch: 12 }, // Status
|
||||||
{ wch: 15 }, // Nomor Perjalanan
|
{ wch: 15 }, // Nomor Perjalanan
|
||||||
];
|
];
|
||||||
|
|||||||
@@ -9,9 +9,9 @@ import SelectInput, {
|
|||||||
import Menu from '@/components/menu/Menu';
|
import Menu from '@/components/menu/Menu';
|
||||||
import MenuItem from '@/components/menu/MenuItem';
|
import MenuItem from '@/components/menu/MenuItem';
|
||||||
import Modal, { useModal } from '@/components/Modal';
|
import Modal, { useModal } from '@/components/Modal';
|
||||||
import Table from '@/components/Table';
|
import Table, { TABLE_DEFAULT_STYLING } from '@/components/Table';
|
||||||
import { isResponseSuccess } from '@/lib/api-helper';
|
import { isResponseSuccess } from '@/lib/api-helper';
|
||||||
import { formatCurrency, formatDate, formatNumber } from '@/lib/helper';
|
import { cn, formatCurrency, formatDate, formatNumber } from '@/lib/helper';
|
||||||
import { SupplierApi } from '@/services/api/master-data';
|
import { SupplierApi } from '@/services/api/master-data';
|
||||||
import {
|
import {
|
||||||
DebtRow,
|
DebtRow,
|
||||||
@@ -31,10 +31,48 @@ import {
|
|||||||
DebtSupplierFilterSchema,
|
DebtSupplierFilterSchema,
|
||||||
DebtSupplierFilterType,
|
DebtSupplierFilterType,
|
||||||
} from '@/components/pages/report/finance/filter/DebtSupplierFilter';
|
} from '@/components/pages/report/finance/filter/DebtSupplierFilter';
|
||||||
import { getFilledFormikValuesCount } from '@/lib/formik-helper';
|
|
||||||
import ButtonFilter from '@/components/helper/ButtonFilter';
|
import ButtonFilter from '@/components/helper/ButtonFilter';
|
||||||
|
import Badge from '@/components/Badge';
|
||||||
|
import { Color } from '@/types/theme';
|
||||||
import { Supplier } from '@/types/api/master-data/supplier';
|
import { Supplier } from '@/types/api/master-data/supplier';
|
||||||
|
|
||||||
|
const dueStatus: Record<string, Color> = {
|
||||||
|
'Sudah Jatuh Tempo': 'error',
|
||||||
|
'Belum Jatuh Tempo': 'success',
|
||||||
|
'Mendekati Jatuh Tempo': 'warning',
|
||||||
|
};
|
||||||
|
|
||||||
|
const paymentStatus: Record<string, Color> = {
|
||||||
|
'Belum Lunas': 'warning',
|
||||||
|
Lunas: 'primary',
|
||||||
|
Pembayaran: 'success',
|
||||||
|
};
|
||||||
|
|
||||||
|
const getPillBadge = (
|
||||||
|
statusText: string,
|
||||||
|
type: 'due' | 'payment' = 'payment'
|
||||||
|
) => {
|
||||||
|
// Get color based on type
|
||||||
|
const color =
|
||||||
|
type === 'due'
|
||||||
|
? dueStatus[statusText] || 'neutral'
|
||||||
|
: paymentStatus[statusText] || 'neutral';
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Badge
|
||||||
|
color={color as Color}
|
||||||
|
size='sm'
|
||||||
|
variant='soft'
|
||||||
|
className={{
|
||||||
|
badge: `py-2.5 px-2 font-medium text-base-content rounded-full border border-${color}`,
|
||||||
|
}}
|
||||||
|
statusIndicator
|
||||||
|
>
|
||||||
|
{statusText}
|
||||||
|
</Badge>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
const DebtSupplierTab = () => {
|
const DebtSupplierTab = () => {
|
||||||
// ===== STATE MANAGEMENT =====
|
// ===== STATE MANAGEMENT =====
|
||||||
const [isPdfExportLoading, setIsPdfExportLoading] = useState(false);
|
const [isPdfExportLoading, setIsPdfExportLoading] = useState(false);
|
||||||
@@ -212,7 +250,17 @@ const DebtSupplierTab = () => {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
await generateDebtSupplierPDF({ data: allDataForExport });
|
await generateDebtSupplierPDF({
|
||||||
|
data: allDataForExport,
|
||||||
|
params: {
|
||||||
|
supplier_name: formik.values.supplierIds
|
||||||
|
?.map((v) => v.label)
|
||||||
|
.join(', '),
|
||||||
|
filter_by: formik.values.filterBy?.label,
|
||||||
|
start_date: formik.values.startDate || undefined,
|
||||||
|
end_date: formik.values.endDate || undefined,
|
||||||
|
},
|
||||||
|
});
|
||||||
toast.success('PDF berhasil dibuat dan diunduh.');
|
toast.success('PDF berhasil dibuat dan diunduh.');
|
||||||
} catch {
|
} catch {
|
||||||
toast.error('Gagal membuat PDF. Silakan coba lagi.');
|
toast.error('Gagal membuat PDF. Silakan coba lagi.');
|
||||||
@@ -227,6 +275,7 @@ const DebtSupplierTab = () => {
|
|||||||
header: 'No',
|
header: 'No',
|
||||||
enableSorting: false,
|
enableSorting: false,
|
||||||
cell: (props) => props.row.index,
|
cell: (props) => props.row.index,
|
||||||
|
footer: () => 'Total',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: 'pr_number',
|
id: 'pr_number',
|
||||||
@@ -331,7 +380,7 @@ const DebtSupplierTab = () => {
|
|||||||
enableSorting: false,
|
enableSorting: false,
|
||||||
cell: (props) => {
|
cell: (props) => {
|
||||||
const value = props.row.original.due_status;
|
const value = props.row.original.due_status;
|
||||||
return value || '-';
|
return value ? (value != '-' ? getPillBadge(value, 'due') : '-') : '-';
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@@ -407,7 +456,11 @@ const DebtSupplierTab = () => {
|
|||||||
enableSorting: false,
|
enableSorting: false,
|
||||||
cell: (props) => {
|
cell: (props) => {
|
||||||
const value = props.row.original.status;
|
const value = props.row.original.status;
|
||||||
return value || '-';
|
return value
|
||||||
|
? value != '-'
|
||||||
|
? getPillBadge(value, 'payment')
|
||||||
|
: '-'
|
||||||
|
: '-';
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@@ -475,9 +528,15 @@ const DebtSupplierTab = () => {
|
|||||||
<Card
|
<Card
|
||||||
key={supplierReport.supplier.id}
|
key={supplierReport.supplier.id}
|
||||||
title={supplierReport.supplier.name}
|
title={supplierReport.supplier.name}
|
||||||
className={{ wrapper: 'w-full' }}
|
className={{
|
||||||
|
wrapper: 'w-full !rounded-lg',
|
||||||
|
body: 'p-0 rounded-lg',
|
||||||
|
title:
|
||||||
|
'ps-2 pt-1 pb-1 font-normal text-md bg-primary text-white',
|
||||||
|
}}
|
||||||
variant='bordered'
|
variant='bordered'
|
||||||
collapsible={true}
|
collapsible={true}
|
||||||
|
defaultCollapsed={true}
|
||||||
>
|
>
|
||||||
<Table
|
<Table
|
||||||
data={[
|
data={[
|
||||||
@@ -491,34 +550,43 @@ const DebtSupplierTab = () => {
|
|||||||
renderFooter={supplierReport.rows.length > 0}
|
renderFooter={supplierReport.rows.length > 0}
|
||||||
className={{
|
className={{
|
||||||
containerClassName: 'w-full',
|
containerClassName: 'w-full',
|
||||||
tableWrapperClassName: 'overflow-x-auto mt-4',
|
tableWrapperClassName: 'overflow-x-auto',
|
||||||
tableClassName: 'w-full table-auto text-sm',
|
headerColumnClassName: cn(
|
||||||
headerRowClassName: 'border-b border-b-gray-200 bg-gray-50',
|
TABLE_DEFAULT_STYLING.headerColumnClassName,
|
||||||
headerColumnClassName:
|
'whitespace-nowrap'
|
||||||
'px-4 py-3 text-xs font-semibold text-gray-700 text-left border border-gray-200',
|
),
|
||||||
bodyRowClassName:
|
bodyColumnClassName: cn(
|
||||||
'hover:bg-gray-50 transition-colors border-b border-l border-r border-b-gray-200 border-l-gray-200 border-r-gray-200',
|
TABLE_DEFAULT_STYLING.bodyColumnClassName,
|
||||||
bodyColumnClassName:
|
'whitespace-nowrap'
|
||||||
'px-4 py-3 text-xs text-gray-900 whitespace-nowrap',
|
),
|
||||||
tableFooterClassName:
|
footerRowClassName: cn(
|
||||||
'bg-gray-100 font-semibold border border-gray-200',
|
TABLE_DEFAULT_STYLING.footerRowClassName,
|
||||||
footerRowClassName: 'border-t-2 border-gray-300',
|
'bg-white'
|
||||||
footerColumnClassName:
|
),
|
||||||
'px-4 py-3 text-xs text-gray-900 whitespace-nowrap',
|
footerColumnClassName: cn(
|
||||||
|
TABLE_DEFAULT_STYLING.footerColumnClassName,
|
||||||
|
'whitespace-nowrap p-3'
|
||||||
|
),
|
||||||
paginationClassName: 'hidden',
|
paginationClassName: 'hidden',
|
||||||
}}
|
}}
|
||||||
renderCustomRow={(row) => {
|
renderCustomRow={(row) => {
|
||||||
if (row.index == 0) {
|
if (row.index == 0) {
|
||||||
return (
|
return (
|
||||||
<tr
|
<tr
|
||||||
className='hover:bg-gray-50 transition-colors border-b border-l border-r border-b-gray-200 border-l-gray-200 border-r-gray-200'
|
className={cn(TABLE_DEFAULT_STYLING.bodyRowClassName)}
|
||||||
key={row.index}
|
key={row.index}
|
||||||
>
|
>
|
||||||
<td
|
<td
|
||||||
className='px-4 py-3 text-xs text-gray-900 whitespace-nowrap'
|
className={cn(
|
||||||
|
TABLE_DEFAULT_STYLING.bodyColumnClassName
|
||||||
|
)}
|
||||||
colSpan={12}
|
colSpan={12}
|
||||||
></td>
|
></td>
|
||||||
<td className='px-4 py-3 text-xs text-gray-900 whitespace-nowrap'>
|
<td
|
||||||
|
className={cn(
|
||||||
|
TABLE_DEFAULT_STYLING.bodyColumnClassName
|
||||||
|
)}
|
||||||
|
>
|
||||||
<div
|
<div
|
||||||
className={`text-right ${row.original.balance < 0 ? 'text-red-500' : ''}`}
|
className={`text-right ${row.original.balance < 0 ? 'text-red-500' : ''}`}
|
||||||
>
|
>
|
||||||
@@ -526,7 +594,9 @@ const DebtSupplierTab = () => {
|
|||||||
</div>
|
</div>
|
||||||
</td>
|
</td>
|
||||||
<td
|
<td
|
||||||
className='px-4 py-3 text-xs text-gray-900 whitespace-nowrap'
|
className={cn(
|
||||||
|
TABLE_DEFAULT_STYLING.bodyColumnClassName
|
||||||
|
)}
|
||||||
colSpan={2}
|
colSpan={2}
|
||||||
></td>
|
></td>
|
||||||
</tr>
|
</tr>
|
||||||
|
|||||||
@@ -21,10 +21,18 @@ import {
|
|||||||
ProjectFlockApi,
|
ProjectFlockApi,
|
||||||
ProjectFlockKandangApi,
|
ProjectFlockKandangApi,
|
||||||
} from '@/services/api/production';
|
} from '@/services/api/production';
|
||||||
import { ProjectFlockKandang } from '@/types/api/production/project-flock-kandang';
|
import {
|
||||||
import { isResponseError } from '@/lib/api-helper';
|
BaseProjectFlockKandang,
|
||||||
|
ProjectFlockKandang,
|
||||||
|
} from '@/types/api/production/project-flock-kandang';
|
||||||
|
import { isResponseError, isResponseSuccess } from '@/lib/api-helper';
|
||||||
import Pagination from '@/components/Pagination';
|
import Pagination from '@/components/Pagination';
|
||||||
import { ProductionResultReportApi } from '@/services/api/report/production-result';
|
import { ProductionResultReportApi } from '@/services/api/report/production-result';
|
||||||
|
import { BaseApiResponse } from '@/types/api/api-general';
|
||||||
|
import { httpClient } from '@/services/http/client';
|
||||||
|
import { ProductionResult } from '@/types/api/report/production-result';
|
||||||
|
import ProductionResultReportPDF from './ProductionResultReportPDF';
|
||||||
|
import { pdf } from '@react-pdf/renderer';
|
||||||
|
|
||||||
const ProductionResultContent = () => {
|
const ProductionResultContent = () => {
|
||||||
const [projectFlockKandangs, setProjectFlockKandangs] = useState<
|
const [projectFlockKandangs, setProjectFlockKandangs] = useState<
|
||||||
@@ -49,6 +57,8 @@ const ProductionResultContent = () => {
|
|||||||
const [isLoadingExportingToExcel, setIsLoadingExportingToExcel] =
|
const [isLoadingExportingToExcel, setIsLoadingExportingToExcel] =
|
||||||
useState(false);
|
useState(false);
|
||||||
|
|
||||||
|
const [isLoadingExportingToPdf, setIsLoadingExportingToPdf] = useState(false);
|
||||||
|
|
||||||
const [selectedArea, setSelectedArea] = useState<OptionType | null>(null);
|
const [selectedArea, setSelectedArea] = useState<OptionType | null>(null);
|
||||||
const [selectedLocation, setSelectedLocation] = useState<OptionType | null>(
|
const [selectedLocation, setSelectedLocation] = useState<OptionType | null>(
|
||||||
null
|
null
|
||||||
@@ -158,6 +168,87 @@ const ProductionResultContent = () => {
|
|||||||
setIsLoadingExportingToExcel(false);
|
setIsLoadingExportingToExcel(false);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const exportToPdfHandler = async () => {
|
||||||
|
setIsLoadingExportingToPdf(true);
|
||||||
|
|
||||||
|
try {
|
||||||
|
let projectFlockKandangsData: BaseProjectFlockKandang[] = [];
|
||||||
|
|
||||||
|
if (selectedProjectFlockKandang) {
|
||||||
|
const projectFlockKandangResponse =
|
||||||
|
await ProjectFlockKandangApi.getSingle(
|
||||||
|
selectedProjectFlockKandang?.value as number
|
||||||
|
);
|
||||||
|
|
||||||
|
projectFlockKandangsData = isResponseSuccess(
|
||||||
|
projectFlockKandangResponse
|
||||||
|
)
|
||||||
|
? [projectFlockKandangResponse.data]
|
||||||
|
: [];
|
||||||
|
} else {
|
||||||
|
const projectFlockKandangsResponse =
|
||||||
|
await ProjectFlockKandangApi.getAll({
|
||||||
|
area_id: selectedArea?.value,
|
||||||
|
project_flock_id: selectedProjectFlock?.value,
|
||||||
|
});
|
||||||
|
|
||||||
|
projectFlockKandangsData = isResponseSuccess(
|
||||||
|
projectFlockKandangsResponse
|
||||||
|
)
|
||||||
|
? projectFlockKandangsResponse.data
|
||||||
|
: [];
|
||||||
|
}
|
||||||
|
|
||||||
|
const mappedProductionResults: {
|
||||||
|
projectFlockKandang: BaseProjectFlockKandang;
|
||||||
|
productionResult: ProductionResult[] | null;
|
||||||
|
}[] = await Promise.all(
|
||||||
|
projectFlockKandangsData.map(async (projectFlockKandang) => {
|
||||||
|
const getProductionResultPath = `${ProductionResultReportApi.basePath}/${projectFlockKandang.id}?page=1&limit=100`;
|
||||||
|
const getProductionResultRes = await httpClient<
|
||||||
|
BaseApiResponse<ProductionResult[]>
|
||||||
|
>(getProductionResultPath);
|
||||||
|
|
||||||
|
return {
|
||||||
|
projectFlockKandang,
|
||||||
|
productionResult: isResponseSuccess(getProductionResultRes)
|
||||||
|
? getProductionResultRes.data
|
||||||
|
: null,
|
||||||
|
};
|
||||||
|
})
|
||||||
|
);
|
||||||
|
|
||||||
|
if (mappedProductionResults.length === 0) {
|
||||||
|
toast.error('Tidak ada data untuk diexport.');
|
||||||
|
setIsLoadingExportingToPdf(false);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const openPdf = async () => {
|
||||||
|
const productionResultPdfBlob = await pdf(
|
||||||
|
<ProductionResultReportPDF
|
||||||
|
mappedProductionResults={mappedProductionResults}
|
||||||
|
/>
|
||||||
|
).toBlob();
|
||||||
|
|
||||||
|
const productionResultReportPdfUrl = URL.createObjectURL(
|
||||||
|
productionResultPdfBlob
|
||||||
|
);
|
||||||
|
window.open(productionResultReportPdfUrl, '_blank');
|
||||||
|
};
|
||||||
|
|
||||||
|
await openPdf();
|
||||||
|
} catch (error) {
|
||||||
|
console.error(error);
|
||||||
|
toast.error('Gagal melakukan export laporan hasil produksi! Coba lagi.');
|
||||||
|
}
|
||||||
|
// await ProductionResultReportApi.exportProductionResultToPdf(
|
||||||
|
// projectFlockKandangs
|
||||||
|
// );
|
||||||
|
|
||||||
|
setIsLoadingExportingToPdf(false);
|
||||||
|
};
|
||||||
|
|
||||||
const searchHandler = async () => {
|
const searchHandler = async () => {
|
||||||
setProjectFlockKandangs(null);
|
setProjectFlockKandangs(null);
|
||||||
setIsLoadingSearch(true);
|
setIsLoadingSearch(true);
|
||||||
@@ -355,6 +446,13 @@ const ProductionResultContent = () => {
|
|||||||
onClick={exportToExcelHandler}
|
onClick={exportToExcelHandler}
|
||||||
className='text-nowrap'
|
className='text-nowrap'
|
||||||
/>
|
/>
|
||||||
|
<MenuItem
|
||||||
|
title='Export to PDF'
|
||||||
|
icon='icon-park-outline:file-pdf-one'
|
||||||
|
isLoading={isLoadingExportingToPdf}
|
||||||
|
onClick={exportToPdfHandler}
|
||||||
|
className='text-nowrap'
|
||||||
|
/>
|
||||||
</Menu>
|
</Menu>
|
||||||
</Dropdown>
|
</Dropdown>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -0,0 +1,388 @@
|
|||||||
|
'use client';
|
||||||
|
|
||||||
|
import React from 'react';
|
||||||
|
import {
|
||||||
|
Document,
|
||||||
|
Page,
|
||||||
|
StyleSheet,
|
||||||
|
Text,
|
||||||
|
View,
|
||||||
|
Image,
|
||||||
|
} from '@react-pdf/renderer';
|
||||||
|
|
||||||
|
import { formatDate, formatNumber } from '@/lib/helper';
|
||||||
|
import { BaseProjectFlockKandang } from '@/types/api/production/project-flock-kandang';
|
||||||
|
import { ProductionResult } from '@/types/api/report/production-result';
|
||||||
|
|
||||||
|
type MappedProductionResultsItem = {
|
||||||
|
projectFlockKandang: BaseProjectFlockKandang;
|
||||||
|
productionResult: ProductionResult[] | null;
|
||||||
|
};
|
||||||
|
|
||||||
|
interface ProductionResultReportPDFProps {
|
||||||
|
mappedProductionResults?: MappedProductionResultsItem[];
|
||||||
|
}
|
||||||
|
|
||||||
|
const styles = StyleSheet.create({
|
||||||
|
page: {
|
||||||
|
paddingTop: 24,
|
||||||
|
paddingBottom: 52,
|
||||||
|
paddingHorizontal: 16,
|
||||||
|
},
|
||||||
|
|
||||||
|
companyInfoHeader: {
|
||||||
|
width: '100%',
|
||||||
|
display: 'flex',
|
||||||
|
flexDirection: 'row',
|
||||||
|
justifyContent: 'space-between',
|
||||||
|
alignItems: 'flex-start',
|
||||||
|
marginBottom: 8,
|
||||||
|
},
|
||||||
|
companyLogo: {
|
||||||
|
width: 64,
|
||||||
|
height: 'auto',
|
||||||
|
},
|
||||||
|
companyInfoHeaderDate: {
|
||||||
|
paddingTop: 8,
|
||||||
|
fontSize: 10,
|
||||||
|
},
|
||||||
|
companyName: {
|
||||||
|
fontSize: 12,
|
||||||
|
fontWeight: 'bold',
|
||||||
|
marginBottom: 4,
|
||||||
|
},
|
||||||
|
companyAddress: {
|
||||||
|
fontSize: 8,
|
||||||
|
maxWidth: 420,
|
||||||
|
marginBottom: 10,
|
||||||
|
},
|
||||||
|
doubleDivider: {
|
||||||
|
width: '100%',
|
||||||
|
height: 6,
|
||||||
|
borderTopWidth: 2,
|
||||||
|
borderTopColor: '#000',
|
||||||
|
borderBottomWidth: 2,
|
||||||
|
borderBottomColor: '#000',
|
||||||
|
},
|
||||||
|
|
||||||
|
title: {
|
||||||
|
marginTop: 14,
|
||||||
|
fontSize: 14,
|
||||||
|
lineHeight: '150%',
|
||||||
|
textAlign: 'center',
|
||||||
|
fontFamily: 'Times-Roman',
|
||||||
|
fontWeight: 'bold',
|
||||||
|
},
|
||||||
|
|
||||||
|
footer: {
|
||||||
|
width: '100%',
|
||||||
|
display: 'flex',
|
||||||
|
flexDirection: 'row',
|
||||||
|
justifyContent: 'space-between',
|
||||||
|
alignItems: 'center',
|
||||||
|
paddingHorizontal: 16,
|
||||||
|
position: 'absolute',
|
||||||
|
fontSize: 8,
|
||||||
|
bottom: 22,
|
||||||
|
left: 0,
|
||||||
|
right: 0,
|
||||||
|
textAlign: 'center',
|
||||||
|
color: 'grey',
|
||||||
|
},
|
||||||
|
|
||||||
|
section: {
|
||||||
|
marginTop: 12,
|
||||||
|
borderWidth: 1,
|
||||||
|
borderColor: '#000',
|
||||||
|
padding: 8,
|
||||||
|
},
|
||||||
|
|
||||||
|
sectionHeader: {
|
||||||
|
marginBottom: 6,
|
||||||
|
flexDirection: 'row',
|
||||||
|
justifyContent: 'space-between',
|
||||||
|
alignItems: 'baseline',
|
||||||
|
},
|
||||||
|
sectionTitle: {
|
||||||
|
fontSize: 10,
|
||||||
|
fontWeight: 'bold',
|
||||||
|
},
|
||||||
|
sectionSubtitle: {
|
||||||
|
fontSize: 8,
|
||||||
|
color: '#444',
|
||||||
|
},
|
||||||
|
|
||||||
|
// Simple grid table (label/value pairs)
|
||||||
|
grid: {
|
||||||
|
width: '100%',
|
||||||
|
borderWidth: 1,
|
||||||
|
borderColor: '#000',
|
||||||
|
},
|
||||||
|
gridRow: {
|
||||||
|
flexDirection: 'row',
|
||||||
|
borderBottomWidth: 1,
|
||||||
|
borderBottomColor: '#000',
|
||||||
|
},
|
||||||
|
gridRowLast: {
|
||||||
|
borderBottomWidth: 0,
|
||||||
|
},
|
||||||
|
gridCellLabel: {
|
||||||
|
width: '40%',
|
||||||
|
paddingVertical: 3,
|
||||||
|
paddingHorizontal: 6,
|
||||||
|
fontSize: 8,
|
||||||
|
borderRightWidth: 1,
|
||||||
|
borderRightColor: '#000',
|
||||||
|
fontWeight: 'bold',
|
||||||
|
},
|
||||||
|
gridCellValue: {
|
||||||
|
width: '60%',
|
||||||
|
paddingVertical: 3,
|
||||||
|
paddingHorizontal: 6,
|
||||||
|
fontSize: 8,
|
||||||
|
textAlign: 'right',
|
||||||
|
},
|
||||||
|
|
||||||
|
// Subsection headings
|
||||||
|
groupTitle: {
|
||||||
|
marginTop: 8,
|
||||||
|
marginBottom: 4,
|
||||||
|
fontSize: 9,
|
||||||
|
fontWeight: 'bold',
|
||||||
|
},
|
||||||
|
|
||||||
|
emptyText: {
|
||||||
|
fontSize: 8,
|
||||||
|
color: '#666',
|
||||||
|
fontStyle: 'italic',
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
function safeNum(v: unknown): number {
|
||||||
|
const n = typeof v === 'number' ? v : Number(v);
|
||||||
|
return Number.isFinite(n) ? n : 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
function valueText(v: unknown) {
|
||||||
|
if (v === null || v === undefined) return '-';
|
||||||
|
if (typeof v === 'number') return formatNumber(v);
|
||||||
|
return String(v);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Render label/value table for one ProductionResult.
|
||||||
|
* Uses a compact grid to keep page readable.
|
||||||
|
*/
|
||||||
|
function ProductionResultGrid({ pr }: { pr: ProductionResult }) {
|
||||||
|
const rows: Array<[string, string]> = [
|
||||||
|
['WOA', valueText(pr.woa)],
|
||||||
|
|
||||||
|
// BW
|
||||||
|
['BW', valueText(pr.bw)],
|
||||||
|
['Std BW', valueText(pr.std_bw)],
|
||||||
|
['Uniformity', valueText(pr.uniformity)],
|
||||||
|
['Std Uniformity', valueText(pr.std_uniformity)],
|
||||||
|
|
||||||
|
// Dep
|
||||||
|
['Dep Kum', valueText(pr.dep_kum)],
|
||||||
|
['Dep Std', valueText(pr.dep_std)],
|
||||||
|
|
||||||
|
// Butiran
|
||||||
|
['Butiran Utuh', valueText(pr.butiran_utuh)],
|
||||||
|
['Butiran Putih', valueText(pr.butiran_putih)],
|
||||||
|
['Butiran Retak', valueText(pr.butiran_retak)],
|
||||||
|
['Butiran Pecah', valueText(pr.butiran_pecah)],
|
||||||
|
['Butiran Jumlah', valueText(pr.butiran_jumlah)],
|
||||||
|
['Total Butir', valueText(pr.total_butir)],
|
||||||
|
|
||||||
|
// Kg
|
||||||
|
['Kg Utuh', valueText(pr.kg_utuh)],
|
||||||
|
['Kg Putih', valueText(pr.kg_putih)],
|
||||||
|
['Kg Retak', valueText(pr.kg_retak)],
|
||||||
|
['Kg Pecah', valueText(pr.kg_pecah)],
|
||||||
|
['Kg Jumlah', valueText(pr.kg_jumlah)],
|
||||||
|
['Total Kg', valueText(pr.total_kg)],
|
||||||
|
|
||||||
|
// %
|
||||||
|
['% Utuh', valueText(pr.persen_utuh)],
|
||||||
|
['% Putih', valueText(pr.persen_putih)],
|
||||||
|
['% Retak', valueText(pr.persen_retak)],
|
||||||
|
['% Pecah', valueText(pr.persen_pecah)],
|
||||||
|
|
||||||
|
// Produksi
|
||||||
|
['HD', valueText(pr.hd)],
|
||||||
|
['HD Std', valueText(pr.hd_std)],
|
||||||
|
['FI', valueText(pr.fi)],
|
||||||
|
['FI Std', valueText(pr.fi_std)],
|
||||||
|
['EM', valueText(pr.em)],
|
||||||
|
['EM Std', valueText(pr.em_std)],
|
||||||
|
['EW', valueText(pr.ew)],
|
||||||
|
['EW Std', valueText(pr.ew_std)],
|
||||||
|
['FCR', valueText(pr.fcr)],
|
||||||
|
['FCR Std', valueText(pr.fcr_std)],
|
||||||
|
['HH', valueText(pr.hh)],
|
||||||
|
['HH Std', valueText(pr.hh_std)],
|
||||||
|
];
|
||||||
|
|
||||||
|
return (
|
||||||
|
<View style={styles.grid}>
|
||||||
|
{rows.map(([label, value], idx) => {
|
||||||
|
const isLast = idx === rows.length - 1;
|
||||||
|
return (
|
||||||
|
<View
|
||||||
|
key={label}
|
||||||
|
style={[styles.gridRow, ...(isLast ? [styles.gridRowLast] : [])]}
|
||||||
|
>
|
||||||
|
<Text style={styles.gridCellLabel}>{label}</Text>
|
||||||
|
<Text style={styles.gridCellValue}>{value}</Text>
|
||||||
|
</View>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</View>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* If there are multiple ProductionResult entries for a kandang,
|
||||||
|
* we show them sequentially with a small header per result.
|
||||||
|
*
|
||||||
|
* You can later change this to render only the latest WOA, or group by week.
|
||||||
|
*/
|
||||||
|
function ProductionResultList({
|
||||||
|
productionResults,
|
||||||
|
}: {
|
||||||
|
productionResults: ProductionResult[];
|
||||||
|
}) {
|
||||||
|
return (
|
||||||
|
<View>
|
||||||
|
{productionResults.map((pr, idx) => {
|
||||||
|
const kandangName =
|
||||||
|
pr.project_flock?.kandang?.name ||
|
||||||
|
pr.project_flock?.kandang?.id?.toString() ||
|
||||||
|
'';
|
||||||
|
|
||||||
|
// Optional: show a compact subheader
|
||||||
|
const headerLeft = `Data #${idx + 1}`;
|
||||||
|
const headerRight =
|
||||||
|
kandangName && pr.woa !== undefined
|
||||||
|
? `${kandangName} • WOA ${safeNum(pr.woa)}`
|
||||||
|
: pr.woa !== undefined
|
||||||
|
? `WOA ${safeNum(pr.woa)}`
|
||||||
|
: '';
|
||||||
|
|
||||||
|
return (
|
||||||
|
<View
|
||||||
|
key={`${pr.project_flock?.id ?? 'pf'}-${idx}`}
|
||||||
|
style={{ marginTop: idx === 0 ? 0 : 10 }}
|
||||||
|
wrap={false}
|
||||||
|
>
|
||||||
|
<View style={styles.sectionHeader}>
|
||||||
|
<Text style={styles.sectionTitle}>{headerLeft}</Text>
|
||||||
|
<Text style={styles.sectionSubtitle}>{headerRight}</Text>
|
||||||
|
</View>
|
||||||
|
|
||||||
|
<ProductionResultGrid pr={pr} />
|
||||||
|
</View>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</View>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* ✅ Main PDF Component
|
||||||
|
*/
|
||||||
|
const ProductionResultReportPDF = ({
|
||||||
|
mappedProductionResults = [],
|
||||||
|
}: ProductionResultReportPDFProps) => {
|
||||||
|
return (
|
||||||
|
<Document>
|
||||||
|
<Page style={styles.page} size='A4'>
|
||||||
|
{/* Header */}
|
||||||
|
<View>
|
||||||
|
<View style={styles.companyInfoHeader}>
|
||||||
|
<Image style={styles.companyLogo} src='/assets/img/lti-logo.png' />
|
||||||
|
<Text style={styles.companyInfoHeaderDate}>
|
||||||
|
{formatDate(Date.now(), 'DD MMMM YYYY')}
|
||||||
|
</Text>
|
||||||
|
</View>
|
||||||
|
|
||||||
|
<View>
|
||||||
|
<Text style={styles.companyName}>PT LUMBUNG TELUR INDONESIA</Text>
|
||||||
|
<Text style={styles.companyAddress}>
|
||||||
|
SOHO Building Lt.3 (Paris Van Java), Jalan Karang Tinggal, Kel.
|
||||||
|
Cipedes, Kec. Sukajadi, Kota Bandung 40162
|
||||||
|
</Text>
|
||||||
|
|
||||||
|
<View style={styles.doubleDivider} />
|
||||||
|
</View>
|
||||||
|
</View>
|
||||||
|
|
||||||
|
<Text style={styles.title}>Laporan Production Result</Text>
|
||||||
|
|
||||||
|
{/* Sections per ProjectFlockKandang */}
|
||||||
|
{mappedProductionResults.length === 0 ? (
|
||||||
|
<View style={{ marginTop: 16 }}>
|
||||||
|
<Text style={styles.emptyText}>Tidak ada data.</Text>
|
||||||
|
</View>
|
||||||
|
) : (
|
||||||
|
mappedProductionResults.map((item, idx) => {
|
||||||
|
const pfk = item.projectFlockKandang;
|
||||||
|
|
||||||
|
// Try to display meaningful identifiers.
|
||||||
|
// Adjust these fields based on your real BaseProjectFlockKandang structure.
|
||||||
|
const kandangName =
|
||||||
|
pfk?.kandang?.name ?? `Kandang #${pfk?.kandang_id ?? idx + 1}`;
|
||||||
|
|
||||||
|
const projectName = pfk?.project_flock?.name ?? '';
|
||||||
|
|
||||||
|
const locationName = pfk?.project_flock?.location?.name ?? '';
|
||||||
|
|
||||||
|
const areaName = pfk?.project_flock?.area?.name ?? '';
|
||||||
|
|
||||||
|
return (
|
||||||
|
<View
|
||||||
|
key={`pfk-${pfk?.id ?? idx}`}
|
||||||
|
style={styles.section}
|
||||||
|
break={idx > 0} // each kandang starts on a new page for clarity
|
||||||
|
>
|
||||||
|
<View style={styles.sectionHeader}>
|
||||||
|
<Text style={styles.sectionTitle}>
|
||||||
|
{projectName
|
||||||
|
? `${projectName} • ${kandangName}`
|
||||||
|
: kandangName}
|
||||||
|
</Text>
|
||||||
|
<Text style={styles.sectionSubtitle}>
|
||||||
|
{[areaName, locationName].filter(Boolean).join(' • ')}
|
||||||
|
</Text>
|
||||||
|
</View>
|
||||||
|
|
||||||
|
{item.productionResult && item.productionResult.length > 0 ? (
|
||||||
|
<ProductionResultList
|
||||||
|
productionResults={item.productionResult}
|
||||||
|
/>
|
||||||
|
) : (
|
||||||
|
<Text style={styles.emptyText}>
|
||||||
|
Tidak ada production result untuk kandang ini.
|
||||||
|
</Text>
|
||||||
|
)}
|
||||||
|
</View>
|
||||||
|
);
|
||||||
|
})
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* Footer */}
|
||||||
|
<View style={styles.footer} fixed>
|
||||||
|
<Text
|
||||||
|
render={({ pageNumber, totalPages }) =>
|
||||||
|
`${pageNumber} / ${totalPages}`
|
||||||
|
}
|
||||||
|
fixed
|
||||||
|
/>
|
||||||
|
</View>
|
||||||
|
</Page>
|
||||||
|
</Document>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default ProductionResultReportPDF;
|
||||||
+22
-18
@@ -10,61 +10,65 @@ export const MAIN_DRAWER_LINKS: SidebarMenuItem[] = [
|
|||||||
text: 'Daily Checklist',
|
text: 'Daily Checklist',
|
||||||
link: '/daily-checklist',
|
link: '/daily-checklist',
|
||||||
icon: 'heroicons-outline:clipboard-check',
|
icon: 'heroicons-outline:clipboard-check',
|
||||||
// TODO: add permission
|
permission: [
|
||||||
// permission: ['lti.daily_checklist.list'],
|
'lti.daily_checklist.dashboard.list',
|
||||||
|
'lti.daily_checklist.create',
|
||||||
|
'lti.daily_checklist.list',
|
||||||
|
'lti.daily_checklist.detail',
|
||||||
|
'lti.daily_checklist.reports',
|
||||||
|
'lti.daily_checklist.master_data.employee',
|
||||||
|
'lti.daily_checklist.master_data.activity',
|
||||||
|
'lti.daily_checklist.master_data.configuration',
|
||||||
|
],
|
||||||
submenu: [
|
submenu: [
|
||||||
{
|
{
|
||||||
text: 'Dashboard',
|
text: 'Dashboard',
|
||||||
link: '/daily-checklist/dashboard',
|
link: '/daily-checklist/dashboard',
|
||||||
icon: 'lucide:layout-dashboard',
|
icon: 'lucide:layout-dashboard',
|
||||||
// TODO: add permission
|
permission: ['lti.daily_checklist.dashboard.list'],
|
||||||
// permission: ['lti.daily_checklist.list'],
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
text: 'Daily Checklist',
|
text: 'Daily Checklist',
|
||||||
link: '/daily-checklist/daily-checklist',
|
link: '/daily-checklist/daily-checklist',
|
||||||
icon: 'lucide:clipboard-check',
|
icon: 'lucide:clipboard-check',
|
||||||
// TODO: add permission
|
permission: ['lti.daily_checklist.create'],
|
||||||
// permission: ['lti.daily_checklist.list'],
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
text: 'Daftar Daily Checklist',
|
text: 'Daftar Daily Checklist',
|
||||||
link: '/daily-checklist/list-daily-checklist',
|
link: '/daily-checklist/list-daily-checklist',
|
||||||
icon: 'lucide:circle-check',
|
icon: 'lucide:circle-check',
|
||||||
// TODO: add permission
|
permission: ['lti.daily_checklist.list'],
|
||||||
// permission: ['lti.daily_checklist.list'],
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
text: 'Laporan',
|
text: 'Laporan',
|
||||||
link: '/daily-checklist/reports',
|
link: '/daily-checklist/reports',
|
||||||
icon: 'lucide:file-text',
|
icon: 'lucide:file-text',
|
||||||
// TODO: add permission
|
permission: ['lti.daily_checklist.reports'],
|
||||||
// permission: ['lti.daily_checklist.list'],
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
text: 'Master Data',
|
text: 'Master Data',
|
||||||
link: '/daily-checklist/master-data',
|
link: '/daily-checklist/master-data',
|
||||||
icon: 'lucide:database',
|
icon: 'lucide:database',
|
||||||
// TODO: add permission
|
permission: [
|
||||||
// permission: ['lti.daily_checklist.list'],
|
'lti.daily_checklist.master_data.employee',
|
||||||
|
'lti.daily_checklist.master_data.activity',
|
||||||
|
'lti.daily_checklist.master_data.configuration',
|
||||||
|
],
|
||||||
submenu: [
|
submenu: [
|
||||||
{
|
{
|
||||||
text: 'Employee (ABK)',
|
text: 'Employee (ABK)',
|
||||||
link: '/daily-checklist/master-data/employee',
|
link: '/daily-checklist/master-data/employee',
|
||||||
// TODO: add permission
|
permission: ['lti.daily_checklist.master_data.employee'],
|
||||||
// permission: ['lti.daily_checklist.list'],
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
text: 'Aktivitas',
|
text: 'Aktivitas',
|
||||||
link: '/daily-checklist/master-data/activity',
|
link: '/daily-checklist/master-data/activity',
|
||||||
// TODO: add permission
|
permission: ['lti.daily_checklist.master_data.activity'],
|
||||||
// permission: ['lti.daily_checklist.list'],
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
text: 'Konfigurasi',
|
text: 'Konfigurasi',
|
||||||
link: '/daily-checklist/master-data/configuration',
|
link: '/daily-checklist/master-data/configuration',
|
||||||
// TODO: add permission
|
permission: ['lti.daily_checklist.master_data.configuration'],
|
||||||
// permission: ['lti.daily_checklist.list'],
|
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -5,22 +5,22 @@ export const ROUTE_PERMISSIONS: Record<string, string[]> = {
|
|||||||
'/dashboard/': ['lti.dashboard.list'],
|
'/dashboard/': ['lti.dashboard.list'],
|
||||||
|
|
||||||
// Daily Checklist
|
// Daily Checklist
|
||||||
// TODO: use real daily checklist permission name
|
'/daily-checklist/dashboard/': ['lti.daily_checklist.dashboard.list'],
|
||||||
// '/daily-checklist/': ['lti.daily_checklist.list'],
|
'/daily-checklist/daily-checklist/': ['lti.daily_checklist.create'],
|
||||||
// '/daily-checklist/dashboard/': ['lti.daily_checklist.list'],
|
'/daily-checklist/list-daily-checklist/': ['lti.daily_checklist.list'],
|
||||||
// '/daily-checklist/list-daily-checklist/': ['lti.daily_checklist.list'],
|
'/daily-checklist/list-daily-checklist/detail/': [
|
||||||
// '/daily-checklist/list-daily-checklist/detail/': ['lti.daily_checklist.detail'],
|
'lti.daily_checklist.detail',
|
||||||
// '/daily-checklist/reports/': ['lti.daily_checklist.reports'],
|
],
|
||||||
// '/daily-checklist/master-data/employee/': ['lti.dashboard.master_data.employee'],
|
'/daily-checklist/reports/': ['lti.daily_checklist.reports'],
|
||||||
// '/daily-checklist/master-data/activity/': ['lti.dashboard.master_data.activity'],
|
'/daily-checklist/master-data/employee/': [
|
||||||
'/daily-checklist/dashboard/': ['lti.dashboard.list'],
|
'lti.daily_checklist.master_data.employee',
|
||||||
'/daily-checklist/daily-checklist/': ['lti.dashboard.list'],
|
],
|
||||||
'/daily-checklist/list-daily-checklist/': ['lti.dashboard.list'],
|
'/daily-checklist/master-data/activity/': [
|
||||||
'/daily-checklist/list-daily-checklist/detail/': ['lti.dashboard.list'],
|
'lti.daily_checklist.master_data.activity',
|
||||||
'/daily-checklist/reports/': ['lti.dashboard.list'],
|
],
|
||||||
'/daily-checklist/master-data/employee/': ['lti.dashboard.list'],
|
'/daily-checklist/master-data/configuration/': [
|
||||||
'/daily-checklist/master-data/activity/': ['lti.dashboard.list'],
|
'lti.daily_checklist.master_data.configuration',
|
||||||
'/daily-checklist/master-data/configuration/': ['lti.dashboard.list'],
|
],
|
||||||
|
|
||||||
// Production
|
// Production
|
||||||
// Production - Project Flock
|
// Production - Project Flock
|
||||||
|
|||||||
@@ -91,10 +91,11 @@ export class ClosingApiService extends BaseApiService<Closing, null, null> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async getProductionData(
|
async getProductionData(
|
||||||
id: number
|
id: number,
|
||||||
|
kandangId?: number
|
||||||
): Promise<BaseApiResponse<ClosingProductionData> | undefined> {
|
): Promise<BaseApiResponse<ClosingProductionData> | undefined> {
|
||||||
try {
|
try {
|
||||||
const getProductionDataPath = `${this.basePath}/${id}/production-data`;
|
const getProductionDataPath = `${this.basePath}/${id}/production-data?kandang_id=${kandangId ? `${kandangId}` : ''}`;
|
||||||
const getProductionDataRes = await httpClient<
|
const getProductionDataRes = await httpClient<
|
||||||
BaseApiResponse<ClosingProductionData>
|
BaseApiResponse<ClosingProductionData>
|
||||||
>(getProductionDataPath);
|
>(getProductionDataPath);
|
||||||
|
|||||||
@@ -38,6 +38,9 @@ export const useFormikErrorList = <T>(
|
|||||||
|
|
||||||
// Validate form
|
// Validate form
|
||||||
const isValid = await handleValidateForm();
|
const isValid = await handleValidateForm();
|
||||||
|
if (isValid) {
|
||||||
|
close();
|
||||||
|
}
|
||||||
|
|
||||||
// Call onAfterValidation callback if validation passed
|
// Call onAfterValidation callback if validation passed
|
||||||
if (options?.onAfterValidation) {
|
if (options?.onAfterValidation) {
|
||||||
|
|||||||
+12
-9
@@ -1,20 +1,20 @@
|
|||||||
import { BaseMetadata } from '@/types/api/api-general';
|
import { BaseMetadata } from '@/types/api/api-general';
|
||||||
import { Uom } from '@/types/api/master-data/uom';
|
import { Uom } from '@/types/api/master-data/uom';
|
||||||
import { ProductCategory } from '@/types/api/master-data/product-category';
|
import { ProductCategory } from '@/types/api/master-data/product-category';
|
||||||
import { Supplier } from '@/types/api/master-data/supplier';
|
import { BaseSupplier, Supplier } from '@/types/api/master-data/supplier';
|
||||||
|
|
||||||
export type BaseProduct = {
|
export type BaseProduct = {
|
||||||
id: number;
|
id: number;
|
||||||
name: string;
|
name: string;
|
||||||
brand: string;
|
brand: string;
|
||||||
sku: string;
|
sku?: string;
|
||||||
product_price: number;
|
product_price: number;
|
||||||
selling_price?: number;
|
selling_price?: number;
|
||||||
tax?: number;
|
tax?: number;
|
||||||
expiry_period: number;
|
expiry_period?: number;
|
||||||
uom: Uom;
|
uom: Uom;
|
||||||
product_category: ProductCategory;
|
product_category: ProductCategory;
|
||||||
suppliers: Supplier[];
|
suppliers: (BaseSupplier & { price: number })[];
|
||||||
flags: string[];
|
flags: string[];
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -23,14 +23,17 @@ export type Product = BaseMetadata & BaseProduct;
|
|||||||
export type CreateProductPayload = {
|
export type CreateProductPayload = {
|
||||||
name: string;
|
name: string;
|
||||||
brand: string;
|
brand: string;
|
||||||
sku: string;
|
sku?: string;
|
||||||
uom_id: number;
|
uom_id: number;
|
||||||
product_category_id: number;
|
product_category_id: number;
|
||||||
product_price: number;
|
product_price: number;
|
||||||
selling_price: number;
|
selling_price?: number;
|
||||||
tax: number;
|
tax?: number;
|
||||||
expiry_period: number;
|
expiry_period?: number;
|
||||||
supplier_ids: number[];
|
suppliers: {
|
||||||
|
supplier_id: number;
|
||||||
|
price: number;
|
||||||
|
}[];
|
||||||
flags: string[];
|
flags: string[];
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user