From 73319bbdfaa6136bc6cf92ff653477a721d1c7ff Mon Sep 17 00:00:00 2001 From: randy-ar Date: Fri, 30 Jan 2026 15:53:51 +0700 Subject: [PATCH 01/21] fix(FE): fixing pixel perfect marketing table --- src/app/marketing/page.tsx | 2 +- .../pages/marketing/MarketingFilter.tsx | 186 ++++ .../pages/marketing/MarketingTable.tsx | 874 +++++++++--------- src/services/api/marketing/marketing.ts | 82 +- src/types/api/marketing/marketing.d.ts | 9 + 5 files changed, 694 insertions(+), 459 deletions(-) create mode 100644 src/components/pages/marketing/MarketingFilter.tsx diff --git a/src/app/marketing/page.tsx b/src/app/marketing/page.tsx index c30ee501..5bdcd48f 100644 --- a/src/app/marketing/page.tsx +++ b/src/app/marketing/page.tsx @@ -2,7 +2,7 @@ import MarketingTable from '@/components/pages/marketing/MarketingTable'; const Marketing = () => { return ( -
+
); diff --git a/src/components/pages/marketing/MarketingFilter.tsx b/src/components/pages/marketing/MarketingFilter.tsx new file mode 100644 index 00000000..3c59e07e --- /dev/null +++ b/src/components/pages/marketing/MarketingFilter.tsx @@ -0,0 +1,186 @@ +'use client'; + +import { RefObject } from 'react'; +import { useFormik } from 'formik'; +import { Icon } from '@iconify/react'; +import Modal from '@/components/Modal'; +import Button from '@/components/Button'; +import SelectInput, { + OptionType, + useSelect, +} from '@/components/input/SelectInput'; +import { CustomerApi, ProductApi } from '@/services/api/master-data'; +import { MARKETING_APPROVAL_LINE } from '@/config/approval-line'; +import { MarketingFilter } from '@/types/api/marketing/marketing'; +import SelectInputCheckbox from '@/components/input/SelectInputCheckbox'; + +interface MarketingFilterModal { + ref: RefObject; + onSubmit?: (values: MarketingFilter) => void; + onReset?: () => void; +} + +const MarketingFilterModal = ({ + ref, + onSubmit, + onReset, +}: MarketingFilterModal) => { + const closeModalHandler = () => { + ref.current?.close(); + }; + + // ===== OPTIONS ===== + const { + options: productsOptions, + isLoadingOptions: isLoadingProductsOptions, + setInputValue: setProductsInputValue, + loadMore: loadMoreProducts, + } = useSelect(ProductApi.basePath, 'id', 'name', '', { + limit: 'limit', + }); + const { + options: customersOptions, + isLoadingOptions: isLoadingCustomersOptions, + setInputValue: setCustomersInputValue, + loadMore: loadMoreCustomers, + } = useSelect(CustomerApi.basePath, 'id', 'name', '', { + limit: 'limit', + }); + const statusOptions = MARKETING_APPROVAL_LINE.map((item) => ({ + value: item.step_name.split(' ').join('_').toUpperCase(), + label: item.step_name, + })); + + const formik = useFormik<{ + product_ids: OptionType[]; + status: OptionType | null; + customer_id: OptionType | null; + }>({ + initialValues: { + product_ids: [], + status: null, + customer_id: null, + }, + + onSubmit: async (values) => { + const formattedValues = { + ...values, + product_ids: values.product_ids.map((item) => Number(item.value)), + status: values.status?.value.toString() || '', + customer_id: Number(values.customer_id?.value), + }; + + onSubmit?.(formattedValues); + closeModalHandler(); + }, + + onReset: () => { + onReset?.(); + closeModalHandler(); + }, + }); + + const productChangeHandler = (val: OptionType | OptionType[] | null) => { + formik.setFieldValue('product_ids', val as OptionType[]); + }; + + const customerChangeHandler = (val: OptionType | OptionType[] | null) => { + formik.setFieldValue('customer_id', val as OptionType); + }; + + const statusChangeHandler = (val: OptionType | OptionType[] | null) => { + formik.setFieldValue('status', val as OptionType); + }; + + return ( + +
+ {/* Modal Header */} +
+
+ +

Filter Data

+
+ + +
+ + {/* Modal Body */} +
+ {/* select multiple product */} + + {/* select status */} + + {/* select customer */} + +
+ + {/* Modal Footer */} +
+ + + +
+
+
+ ); +}; + +export default MarketingFilterModal; diff --git a/src/components/pages/marketing/MarketingTable.tsx b/src/components/pages/marketing/MarketingTable.tsx index 8f1a6cf9..3632ae05 100644 --- a/src/components/pages/marketing/MarketingTable.tsx +++ b/src/components/pages/marketing/MarketingTable.tsx @@ -22,11 +22,15 @@ import { SalesOrderApi, } from '@/services/api/marketing/marketing'; import { useTableFilter } from '@/services/hooks/useTableFilter'; -import { BaseSalesOrder, Marketing } from '@/types/api/marketing/marketing'; +import { + BaseSalesOrder, + Marketing, + MarketingFilter, +} from '@/types/api/marketing/marketing'; import { Icon } from '@iconify/react'; -import { CellContext, Row } from '@tanstack/react-table'; +import { CellContext, ColumnDef, Row } from '@tanstack/react-table'; import { useRouter } from 'next/navigation'; -import { useCallback, useState } from 'react'; +import { useCallback, useMemo, useState } from 'react'; import toast from 'react-hot-toast'; import useSWR from 'swr'; import RequirePermission from '@/components/helper/RequirePermission'; @@ -34,100 +38,125 @@ import { useAuth } from '@/services/hooks/useAuth'; import { CustomerApi, ProductApi } from '@/services/api/master-data'; import { MARKETING_APPROVAL_LINE } from '@/config/approval-line'; import Badge from '@/components/Badge'; +import ButtonFilter from '@/components/helper/ButtonFilter'; +import Menu from '@/components/menu/Menu'; +import MenuItem from '@/components/menu/MenuItem'; +import Dropdown from '@/components/Dropdown'; +import PopoverButton from '@/components/popover/PopoverButton'; +import PopoverContent from '@/components/popover/PopoverContent'; +import StatusBadge from '@/components/helper/StatusBadge'; +import MarketingFilterModal from '@/components/pages/marketing/MarketingFilter'; const RowsOptionsMenu = ({ - type = 'dropdown', props, deleteClickHandler, deliveryClickHandler, + popoverPosition, }: { type: 'dropdown' | 'collapse'; props: CellContext; deleteClickHandler: () => void; deliveryClickHandler?: () => void; + popoverPosition?: 'top' | 'bottom'; }) => { + const showEditButton = + props.row.original.latest_approval.action !== 'APPROVED' && + props.row.original.latest_approval.action !== 'REJECTED'; + + const showDeleteButton = showEditButton; + + const popoverId = `marketing#${props.row.original.id}`; + const popoverAnchorName = `--anchor-marketing#${props.row.original.id}`; + return ( -
-
- - - - {props.row.original.latest_approval.step_number != 1 && ( - <> - + + + + +
+ + + + {props.row.original.latest_approval.step_number != 1 && ( + <> + { - if (props.row.original.latest_approval.step_number == 2) { - deliveryClickHandler?.(); + > + - - - )} - {props.row.original.latest_approval.step_number != 3 && ( - <> - - - - - )} - - - -
+ onClick={() => { + if (props.row.original.latest_approval.step_number == 2) { + deliveryClickHandler?.(); + } + }} + variant='ghost' + color='none' + className='p-3 justify-start text-sm font-semibold w-full' + > + + Deliver Item + +
+ + )} + {props.row.original.latest_approval.step_number != 3 && ( + <> + + + + + )} + + + +
+
); }; @@ -147,6 +176,8 @@ const MarketingTable = () => { const confirmationModal = useModal(); const productsModal = useModal(); const deliveryModal = useModal(); + const filterModal = useModal(); + const { state: tableFilterState, updateFilter, @@ -159,8 +190,6 @@ const MarketingTable = () => { product_ids: '', status: '', customer_id: '', - page: 1, - limit: 10, }, paramMap: { page: 'page', @@ -181,46 +210,27 @@ const MarketingTable = () => { MarketingApi.getAllFetcher ); - // ===== OPTIONS ===== - const { - options: productsOptions, - isLoadingOptions: isLoadingProductsOptions, - setInputValue: setProductsInputValue, - loadMore: loadMoreProducts, - } = useSelect(ProductApi.basePath, 'id', 'name', '', { - limit: 'limit', - }); - const { - options: customersOptions, - isLoadingOptions: isLoadingCustomersOptions, - setInputValue: setCustomersInputValue, - loadMore: loadMoreCustomers, - } = useSelect(CustomerApi.basePath, 'id', 'name', '', { - limit: 'limit', - }); - const statusOptions = MARKETING_APPROVAL_LINE.map((item) => ({ - value: item.step_number, - label: item.step_name, - })); - // ===== HANDLER ===== - const searchChangeHandler = useCallback( - (e: React.ChangeEvent) => { - setSearch(e.target.value); - updateFilter('page', 1); - updateFilter('search', e.target.value); - }, - [] - ); - const pageSizeChangeHandler = useCallback( - (val: OptionType | OptionType[] | null) => { - const newVal = val as OptionType; - setPageSize(newVal.value as number); - updateFilter('page', 1); - updateFilter('limit', newVal.value as number); - }, - [] - ); + const filterSubmitHandler = (values: MarketingFilter) => { + updateFilter( + 'product_ids', + values.product_ids?.map((item) => item.toString()).join(',') + ); + updateFilter('status', values.status ? values.status.toString() : ''); + updateFilter( + 'customer_id', + values.customer_id ? values.customer_id.toString() : '' + ); + }; + + const [isLoadingExportingToExcel, setIsLoadingExportingToExcel] = + useState(false); + + const filterResetHandler = () => { + updateFilter('product_ids', ''); + updateFilter('status', ''); + updateFilter('customer_id', ''); + }; const approveClickHandler = () => { setApproveAction('APPROVED'); @@ -327,354 +337,305 @@ const MarketingTable = () => { return approval?.step_number === 1 && approval?.action !== 'REJECTED'; }; + const exportToExcelHandler = async () => { + setIsLoadingExportingToExcel(true); + + await MarketingApi.exportToExcel(getTableFilterToQueryString()); + + setIsLoadingExportingToExcel(false); + }; + + const columns = useMemo[]>(() => { + return [ + { + id: 'select', + size: 1, + header: ({ table }) => { + const allRows = table.getRowModel().rows; + const selectableRows = allRows.filter(getRowCanSelect); + + const allSelected = + selectableRows.length > 0 && + selectableRows.every((row) => row.getIsSelected()); + + const someSelected = + selectableRows.some((row) => row.getIsSelected()) && !allSelected; + + const toggleSelectableRows = () => { + const shouldSelect = !allSelected; + selectableRows.forEach((row) => row.toggleSelected(shouldSelect)); + }; + + return ( +
+ +
+ ); + }, + cell: ({ row }) => { + const canSelect = getRowCanSelect(row); + return ( +
+ +
+ ); + }, + }, + { + accessorKey: 'so_number', + header: 'No. Order', + }, + { + accessorKey: 'so_date', + header: 'Tanggal', + cell: (props) => { + return formatDate(props.row.original.so_date, 'DD MMM yyyy'); + }, + }, + { + accessorKey: 'approval.step_name', + header: 'Status', + cell: (props) => { + const approval = props.row.original.latest_approval; + const isRejected = approval?.action == 'REJECTED'; + const isApproved = approval?.action == 'APPROVED'; + return ( + + ); + }, + }, + { + accessorKey: 'customer.name', + header: 'Customer', + }, + { + accessorFn: (row) => + row.sales_order + ?.map((product) => product.total_price) + .reduce((a, b) => a + b, 0) ?? 0, + header: 'Grand Total', + cell: (props) => { + return formatCurrency( + props.row.original?.sales_order + ?.map((product) => product.total_price) + .reduce((a, b) => a + b, 0) ?? 0 + ); + }, + }, + { + accessorKey: 'marketing_products.length', + header: 'Product Details', + cell: (props) => { + if (props?.row?.original?.sales_order?.length) { + if (props?.row?.original?.sales_order?.length > 1) { + return ( + + ); + } else { + const product = props?.row?.original?.sales_order[0]; + return <>{product?.product_warehouse?.product?.name}; + } + } + }, + }, + { + id: 'actions', + maxSize: 80, + 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 deleteClickHandler = () => { + setSelectedItem(props.row.original); + deleteModal.openModal(); + }; + + const deliveryClickHandler = () => { + setSelectedItem(props.row.original); + deliveryModal.openModal(); + }; + + return ( + + ); + }, + }, + ]; + }, []); + return ( <> -
-
- -
- - - - - - + {idsToProcess.length > 0 && ( + <> +
+ + + + + + + + )}
- - {/* select multiple product */} - - productsOptions.find( - (option) => option.value === Number(id) - ) - ) - .filter( - (option): option is { value: number; label: string } => - option !== undefined - ) ?? null - } - onChange={(value: OptionType | OptionType[] | null) => - updateFilter( - 'product_ids', - (value as OptionType[]) - ?.map((item: OptionType) => item.value.toString()) - .join(',') || '' - ) - } - onInputChange={setProductsInputValue} - onMenuScrollToBottom={loadMoreProducts} - isMulti +
+ { + const { page, pageSize, ...rest } = tableFilterState; + return rest; + })()} + onClick={() => { + filterModal.openModal(); + }} + className='rounded-lg px-3 py-2.5' /> - {/* select status */} - - option.value === Number(tableFilterState.status) - ) - : null + + + Export +
+ +
+ } - onChange={(value: OptionType | OptionType[] | null) => - updateFilter( - 'status', - (value as OptionType)?.value.toString() || '' - ) - } - /> - {/* select customer */} - - option.value === Number(tableFilterState.customer_id) - ) - : null - } - onChange={(value: OptionType | OptionType[] | null) => - updateFilter( - 'customer_id', - (value as OptionType)?.value.toString() || '' - ) - } - onInputChange={setCustomersInputValue} - onMenuScrollToBottom={loadMoreCustomers} - /> - + className={{ + content: + 'mt-1 rounded-xl border border-base-content/5 shadow-sm overflow-hidden', + }} + > + +
+
{ - const allRows = table.getRowModel().rows; - const selectableRows = allRows.filter(getRowCanSelect); - - const allSelected = - selectableRows.length > 0 && - selectableRows.every((row) => row.getIsSelected()); - - const someSelected = - selectableRows.some((row) => row.getIsSelected()) && - !allSelected; - - const toggleSelectableRows = () => { - const shouldSelect = !allSelected; - selectableRows.forEach((row) => - row.toggleSelected(shouldSelect) - ); - }; - - return ( -
- -
- ); - }, - cell: ({ row }) => { - const canSelect = getRowCanSelect(row); - return ( -
- -
- ); - }, - }, - { - accessorKey: 'so_number', - header: 'No. Order', - }, - { - accessorKey: 'so_date', - header: 'Tanggal', - cell: (props) => { - return formatDate(props.row.original.so_date, 'DD MMM yyyy'); - }, - }, - { - accessorKey: 'approval.step_name', - header: 'Status', - cell: (props) => { - const approval = props.row.original.latest_approval; - const isRejected = approval?.action == 'REJECTED'; - const isApproved = approval?.action == 'APPROVED'; - return ( - - - {isRejected - ? 'Ditolak' - : formatTitleCase(approval?.step_name || '')} - - ); - }, - }, - { - accessorKey: 'customer.name', - header: 'Customer', - }, - { - accessorFn: (row) => - row.sales_order - ?.map((product) => product.total_price) - .reduce((a, b) => a + b, 0) ?? 0, - header: 'Grand Total', - cell: (props) => { - return formatCurrency( - props.row.original?.sales_order - ?.map((product) => product.total_price) - .reduce((a, b) => a + b, 0) ?? 0 - ); - }, - }, - { - accessorKey: 'marketing_products.length', - header: 'Product Details', - cell: (props) => { - if (props?.row?.original?.sales_order?.length) { - if (props?.row?.original?.sales_order?.length > 1) { - return ( - - ); - } else { - const product = props?.row?.original?.sales_order[0]; - return <>{product?.product_warehouse?.product?.name}; - } - } - }, - }, - { - 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 deleteClickHandler = () => { - setSelectedItem(props.row.original); - deleteModal.openModal(); - }; - - const deliveryClickHandler = () => { - setSelectedItem(props.row.original); - deliveryModal.openModal(); - }; - - return ( - <> - {currentPageSize > 2 && ( - - - - )} - - {currentPageSize <= 2 && ( - - - - )} - - ); - }, - }, - ]} + columns={columns} pageSize={tableFilterState.pageSize} page={tableFilterState.page} - onPageChange={setPage} + isLoading={isLoadingMarketing} 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', + containerClassName: cn('p-3', { + 'w-full mb-20': + isResponseSuccess(marketing) && marketing?.data?.length === 0, + }), bodyColumnClassName: - 'px-6 py-3 last:flex last:flex-row last:justify-end', + 'last:text-end last:w-17 first:text-start first:w-5', }} /> @@ -792,6 +753,11 @@ const MarketingTable = () => { isLoading={isLoadingMarketing} /> + ); }; diff --git a/src/services/api/marketing/marketing.ts b/src/services/api/marketing/marketing.ts index 05afaa30..76f9b2ea 100644 --- a/src/services/api/marketing/marketing.ts +++ b/src/services/api/marketing/marketing.ts @@ -1,5 +1,6 @@ +import { isResponseError } from '@/lib/api-helper'; import { BaseApiService } from '@/services/api/base'; -import { httpClient } from '@/services/http/client'; +import { httpClient, httpClientFetcher } from '@/services/http/client'; import { BaseApiResponse } from '@/types/api/api-general'; import { Marketing, @@ -8,6 +9,9 @@ import { CreateDeliveryOrderPayload, UpdateDeliveryOrderPayload, } from '@/types/api/marketing/marketing'; +import toast from 'react-hot-toast'; +import * as XLSX from 'xlsx'; +import { formatCurrency, formatDate, formatTitleCase } from '@/lib/helper'; /** * 💡 Helper untuk membuat respons dummy @@ -97,6 +101,78 @@ export class SalesOrderService extends BaseApiService< } } } +class MarketingExportService extends BaseApiService< + Marketing, + unknown, + unknown +> { + constructor(basePath: string = '/marketing') { + super(basePath); + } + + /** + * Export to Excel + */ + async exportToExcel(initialQueryString: string) { + const params = new URLSearchParams(initialQueryString); + const queryString = `?${params.toString()}`; + + try { + const marketingData = await httpClientFetcher< + BaseApiResponse + >(`${this.basePath}${queryString}`); + + if (isResponseError(marketingData)) { + toast.error('Gagal melakukan export marketing! Coba lagi.'); + return; + } + + const rows = marketingData.data; + + const formattedRows = []; + + for (let i = 0; i < rows.length; i++) { + const row = rows[i]; + const approval = row.latest_approval; + const isRejected = approval?.action === 'REJECTED'; + + // Calculate grand total from sales_order products + const grandTotal = + row.sales_order + ?.map((product) => product.total_price) + .reduce((a, b) => a + b, 0) ?? 0; + + // Get product names + const products = + row.sales_order + ?.map((product) => product.product_warehouse?.product?.name) + .filter(Boolean) + .join(', ') ?? ''; + + formattedRows.push({ + 'No. Order': row.so_number, + Tanggal: formatDate(row.so_date, 'DD-MM-YYYY'), + Status: isRejected + ? 'Ditolak' + : formatTitleCase(approval?.step_name || ''), + Customer: row.customer?.name || '', + 'Grand Total': formatCurrency(grandTotal), + Products: products, + Notes: row.notes || '', + }); + } + + const ws = XLSX.utils.json_to_sheet(formattedRows); + const wb = XLSX.utils.book_new(); + XLSX.utils.book_append_sheet(wb, ws, 'marketing'); + + // triggers download in browser + XLSX.writeFile(wb, 'marketing.xlsx'); + } catch (error) { + toast.error('Gagal melakukan export marketing! Coba lagi.'); + } + } +} export const SalesOrderApi = new SalesOrderService('/marketing/sales-orders'); export const DeliveryOrderApi = new BaseApiService< @@ -104,6 +180,4 @@ export const DeliveryOrderApi = new BaseApiService< CreateDeliveryOrderPayload, UpdateDeliveryOrderPayload >('/marketing/delivery-orders'); -export const MarketingApi = new BaseApiService( - '/marketing' -); +export const MarketingApi = new MarketingExportService('/marketing'); diff --git a/src/types/api/marketing/marketing.d.ts b/src/types/api/marketing/marketing.d.ts index 931184f9..7d0e390c 100644 --- a/src/types/api/marketing/marketing.d.ts +++ b/src/types/api/marketing/marketing.d.ts @@ -82,6 +82,15 @@ export type MarketingDeliveryProducts = { export type Marketing = BaseMetadata & BaseMarketing; +/** + * Filter Data Types + */ +export type MarketingFilter = { + product_ids: number[]; + status: string; + customer_id: number; +}; + /** * Base Data Payload */ From 70bb40d4f2ff073cfbd8e8cdccc8cf3d8f997375 Mon Sep 17 00:00:00 2001 From: randy-ar Date: Sat, 31 Jan 2026 13:15:47 +0700 Subject: [PATCH 02/21] fix(FE): refactor sales order form create --- src/app/marketing/page.tsx | 3 + src/components/helper/form/FormErrors.tsx | 51 +- src/components/input/DebouncedTextArea.tsx | 7 + src/components/input/TextArea.tsx | 3 + .../pages/marketing/MarketingTable.tsx | 10 +- .../pages/marketing/SalesOrderFormModal.tsx | 744 ++++++++++++++++++ .../pages/marketing/form/MarketingForm.tsx | 4 +- .../sales-order/SalesOrderProductForm.tsx | 321 ++++---- .../table-view/SalesOrderProductTable.tsx | 149 ++-- src/services/hooks/useFormikErrorList.ts | 6 + 10 files changed, 1060 insertions(+), 238 deletions(-) create mode 100644 src/components/pages/marketing/SalesOrderFormModal.tsx diff --git a/src/app/marketing/page.tsx b/src/app/marketing/page.tsx index 5bdcd48f..70eb4549 100644 --- a/src/app/marketing/page.tsx +++ b/src/app/marketing/page.tsx @@ -1,9 +1,12 @@ import MarketingTable from '@/components/pages/marketing/MarketingTable'; +import SalesOrderFormModal from '@/components/pages/marketing/SalesOrderFormModal'; const Marketing = () => { return (
+ +
); }; diff --git a/src/components/helper/form/FormErrors.tsx b/src/components/helper/form/FormErrors.tsx index 1fd7b58f..d4959e8b 100644 --- a/src/components/helper/form/FormErrors.tsx +++ b/src/components/helper/form/FormErrors.tsx @@ -1,5 +1,6 @@ import Alert from '@/components/Alert'; import Button from '@/components/Button'; +import { cn } from '@/lib/helper'; import { Icon } from '@iconify/react'; import { useState } from 'react'; @@ -10,34 +11,66 @@ import { useState } from 'react'; */ const AlertErrorList = ({ formErrorList, + className, onClose, }: { formErrorList: string[]; + className?: { + alert?: string; + button?: string; + headerWrapper?: string; + headerIcon?: string; + headerText?: string; + titleWrapper?: string; + ul?: string; + li?: string; + }; onClose: () => void; }) => { if (formErrorList.length === 0) return null; return ( - -
-
- - + +
+
+ + Terdapat {formErrorList.length} error pada form:
-
    +
      {formErrorList.map((error, index) => ( -
    • +
    • {error}
    • ))} diff --git a/src/components/input/DebouncedTextArea.tsx b/src/components/input/DebouncedTextArea.tsx index 3df2c032..24d4b4e7 100644 --- a/src/components/input/DebouncedTextArea.tsx +++ b/src/components/input/DebouncedTextArea.tsx @@ -7,6 +7,7 @@ import TextArea, { TextAreaProps } from '@/components/input/TextArea'; interface DebouncedTextAreaProps extends TextAreaProps { delay?: number; + ref?: React.RefObject; } const DebouncedTextArea = (props: DebouncedTextAreaProps) => { @@ -19,6 +20,11 @@ const DebouncedTextArea = (props: DebouncedTextAreaProps) => { const [debouncedChangeEvent] = useDebounce(internalChangeEvent, delay ?? 300); const [debouncedValue] = useDebounce(internalValue, delay ?? 300); + // Sync internal value with external props.value when it changes (e.g., form reset) + useEffect(() => { + setInternalValue(props.value); + }, [props.value]); + const internalChangeHandler: ChangeEventHandler = ( e ) => { @@ -35,6 +41,7 @@ const DebouncedTextArea = (props: DebouncedTextAreaProps) => { return (