import React, { useState, useEffect, useRef, useCallback, memo } from 'react'; import { motion, AnimatePresence } from 'framer-motion'; import { cn } from '../utils/cn'; import { DialogMode, DialogType, BaseTableData } from '../types'; import { useFormHandling } from '../contexts/FormHandlingContext'; // Separate component for dialog content to prevent unnecessary re-renders const DialogContent = memo( ({ children, error, description, isLoading }: { children: React.ReactNode; error?: string | Error | null; description?: string; isLoading: boolean; }) => { const errorMessage = error ? (typeof error === 'string' ? error : error.message || 'An error occurred') : null; return (
{description &&

{description}

} {errorMessage && (
{errorMessage}
)}
{isLoading && (
Loading...
)} {children}
); }, (prevProps, nextProps) => { return ( prevProps.isLoading === nextProps.isLoading && prevProps.error === nextProps.error && prevProps.description === nextProps.description ); } ); DialogContent.displayName = 'DialogContent'; // Separate component for dialog footer to prevent unnecessary re-renders const DialogFooter = memo( ({ dialogType, onCancel, onSubmit, isSubmitting, buttonSize = 'md', actionsPosition = 'right', elevatedButtons = true, isReadOnly = false, isFormValid = true, disabled = false }: { dialogType: DialogType | null; onCancel: () => void; onSubmit: () => void; isSubmitting: boolean; buttonSize?: string; actionsPosition?: string; elevatedButtons?: boolean; isReadOnly?: boolean; isFormValid?: boolean; disabled?: boolean; }) => { // Determine submit button label based on dialog type const submitLabel = { create: 'Tạo mới', edit: 'Lưu thay đổi', delete: 'Xóa', view: '', custom: 'Xác nhận' }[dialogType ?? 'custom'] || 'Xác nhận'; // Determine loading label based on dialog type const loadingLabel = { create: 'Đang tạo...', edit: 'Đang lưu...', delete: 'Đang xóa...', view: '', custom: 'Đang xử lý...' }[dialogType ?? "custom"] || 'Đang xử lý...'; const isSubmitButtonDisabled = isSubmitting || disabled || (dialogType !== 'delete' && !isFormValid); return (
{dialogType !== 'view' && !isReadOnly && ( )}
); } ); DialogFooter.displayName = 'DialogFooter'; // Types for the form component props export interface OptimizedFormComponentProps { data?: any; onSubmit?: (data: any, type: DialogType | null) => Promise | boolean; onClose?: () => void; dialogType: DialogType | null; loading?: boolean; error?: string | Error | null; isReadOnly?: boolean; ref?: React.Ref; onFormDirty?: (isDirty: boolean) => void; skipInitialValidation?: boolean; dialogRef?: React.RefObject; } // Types for the dialog props export interface OptimizedDialogProps { open?: boolean; dialogType: DialogType | null; dialogTitle?: string; dialogDescription?: string; data?: T | null; formComponent?: React.ComponentType; onClose?: () => void; onSubmit?: (data: any, type: DialogType | null) => Promise | boolean; loading?: boolean; error?: string | Error | null; width?: string | number; closeOnClickOutside?: boolean; closeOnEsc?: boolean; actionsPosition?: 'left' | 'right' | 'center' | 'between'; buttonSize?: 'xs' | 'sm' | 'md' | 'lg' | 'xl'; elevatedButtons?: boolean; reducedMotion?: boolean; disableSubmitUntilDirty?: boolean; validateOnMount?: boolean; skipInitialValidation?: boolean; } // Simple dialog implementation without conditional hooks function SimpleDialogImplementation({ open = false, dialogType = 'custom', dialogTitle, dialogDescription, data, formComponent: FormComponent, onClose, onSubmit, loading = false, error, width = '500px', closeOnClickOutside = true, closeOnEsc = true, actionsPosition = 'right', buttonSize = 'md', elevatedButtons = true, reducedMotion = false, validateOnMount = false, skipInitialValidation = true }: OptimizedDialogProps) { // ALL HOOKS ARE CALLED UNCONDITIONALLY AT THE TOP LEVEL const [isSubmitting, setIsSubmitting] = useState(false); const [isFormDirty, setIsFormDirty] = useState(false); const [isFormValid, setIsFormValid] = useState(false); const [hasInteracted, setHasInteracted] = useState(false); const [localError, setLocalError] = useState(null); const [hasAttemptedSubmit, setHasAttemptedSubmit] = useState(false); const formRef = useRef(null); const dialogRef = useRef(null); const dataRef = useRef(data); const dialogTypeRef = useRef(dialogType); const onSubmitRef = useRef(onSubmit); const [formTouched, setFormTouched] = useState(false); // ALWAYS call useFormHandling - never conditionally const formContext = useFormHandling(); // ALL useEffect hooks are called unconditionally useEffect(() => { dataRef.current = data; dialogTypeRef.current = dialogType; }, [data, dialogType]); useEffect(() => { onSubmitRef.current = onSubmit; }, [onSubmit]); // Reset states effect useEffect(() => { if (!open) { setIsSubmitting(false); setIsFormDirty(false); setIsFormValid(false); setFormTouched(false); setLocalError(null); setHasAttemptedSubmit(false); setHasInteracted(false); return; } if (dialogType === 'delete') { setIsFormDirty(true); setIsFormValid(true); } else { setIsFormDirty(false); setIsFormValid(!validateOnMount); setFormTouched(false); setLocalError(null); setHasAttemptedSubmit(false); setHasInteracted(false); } }, [open, dialogType, validateOnMount]); // Escape key effect useEffect(() => { const handleEscapeKey = (e: KeyboardEvent) => { if (open && closeOnEsc && e.key === 'Escape' && !isSubmitting && !loading) { onClose?.(); } }; document.addEventListener('keydown', handleEscapeKey); return () => document.removeEventListener('keydown', handleEscapeKey); }, [closeOnEsc, open, isSubmitting, loading, onClose]); // Form validity tracking effect useEffect(() => { if (dialogType === 'delete') { setIsFormValid(true); return; } if (dialogType === 'create' && !validateOnMount) { setIsFormValid(true); return; } if (formContext && dialogType && open) { try { const initialValidity = formContext.isFormValid(dialogType as DialogMode); setIsFormValid(initialValidity); } catch (error) { console.error('Error checking form validity:', error); setIsFormValid(true); } } }, [dialogType, formContext, open, validateOnMount]); // Form change detection effect useEffect(() => { if (!open || !dialogRef.current) return; if (formContext || (formRef.current && (typeof formRef.current.isDirty === 'function'))) { return; } const formElements = dialogRef.current.querySelectorAll('input, select, textarea'); const handleFormChange = () => { setFormTouched(true); setIsFormDirty(true); if (!hasAttemptedSubmit) { setLocalError(null); } }; formElements.forEach(element => { element.addEventListener('input', handleFormChange); element.addEventListener('change', handleFormChange); }); return () => { formElements.forEach(element => { element.removeEventListener('input', handleFormChange); element.removeEventListener('change', handleFormChange); }); }; }, [open, dialogType, formContext, hasAttemptedSubmit]); // ALL useCallback hooks are called unconditionally const handleBackdropClick = useCallback((e: React.MouseEvent) => { if (e.target === e.currentTarget && closeOnClickOutside && !isSubmitting && !loading) { onClose?.(); } }, [closeOnClickOutside, isSubmitting, loading, onClose]); const handleFormDirty = useCallback((isDirty: boolean) => { setIsFormDirty(isDirty); if (isDirty) { setHasInteracted(true); setFormTouched(true); } if (formContext && dialogTypeRef.current) { formContext.setFormDirty(isDirty, dialogTypeRef.current as DialogMode); } }, [formContext]); const handleSubmit = useCallback(async () => { const currentDialogType = dialogTypeRef.current; const currentData = dataRef.current; if (!currentDialogType || !onSubmitRef.current || isSubmitting || loading) { return; } setHasAttemptedSubmit(true); setHasInteracted(true); setIsSubmitting(true); setLocalError(null); try { if (currentDialogType === 'delete') { const result = onSubmitRef.current(currentData || {}, currentDialogType); const finalResult = result instanceof Promise ? await result : result; setIsSubmitting(false); // Only close dialog if submission was successful if (finalResult === true || finalResult === undefined) { onClose?.(); } return; } if (formContext && currentDialogType) { try { const { isValid, data: formData, errors } = await formContext.validateAndGetFormData(currentDialogType as DialogMode); if (!isValid || !formData) { const errorMessage = errors && typeof errors === 'object' && '_form' in errors ? String(errors._form) : 'Form validation failed'; setLocalError(errorMessage); setIsSubmitting(false); // CRITICAL FIX: Don't reset form on validation failure return; } if (formData && Object.keys(formData).length === 0 && (currentDialogType as DialogMode) !== 'delete') { setLocalError('Không có dữ liệu được nhập. Vui lòng điền vào form.'); setIsSubmitting(false); // CRITICAL FIX: Don't reset form when no data return; } const finalData = currentDialogType === 'edit' && currentData ? { ...currentData, ...formData } : formData; if (currentDialogType === 'edit' && currentData?.id && !finalData.id) { finalData.id = currentData.id; } const result = onSubmitRef.current(finalData, currentDialogType); const finalResult = result instanceof Promise ? await result : result; setIsSubmitting(false); // Only close dialog and potentially reset form if submission was successful if (finalResult === true || finalResult === undefined) { // Success: Close dialog (form will be reset when dialog closes) onClose?.(); } else if (finalResult === false) { // Failure: Keep dialog open, preserve form data, show error console.log('Submission failed, keeping dialog open for user feedback'); setLocalError('Submission failed. Please check your input and try again.'); } } catch (err) { setLocalError(err instanceof Error ? err.message : "An unexpected error occurred"); setIsSubmitting(false); // CRITICAL FIX: Don't close dialog or reset form on error console.error('Form submission error, preserving user data:', err); } return; } // Fallback: use original data const result = onSubmitRef.current(currentData || {}, currentDialogType); const finalResult = result instanceof Promise ? await result : result; setIsSubmitting(false); // Only close dialog if submission was successful if (finalResult === true || finalResult === undefined) { onClose?.(); } else if (finalResult === false) { // Keep dialog open if submission failed console.log('Fallback submission failed, keeping dialog open for user feedback'); setLocalError('Submission failed. Please check your input and try again.'); } } catch (err) { setLocalError(err instanceof Error ? err.message : "Đã xảy ra lỗi khi gửi biểu mẫu"); setIsSubmitting(false); // CRITICAL FIX: Don't close dialog on error - preserve user data console.error('Unexpected error during submission, preserving user data:', err); } }, [isSubmitting, loading, onClose, formContext]); // Don't render if not open if (!open) { return null; } // Determine dialog title based on type const title = dialogTitle || { create: 'Thêm mới', edit: 'Chỉnh sửa', view: 'Xem chi tiết', delete: 'Xác nhận xóa', custom: 'Thao tác' }[dialogType ?? 'custom'] || 'Dialog'; // Animation settings based on reduced motion preference const animationSettings = reducedMotion ? { initial: {}, animate: {}, exit: {}, transition: { duration: 0.1 } } : { initial: { opacity: 0, y: 20, scale: 0.95 }, animate: { opacity: 1, y: 0, scale: 1 }, exit: { opacity: 0, y: 20, scale: 0.95 }, transition: { duration: 0.2 } }; const isReadOnly = dialogType === 'view'; const displayError = localError || error; const isDelete = dialogType === 'delete'; return (
e.stopPropagation()} {...animationSettings} >

{title}

{dialogDescription && (

{dialogDescription}

)}
{FormComponent && ( )}
); } // Safe wrapper that handles AnimatePresence export function OptimizedDialog(props: OptimizedDialogProps) { return ( {props.open && } ); } export default OptimizedDialog;