Merge branch 'development' of https://gitlab.com/mbugroup/lti-web-client into feat/closing-finance-kandang

This commit is contained in:
randy-ar
2026-01-15 17:23:57 +07:00
40 changed files with 1646 additions and 381 deletions
@@ -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)}{' '}
@@ -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)'
@@ -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();
@@ -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(),
@@ -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 &gt; Rekapitulasi Hutang ke Supplier Laporan &gt; 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
View File
@@ -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'],
}, },
], ],
}, },
+16 -16
View File
@@ -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
+3 -2
View File
@@ -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);
+3
View File
@@ -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
View File
@@ -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[];
}; };