-
+ {/* Sidebar Content */}
+
{sidebarContent}
diff --git a/src/components/FloatingActionsButton.tsx b/src/components/FloatingActionsButton.tsx
new file mode 100644
index 00000000..c0033d72
--- /dev/null
+++ b/src/components/FloatingActionsButton.tsx
@@ -0,0 +1,141 @@
+'use client';
+
+import Button from '@/components/Button';
+import Tooltip from '@/components/Tooltip';
+import { cn } from '@/lib/helper';
+import { Icon } from '@iconify/react';
+
+type FloatingActionsButtonProps = {
+ actions: {
+ action: 'DETAIL' | 'EDIT' | 'DELETE';
+ icon: string;
+ label?: string;
+ onClick?: () => void;
+ hidden?: boolean;
+ disabled?: boolean;
+ }[];
+ approvals: {
+ action: 'APPROVED' | 'REJECTED';
+ icon: string;
+ label?: string;
+ onClick?: () => void;
+ disabled?: boolean;
+ }[];
+ selectedRowIds: number[];
+ onClose: () => void;
+};
+
+const FloatingActionsButton = ({
+ actions,
+ approvals,
+ selectedRowIds,
+ onClose,
+}: FloatingActionsButtonProps) => {
+ // Jika tidak ada baris yang dipilih, jangan tampilkan FAB
+ const positionStyles =
+ selectedRowIds.length > 0 ? 'bottom-[10%]' : 'bottom-[-100%]';
+
+ // Helper untuk menentukan gaya warna tombol approval
+ const getApprovalColor = (action: 'APPROVED' | 'REJECTED') => {
+ if (action === 'APPROVED') return 'success';
+ if (action === 'REJECTED') return 'error';
+ return 'primary';
+ };
+
+ const getActionColor = (action: 'DETAIL' | 'EDIT' | 'DELETE') => {
+ if (action === 'DETAIL') return 'white';
+ if (action === 'EDIT') return 'warning';
+ if (action === 'DELETE') return 'error';
+ return 'primary';
+ };
+
+ return (
+ // Container utama FAB
+
+
+ {/* === BARIS ATAS: Status Seleksi dan Actions (Termasuk Close) === */}
+
+
+ {selectedRowIds.length} Selected
+
+
+
+
+ {/* Render Aksi dari props.actions */}
+ {actions
+ .filter((action) => !action.hidden)
+ .map((action, index) => {
+ return (
+
+ );
+ })}
+
+
+
+ {/* Tombol Close */}
+
+
+
+
+
+ {/* === BARIS BAWAH: Approval Buttons (Approve/Reject) === */}
+
+ {approvals.map((approval, index) => (
+
+ ))}
+
+
+
+ );
+};
+
+export default FloatingActionsButton;
diff --git a/src/components/helper/drawer/DrawerHeader.tsx b/src/components/helper/drawer/DrawerHeader.tsx
new file mode 100644
index 00000000..f9d70a04
--- /dev/null
+++ b/src/components/helper/drawer/DrawerHeader.tsx
@@ -0,0 +1,104 @@
+'use client';
+
+import { Icon } from '@iconify/react';
+import Link from 'next/link';
+import { ReactNode } from 'react';
+import { cn } from '@/lib/helper';
+
+export interface DrawerHeaderProps {
+ // Left side props
+ leftIcon?: string;
+ leftIconSize?: number;
+ leftIconHref?: string;
+ leftIconOnClick?: () => void;
+ leftIconClassName?: string;
+
+ // Subtitle/label props
+ subtitle?: string | ReactNode;
+ subtitleClassName?: string;
+
+ // Right side actions (children)
+ children?: ReactNode;
+
+ // Container props
+ className?: string;
+ showDivider?: boolean;
+}
+
+const DrawerHeader = ({
+ leftIcon = 'mdi:close',
+ leftIconSize = 24,
+ leftIconHref,
+ leftIconOnClick,
+ leftIconClassName,
+ subtitle,
+ subtitleClassName,
+ children,
+ className,
+ showDivider = true,
+}: DrawerHeaderProps) => {
+ const renderLeftIcon = () => {
+ const iconElement = (
+
+ );
+
+ if (leftIconHref) {
+ return (
+
+ {iconElement}
+
+ );
+ }
+
+ if (leftIconOnClick) {
+ return (
+
+ );
+ }
+
+ return iconElement;
+ };
+
+ return (
+
+ {/* Left Side */}
+
+ {renderLeftIcon()}
+
+ {showDivider && subtitle && (
+
+ )}
+
+ {subtitle && (
+
+ {subtitle}
+
+ )}
+
+
+ {/* Right Side Actions */}
+ {children && (
+
+ {children}
+
+ )}
+
+ );
+};
+
+export default DrawerHeader;
diff --git a/src/components/input/DateInput.tsx b/src/components/input/DateInput.tsx
index 77267090..2d55fe6d 100644
--- a/src/components/input/DateInput.tsx
+++ b/src/components/input/DateInput.tsx
@@ -7,11 +7,11 @@ import {
useState,
} from 'react';
import { cn, formatDate } from '@/lib/helper';
-import Modal, { useModal } from '../Modal';
import { DateRange, DayPicker, Matcher } from 'react-day-picker';
import 'react-day-picker/dist/style.css';
-import Button from '../Button';
import { Icon } from '@iconify/react';
+import Modal, { useModal } from '@/components/Modal';
+import Button from '@/components/Button';
export interface DateInputProps {
label?: string;
diff --git a/src/components/input/RadioInput.tsx b/src/components/input/RadioInput.tsx
index 71a731aa..e508e7ba 100644
--- a/src/components/input/RadioInput.tsx
+++ b/src/components/input/RadioInput.tsx
@@ -1,6 +1,11 @@
'use client';
-import { ChangeEventHandler, ReactNode } from 'react';
+import {
+ ChangeEventHandler,
+ ReactNode,
+ createContext,
+ useContext,
+} from 'react';
import { cn } from '@/lib/helper';
export interface RadioOption {
@@ -8,37 +13,74 @@ export interface RadioOption {
value: string;
}
-export interface RadioInputProps {
- label?: string;
- bottomLabel?: string;
+// DaisyUI Radio Colors
+export type RadioColor =
+ | 'neutral'
+ | 'primary'
+ | 'secondary'
+ | 'accent'
+ | 'success'
+ | 'warning'
+ | 'info'
+ | 'error';
+
+// DaisyUI Radio Sizes
+export type RadioSize = 'xs' | 'sm' | 'md' | 'lg' | 'xl';
+
+// Context untuk RadioGroup
+interface RadioGroupContextValue {
name: string;
value?: string;
- options: RadioOption[];
- variant?: string;
- className?: {
- wrapper?: string;
- label?: string;
- radioWrapper?: string;
- radio?: string;
- };
- isError?: boolean;
- isValid?: boolean;
- errorMessage?: string;
- required?: boolean;
+ color?: RadioColor;
+ size?: RadioSize;
disabled?: boolean;
- startAdornment?: ReactNode;
- endAdornment?: ReactNode;
onChange?: ChangeEventHandler
;
onBlur?: (e: React.FocusEvent) => void;
}
-const RadioInput = ({
+const RadioGroupContext = createContext(
+ undefined
+);
+
+const useRadioGroup = () => {
+ const context = useContext(RadioGroupContext);
+ if (!context) {
+ throw new Error('RadioGroupItem must be used within RadioGroup');
+ }
+ return context;
+};
+
+// RadioGroup Component
+export interface RadioGroupProps {
+ label?: string;
+ bottomLabel?: string;
+ name: string;
+ value?: string;
+ options?: RadioOption[];
+ color?: RadioColor;
+ size?: RadioSize;
+ className?: {
+ wrapper?: string;
+ label?: string;
+ radioWrapper?: string;
+ };
+ isError?: boolean;
+ errorMessage?: string;
+ required?: boolean;
+ disabled?: boolean;
+ onChange?: ChangeEventHandler;
+ onBlur?: (e: React.FocusEvent) => void;
+ children?: ReactNode;
+}
+
+export const RadioGroup = ({
label,
bottomLabel,
name,
value,
options,
- variant = 'radio-primary',
+ color = 'primary',
+ size = 'md',
className,
isError,
errorMessage,
@@ -46,68 +88,125 @@ const RadioInput = ({
disabled = false,
onChange,
onBlur,
-}: RadioInputProps) => {
- return (
-
- {/* Label atas */}
- {label && (
-
- )}
+ children,
+}: RadioGroupProps) => {
+ const contextValue: RadioGroupContextValue = {
+ name,
+ value,
+ color,
+ size,
+ disabled,
+ onChange,
+ onBlur,
+ };
- {/* Daftar opsi radio */}
-
- {options.map((option) => (
+ return (
+
+
+ {/* Label atas */}
+ {label && (
- ))}
+ )}
+
+ {/* Daftar opsi radio */}
+
+ {/* Jika options diberikan, render otomatis */}
+ {options &&
+ options.map((option) => (
+
+ ))}
+
+ {/* Atau gunakan children untuk custom rendering */}
+ {children}
+
+
+ {/* Label bawah */}
+ {!isError && bottomLabel && (
+
{bottomLabel}
+ )}
+
+ {/* Pesan error */}
+ {isError && errorMessage && (
+
{errorMessage}
+ )}
-
- {/* Label bawah */}
- {!isError && bottomLabel && (
- {bottomLabel}
- )}
-
- {/* Pesan error */}
- {isError && errorMessage && (
- {errorMessage}
- )}
-
+
);
};
-export default RadioInput;
+// RadioGroupItem Component
+export interface RadioGroupItemProps {
+ value: string;
+ label?: string;
+ className?: string;
+ disabled?: boolean;
+ color?: RadioColor;
+ size?: RadioSize;
+}
+
+export const RadioGroupItem = ({
+ value,
+ label,
+ className,
+ disabled: itemDisabled,
+ color: itemColor,
+ size: itemSize,
+}: RadioGroupItemProps) => {
+ const {
+ name,
+ value: groupValue,
+ color: groupColor,
+ size: groupSize,
+ disabled: groupDisabled,
+ onChange,
+ onBlur,
+ } = useRadioGroup();
+
+ const isDisabled = itemDisabled ?? groupDisabled;
+ const radioColor = itemColor ?? groupColor;
+ const radioSize = itemSize ?? groupSize;
+
+ return (
+
+ );
+};
diff --git a/src/components/pages/inventory/adjustment/InventoryAdjustmentTable.tsx b/src/components/pages/inventory/adjustment/InventoryAdjustmentTable.tsx
index 049b0661..30807d1c 100644
--- a/src/components/pages/inventory/adjustment/InventoryAdjustmentTable.tsx
+++ b/src/components/pages/inventory/adjustment/InventoryAdjustmentTable.tsx
@@ -6,7 +6,7 @@ import Table from '@/components/Table';
import { ROWS_OPTIONS } from '@/config/constant';
import { isResponseSuccess } from '@/lib/api-helper';
import { cn } from '@/lib/helper';
-import { inventoryAdjustmentApi } from '@/services/api/inventory';
+import { InventoryAdjustmentApi } from '@/services/api/inventory';
import { useTableFilter } from '@/services/hooks/useTableFilter';
import { InventoryAdjustment } from '@/types/api/inventory/adjustment';
import { Icon } from '@iconify/react';
@@ -41,8 +41,8 @@ const InventoryAdjustmentTable = () => {
// Fetch Data
const { data: inventoryAdjustments, isLoading } = useSWR(
- `${inventoryAdjustmentApi.basePath}${getTableFilterQueryString()}`,
- inventoryAdjustmentApi.getAllFetcher
+ `${InventoryAdjustmentApi.basePath}${getTableFilterQueryString()}`,
+ InventoryAdjustmentApi.getAllFetcher
);
// State
diff --git a/src/components/pages/inventory/adjustment/form/InventoryAdjustmentForm.tsx b/src/components/pages/inventory/adjustment/form/InventoryAdjustmentForm.tsx
index bbfb3154..2c6c463c 100644
--- a/src/components/pages/inventory/adjustment/form/InventoryAdjustmentForm.tsx
+++ b/src/components/pages/inventory/adjustment/form/InventoryAdjustmentForm.tsx
@@ -1,7 +1,7 @@
'use client';
import { isResponseError, isResponseSuccess } from '@/lib/api-helper';
-import { inventoryAdjustmentApi } from '@/services/api/inventory';
+import { InventoryAdjustmentApi } from '@/services/api/inventory';
import {
CreateInventoryAdjustmentPayload,
InventoryAdjustment,
@@ -24,7 +24,7 @@ import Button from '@/components/Button';
import { Icon } from '@iconify/react';
import SelectInput, { OptionType } from '@/components/input/SelectInput';
import TextInput from '@/components/input/TextInput';
-import RadioInput from '@/components/input/RadioInput';
+import { RadioGroup } from '@/components/input/RadioInput';
import TextArea from '@/components/input/TextArea';
interface InventoryAdjustmentFormProps {
@@ -52,7 +52,7 @@ const InventoryAdjustmentForm = ({
const createInventoryAdjustmentHandler = useCallback(
async (payload: CreateInventoryAdjustmentPayload) => {
const createInventoryAdjustmentRes =
- await inventoryAdjustmentApi.create(payload);
+ await InventoryAdjustmentApi.create(payload);
if (isResponseError(createInventoryAdjustmentRes)) {
setInventoryAdjustmentFormErrorMessage(
@@ -347,7 +347,7 @@ const InventoryAdjustmentForm = ({
/>
{/* Radio Button Flag Stock */}
-
;
+}) => (
+
+
+
+);
+
+const InventoryProductTable = () => {
+ const {
+ state: tableFilterState,
+ updateFilter,
+ setPage,
+ setPageSize,
+ toQueryString: getTableFilterQueryString,
+ } = useTableFilter({
+ initial: {
+ search: '',
+ },
+ paramMap: {
+ page: 'page',
+ pageSize: 'limit',
+ },
+ });
+
+ const [sorting, setSorting] = useState([]);
+
+ const { data: inventoryProducts, isLoading } = useSWR(
+ `${InventoryProductApi.basePath}${getTableFilterQueryString()}`,
+ InventoryProductApi.getAllFetcher
+ );
+
+ const searchChangeHandler: ChangeEventHandler = (e) => {
+ updateFilter('search', e.target.value);
+ };
+
+ const pageSizeChangeHandler = (val: OptionType | OptionType[] | null) => {
+ const newVal = val as OptionType;
+ setPageSize(newVal.value as number);
+ setPage(1);
+ };
+
+ const columns: ColumnDef[] = useMemo(
+ () => [
+ {
+ header: '#',
+ cell: (props) =>
+ tableFilterState.pageSize * (tableFilterState.page - 1) +
+ props.row.index +
+ 1,
+ },
+ {
+ accessorKey: 'name',
+ header: 'Nama',
+ },
+ {
+ accessorKey: 'product_price',
+ header: 'Harga Beli',
+ cell: (props) => {
+ return props.row.original.product_price
+ ? formatCurrency(props.row.original.product_price)
+ : '-';
+ },
+ },
+ {
+ accessorKey: 'selling_price',
+ header: 'Harga Jual',
+ cell: (props) => {
+ return props.row.original.selling_price
+ ? formatCurrency(props.row.original.selling_price)
+ : '-';
+ },
+ },
+ {
+ accessorFn: (row) => row.product_category.name,
+ header: 'Kategori',
+ },
+ {
+ accessorFn: (row) => row.total_stock,
+ header: 'Stok',
+ cell: (props) => {
+ return props.row.original.total_stock
+ ? formatNumber(props.row.original.total_stock)
+ : '0';
+ },
+ },
+ {
+ accessorFn: (row) => row.uom.name,
+ header: 'Satuan',
+ },
+ {
+ header: 'Aksi',
+ cell: (props) => {
+ const currentPageSize =
+ props.table.getPaginationRowModel().rows.length;
+ const currentPageRows = props.table.getPaginationRowModel().flatRows;
+ const currentRowRelativeIndex =
+ currentPageRows.findIndex((r) => r.id === props.row.id) + 1;
+
+ const isLast2Rows = currentRowRelativeIndex > currentPageSize - 2;
+
+ return (
+ <>
+ {currentPageSize > 2 && (
+
+
+
+ )}
+
+ {currentPageSize <= 2 && (
+
+
+
+ )}
+ >
+ );
+ },
+ },
+ ],
+ []
+ );
+
+ return (
+ <>
+
+
+
+
+ data={
+ isResponseSuccess(inventoryProducts) ? inventoryProducts?.data : []
+ }
+ columns={columns}
+ pageSize={tableFilterState.pageSize}
+ page={
+ isResponseSuccess(inventoryProducts)
+ ? inventoryProducts?.meta?.page
+ : 0
+ }
+ totalItems={
+ isResponseSuccess(inventoryProducts)
+ ? inventoryProducts?.meta?.total_results
+ : 0
+ }
+ onPageChange={setPage}
+ isLoading={isLoading}
+ sorting={sorting}
+ setSorting={setSorting}
+ className={{
+ containerClassName: cn({
+ 'mb-20':
+ isResponseSuccess(inventoryProducts) &&
+ inventoryProducts?.data?.length === 0,
+ }),
+ tableWrapperClassName: 'overflow-x-auto min-h-full!',
+ tableClassName: 'font-inter w-full table-auto min-h-full!',
+ headerRowClassName: 'border-b border-b-gray-200',
+ headerColumnClassName:
+ 'px-6 py-3 text-xs font-semibold text-gray-500 last:flex last:flex-row last:justify-end',
+ bodyRowClassName: 'border-b border-b-gray-200',
+ bodyColumnClassName:
+ 'px-6 py-3 last:flex last:flex-row last:justify-end',
+ }}
+ />
+
+ >
+ );
+};
+
+export default InventoryProductTable;
diff --git a/src/components/pages/inventory/product/detail/InventoryProductDetail.tsx b/src/components/pages/inventory/product/detail/InventoryProductDetail.tsx
new file mode 100644
index 00000000..ad523929
--- /dev/null
+++ b/src/components/pages/inventory/product/detail/InventoryProductDetail.tsx
@@ -0,0 +1,118 @@
+import Card from '@/components/Card';
+import { FormHeader } from '@/components/helper/form/FormHeader';
+import StockLogTable from '@/components/pages/inventory/product/detail/StockLogTable';
+import StockProductWarehouseTable from '@/components/pages/inventory/product/detail/StockProductWarehouseTable';
+import { formatCurrency, formatNumber } from '@/lib/helper';
+import { InventoryProduct } from '@/types/api/inventory/product';
+import { useMemo } from 'react';
+
+const InventoryProductDetail = ({
+ inventoryProduct,
+}: {
+ inventoryProduct?: InventoryProduct;
+}) => {
+ const stockLogs = useMemo(() => {
+ return (
+ inventoryProduct?.product_warehouses?.flatMap(
+ (warehouse) => warehouse.stock_logs || []
+ ) || []
+ );
+ }, [inventoryProduct]);
+
+ return (
+
+
+
+
+
+
+
+
+
+ | SKU |
+ : |
+ {inventoryProduct?.sku} |
+
+
+ | Nama Produk |
+ : |
+ {inventoryProduct?.name} |
+
+
+ | Kategory |
+ : |
+ {inventoryProduct?.product_category.name} |
+
+
+ | Satuan |
+ : |
+ {inventoryProduct?.uom.name} |
+
+
+
+
+
+
+
+
+
+ | Harga Jual |
+ : |
+
+ {inventoryProduct?.selling_price
+ ? formatCurrency(inventoryProduct.selling_price)
+ : '-'}
+ |
+
+
+ | Harga Beli |
+ : |
+
+ {inventoryProduct?.product_price
+ ? formatCurrency(inventoryProduct?.product_price)
+ : '-'}
+ |
+
+
+ | Pajak |
+ : |
+
+ {inventoryProduct?.tax
+ ? formatCurrency(inventoryProduct?.tax)
+ : '-'}
+ |
+
+
+ | Total Stok |
+ : |
+
+ {inventoryProduct?.total_stock
+ ? formatNumber(inventoryProduct?.total_stock)
+ : '0'}
+ |
+
+
+
+
+
+
+
+
+
+
+
+ );
+};
+
+export default InventoryProductDetail;
diff --git a/src/components/pages/inventory/product/detail/StockLogTable.tsx b/src/components/pages/inventory/product/detail/StockLogTable.tsx
new file mode 100644
index 00000000..42f7bc29
--- /dev/null
+++ b/src/components/pages/inventory/product/detail/StockLogTable.tsx
@@ -0,0 +1,81 @@
+import Card from '@/components/Card';
+import Table from '@/components/Table';
+import { formatDate, formatNumber, formatTitleCase } from '@/lib/helper';
+import { StockLog } from '@/types/api/inventory/product';
+
+const StockLogTable = ({ stockLogs }: { stockLogs: StockLog[] }) => {
+ return (
+
+
+ data={stockLogs}
+ columns={[
+ {
+ header: 'ID',
+ accessorKey: 'id',
+ },
+ {
+ header: 'Tanggal',
+ accessorKey: 'created_at',
+ cell: (props) => {
+ return formatDate(props.row.original.created_at, 'DD-MMM-yyyy');
+ },
+ },
+ {
+ header: 'Peningkatan',
+ accessorKey: 'increase',
+ cell: (props) => {
+ return formatNumber(props.row.original.increase);
+ },
+ },
+ {
+ header: 'Penurunan',
+ accessorKey: 'decrease',
+ cell: (props) => {
+ return formatNumber(props.row.original.decrease);
+ },
+ },
+ {
+ header: 'Jenis Transaksi',
+ accessorKey: 'loggable_type',
+ cell: (props) => {
+ return props.row.original.loggable_type
+ ? formatTitleCase(props.row.original.loggable_type)
+ : '-';
+ },
+ },
+ {
+ header: 'Catatan',
+ accessorKey: 'notes',
+ cell: (props) => {
+ return props.row.original.notes ? props.row.original.notes : '-';
+ },
+ },
+ {
+ header: 'Oleh',
+ accessorKey: 'created_user.name',
+ },
+ ]}
+ className={{
+ containerClassName: 'mt-6',
+ tableWrapperClassName: 'overflow-x-auto min-h-full!',
+ tableClassName: 'font-inter w-full table-auto min-h-full!',
+ headerRowClassName: 'border-b border-b-gray-200',
+ headerColumnClassName:
+ 'px-6 py-3 text-xs font-semibold text-gray-500 last:flex last:flex-row last:justify-end',
+ bodyRowClassName: 'border-b border-b-gray-200',
+ bodyColumnClassName:
+ 'px-6 py-3 last:flex last:flex-row last:justify-end',
+ }}
+ />
+
+ );
+};
+
+export default StockLogTable;
diff --git a/src/components/pages/inventory/product/detail/StockProductWarehouseTable.tsx b/src/components/pages/inventory/product/detail/StockProductWarehouseTable.tsx
new file mode 100644
index 00000000..6f48f7cd
--- /dev/null
+++ b/src/components/pages/inventory/product/detail/StockProductWarehouseTable.tsx
@@ -0,0 +1,65 @@
+import Card from '@/components/Card';
+import Table from '@/components/Table';
+import { formatNumber } from '@/lib/helper';
+import {
+ InventoryProduct,
+ ProductWarehouseStock,
+} from '@/types/api/inventory/product';
+
+const StockProductWarehouseTable = ({
+ productWarehouseStock,
+}: {
+ productWarehouseStock?: ProductWarehouseStock[];
+}) => {
+ return (
+
+
+ data={productWarehouseStock ?? []}
+ columns={[
+ {
+ header: 'Nama Gudang',
+ accessorKey: 'warehouse_name',
+ },
+ {
+ header: 'Lokasi',
+ accessorKey: 'location',
+ cell: (props) => {
+ return props.row.original.location != null
+ ? props.row.original.location.name
+ : '-';
+ },
+ },
+ {
+ header: 'Stok',
+ accessorFn(row) {
+ return row.current_stock;
+ },
+ cell: (props) => {
+ return formatNumber(props.row.original.current_stock);
+ },
+ },
+ ]}
+ className={{
+ containerClassName: 'mt-6',
+ tableWrapperClassName: 'overflow-x-auto min-h-full!',
+ tableClassName: 'font-inter w-full table-auto min-h-full!',
+ headerRowClassName: 'border-b border-b-gray-200',
+ headerColumnClassName:
+ 'px-6 py-3 text-xs font-semibold text-gray-500 last:flex last:flex-row last:justify-end',
+ bodyRowClassName: 'border-b border-b-gray-200',
+ bodyColumnClassName:
+ 'px-6 py-3 last:flex last:flex-row last:justify-end',
+ }}
+ />
+
+ );
+};
+
+export default StockProductWarehouseTable;
diff --git a/src/components/pages/marketing/form/MarketingForm.schema.ts b/src/components/pages/marketing/form/MarketingForm.schema.ts
index 0c427a9a..d81cdb9c 100644
--- a/src/components/pages/marketing/form/MarketingForm.schema.ts
+++ b/src/components/pages/marketing/form/MarketingForm.schema.ts
@@ -6,7 +6,7 @@ import {
import {
DeliveryOrderProductFormValues,
DeliveryOrderProductSchema,
-} from './repeater/delivery-order/DeliverOrderProduct.schema';
+} from '@/components/pages/marketing/form/repeater/delivery-order/DeliverOrderProduct.schema';
type MarketingSchemaType = {
customer_id: number | undefined;
diff --git a/src/components/pages/marketing/form/MarketingForm.tsx b/src/components/pages/marketing/form/MarketingForm.tsx
index b90febfe..326eac72 100644
--- a/src/components/pages/marketing/form/MarketingForm.tsx
+++ b/src/components/pages/marketing/form/MarketingForm.tsx
@@ -8,7 +8,6 @@ import SelectInput, {
OptionType,
useSelect,
} from '@/components/input/SelectInput';
-import TextArea from '@/components/input/TextArea';
import Modal, { useModal } from '@/components/Modal';
import { formatCurrency, formatDate } from '@/lib/helper';
import {
@@ -31,23 +30,23 @@ import {
DeliveryOrderSchema,
SalesOrderFormValues,
SalesOrderSchema,
-} from './MarketingForm.schema';
+} from '@/components/pages/marketing/form/MarketingForm.schema';
import { isResponseError, isResponseSuccess } from '@/lib/api-helper';
import {
DeliveryOrderApi,
MarketingApi,
SalesOrderApi,
} from '@/services/api/marketing/marketing';
-import { SalesOrderProductFormValues } from './repeater/sales-order/SalesOrderProduct.schema';
import ConfirmationModal from '@/components/modal/ConfirmationModal';
import toast from 'react-hot-toast';
import { useRouter } from 'next/navigation';
-import SalesOrderProductTable from './table-view/SalesOrderProductTable';
-import SalesOrderProductForm from './repeater/sales-order/SalesOrderProductForm';
-import DeliveryOrderProductTable from './table-view/DeliveryOrderProductTable';
-import DeliveryOrderProductForm from './repeater/delivery-order/DeliverOrderProduct';
-import { DeliveryOrderProductFormValues } from './repeater/delivery-order/DeliverOrderProduct.schema';
import DebouncedTextArea from '@/components/input/DebouncedTextArea';
+import SalesOrderProductTable from '@/components/pages/marketing/form/table-view/SalesOrderProductTable';
+import SalesOrderProductForm from '@/components/pages/marketing/form/repeater/sales-order/SalesOrderProductForm';
+import DeliveryOrderProductTable from '@/components/pages/marketing/form/table-view/DeliveryOrderProductTable';
+import DeliveryOrderProductForm from '@/components/pages/marketing/form/repeater/delivery-order/DeliverOrderProduct';
+import { SalesOrderProductFormValues } from '@/components/pages/marketing/form/repeater/sales-order/SalesOrderProduct.schema';
+import { DeliveryOrderProductFormValues } from '@/components/pages/marketing/form/repeater/delivery-order/DeliverOrderProduct.schema';
const MemoizedSalesOrderProductTable = memo(SalesOrderProductTable);
const MemoizedSalesOrderProductForm = memo(SalesOrderProductForm);
@@ -156,8 +155,6 @@ export const recalculate = (
field: string,
values: ProductCalculationFields
) => {
- console.log('Values');
- console.log(values);
const { qty, unit_price, total_price, avg_weight, total_weight } = values;
const result: Partial = {};
if (field == 'unit_price' || field == 'total_price' || field == 'qty') {
@@ -174,8 +171,6 @@ export const recalculate = (
result.avg_weight = Number(total_weight) / Number(qty);
}
}
- console.log('Result');
- console.log(result);
return result;
};
export const getSubmitField = (values: ProductCalculationFields) => {
@@ -327,8 +322,6 @@ const MarketingForm = ({
})
.filter((item) => Boolean(item)),
} as UpdateDeliveryOrderPayload);
- console.log('PAYLOAD');
- console.log(payload);
switch (formType) {
case 'add':
await createMarketingHandler(payload as CreateSalesOrderPayload);
@@ -352,7 +345,6 @@ const MarketingForm = ({
// ================== FORM REPEATER HANDLER ==================
const createMarketingHandler = async (values: CreateSalesOrderPayload) => {
setIsLoading(true);
- console.log(values);
const createMarketingRes = await SalesOrderApi.create(values);
if (isResponseSuccess(createMarketingRes)) {
toast.success(createMarketingRes?.message as string);
@@ -365,7 +357,6 @@ const MarketingForm = ({
};
const updateMarketingHandler = async (values: UpdateSalesOrderPayload) => {
setIsLoading(true);
- console.log(values);
const updateMarketingRes = await SalesOrderApi.update(
initialValues?.id as number,
values
@@ -381,10 +372,8 @@ const MarketingForm = ({
};
const createDeliveryHandler = async (values: CreateDeliveryOrderPayload) => {
setIsLoading(true);
- console.log(initialValues?.id);
const createDeliveryRes = await DeliveryOrderApi.create(values);
if (isResponseSuccess(createDeliveryRes)) {
- console.log(createDeliveryRes);
toast.success(createDeliveryRes?.message as string);
setDeliveryOrderValues(
createDeliveryRes.data?.delivery_order?.flatMap((delivery) =>
@@ -397,20 +386,17 @@ const MarketingForm = ({
router.push(`/marketing/detail?marketingId=${initialValues?.id}`);
}
if (isResponseError(createDeliveryRes)) {
- console.log(createDeliveryRes);
toast.error(createDeliveryRes?.message as string);
}
setIsLoading(false);
};
const updateDeliveryHandler = async (values: UpdateDeliveryOrderPayload) => {
setIsLoading(true);
- console.log(initialValues?.id);
const updateDeliveryRes = await DeliveryOrderApi.update(
initialValues?.id as number,
values
);
if (isResponseSuccess(updateDeliveryRes)) {
- console.log(updateDeliveryRes);
toast.success(updateDeliveryRes?.message as string);
setDeliveryOrderValues(
mergeSOwithDO(
@@ -426,7 +412,6 @@ const MarketingForm = ({
router.push(`/marketing/detail?marketingId=${initialValues?.id}`);
}
if (isResponseError(updateDeliveryRes)) {
- console.log(updateDeliveryRes);
toast.error(updateDeliveryRes?.message as string);
}
setIsLoading(false);
@@ -435,16 +420,13 @@ const MarketingForm = ({
// ================== MARKETING HANDLER ==================
const deleteMarketingHandler = async () => {
setIsLoading(true);
- console.log(initialValues?.id);
const deleteMarketingRes = await MarketingApi.delete(
initialValues?.id as number
);
if (isResponseSuccess(deleteMarketingRes)) {
- console.log(deleteMarketingRes);
toast.success(deleteMarketingRes?.message as string);
}
if (isResponseError(deleteMarketingRes)) {
- console.log(deleteMarketingRes);
toast.error(deleteMarketingRes?.message as string);
}
setIsLoading(false);
diff --git a/src/components/pages/marketing/form/repeater/delivery-order/DeliverOrderProduct.tsx b/src/components/pages/marketing/form/repeater/delivery-order/DeliverOrderProduct.tsx
index 4fe4179f..2dae2da5 100644
--- a/src/components/pages/marketing/form/repeater/delivery-order/DeliverOrderProduct.tsx
+++ b/src/components/pages/marketing/form/repeater/delivery-order/DeliverOrderProduct.tsx
@@ -2,7 +2,7 @@ import { useEffect, useState } from 'react';
import {
DeliveryOrderProductFormValues,
DeliveryOrderProductSchema,
-} from './DeliverOrderProduct.schema';
+} from '@/components/pages/marketing/form/repeater/delivery-order/DeliverOrderProduct.schema';
import { useFormik } from 'formik';
import Alert from '@/components/Alert';
import Button from '@/components/Button';
diff --git a/src/components/pages/marketing/pdf/DeliveryOrderExport.tsx b/src/components/pages/marketing/pdf/DeliveryOrderExport.tsx
index c2b19660..46e85a23 100644
--- a/src/components/pages/marketing/pdf/DeliveryOrderExport.tsx
+++ b/src/components/pages/marketing/pdf/DeliveryOrderExport.tsx
@@ -3,10 +3,10 @@ import { BaseDeliveryOrder, Marketing } from '@/types/api/marketing/marketing';
import { Icon } from '@iconify/react';
import { Document, Image, Page, pdf, Text, View } from '@react-pdf/renderer';
import { useMemo, useState } from 'react';
-import pdfStyles from './styles/MarketingPDFStyles';
import { formatDate, formatNumber, formatVechicleNumber } from '@/lib/helper';
import { format } from 'path';
import { date } from 'yup';
+import pdfStyles from '@/components/pages/marketing/pdf/styles/MarketingPDFStyles';
interface DeliveryOrderExportProps {
data?: Marketing;
diff --git a/src/components/pages/marketing/pdf/SalesOrderExport.tsx b/src/components/pages/marketing/pdf/SalesOrderExport.tsx
index e7fa9a71..f9f0a6c5 100644
--- a/src/components/pages/marketing/pdf/SalesOrderExport.tsx
+++ b/src/components/pages/marketing/pdf/SalesOrderExport.tsx
@@ -3,8 +3,8 @@ import { Marketing } from '@/types/api/marketing/marketing';
import { Icon } from '@iconify/react';
import { Document, Image, Page, pdf, Text, View } from '@react-pdf/renderer';
import { useMemo, useState } from 'react';
-import pdfStyles from './styles/MarketingPDFStyles';
import { formatDate, formatNumber } from '@/lib/helper';
+import pdfStyles from '@/components/pages/marketing/pdf/styles/MarketingPDFStyles';
interface SalesOrderExportProps {
data?: Marketing;
diff --git a/src/components/pages/master-data/supplier/form/SupplierForm.tsx b/src/components/pages/master-data/supplier/form/SupplierForm.tsx
index da69a52e..429c3bb6 100644
--- a/src/components/pages/master-data/supplier/form/SupplierForm.tsx
+++ b/src/components/pages/master-data/supplier/form/SupplierForm.tsx
@@ -306,7 +306,6 @@ const SupplierForm = ({
label='Hatchery'
value={hatcheryOptionsValues}
onChange={(val) => {
- console.log(val); // pastikan val = array of { value, label }
setHatcheryOptionValues(val as OptionType[]);
}}
isError={
diff --git a/src/components/pages/production/chickin/form/ChickinForm.tsx b/src/components/pages/production/chickin/form/ChickinForm.tsx
index 1f56459f..84c5b5a5 100644
--- a/src/components/pages/production/chickin/form/ChickinForm.tsx
+++ b/src/components/pages/production/chickin/form/ChickinForm.tsx
@@ -7,13 +7,16 @@ import { formatNumber } from '@/lib/helper';
import { Kandang } from '@/types/api/master-data/kandang';
import { ProjectFlockKandang } from '@/types/api/production/project-flock-kandang';
import Tabs from '@/components/Tabs';
-import ChickinFormView from './tabs/ChickinFormView';
-import ChickinLogsView from './tabs/ChickLogsView';
import { useState } from 'react';
import ApprovalSteps, {
useApprovalSteps,
} from '@/components/pages/ApprovalSteps';
import { PROJECT_FLOCK_KANDANG_APPROVAL_LINE } from '@/config/approval-line';
+import ChickinFormView from '@/components/pages/production/chickin/form/tabs/ChickinFormView';
+import ChickinLogsView from '@/components/pages/production/chickin/form/tabs/ChickLogsView';
+import DrawerHeader from '@/components/helper/drawer/DrawerHeader';
+import { Icon } from '@iconify/react';
+import Badge from '@/components/Badge';
const ChickinFormKandang = ({
formType = 'add',
initialValues,
@@ -23,7 +26,7 @@ const ChickinFormKandang = ({
initialValues: ProjectFlockKandang;
afterSubmit?: () => void;
}) => {
- const [activeTabId, setActiveTabId] = useState('formChickIn');
+ const [openChickin, setOpenChickin] = useState(false);
const {
approvals,
@@ -37,108 +40,148 @@ const ChickinFormKandang = ({
});
const afterSubmitFormChickin = () => {
- setActiveTabId('logsChickIn');
+ setOpenChickin(true);
afterSubmit && afterSubmit();
refreshApprovals();
};
return (
-
-
+
- {approvals && !approvalsLoading && (
-
- )}
+ {/* Informasi Kandang */}
+
+
+
Informasi Kandang
-
-
- emptyContent={
-
-
- Informasi Kandang belum tersedia...
-
-
- }
- data={[initialValues?.kandang]}
- columns={[
- {
- header: 'Area',
- accessorFn: () => initialValues?.project_flock?.area.name || '-',
- },
- {
- header: 'Lokasi',
- accessorFn: () =>
- initialValues?.project_flock?.location.name || '-',
- },
- {
- header: 'Flock',
- accessorFn: () => initialValues?.project_flock?.flock_name || '-',
- },
- {
- header: 'Kandang',
- accessorFn: (row) => row?.name || '-',
- },
- {
- header: 'Kapasitas',
- accessorFn: (row) =>
- (row?.capacity && formatNumber(row?.capacity)) || '-',
- },
- {
- header: 'Penanggung Jawab',
- accessorFn: (row) => row?.pic?.name || '-',
- },
- ]}
- className={{
- tableWrapperClassName: 'overflow-x-auto min-h-full!',
- tableClassName: 'font-inter w-full table-auto min-h-full!',
- headerRowClassName: 'border-b border-b-gray-200',
- headerColumnClassName:
- 'px-6 py-3 text-xs font-semibold text-gray-500 last:flex last:flex-row last:justify-end',
- bodyRowClassName: 'border-b border-b-gray-200',
- bodyColumnClassName:
- 'px-6 py-3 last:flex last:flex-row last:justify-end',
- paginationClassName: 'hidden',
- }}
+ {approvals && !approvalsLoading && (
+
+ )}
+
+ {/* Badge Row */}
+
+
+ {' '}
+ Aktif
+
+
+
+
+ {` Kapasitas ${formatNumber(initialValues.kandang.capacity)} Ekor`}
+
+
+
+ {/* Information Grid */}
+
+ {/* Area */}
+
+ Area
+
+
+ {initialValues.project_flock.area.name}
+
+
+ {/* Lokasi */}
+
+ Lokasi
+
+
+ {initialValues.project_flock?.location.name}
+
+
+ {/* Kandang */}
+
+ Kandang
+
+
{initialValues.kandang.name}
+
+ {/* Jumlah DOC */}
+
+ Jumlah DOC
+
+
+ {formatNumber(
+ initialValues.chickins?.reduce(
+ (total, chickin) => total + chickin.usage_qty,
+ 0
+ ) ?? 0
+ )}{' '}
+ Ekor
+
+
+
+
+
+
+
Informasi Chick In
+ {/* Badge Row */}
+
+
+ {' '}
+ Perlu Chick In ({initialValues.available_qtys?.length ?? 0})
+
+
+
setOpenChickin(!openChickin)}
+ >
+ {`Riwayat Chick In ${formatNumber(initialValues.chickins?.length ?? 0)}`}
+
+
+
+
+ {openChickin && (
+
-
-
- ),
- },
- {
- content: (
-
- ),
- id: 'logsChickIn',
- label: 'Riwayat Chick In',
- },
- ]}
- variant='lifted'
+ )}
+
-
+ >
);
};
diff --git a/src/components/pages/production/chickin/form/tabs/ChickLogsView.tsx b/src/components/pages/production/chickin/form/tabs/ChickLogsView.tsx
index 8accf9ae..865091d7 100644
--- a/src/components/pages/production/chickin/form/tabs/ChickLogsView.tsx
+++ b/src/components/pages/production/chickin/form/tabs/ChickLogsView.tsx
@@ -2,17 +2,12 @@ import Alert from '@/components/Alert';
import Button from '@/components/Button';
import Card from '@/components/Card';
import { useModal } from '@/components/Modal';
-import ConfirmationModal from '@/components/modal/ConfirmationModal';
import ConfirmationModalWithNotes from '@/components/modal/ConfirmationModalWithNotes';
import PillBadge from '@/components/PillBadge';
-import Table from '@/components/Table';
import { isResponseError, isResponseSuccess } from '@/lib/api-helper';
-import { cn, formatDate, formatNumber } from '@/lib/helper';
+import { formatDate, formatNumber } from '@/lib/helper';
import { ChickinApi } from '@/services/api/production/chickin';
-import {
- Chickin,
- ProjectFlockKandang,
-} from '@/types/api/production/project-flock-kandang';
+import { ProjectFlockKandang } from '@/types/api/production/project-flock-kandang';
import { Icon } from '@iconify/react';
import { useState } from 'react';
import toast from 'react-hot-toast';
@@ -54,105 +49,120 @@ const ChickinLogsView = ({
return (
<>
-
-
- {initialValues?.approval?.step_number == 1 && (
-
- )}
-
-
- data={initialValues?.chickins || []}
- columns={[
- {
- header: '#',
- cell: (props) => props.row.index + 1,
- },
- {
- accessorFn: (row) => row.chick_in_date,
- header: 'Tanggal Chick In',
- cell: (props) => {
- return formatDate(props.getValue() as string, 'DD MMM YYYY');
- },
- },
- {
- accessorFn: (row) => row.product_warehouse?.warehouse?.name,
- header: 'Kandang',
- },
- {
- accessorFn: (row) => row.product_warehouse?.product?.name,
- header: 'Produk',
- },
- {
- accessorFn: (row) => row.usage_qty ?? row.pending_usage_qty,
- header: 'Jumlah Chick In',
- cell: (props) => {
- if (props.row.original.usage_qty != 0) {
- return formatNumber(props.row.original.usage_qty);
- } else if (props.row.original.pending_usage_qty != 0) {
- return formatNumber(props.row.original.pending_usage_qty);
- } else {
- return '-';
- }
- },
- },
- {
- accessorFn: (row) => row.pending_usage_qty,
- header: 'Status',
- cell: (props) => {
- return (
-
- );
- },
- },
- ]}
- className={{
- containerClassName: cn({
- 'mb-20': initialValues?.chickins?.length === 0,
- }),
- tableWrapperClassName: 'overflow-x-auto min-h-full!',
- tableClassName: 'font-inter w-full table-auto min-h-full!',
- headerRowClassName: 'border-b border-b-gray-200',
- headerColumnClassName:
- 'px-6 py-3 text-xs font-semibold text-gray-500 last:flex last:flex-row last:justify-end',
- bodyRowClassName: 'border-b border-b-gray-200',
- bodyColumnClassName:
- 'px-6 py-3 last:flex last:flex-row last:justify-end',
- paginationClassName: 'hidden',
- }}
- />
+
+ {/* Card List Chickin Logs */}
+ {(initialValues?.chickins || []).length === 0 ? (
+
+
+ Belum ada riwayat Chick In...
+
+
+ ) : (
+ (initialValues?.chickins || []).map((chickin, index) => {
+ const isApproved = chickin.usage_qty !== 0;
+ const isPending = chickin.pending_usage_qty !== 0;
+ const quantity = isApproved
+ ? chickin.usage_qty
+ : isPending
+ ? chickin.pending_usage_qty
+ : 0;
+
+ return (
+
+
+ {/* Header with Status Badge */}
+
+
+ Chick In #{index + 1}
+
+
+
+
+ {/* Tanggal Chick In */}
+
+
+ {' '}
+ Tanggal Chick In
+
+
+ {formatDate(chickin.chick_in_date, 'DD MMM YYYY')}
+
+
+
+ {/* Kandang */}
+
+
+ {' '}
+ Kandang
+
+
+ {chickin.product_warehouse?.warehouse?.name || '-'}
+
+
+
+ {/* Produk */}
+
+
+ {' '}
+ Produk
+
+
+ {chickin.product_warehouse?.product?.name || '-'}
+
+
+
+ {/* Jumlah Chick In */}
+
+
+ {' '}
+ Jumlah Chick In
+
+
+ {quantity > 0 ? `${formatNumber(quantity)} Ekor` : '-'}
+
+
+
+
+ );
+ })
+ )}
+
+ {initialValues?.approval?.step_number == 1 && (
+
+ )}
+
{chickinErrorMessage && (
setChickinErrorMessage('')}>
{chickinErrorMessage}
)}
-
+
+
{
handleReset();
}}
onSubmit={formik.handleSubmit}
>
-
-
- data={formik.values.chickin_requests || []}
- columns={[
- {
- accessorFn: (row) => row.chick_in_date,
- header: 'Tanggal Chick In',
- cell(props) {
- return (
-
- );
- },
- },
- {
- accessorFn: (row) => row.product_warehouse_id,
- header: 'Produk',
- cell(props) {
- const availableQty = initialValues?.available_qtys?.find(
- (availableQty) =>
- availableQty.product_warehouse.id ===
- props.row.original.product_warehouse_id
- );
- return (
- {availableQty?.product_warehouse?.product?.name}
- );
- },
- },
- {
- accessorFn: (row) => row.product_warehouse_id,
- header: 'Jumlah (ekor)',
- cell(props) {
- const availableQty = initialValues?.available_qtys?.find(
- (availableQty) =>
- availableQty.product_warehouse.id ===
- props.row.original.product_warehouse_id
- );
- return (
-
- {availableQty?.available_qty
- ? formatNumber(availableQty?.available_qty)
- : '-'}
-
- );
- },
- },
- ]}
- className={{
- tableWrapperClassName: 'overflow-x-auto min-h-full!',
- tableClassName: 'font-inter w-full table-auto min-h-full!',
- headerRowClassName: 'border-b border-b-gray-200',
- headerColumnClassName:
- 'px-2 py-3 text-xs font-semibold text-gray-500 last:flex last:flex-row last:justify-end',
- bodyRowClassName: 'border-b border-b-gray-200',
- bodyColumnClassName:
- 'px-2 py-2 last:flex last:flex-row last:justify-end',
- paginationClassName: 'hidden',
- }}
- emptyContent={
-
-
- Isi persediaan DOC untuk kandang belum tersedia...
-
+ {(formik.values.chickin_requests || []).map((chickinRequest, index) => {
+ const availableQty = initialValues?.available_qtys?.find(
+ (availableQty) =>
+ availableQty.product_warehouse.id ===
+ chickinRequest.product_warehouse_id
+ );
+ return (
+
+
+
+ {formatNumber(availableQty?.available_qty ?? 0)} Ekor -{' '}
+ {availableQty?.product_warehouse?.product?.name}
+
+ {chickinRequest.chick_in_date && (
+
+ )}
- }
- />
-
-
-
+
+
+ );
+ })}
+ {/*
+ data={formik.values.chickin_requests || []}
+ columns={[
+ {
+ accessorFn: (row) => row.chick_in_date,
+ header: 'Tanggal Chick In',
+ cell(props) {
+ return (
+
+ );
+ },
+ },
+ {
+ accessorFn: (row) => row.product_warehouse_id,
+ header: 'Produk',
+ cell(props) {
+ const availableQty = initialValues?.available_qtys?.find(
+ (availableQty) =>
+ availableQty.product_warehouse.id ===
+ props.row.original.product_warehouse_id
+ );
+ return (
+ {availableQty?.product_warehouse?.product?.name}
+ );
+ },
+ },
+ {
+ accessorFn: (row) => row.product_warehouse_id,
+ header: 'Jumlah (ekor)',
+ cell(props) {
+ const availableQty = initialValues?.available_qtys?.find(
+ (availableQty) =>
+ availableQty.product_warehouse.id ===
+ props.row.original.product_warehouse_id
+ );
+ return (
+
+ {availableQty?.available_qty
+ ? formatNumber(availableQty?.available_qty)
+ : '-'}
+
+ );
+ },
+ },
+ ]}
+ className={{
+ tableWrapperClassName: 'overflow-x-auto min-h-full!',
+ tableClassName: 'font-inter w-full table-auto min-h-full!',
+ headerRowClassName: 'border-b border-b-gray-200',
+ headerColumnClassName:
+ 'px-2 py-3 text-xs font-semibold text-gray-500 last:flex last:flex-row last:justify-end',
+ bodyRowClassName: 'border-b border-b-gray-200',
+ bodyColumnClassName:
+ 'px-2 py-2 last:flex last:flex-row last:justify-end',
+ paginationClassName: 'hidden',
+ }}
+ emptyContent={
+
+
+ Isi persediaan DOC untuk kandang belum tersedia...
+
+
+ }
+ /> */}
+ {formik.values.chickin_requests?.length > 0 && (
-
+ )}
{chickinErrorMessage && (
setChickinErrorMessage('')}>
{chickinErrorMessage}
diff --git a/src/components/pages/production/project-flock/ProjectFlockTable.tsx b/src/components/pages/production/project-flock/ProjectFlockTable.tsx
index a315b332..4be30f7a 100644
--- a/src/components/pages/production/project-flock/ProjectFlockTable.tsx
+++ b/src/components/pages/production/project-flock/ProjectFlockTable.tsx
@@ -1,6 +1,8 @@
'use client';
+import Badge from '@/components/Badge';
import Button from '@/components/Button';
+import FloatingActionsButton from '@/components/FloatingActionsButton';
import CheckboxInput from '@/components/input/CheckboxInput';
import DebouncedTextInput from '@/components/input/DebouncedTextInput';
import SelectInput, { OptionType } from '@/components/input/SelectInput';
@@ -8,23 +10,18 @@ import { useModal } from '@/components/Modal';
import ConfirmationModal from '@/components/modal/ConfirmationModal';
import ConfirmationModalWithNotes from '@/components/modal/ConfirmationModalWithNotes';
import Table from '@/components/Table';
-import RowCollapseOptions from '@/components/table/RowCollapseOptions';
-import RowDropdownOptions from '@/components/table/RowDropdownOptions';
import { ROWS_OPTIONS } from '@/config/constant';
import { isResponseError, isResponseSuccess } from '@/lib/api-helper';
-import { cn } from '@/lib/helper';
+import { cn, formatDate } from '@/lib/helper';
import { AreaApi, KandangApi, LocationApi } from '@/services/api/master-data';
import { ProjectFlockApi } from '@/services/api/production/project-flock';
import { useTableFilter } from '@/services/hooks/useTableFilter';
-import { BaseApiResponse } from '@/types/api/api-general';
import { Kandang } from '@/types/api/master-data/kandang';
-import {
- ProjectFlockApprovalPayload,
- ProjectFlock,
-} from '@/types/api/production/project-flock';
+import { ProjectFlock } from '@/types/api/production/project-flock';
import { Icon } from '@iconify/react';
import { CellContext, SortingState } from '@tanstack/react-table';
-import { ChangeEventHandler, useState } from 'react';
+import { useRouter } from 'next/navigation';
+import { ChangeEventHandler, useEffect, useMemo, useState } from 'react';
import toast from 'react-hot-toast';
import useSWR from 'swr';
@@ -98,7 +95,7 @@ const RowOptionsMenu = ({
);
};
-const ProjectFlockTable = () => {
+const ProjectFlockTable = ({ refresh }: { refresh?: () => void }) => {
const {
state: tableFilterState,
updateFilter,
@@ -123,8 +120,9 @@ const ProjectFlockTable = () => {
periodFilter: 'period',
},
});
+ const router = useRouter();
- // State
+ // ===== State =====
const [rowSelection, setRowSelection] = useState
>({});
const selectedRowIds = Object.keys(rowSelection)
.filter((id) => rowSelection[id])
@@ -151,14 +149,15 @@ const ProjectFlockTable = () => {
const [isDeleteLoading, setIsDeleteLoading] = useState(false);
const [isApproveLoading, setIsApproveLoading] = useState(false);
- // Fetch Data
+ // ===== Fetch Data =====
const {
data: projectFlocks,
isLoading,
mutate: refreshProjectFlocks,
} = useSWR(
`${ProjectFlockApi.basePath}${getTableFilterQueryString()}`,
- ProjectFlockApi.getAllFetcher
+ ProjectFlockApi.getAllFetcher,
+ { revalidateOnMount: true }
);
const areaUrl = `${AreaApi.basePath}?${new URLSearchParams({
@@ -191,7 +190,7 @@ const ProjectFlockTable = () => {
KandangApi.getAllFetcher
);
- // Data to Options Mapping
+ // ===== Data to Options Mapping ======
const optionsArea = isResponseSuccess(areas)
? areas?.data.map((area) => ({
value: area.id,
@@ -211,7 +210,7 @@ const ProjectFlockTable = () => {
}))
: [];
- // Handler
+ // ====== HANDLER ======
const pageSizeChangeHandler = (val: OptionType | OptionType[] | null) => {
const newVal = val as OptionType;
setPageSize(newVal.value as number);
@@ -219,17 +218,17 @@ const ProjectFlockTable = () => {
const confirmationModalDeleteClickHandler = async () => {
setIsDeleteLoading(true);
- await ProjectFlockApi.delete(selectedProjectFlock?.id as number);
+ await ProjectFlockApi.delete(selectedSingleRow?.id as number);
refreshProjectFlocks();
deleteModal.closeModal();
toast.success('Successfully delete Project Flock!');
setIsDeleteLoading(false);
+ setRowSelection({});
};
const searchChangeHandler: ChangeEventHandler = (e) => {
updateFilter('search', e.target.value);
};
-
const confirmApprovalHandler = async (
notes: string,
approvalAction: 'APPROVED' | 'REJECTED'
@@ -259,22 +258,44 @@ const ProjectFlockTable = () => {
setIsApproveLoading(false);
};
+ // ====== EFFECT ======
+ useEffect(() => {
+ refreshProjectFlocks();
+ }, [refresh]);
+
+ // ====== MEMO ======
+ const selectedSingleRow: ProjectFlock | null | undefined = useMemo(() => {
+ return selectedRowIds.length === 1
+ ? isResponseSuccess(projectFlocks)
+ ? projectFlocks?.data.find((row) => row.id === selectedRowIds[0])
+ : null
+ : null;
+ }, [rowSelection]);
+
+ const canApprove = useMemo(() => {
+ if (!selectedSingleRow || isApproveLoading) return false;
+
+ const isPengajuan = selectedSingleRow.approval.step_number == 1;
+ const isNotRejected = selectedSingleRow.approval.action != 'REJECTED';
+
+ return isPengajuan && isNotRejected;
+ }, [selectedSingleRow, isApproveLoading]);
+
return (
<>
-
+
-
+ */}
{
id: 'select',
header: ({ table }) => {
const allRows = table.getRowModel().rows;
- const selectableRows = allRows.filter(
- (row) => row.original?.approval?.step_number == 1
- );
+ const selectableRows = allRows;
const allSelected =
selectableRows.every((row) => row.getIsSelected()) &&
@@ -417,12 +436,6 @@ const ProjectFlockTable = () => {
checked={allSelected}
indeterminate={someSelected}
onChange={toggleSelectableRows}
- disabled={
- isResponseSuccess(projectFlocks) &&
- projectFlocks?.data?.filter(
- (flock) => flock.approval.step_number == 1
- ).length == 0
- }
/>
);
@@ -431,14 +444,8 @@ const ProjectFlockTable = () => {
return (
@@ -469,6 +476,40 @@ const ProjectFlockTable = () => {
{
accessorKey: 'approval.step_name',
header: 'Status',
+ cell: (props) => {
+ const approval = props.row.original.approval;
+
+ return (
+
+
+ {approval.step_name}
+
+ );
+ },
},
{
header: 'Kandang',
@@ -496,51 +537,51 @@ const ProjectFlockTable = () => {
accessorKey: 'created_at',
header: 'Dibuat pada',
cell: (props) =>
- new Date(props.row.original.created_at).toLocaleDateString(),
+ formatDate(props.row.original.created_at, 'MMM DD, YYYY'),
},
- {
- header: 'Aksi',
- cell: (props) => {
- const currentPageSize =
- props.table.getPaginationRowModel().rows.length;
- const currentPageRows =
- props.table.getPaginationRowModel().flatRows;
- const currentRowRelativeIndex =
- currentPageRows.findIndex((r) => r.id === props.row.id) + 1;
+ // {
+ // header: 'Aksi',
+ // cell: (props) => {
+ // const currentPageSize =
+ // props.table.getPaginationRowModel().rows.length;
+ // const currentPageRows =
+ // props.table.getPaginationRowModel().flatRows;
+ // const currentRowRelativeIndex =
+ // currentPageRows.findIndex((r) => r.id === props.row.id) + 1;
- const isLast2Rows =
- currentRowRelativeIndex > currentPageSize - 2;
+ // const isLast2Rows =
+ // currentRowRelativeIndex > currentPageSize - 2;
- const deleteClickHandler = () => {
- setSelectedProjectFlock(props.row.original);
- deleteModal.openModal();
- };
+ // const deleteClickHandler = () => {
+ // setSelectedProjectFlock(props.row.original);
+ // deleteModal.openModal();
+ // };
- return (
- <>
- {currentPageSize > 2 && (
-
-
-
- )}
+ // return (
+ // <>
+ // {currentPageSize > 2 && (
+ //
+ //
+ //
+ // )}
- {currentPageSize <= 2 && (
-
-
-
- )}
- >
- );
- },
- },
+ // {currentPageSize <= 2 && (
+ //
+ //
+ //
+ // )}
+ // >
+ // );
+ // },
+ // },
]}
pageSize={tableFilterState.pageSize}
page={
@@ -576,6 +617,57 @@ const ProjectFlockTable = () => {
+
{
+ deleteModal.openModal();
+ },
+ },
+ ]}
+ approvals={[
+ {
+ icon: 'material-symbols:check',
+ label: 'Approve',
+ action: 'APPROVED',
+ onClick: () => {
+ setApprovalAction('APPROVED');
+ confirmModal.openModal();
+ },
+ disabled: !canApprove,
+ },
+ {
+ icon: 'mdi:times',
+ label: 'Reject',
+ action: 'REJECTED',
+ onClick: () => {
+ setApprovalAction('REJECTED');
+ confirmModal.openModal();
+ },
+ },
+ ]}
+ selectedRowIds={selectedRowIds}
+ onClose={() => {
+ setRowSelection({});
+ }}
+ />
+
-
+
+
+
+
+
+
+ Chick In {projectFlock?.flock_name}
+
+
+
+ {/*
-
+ backUrl={`/production/project-flock/detail?projectFlockId=${projectFlock?.id}`}
+ /> */}
+ {/*
-
*/}
+ {/* Informasi Umum */}
+ {projectFlock && (
+
+
+
Informasi Umum
+ {/* Badge Row */}
+
+
= 3
+ ? 'error'
+ : undefined
+ }
+ className={{
+ badge: 'rounded-lg px-2',
+ }}
+ >
+ = 3
+ ? 'error'
+ : undefined
+ }
+ />{' '}
+ {projectFlock.approval.step_name}
+
+
+
+
+ {` ${formatTitleCase(projectFlock.category)}`}
+
+
+ {/* Information Grid */}
+
+
+ Submitted
+
+
+
+ {' '}
+ {projectFlock.created_user.name}
+
+
+
+
+ History
+
+
+
+
+
+ {/* BARIS 1 */}
+
+ Area
+
+
{projectFlock.area.name}
+
+ {/* BARIS 2 */}
+
+ Lokasi
+
+
{projectFlock.location.name}
+
+
+ FCR
+
+
{projectFlock.fcr.name}
+
+ {/* BARIS 3 (Terakhir - TIDAK PERLU garis di bawahnya) */}
+
+ {' '}
+ Kategori
+
+
+ {formatTitleCase(projectFlock.category)}
+
+
+
+
+ )}
+ {/*
-
-
*/}
+ {/* Card Kandangs */}
+
+
+
Daftar Kandang
+ {isResponseSuccess(listProjectFlock) ? (
+ <>
+ {/* Badge Row */}
+
+
+ {' '}
+ Disetujui (
+ {isResponseSuccess(listProjectFlockKandang) &&
+ listProjectFlockKandang.data.filter(
+ (k) => k.approval?.step_number == 1
+ ).length}
+ )
+
+
+
+ {' '}
+ Pengajuan (
+ {isResponseSuccess(listProjectFlockKandang) &&
+ listProjectFlockKandang.data.filter(
+ (k) => k.approval?.step_number == 2
+ ).length}
+ )
+
+
+
+
+ Belum Chickin (
+ {isResponseSuccess(listProjectFlockKandang) &&
+ listProjectFlockKandang.data.filter(
+ (k) => k.approval == null
+ ).length}
+ )
+
+
+ {/* Card Kandang */}
+
+
+ {isResponseSuccess(listProjectFlockKandang) &&
+ listProjectFlockKandang.data.map((kandang) => (
+
+
+
+
+
+
+ {kandang.kandang.name}
+
+
+
+
+ ))}
+
+
+ >
+ ) : (
+
+
+ Pilih project flock terlebih dahulu...
+
+
+ )}
+
+
+ {/*
-
+ */}
>
);
};
diff --git a/src/components/pages/production/project-flock/closing/ProjectFlockClosingForm.tsx b/src/components/pages/production/project-flock/closing/ProjectFlockClosingForm.tsx
new file mode 100644
index 00000000..bcfb7795
--- /dev/null
+++ b/src/components/pages/production/project-flock/closing/ProjectFlockClosingForm.tsx
@@ -0,0 +1,304 @@
+'use client';
+
+import Button from '@/components/Button';
+import DrawerHeader from '@/components/helper/drawer/DrawerHeader';
+import Table from '@/components/Table';
+import Badge from '@/components/Badge';
+import { cn, formatDate, formatNumber, formatTitleCase } from '@/lib/helper';
+import { ProjectFlock } from '@/types/api/production/project-flock';
+import {
+ ClosingExpense,
+ ProjectFlockKandang,
+} from '@/types/api/production/project-flock-kandang';
+import { Icon } from '@iconify/react';
+import useSWR from 'swr';
+import { ProjectFlockKandangApi } from '@/services/api/production/project-flock-kandang';
+import { isResponseError, isResponseSuccess } from '@/lib/api-helper';
+import { useModal } from '@/components/Modal';
+import ConfirmationModal from '@/components/modal/ConfirmationModal';
+import { useMemo, useState } from 'react';
+import toast from 'react-hot-toast';
+import { useRouter } from 'next/navigation';
+import { ProductWarehouse } from '@/types/api/inventory/product-warehouse';
+
+const ProjectFlockClosingForm = ({
+ projectFlock,
+ projectFlockKandang,
+}: {
+ projectFlock: ProjectFlock;
+ projectFlockKandang: ProjectFlockKandang;
+}) => {
+ const router = useRouter();
+ const closeModal = useModal();
+ const isCanClose = projectFlock.approval?.step_number <= 2;
+ const [isClosingLoading, setIsClosingLoading] = useState(false);
+
+ const { data: closingData, isLoading } = useSWR(
+ `${ProjectFlockKandangApi.basePath}/${projectFlockKandang.id}/closing`,
+ () => ProjectFlockKandangApi.checkClosing(projectFlockKandang.id)
+ );
+
+ const confirmationModalCloseClickHandler = async () => {
+ setIsClosingLoading(true);
+ const deleteProjectFlockRes = await ProjectFlockKandangApi.closing(
+ projectFlock?.id as number,
+ {
+ closed_date: formatDate(new Date(), 'yyyy-MM-dd'),
+ action: isCanClose ? 'close' : 'unclose',
+ }
+ );
+
+ if (isResponseSuccess(deleteProjectFlockRes)) {
+ toast.success(deleteProjectFlockRes?.message as string);
+ router.push(`/production/project-flock`);
+ }
+ if (isResponseError(deleteProjectFlockRes)) {
+ toast.error(deleteProjectFlockRes?.message as string);
+ }
+ setIsClosingLoading(false);
+ closeModal.closeModal();
+ };
+
+ const errorStock = useMemo(() => {
+ return isResponseSuccess(closingData)
+ ? closingData?.data?.stock_remaining.every((stock) => stock.quantity > 0)
+ : false;
+ }, [closingData]);
+
+ const errorExpense = useMemo(() => {
+ return isResponseSuccess(closingData)
+ ? closingData?.data?.expenses.every((expense) => expense.step < 5)
+ : false;
+ }, [closingData]);
+
+ const isCanCloseValid = !errorStock && !errorExpense;
+
+ return (
+ <>
+
+
+ {/* Informasi Kandang */}
+
+
+
Informasi Kandang
+
+ {/* Badge Row */}
+
+
+ {' '}
+ Aktif
+
+
+
+
+ {` Kapasitas ${formatNumber(projectFlockKandang.kandang?.capacity)} Ekor`}
+
+
+
+ {/* Information Grid */}
+
+ {/* Area */}
+
+ Area
+
+
{projectFlock.area?.name}
+
+ {/* Lokasi */}
+
+ Lokasi
+
+
{projectFlock.location?.name}
+
+ {/* Kandang */}
+
+ Kandang
+
+
{projectFlockKandang.kandang?.name}
+
+ {/* Jumlah DOC */}
+
+ Jumlah DOC
+
+
+ {formatNumber(
+ projectFlockKandang.chickins?.reduce(
+ (total, chickin) => total + chickin.usage_qty,
+ 0
+ ) ?? 0
+ )}{' '}
+ Ekor
+
+
+
+
+ {/* Table Biaya */}
+
+
+
Biaya
+
+ data={
+ isResponseSuccess(closingData) ? closingData.data?.expenses : []
+ }
+ columns={[
+ {
+ header: 'PO Number',
+ accessorKey: 'po_number',
+ },
+ {
+ header: 'Total',
+ accessorKey: 'total',
+ },
+ {
+ header: 'Status',
+ accessorKey: 'status',
+ cell(props) {
+ return (
+
+ {formatTitleCase(props.row.original.status)}
+
+ );
+ },
+ },
+ ]}
+ className={{
+ containerClassName: cn('my-4'),
+ tableWrapperClassName: 'overflow-x-auto min-h-full! max-w-120',
+ tableClassName: 'font-inter w-full table-sm min-h-full!',
+ headerRowClassName: 'border-b border-b-gray-200',
+ headerColumnClassName:
+ 'px-3 py-3 text-xs font-semibold text-gray-500 last:flex last:flex-row last:justify-end',
+ bodyRowClassName: 'border-b border-b-gray-200',
+ bodyColumnClassName:
+ 'px-3 py-3 last:flex last:flex-row last:justify-end',
+ paginationClassName: 'hidden',
+ }}
+ />
+ {errorExpense && (
+
+ *Pastikan semua biaya sudah selesai sebelum melakukan closing.
+
+ )}
+
+
+ {/* Table Persediaan Gudang */}
+
+
+
Persediaan Gudang
+
+ data={
+ isResponseSuccess(closingData)
+ ? closingData.data?.stock_remaining
+ : []
+ }
+ columns={[
+ {
+ header: 'Product',
+ accessorKey: 'product.name',
+ },
+ {
+ header: 'Kategori',
+ accessorKey: 'product.product_category.name',
+ },
+ {
+ header: 'Quantity',
+ accessorKey: 'quantity',
+ },
+ {
+ header: 'UOM',
+ accessorKey: 'product.uom.name',
+ },
+ ]}
+ className={{
+ containerClassName: cn('my-4'),
+ tableWrapperClassName: 'overflow-x-auto min-h-full! max-w-120',
+ tableClassName: 'font-inter w-full table-sm min-h-full!',
+ headerRowClassName: 'border-b border-b-gray-200',
+ headerColumnClassName:
+ 'px-3 py-3 text-xs font-semibold text-gray-500 last:flex last:flex-row last:justify-end',
+ bodyRowClassName: 'border-b border-b-gray-200',
+ bodyColumnClassName:
+ 'px-3 py-3 last:flex last:flex-row last:justify-end',
+ paginationClassName: 'hidden',
+ }}
+ />
+ {errorStock && (
+
+ *Masih ada sisa stock yang belum dihabiskan.
+
+ )}
+
+
+
+
+
+
+
+ >
+ );
+};
+
+export default ProjectFlockClosingForm;
diff --git a/src/components/pages/production/project-flock/detail/ProjectFlockDetail.tsx b/src/components/pages/production/project-flock/detail/ProjectFlockDetail.tsx
new file mode 100644
index 00000000..5b54b10e
--- /dev/null
+++ b/src/components/pages/production/project-flock/detail/ProjectFlockDetail.tsx
@@ -0,0 +1,439 @@
+import Badge from '@/components/Badge';
+import Button from '@/components/Button';
+import Card from '@/components/Card';
+import { RadioGroup, RadioGroupItem } from '@/components/input/RadioInput';
+import Tooltip from '@/components/Tooltip';
+import DrawerHeader from '@/components/helper/drawer/DrawerHeader';
+import {
+ formatCurrency,
+ formatDate,
+ formatNumber,
+ formatTitleCase,
+} from '@/lib/helper';
+import { ProjectFlock } from '@/types/api/production/project-flock';
+import { Icon } from '@iconify/react';
+import Link from 'next/link';
+import { useRouter } from 'next/navigation';
+import { useState } from 'react';
+import { useModal } from '@/components/Modal';
+import ConfirmationModal from '@/components/modal/ConfirmationModal';
+import { ProjectFlockApi } from '@/services/api/production/project-flock';
+import { isResponseError, isResponseSuccess } from '@/lib/api-helper';
+import toast from 'react-hot-toast';
+import ApprovalSteps, {
+ useApprovalSteps,
+} from '@/components/pages/ApprovalSteps';
+import { PROJECT_FLOCK_APPROVAL_LINE } from '@/config/approval-line';
+
+const ProjectFlockDetail = ({
+ projectFlock,
+}: {
+ projectFlock: ProjectFlock;
+}) => {
+ const router = useRouter();
+ const deleteModal = useModal();
+ const [isDeleteLoading, setIsDeleteLoading] = useState(false);
+ const [openBudgets, setOpenBudget] = useState(false);
+ const [selectedKandangId, setSelectedKamdangId] = useState(
+ null
+ );
+
+ const selectedKandang = projectFlock.kandangs.find(
+ (kandang) => kandang.id === Number(selectedKandangId)
+ );
+
+ const {
+ approvals,
+ isLoading: approvalsLoading,
+ refresh: refreshApprovals,
+ } = useApprovalSteps({
+ latestApproval: projectFlock?.approval,
+ approvalLines: PROJECT_FLOCK_APPROVAL_LINE,
+ moduleName: 'PROJECT_FLOCKS',
+ moduleId: projectFlock?.id.toString() ?? '',
+ });
+
+ const confirmationModalDeleteClickHandler = async () => {
+ setIsDeleteLoading(true);
+ const deleteProjectFlockRes = await ProjectFlockApi.delete(
+ projectFlock?.id as number
+ );
+
+ if (isResponseSuccess(deleteProjectFlockRes)) {
+ toast.success(deleteProjectFlockRes?.message as string);
+ router.push('/production/project-flock');
+ }
+ if (isResponseError(deleteProjectFlockRes)) {
+ toast.error(deleteProjectFlockRes?.message as string);
+ }
+ setIsDeleteLoading(false);
+ };
+
+ return (
+ <>
+
+ {/* Header */}
+
+
+
+
+
+
+
+
+
+ {/* Informasi Umum */}
+
+
+
Informasi Umum
+ {/* Status Approval */}
+ {approvals && !approvalsLoading && (
+
+ )}
+ {/* Badge Row */}
+
+
= 3
+ ? 'error'
+ : undefined
+ }
+ className={{
+ badge: 'rounded-lg px-2',
+ }}
+ >
+ = 3
+ ? 'error'
+ : undefined
+ }
+ />{' '}
+ {projectFlock.approval?.step_name}
+
+
+
+
+ {` ${formatTitleCase(projectFlock.category)}`}
+
+
+ {/* Information Grid */}
+
+
+ Submitted
+
+
+
+ {' '}
+ {projectFlock.created_user.name}
+
+
+
+ {/*
+ History
+
+
+
+
*/}
+
+ {/* BARIS 1 */}
+
+ Area
+
+
{projectFlock.area.name}
+
+ {/* BARIS 2 */}
+
+ Lokasi
+
+
{projectFlock.location.name}
+
+
+ FCR
+
+
{projectFlock.fcr.name}
+
+ {/* BARIS 3 (Terakhir - TIDAK PERLU garis di bawahnya) */}
+
+ {' '}
+ Kategori
+
+
+ {formatTitleCase(projectFlock.category)}
+
+
+
+
+
+ {/* Kandang Aktif */}
+
+
+
Kandang Aktif
+ {/* Badge Row */}
+
+
+ {' '}
+ Kandang Aktif ({projectFlock.kandangs.length})
+
+
+
{
+ setOpenBudget(!openBudgets);
+ }}
+ >
+ {` ${formatCurrency(
+ (projectFlock.project_budgets ?? []).reduce(
+ (acc, curr) => acc + curr.price * curr.qty,
+ 0
+ )
+ )}`}
+
+
+
+
+ {/* Card List Project Budgets */}
+ {openBudgets &&
+ (projectFlock.project_budgets ?? []).map((budget) => (
+
+
+
+
+ {' '}
+ Jenis Produk
+
+
+ {budget.nonstock?.name}
+
+
+
+
+ {' '}
+ Nama Satuan
+
+
+ {budget.nonstock?.uom.name}
+
+
+
+
+ {' '}
+ Jumlah Pembelian
+
+
+ {formatNumber(budget.qty)}
+
+
+
+
+ {' '}
+ Harga Satuan
+
+
+ {formatCurrency(budget.price)}
+
+
+
+
+ {' '}
+ Total Harga
+
+
+ {formatCurrency(budget.price * budget.qty)}
+
+
+
+
+ ))}
+
+ {/* Card Kandangs */}
+
+ setSelectedKamdangId(e.target.value)}
+ value={selectedKandangId?.toString()}
+ size='md'
+ color='neutral'
+ disabled={projectFlock.approval.step_number == 1}
+ >
+ {projectFlock.kandangs.map((kandang) => (
+
+ projectFlock.approval.step_number > 1 &&
+ setSelectedKamdangId(kandang.id.toString())
+ }
+ >
+
+
+
+ Kapasitas {kandang.capacity} Ekor
+
+
+
+ ))}
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ >
+ );
+};
+
+export default ProjectFlockDetail;
diff --git a/src/components/pages/production/project-flock/form/ProjectFlockForm.schema.ts b/src/components/pages/production/project-flock/form/ProjectFlockForm.schema.ts
index ca27f64b..9ac07c0f 100644
--- a/src/components/pages/production/project-flock/form/ProjectFlockForm.schema.ts
+++ b/src/components/pages/production/project-flock/form/ProjectFlockForm.schema.ts
@@ -1,52 +1,124 @@
import * as Yup from 'yup';
-export const ProjectFlockFormSchema = Yup.object({
- // Flock
- flock: Yup.object({
- value: Yup.number().required('ID Flock wajib diisi!'),
- label: Yup.string().required('Nama Flock wajib diisi!'),
- }).nullable(),
- flock_name: Yup.string().required('Nama Flock wajib diisi!'),
+type ProjectFlockFormSchemaType = {
+ flock: {
+ value: number | string;
+ label: string;
+ } | null;
+ flock_name: string;
+ area: {
+ value: number | string;
+ label: string;
+ } | null;
+ area_id: number;
+ category_option: {
+ value: string;
+ label: string;
+ } | null;
+ category: string;
+ fcr: {
+ value: number | string;
+ label: string;
+ } | null;
+ fcr_id: number;
+ location: {
+ value: number | string;
+ label: string;
+ } | null;
+ location_id: number;
+ kandang_ids: number[];
+ project_budgets: ProjectFlockBudgetsSchemaType[];
+};
- // Area
- area: Yup.object({
- value: Yup.number().required('ID Area wajib diisi!'),
- label: Yup.string().required('Nama Area wajib diisi!'),
- }).nullable(),
- area_id: Yup.number()
- .min(1, 'Area wajib diisi!')
- .required('Area wajib diisi!'),
+export type ProjectFlockBudgetsSchemaType = {
+ nonstock: {
+ value: number | string;
+ label: string;
+ } | null;
+ nonstock_id: number | string;
+ qty: number | string;
+ price: number | string;
+ total_price: number | string;
+};
- // Kategori
- category_option: Yup.object({
- value: Yup.string().required('Nilai Kategori wajib diisi!'),
- label: Yup.string().required('Label Kategori wajib diisi!'),
- }).nullable(),
- category: Yup.string()
- .oneOf(['GROWING', 'LAYING'], 'Kategori wajib diisi!')
- .required('Kategori wajib diisi!'),
+export const ProjectFlockBudgetsSchema: Yup.ObjectSchema =
+ Yup.object({
+ nonstock: Yup.object({
+ value: Yup.number().required('ID Nonstock wajib diisi!'),
+ label: Yup.string().required('Nama Nonstock wajib diisi!'),
+ }).required('Nonstock wajib diisi!'),
+ nonstock_id: Yup.number()
+ .min(1, 'Nonstock wajib diisi!')
+ .required('Nonstock wajib diisi!'),
+ qty: Yup.number()
+ .typeError('Jumlah harus berupa angka!')
+ .min(1, 'Jumlah minimal 1!')
+ .required('Jumlah wajib diisi!'),
+ price: Yup.number()
+ .typeError('Harga harus berupa angka!')
+ .min(1, 'Harga minimal 1!')
+ .required('Harga wajib diisi!'),
+ total_price: Yup.number()
+ .typeError('Harga harus berupa angka!')
+ .min(1, 'Harga minimal 1!')
+ .required('Harga wajib diisi!'),
+ });
- // FCR
- fcr: Yup.object({
- value: Yup.number().required('ID FCR wajib diisi!'),
- label: Yup.string().required('Nama FCR wajib diisi!'),
- }).nullable(),
- fcr_id: Yup.number().min(1, 'FCR wajib diisi!').required('FCR wajib diisi!'),
+export const ProjectFlockFormSchema: Yup.ObjectSchema =
+ Yup.object({
+ // Flock
+ flock: Yup.object({
+ value: Yup.number().required('ID Flock wajib diisi!'),
+ label: Yup.string().required('Nama Flock wajib diisi!'),
+ }).nullable(),
+ flock_name: Yup.string().required('Nama Flock wajib diisi!'),
- // Location
- location: Yup.object({
- value: Yup.number().required('ID Lokasi wajib diisi!'),
- label: Yup.string().required('Nama Lokasi wajib diisi!'),
- }).nullable(),
- location_id: Yup.number()
- .min(1, 'Lokasi wajib diisi!')
- .required('Lokasi wajib diisi!'),
+ // Area
+ area: Yup.object({
+ value: Yup.number().required('ID Area wajib diisi!'),
+ label: Yup.string().required('Nama Area wajib diisi!'),
+ }).nullable(),
+ area_id: Yup.number()
+ .min(1, 'Area wajib diisi!')
+ .required('Area wajib diisi!'),
- kandang_ids: Yup.array()
- .of(Yup.number().typeError('Kandang tidak valid!'))
- .min(1, 'Minimal harus ada 1 kandang!')
- .required('Kandang wajib diisi!'),
-});
+ // Kategori
+ category_option: Yup.object({
+ value: Yup.string().required('Nilai Kategori wajib diisi!'),
+ label: Yup.string().required('Label Kategori wajib diisi!'),
+ }).nullable(),
+ category: Yup.string()
+ .oneOf(['GROWING', 'LAYING'], 'Kategori wajib diisi!')
+ .required('Kategori wajib diisi!'),
+
+ // FCR
+ fcr: Yup.object({
+ value: Yup.number().required('ID FCR wajib diisi!'),
+ label: Yup.string().required('Nama FCR wajib diisi!'),
+ }).nullable(),
+ fcr_id: Yup.number()
+ .min(1, 'FCR wajib diisi!')
+ .required('FCR wajib diisi!'),
+
+ // Location
+ location: Yup.object({
+ value: Yup.number().required('ID Lokasi wajib diisi!'),
+ label: Yup.string().required('Nama Lokasi wajib diisi!'),
+ }).nullable(),
+ location_id: Yup.number()
+ .min(1, 'Lokasi wajib diisi!')
+ .required('Lokasi wajib diisi!'),
+
+ kandang_ids: Yup.array()
+ .of(Yup.number().required('Kandang tidak valid!'))
+ .min(1, 'Minimal harus ada 1 kandang!')
+ .required('Kandang wajib diisi!'),
+
+ project_budgets: Yup.array()
+ .of(ProjectFlockBudgetsSchema)
+ .min(1, 'Minimal harus ada 1 data budget!')
+ .required('Data budget wajib diisi!'),
+ });
export type ProjectFlockFormValues = Yup.InferType<
typeof ProjectFlockFormSchema
diff --git a/src/components/pages/production/project-flock/form/ProjectFlockForm.tsx b/src/components/pages/production/project-flock/form/ProjectFlockForm.tsx
index ae60b020..9e5eaeef 100644
--- a/src/components/pages/production/project-flock/form/ProjectFlockForm.tsx
+++ b/src/components/pages/production/project-flock/form/ProjectFlockForm.tsx
@@ -12,37 +12,44 @@ import {
FlockApi,
KandangApi,
LocationApi,
+ NonstockApi,
} from '@/services/api/master-data';
import { Icon } from '@iconify/react';
-import { useFormik } from 'formik';
+import { FormikErrors, useFormik } from 'formik';
import { useRouter } from 'next/navigation';
import { useEffect, useMemo, useState } from 'react';
import useSWR, { KeyedMutator } from 'swr';
import {
+ ProjectFlockBudgetsSchemaType,
ProjectFlockFormSchema,
ProjectFlockFormValues,
UpdateProjectFlockFormSchema,
} from '@/components/pages/production/project-flock/form/ProjectFlockForm.schema';
import {
- ProjectFlockApprovalPayload,
CreateProjectFlockPayload,
ProjectFlock,
+ ProjectFlockBudget,
} from '@/types/api/production/project-flock';
import toast from 'react-hot-toast';
import { Kandang } from '@/types/api/master-data/kandang';
-import Collapse from '@/components/Collapse';
import { ProjectFlockApi } from '@/services/api/production/project-flock';
import { BaseApiResponse } from '@/types/api/api-general';
import { FLOCK_CATEGORY_OPTIONS } from '@/config/constant';
import { useModal } from '@/components/Modal';
import ConfirmationModal from '@/components/modal/ConfirmationModal';
-import ProjectFlockKandangTable from './ProjectFlockKandangTable';
import ApprovalSteps, {
useApprovalSteps,
} from '@/components/pages/ApprovalSteps';
import { PROJECT_FLOCK_APPROVAL_LINE } from '@/config/approval-line';
import ConfirmationModalWithNotes from '@/components/modal/ConfirmationModalWithNotes';
import NumberInput from '@/components/input/NumberInput';
+import Card from '@/components/Card';
+import ProjectFlockKandangTable from '@/components/pages/production/project-flock/form/ProjectFlockKandangTable';
+import { Nonstock } from '@/types/api/master-data/nonstock';
+import { useUiStore } from '@/stores/ui/ui.store';
+import Link from 'next/link';
+import DrawerHeader from '@/components/helper/drawer/DrawerHeader';
+import { formatDate } from '@/lib/helper';
interface ProjectFlockFormProps {
formType?: 'add' | 'edit' | 'detail';
@@ -79,6 +86,8 @@ const ProjectFlockForm = ({
initialValues?.flock_name?.lastIndexOf(' ')
) ?? ''
);
+ const subscribeValidate = useUiStore((s) => s.subscribeValidate);
+ const setIsValid = useUiStore((s) => s.setIsValid);
const deleteModal = useModal();
const confirmModal = useModal();
@@ -104,19 +113,6 @@ const ProjectFlockForm = ({
)
);
- useEffect(() => {
- if (initialValues?.approval?.step_name) {
- const pengajuanRejected =
- initialValues.approval.step_number == 1 &&
- initialValues.approval.action == 'REJECTED';
- const approvedDisabled =
- initialValues.approval.step_number !== 1 || pengajuanRejected;
- setIsApprovedDisabled(approvedDisabled);
- setIsRejectedDisabled(!approvedDisabled || pengajuanRejected);
- setApprovalAction(!approvedDisabled ? 'APPROVED' : 'REJECTED');
- }
- }, [initialValues]);
-
// Fetch Data
const { isLoadingOptions: isLoadingFlocks, options: optionsFlock } =
useSelect(FlockApi.basePath, 'id', 'name');
@@ -156,6 +152,12 @@ const ProjectFlockForm = ({
() => ProjectFlockApi.getNextPeriod(parseInt(selectedLocation as string))
);
+ const {
+ options: optionsNonstock,
+ rawData: nonstocks,
+ isLoadingOptions: isLoadingNonstocks,
+ } = useSelect(NonstockApi.basePath, 'id', 'name');
+
const {
approvals,
isLoading: approvalsLoading,
@@ -209,7 +211,12 @@ const ProjectFlockForm = ({
formik.setFieldValue('area_id', (val as OptionType)?.value);
formik.setFieldValue('area', val);
- formik.setFieldTouched('area_id', true);
+ if (Boolean(val)) {
+ formik.setFieldTouched('area_id', false);
+ formik.setFieldError('area_id', '');
+ } else {
+ formik.setFieldTouched('area_id', true);
+ }
setSelectedArea((val as OptionType)?.value as string);
setSelectedLocation('');
@@ -242,7 +249,12 @@ const ProjectFlockForm = ({
val ? (val as OptionType)?.value : 0
);
- formik.setFieldTouched(`${inputName}_id`, true);
+ if (Boolean(val)) {
+ formik.setFieldTouched(`${inputName}_id`, false);
+ formik.setFieldError(`${inputName}_id`, '');
+ } else {
+ formik.setFieldTouched(`${inputName}_id`, true);
+ }
};
const categoryChangeHandler = (val: OptionType | OptionType[] | null) => {
@@ -259,6 +271,7 @@ const ProjectFlockForm = ({
if (isResponseSuccess(createProjectFlockRes)) {
toast.success(createProjectFlockRes?.message as string);
+ handleReset();
router.push('/production/project-flock');
}
if (isResponseError(createProjectFlockRes)) {
@@ -269,13 +282,14 @@ const ProjectFlockForm = ({
const updateProjectFlockHandler = async (
payload: CreateProjectFlockPayload
) => {
- const updateProjectFlockRes = await ProjectFlockApi.update(
+ const updateProjectFlockRes = await ProjectFlockApi.resubmit(
initialValues?.id as number,
payload
);
if (isResponseSuccess(updateProjectFlockRes)) {
toast.success(updateProjectFlockRes?.message as string);
+ handleReset();
router.push('/production/project-flock');
}
if (isResponseError(updateProjectFlockRes)) {
@@ -283,6 +297,15 @@ const ProjectFlockForm = ({
toast.error(updateProjectFlockRes?.message as string);
}
};
+ const handleReset = () => {
+ formik.resetForm();
+ setSelectedArea('');
+ setSelectedLocation('');
+ setDisabledLocation(true);
+ setOpenSelectKandangs(false);
+ setOptionsKandang([]);
+ formikSetValues(formikInitialValues);
+ };
// Formik InitialValue
const formikInitialValues = useMemo(() => {
@@ -291,21 +314,14 @@ const ProjectFlockForm = ({
0,
initialValues?.flock_name?.lastIndexOf(' ')
) ?? '';
+ const optionFind = optionsFlock.find((flock) => {
+ return flock.label == trimFlock;
+ }) as OptionType;
return {
- flock: initialValues?.flock_name
- ? {
- value:
- optionsFlock.find((flock) => {
- return flock.label == trimFlock;
- })?.value ?? 0,
- label:
- formType != 'detail'
- ? (optionsFlock.find((flock) => {
- return flock.label == trimFlock;
- })?.label ?? '')
- : initialValues?.flock_name,
- }
- : null,
+ flock:
+ optionsFlock.find((flock) => {
+ return flock.label == trimFlock;
+ }) ?? null,
area: initialValues?.area
? {
value: initialValues.area?.id,
@@ -332,109 +348,50 @@ const ProjectFlockForm = ({
: null,
flock_name:
optionsFlock.find((flock) => {
- return (
- flock.label ==
- initialValues?.flock_name?.slice(
- 0,
- initialValues?.flock_name?.lastIndexOf(' ')
- )
- );
- })?.label ?? '',
+ return flock.label == trimFlock;
+ })?.label ?? trimFlock,
area_id: initialValues?.area?.id ?? 0,
category: initialValues?.category as NonNullable<
'GROWING' | 'LAYING' | undefined
>,
fcr_id: initialValues?.fcr?.id ?? 0,
location_id: initialValues?.location?.id ?? 0,
- kandang_ids: initialValues?.kandangs?.map((k: Kandang) => k.id) as (
- | number
- | undefined
- )[],
+ kandang_ids: initialValues?.kandangs?.map(
+ (k: Kandang) => k.id
+ ) as number[],
+ project_budgets: initialValues?.project_budgets?.map((budget) => {
+ return {
+ nonstock: {
+ value: budget.nonstock?.id ?? '',
+ label: budget.nonstock?.name ?? '',
+ },
+ nonstock_id: budget.nonstock?.id ?? '',
+ qty: budget.qty,
+ price: budget.price,
+ total_price: budget.qty * budget.price,
+ };
+ }) ?? [
+ {
+ nonstock: null,
+ nonstock_id: '',
+ qty: '',
+ price: '',
+ total_price: '',
+ },
+ ],
};
}, [initialValues, optionsFlock]);
// Formik
const formik = useFormik({
initialValues: {
- flock: initialValues?.flock_name
- ? {
- value:
- optionsFlock.find((flock) => {
- return (
- flock.label ==
- initialValues?.flock_name?.slice(
- 0,
- initialValues?.flock_name?.lastIndexOf(' ')
- )
- );
- })?.value ?? 0,
- label:
- formType != 'detail'
- ? (optionsFlock.find((flock) => {
- return (
- flock.label ==
- initialValues?.flock_name?.slice(
- 0,
- initialValues?.flock_name?.lastIndexOf(' ')
- )
- );
- })?.label ?? '')
- : initialValues?.flock_name,
- }
- : null,
- area: initialValues?.area
- ? {
- value: initialValues.area?.id,
- label: initialValues.area.name,
- }
- : null,
- category_option: initialValues?.category
- ? {
- value: initialValues.category,
- label: initialValues.category,
- }
- : null,
- fcr: initialValues?.fcr
- ? {
- value: initialValues.fcr?.id,
- label: initialValues.fcr.name,
- }
- : null,
- location: initialValues?.location
- ? {
- value: initialValues.location?.id,
- label: initialValues.location.name,
- }
- : null,
- flock_name:
- formType != 'detail'
- ? optionsFlock.find((flock) => {
- return (
- flock.label ==
- initialValues?.flock_name?.slice(
- 0,
- initialValues?.flock_name?.lastIndexOf(' ')
- )
- );
- })?.label
- : (initialValues?.flock_name ?? ''),
- area_id: initialValues?.area?.id ?? 0,
- category: initialValues?.category as NonNullable<
- 'GROWING' | 'LAYING' | undefined
- >,
- fcr_id: initialValues?.fcr?.id ?? 0,
- location_id: initialValues?.location?.id ?? 0,
- kandang_ids: initialValues?.kandangs?.map((k: Kandang) => k.id) as (
- | number
- | undefined
- )[],
+ ...formikInitialValues,
} as ProjectFlockFormValues,
- enableReinitialize: true,
validationSchema:
formType == 'add' ? ProjectFlockFormSchema : UpdateProjectFlockFormSchema,
validateOnBlur: true,
- validateOnChange: true,
- validateOnMount: true,
+ // validateOnChange: true,
+ // validateOnMount: true,
onSubmit: async (values) => {
setProjectFlockFormErrorMessage('');
const payload: CreateProjectFlockPayload = {
@@ -444,6 +401,13 @@ const ProjectFlockForm = ({
fcr_id: values.fcr_id as number,
location_id: values.location_id as number,
kandang_ids: values.kandang_ids as number[],
+ project_budgets: values.project_budgets.flatMap((budget) => {
+ return {
+ nonstock_id: budget.nonstock_id,
+ qty: budget.qty,
+ price: budget.price,
+ } as ProjectFlockBudget;
+ }),
};
switch (formType) {
@@ -458,8 +422,8 @@ const ProjectFlockForm = ({
}
},
});
-
const { setValues: formikSetValues } = formik;
+
// Effect Initial
useEffect(() => {
if (formType == 'detail') {
@@ -475,7 +439,18 @@ const ProjectFlockForm = ({
}, [initialValues, setSelectedArea, formType]);
useEffect(() => {
- formikSetValues(formikInitialValues);
+ const trimFlock =
+ initialValues?.flock_name?.slice(
+ 0,
+ initialValues?.flock_name?.lastIndexOf(' ')
+ ) ?? '';
+ formikSetValues({
+ ...formikInitialValues,
+ flock: optionsFlock.find((flock) => {
+ return flock.label == trimFlock;
+ }) as OptionType,
+ flock_name: trimFlock ?? '',
+ });
}, [formikSetValues]);
// Aktifkan lokasi jika formType = 'detail'
@@ -495,10 +470,6 @@ const ProjectFlockForm = ({
}
}, [formType, initialValues]);
- useEffect(() => {
- formik.validateForm();
- }, [formik.values]);
-
useEffect(() => {
const selectedRowIds = Object.keys(rowSelection)
.filter((id) => rowSelection[id])
@@ -509,6 +480,46 @@ const ProjectFlockForm = ({
});
}, [rowSelection, formikSetValues]);
+ useEffect(() => {
+ const unsub = subscribeValidate(() => {
+ formik.validateForm().then((errors) => {
+ if (Object.keys(errors).length > 0) {
+ // Membentuk touched object yang strongly-typed
+ const touched: Record[]> =
+ {};
+ Object.keys(formik.values).forEach((key) => {
+ if (
+ key === 'project_budgets' &&
+ Array.isArray(formik.values.project_budgets)
+ ) {
+ touched[key] = formik.values.project_budgets.map(() => ({})); // Mark each item as touched if it's an array
+ } else {
+ touched[key] = true;
+ }
+ });
+
+ formik.setTouched(touched, true);
+ }
+ setIsValid(Object.keys(errors).length === 0);
+ });
+ });
+
+ return unsub;
+ }, []);
+
+ useEffect(() => {
+ if (initialValues?.approval?.step_name) {
+ const pengajuanRejected =
+ initialValues.approval.step_number == 1 &&
+ initialValues.approval.action == 'REJECTED';
+ const approvedDisabled =
+ initialValues.approval.step_number !== 1 || pengajuanRejected;
+ setIsApprovedDisabled(approvedDisabled);
+ setIsRejectedDisabled(!approvedDisabled || pengajuanRejected);
+ setApprovalAction(!approvedDisabled ? 'APPROVED' : 'REJECTED');
+ }
+ }, [initialValues]);
+
// Actions handler
const confirmationModalDeleteClickHandler = async () => {
setIsDeleteLoading(true);
@@ -526,6 +537,42 @@ const ProjectFlockForm = ({
setIsDeleteLoading(false);
};
+ const onAddBudgetRowHandler = () => {
+ const newProjectBudgets = [
+ ...(formik.values.project_budgets ?? []),
+ {
+ nonstock: null,
+ nonstock_id: '',
+ qty: '',
+ price: '',
+ },
+ ];
+ formik.setFieldValue('project_budgets', newProjectBudgets);
+ };
+
+ const onDeleteBudgetRowHandler = (nonstock_id: number, index?: number) => {
+ console.log(`nonstock_id: ${nonstock_id}, index: ${index}`);
+ if (!nonstock_id) {
+ const updatedBudgets = formik.values.project_budgets
+ .map((budget, i) => {
+ if (i == index) {
+ console.log(`buget: ${null}, index: ${index}, i: ${i}`);
+ return null;
+ } else {
+ console.log(`buget: ${budget}, index: ${index}, i: ${i}`);
+ return budget;
+ }
+ })
+ .filter((budget) => budget != null);
+ formik.setFieldValue('project_budgets', updatedBudgets);
+ } else {
+ const updatedBudgets = (formik.values.project_budgets ?? []).filter(
+ (budget) => budget.nonstock_id !== nonstock_id
+ );
+ formik.setFieldValue('project_budgets', updatedBudgets);
+ }
+ };
+
const confirmApprovalHandler = async (
notes: string,
approvalAction: 'REJECTED' | 'APPROVED'
@@ -549,6 +596,67 @@ const ProjectFlockForm = ({
setIsApproveLoading(false);
};
+ const handleBudgetChange = (
+ index: number,
+ fieldName: 'qty' | 'price' | 'total_price',
+ value: string
+ ) => {
+ const updatedBudgets = [...formik.values.project_budgets];
+ const currentBudget = updatedBudgets[index];
+
+ const isNewValueEmpty = value === '';
+
+ let numericValue: number;
+
+ if (isNewValueEmpty) {
+ (currentBudget[fieldName] as string) = '';
+ numericValue = 0;
+
+ formik.setFieldValue('project_budgets', updatedBudgets);
+ return;
+ } else {
+ numericValue = Math.max(0, parseFloat(value) || 0);
+
+ (currentBudget[fieldName] as number) = numericValue;
+ }
+
+ const getSafeNumber = (val: string | number) =>
+ Math.max(0, parseFloat(String(val)) || 0);
+
+ const currentQty = getSafeNumber(currentBudget.qty);
+ const currentPrice = getSafeNumber(currentBudget.price);
+ const currentTotal = getSafeNumber(currentBudget.total_price);
+
+ let newQty = currentQty;
+ let newPrice = currentPrice;
+ let newTotal = currentTotal;
+
+ if (fieldName === 'price') {
+ // Jika Harga Satuan diubah, hitung Total Harga
+ newTotal = newQty * numericValue;
+ newPrice = numericValue;
+ } else if (fieldName === 'qty') {
+ // Jika Kuantitas diubah, hitung Total Harga
+ newTotal = numericValue * newPrice;
+ newQty = numericValue;
+ } else if (fieldName === 'total_price') {
+ // Jika Total Harga diubah, hitung Harga Satuan
+ newTotal = numericValue;
+ if (newQty > 0) {
+ newPrice = newTotal / newQty;
+ } else {
+ // Jika Qty 0, Harga Satuan tetap 0
+ newPrice = 0;
+ }
+ }
+
+ currentBudget.qty = newQty;
+ currentBudget.price = newPrice;
+ currentBudget.total_price = newTotal;
+
+ formik.setFieldValue('project_budgets', updatedBudgets);
+ };
+
const selectedPeriod = isResponseSuccess(periodFlocks)
? periodFlocks.data.find((kandang) =>
formik.values.kandang_ids?.includes(kandang.id)
@@ -557,25 +665,50 @@ const ProjectFlockForm = ({
const inputPeriod =
(initialValues?.period ?? selectedPeriod == 0) ? 1 : selectedPeriod;
+ const filteredNonStockOptions = optionsNonstock.filter((nonstock) => {
+ const isNonstockAlreadyInBudgets = (
+ formik.values.project_budgets ?? []
+ ).some((budget) => budget.nonstock_id === nonstock.value);
+
+ return !isNonstockAlreadyInBudgets;
+ });
+
return (
<>
-
-
-
-
- {formType === 'add' && 'Tambah Project Flock'}
- {formType === 'edit' && 'Edit Project Flock'}
- {formType === 'detail' && 'Detail Project Flock'}
-
-
+ {/* Header */}
+
+ {formType == 'edit' && (
+
+ )}
+
{projectFlockFormErrorMessage && (
@@ -631,21 +764,6 @@ const ProjectFlockForm = ({
Reject
- {initialValues?.approval?.step_number == 2 && (
-
- )}
)}