const CheckBoxCtl = ({ label, checked, onChange, disabled = false, id, name, style, tabIndex='-1', type='checkbox' // checkbox | switch }) => { const { Form } =ReactBootstrap return ( ); }; const TextInputCtl = ({ label, placeholder, value, onChange, onEnter, onBlur, autoFocus = false, type = 'text', required = false, disabled = false, size = 'sm', feedback, style, styleInput, id, name, styleLabel={fontSize:11}, min, max, inValidInput, inputRef=null, onFocus= null, tabIndex="-1", // Thêm option mới allowClear = false, error }) => { const { Form } = ReactBootstrap const handleKeyDown = (e) => { if (e.key === 'Enter' && onEnter) { onEnter(); } }; const handleBlur = () => { if (onBlur) { onBlur(); } }; // Hàm xử lý xóa text const handleClear = () => { if (onChange) { // Giả lập một sự kiện target để phù hợp với logic onChange hiện tại của bạn onChange({ target: { name: name, value: '', id: id } }); } }; let message = feedback const invalidObj = inValidInput?.find((n) => n.id === id) let isInvalid = invalidObj?.isInvalid || false if (error) { isInvalid = true; message = error; } else if (invalidObj){ message = invalidObj?.msg } return ( {label && {label}}
{/* Icon Clear Text */} {allowClear && (value !== '' && value !== null && value !== undefined) && !disabled && ( (e.currentTarget.style.color = '#dc3545')} onMouseOut={(e) => (e.currentTarget.style.color = '#aaa')} > )} {isInvalid && message && (
!
{message}
)}
{message && {message} }
); }; const DropdownCtl = ({ label, options = [], value, onChange, onBlur, autoFocus = false, required = false, disabled = false, size = 'sm', feedback, style, valueFieldName = 'id', labelFieldName = 'name', labelRenderer, optionDefault = 'All', name, id, inValidInput, styleLabel = { fontSize: 11, marginBottom: '0px' }, allowNull = true, isSort = true, multiple = false, closeOnSelect = true, error }) => { const { Form, Dropdown, Badge } = ReactBootstrap; const [searchTerm, setSearchTerm] = React.useState(""); const getOptionLabel = (opt) => { if (!opt) return ""; if (typeof labelRenderer === 'function') { return labelRenderer(opt); } return opt[labelFieldName] !== undefined && opt[labelFieldName] !== null ? String(opt[labelFieldName]) : ""; }; // 1. Filter & Sort Options const processedOptions = React.useMemo(() => { let list = Array.isArray(options) ? [...options] : []; if (isSort) { list.sort((a, b) => String(getOptionLabel(a)).localeCompare(String(getOptionLabel(b)))); } if (!searchTerm) return list; const searchLower = searchTerm.toLowerCase(); return list.filter(opt => String(getOptionLabel(opt)).toLowerCase().includes(searchLower) || String(opt[valueFieldName]).toLowerCase().includes(searchLower) ); }, [options, searchTerm, isSort, labelFieldName, valueFieldName, labelRenderer]); // 2. Selected Label Logic const selectedLabel = React.useMemo(() => { if (multiple) { if (!Array.isArray(value) || value.length === 0) return optionDefault; if (value.length === options.length && options.length > 0) return "Tất cả (" + options.length + ")"; if (value.length > 1) { const firstItem = options.find(opt => String(opt[valueFieldName]) === String(value[0])); const firstLabel = firstItem ? getOptionLabel(firstItem) : ""; return `${firstLabel} (+${value.length - 1})`; } const selectedItem = options.find(opt => String(opt[valueFieldName]) === String(value[0])); return selectedItem ? getOptionLabel(selectedItem) : optionDefault; } if (value === "None" || value === null || value === undefined || value === "") return optionDefault; const found = options.find(opt => String(opt[valueFieldName]) === String(value)); return found ? getOptionLabel(found) : optionDefault; }, [value, options, multiple, valueFieldName, labelFieldName, optionDefault, labelRenderer]); // 3. Handlers const handleItemClick = (val) => { let newValue; if (multiple) { const currentArray = Array.isArray(value) ? value : []; const isExist = currentArray.some(v => String(v) === String(val)); newValue = isExist ? currentArray.filter(v => String(v) !== String(val)) : [...currentArray, val]; } else { newValue = val; setSearchTerm(""); } onChange({ target: { name, id, value: newValue } }); }; const handleSelectAll = (e) => { e.stopPropagation(); const isAllSelected = Array.isArray(value) && value.length === options.length; const newValue = isAllSelected ? [] : options.map(opt => opt[valueFieldName]); onChange({ target: { name, id, value: newValue } }); }; const handleClear = (e) => { e.stopPropagation(); const resetValue = multiple ? [] : ""; onChange({ target: { name, id, value: resetValue } }); setSearchTerm(""); }; const isChecked = (val) => { if (multiple) return Array.isArray(value) && value.some(v => String(v) === String(val)); return String(value) === String(val); }; const hasValue = multiple ? (Array.isArray(value) && value.length > 0) : (value !== null && value !== undefined && value !== "" && value !== "None"); const invalidObj = inValidInput?.find((n) => String(n.id) === String(id)); let isInvalid = invalidObj?.isInvalid || false; let message = value == null ? feedback : ''; if (error) { isInvalid = true; message = error; } else if (isInvalid) { message = invalidObj?.msg; } return ( {label && {label}}
{/* Phần chữ hiển thị bên trái */}
{selectedLabel} {multiple && Array.isArray(value) && value.length > 1 && ( {value.length} )}
{/* Cụm icon bên phải - Phân tách xa nhau */}
{hasValue && !disabled && ( )}
setSearchTerm(e.target.value)} onClick={(e) => e.stopPropagation()} /> {multiple && options.length > 0 && !searchTerm && (
{}} style={{ pointerEvents: 'none' }} />
)}
{processedOptions.length > 0 ? processedOptions.map((option, index) => { const optValue = option[valueFieldName]; const isSel = isChecked(optValue); return ( handleItemClick(optValue)} > {multiple && ( {}} className="me-2" style={{ pointerEvents: 'none' }} /> )} {getOptionLabel(option)} ); }) :
Không có dữ liệu
}
{isInvalid && message && (
!
{message}
)}
{message && ( {message} )}
); }; const DateTimeCtl = ({ id,value, setValue, disabled=false, showTimePicker = true, labelDate='Choose Date', labelTime='Choose Time', size='sm', feedback=null, name, inValidInput,styleLabel={fontSize:11, marginBottom:'0px'} }) => { const {Form,Row, Col} = ReactBootstrap const dateValue = value ? value.split(' ')[0] : ''; const timeValue = value ? value.split(' ')[1] : ''; const handleDateChange = (e) => { const newDate = e.target.value; const newValue = showTimePicker ? `${newDate} ${timeValue}` : newDate; setValue(newValue); }; const handleTimeChange = (e) => { const newTime = e.target.value; const newValue = `${dateValue} ${newTime}`; setValue(newValue); }; let message = feedback const invalidObj = inValidInput?.find((n) => n.id === id) const isInvalid = invalidObj?.isInvalid || false if (invalidObj){ message = invalidObj?.msg } return ( { labelDate && {labelDate} } {message && {message}} {showTimePicker && ( {labelTime} )} ); }; const LabelInput=(props)=>{ const {label, htmlFor, style={fontSize:'12px', padding:'0px'}} = props const {Form} = ReactBootstrap return( {label} ) } const DropdownSuggest = (props) => { const {id,data, setData, searchText, setSearchText, caption, itemSelected, setItemSelected, handleCustom,onEnter,inValidInput ,height, width='100%', lineId,styleLabel, feedback} = props const { Dropdown, FormControl,InputGroup, Button, Form } = ReactBootstrap; const dropdownStyle = { width: width || 'auto', // Mặc định là auto nếu không có width border:'1px solid #ced4da', borderRadius:4 }; const toggleStyle = { height: '30px', // Chiều cao của Dropdown.Toggle width: width, // Chiều rộng của Dropdown.Toggle textAlign:'left', lineHeight:'10px', overflow:'hidden', fontSize:'14px', color:'gray' }; const itemStyle = { whiteSpace: 'nowrap', // Ngăn không cho nội dung vượt quá một dòng overflow: 'hidden', // Ẩn nội dung thừa textOverflow: 'ellipsis', // Hiển thị dấu ba chấm nếu nội dung bị cắt maxWidth: '100%', // Đảm bảo nội dung không vượt quá chiều rộng của item }; let message = feedback const invalidObj = inValidInput?.find((n) => n.id === id) const isInvalid = invalidObj?.isInvalid || false if (invalidObj){ message = invalidObj?.msg } return ( <> {caption} {itemSelected ? itemSelected?.name : caption} setSearchText(e.target.value)} onKeyUp={(e)=> onEnter(e)} autoFocus={true} id={id} /> {data == null || data?.length === 0 ? ( Loading... ) : ( data?.map((row) => ( { if(setItemSelected){ setItemSelected(row); // Cập nhật khách hàng đã chọn } if(handleCustom){ handleCustom(row, lineId) } setSearchText(''); // Xóa ô tìm kiếm }} style={itemStyle} // Áp dụng kiểu cho từng item > {row.name} )) )} { setItemSelected(null); // Đặt itemSelected về null setSearchText(''); setData([]) // Xóa ô tìm kiếm }} style={{ textAlign: 'left' }} // Đặt kiểu cho nút Clear > {message && {message}} ); }; const TextAreaCtl =(props)=>{ var Form = ReactBootstrap.Form const InputGroup = ReactBootstrap.InputGroup const {id,value, setValue, onChangeMaster, placeholder, width='100%', disabled = false, rows=3, label,styleLabel={fontSize:11}} = props const onChange =(e)=>{ setValue(e.target.value) } return( <> {label} ) } const RichTextBox = (props) => { const {id, title, text, setText, readOnly=false, disabled=false, handleOnchange,autoFocus=false, inValidInput } = props; const editorRef = useRef(); const {CKEditor} = window.CKEditor const ClassicEditor = window.ClassicEditor const invalidObj = inValidInput?.find((n) => n.id === id) const isInvalid = invalidObj?.isInvalid || false const message = invalidObj?.msg useEffect(() => { console.log('5345DFGDFGDFGDFGD') if (editorRef.current && editorRef.current.editorInstance === null) { ClassicEditor.create(editorRef.current) .then(editor => { editorRef.current.editorInstance = editor; editor.setData(text); }) .catch(error => { console.error('Error initializing editor', error); }); } }, []); const handleEditorReady = (editor) => { console.log('Editor is ready to use!', editor); }; const handleEditorChange = (event, editor) => { const newData = editor.getData(); handleOnchange(newData) setText(newData) }; return (
{title}
{ isInvalid==true && {message} }
); } const HtmlContentEditor = (props) => { const { id, title, text, setText, isInvalid, message, disabled = false } = props; const editorRef = React.useRef(null); const [isFocused, setIsFocused] = React.useState(false); React.useEffect(() => { if (editorRef.current && editorRef.current.innerHTML !== text) { editorRef.current.innerHTML = text || ""; } }, [text]); const execCommand = (command, value = null) => { if (disabled) return; document.execCommand(command, false, value); editorRef.current.focus(); }; const handleInput = () => { if (editorRef.current) { setText(editorRef.current.innerHTML); } }; const handlePaste = (e) => { e.preventDefault(); const text = e.clipboardData.getData('text/plain'); document.execCommand('insertText', false, text); }; const ToolbarBtn = ({ cmd, icon, title }) => ( ); return (
{title && }
{/* Toolbar chỉ hiện các nút cơ bản để tiết kiệm diện tích */}
{/* Hiển thị số ký tự hoặc trạng thái nhỏ bên góc phải nếu cần */}
HTML Editor
setIsFocused(true)} onBlur={() => setIsFocused(false)} className="editor-editable-area" style={{ // ĐÂY LÀ PHẦN THAY ĐỔI CHÍNH minHeight: '38px', // Tương đương 1 hàng input thông thường height: 'auto', // Tự động nở theo nội dung maxHeight: '600px', // Giới hạn chiều cao tối đa để không làm vỡ layout padding: '8px 12px' }} />
{isInvalid && {message}}
); }; const TagInputCtl = ({ label, value = [], onChange, disabled }) => { // Đảm bảo value luôn là mảng để tránh lỗi .map const tags = Array.isArray(value) ? value : []; const handleKeyDown = (e) => { if (e.key === 'Enter') { // Chặn tuyệt đối việc trigger submit form hoặc các sự kiện cha e.preventDefault(); e.stopPropagation(); const inputValue = e.target.value.trim(); // Kiểm tra: Không rỗng và không trùng lặp if (inputValue && !tags.includes(inputValue)) { const newTags = [...tags, inputValue]; onChange(newTags); e.target.value = ''; // Reset input } } }; const removeTag = (idxToRemove, e) => { e.preventDefault(); e.stopPropagation(); const newTags = tags.filter((_, i) => i !== idxToRemove); onChange(newTags); }; return (
{label && }
!disabled && document.getElementById(`input-${label}`)?.focus()} > {tags.map((tag, idx) => ( {tag} {!disabled && ( removeTag(idx, e)} > )} ))} {!disabled && ( { // Tùy chọn: Tự động thêm tag khi mất focus (blur) if (e.target.value.trim()) handleKeyDown({ ...e, key: 'Enter', preventDefault: () => {}, stopPropagation: () => {} }); }} /> )}
); }; const DynamicMetaCtl = ({ title = "Thông tin mở rộng", meta = {}, setMeta, inValidInput = [], // Config default config = { field_name_tag: { type: "TagInputCtl", visible: true, disable:false, caption: "Field name tag" }, field_name_text: { type: "TextInputCtl", visible: true, disable:false, caption: "Field name text" }, field_name_html: { type: "HtmlContentEditor", visible: true, disable:false, disable:false, caption: "Field name html" }, field_name_number: { type: "Number", visible: true, disable:false, disable:false, caption: "Field name number" }, field_name_check: { type: "Check", visible: true, disable:false, disable:false, caption: "Field name check" }, } }) => { const { Row, Col, Accordion, Form } = ReactBootstrap; const [isOpen, setIsOpen] = React.useState(false); const handleMetaChange = (key, value) => { // Chặn cập nhật nếu config định nghĩa field này là disable if (config[key]?.disable) return; setMeta(prev => ({ ...prev, [key]: value })); }; const renderControl = (key, value) => { const fieldConfig = config[key]; // Chỉ render nếu visible: true if (!fieldConfig || fieldConfig.visible === false) return null; // Ưu tiên hiển thị đúng CAPTION từ config const label = fieldConfig.caption; const isFieldDisabled = fieldConfig.disable; // Trạng thái khóa field const invalidObj = inValidInput?.find(n => n.id === key); // Render dựa theo type switch (fieldConfig.type) { case 'TagInputCtl': return ( handleMetaChange(key, val)} disabled={isFieldDisabled} // Không thể chỉnh sửa /> ); case 'HtmlContentEditor': return ( handleMetaChange(key, val)} disabled={isFieldDisabled} // Khóa trình soạn thảo isInvalid={invalidObj?.isInvalid} message={invalidObj?.msg} /> ); case 'Check': return ( {label}} checked={!!value} disabled={isFieldDisabled} // Khóa checkbox onChange={(e) => handleMetaChange(key, e.target.checked)} /> ); case 'RadioOptionsCtl': // Tách chuỗi "1_INV|3_PXK" thành mảng ["1_INV", "3_PXK"] const options = fieldConfig.default_value ? fieldConfig.default_value.split('|') : []; return ( {label}
{options.map((opt, index) => ( {opt}} name={`group-${key}`} // Đảm bảo chỉ chọn được 1 trong group value={opt} checked={value === opt} disabled={isFieldDisabled} onChange={() => handleMetaChange(key, opt)} // Tăng diện tích chạm cho mobile className="d-flex align-items-center me-3" style={{ cursor: isFieldDisabled ? 'not-allowed' : 'pointer' }} /> ))}
{invalidObj?.isInvalid && ( {invalidObj?.msg} )}
); case 'Number': case 'TextInputCtl': default: return ( handleMetaChange(key, e.target.value)} size="sm" disabled={isFieldDisabled} // Input sẽ mờ và không thể gõ allowClear={!isFieldDisabled} id={key} inValidInput={inValidInput} /> ); } }; return ( setIsOpen(!isOpen)} className="bg-white">
{title}
{/* Map qua config để đảm bảo thứ tự hiển thị và dùng đúng key */} {Object.keys(config).map(key => renderControl(key, meta[key]))}
); }; const MetaRender = ({ configStructureMeta, meta, setMeta, level = 0, actionCallBack, parentList, parentIndex, errors = {} }) => { const { Form, Row, Col, Button } = ReactBootstrap; // Helper: Định dạng số hàng nghìn const isVi = window.config?.lang === 'vi' || !window.config?.lang; const thousandSep = isVi ? '.' : ','; const decimalSep = isVi ? ',' : '.'; const formatNumber = (val) => { if (val === undefined || val === null || val === '') return ''; let str = val.toString(); // Preserve negative sign if it is alone if (str === '-') return '-'; const isNegative = str.startsWith('-'); if (isNegative) { str = str.substring(1); } const parts = str.split('.'); const intPart = parseFloat(parts[0]); if (isNaN(intPart)) return isNegative ? '-' : ''; const formattedInt = new Intl.NumberFormat(isVi ? 'vi-VN' : 'en-US').format(intPart); let result = formattedInt; if (parts.length > 1) { result = formattedInt + decimalSep + parts[1]; } return isNegative ? '-' + result : result; }; const styles = { container: { fontSize: '13px', fontFamily: '-apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif' }, fieldWrapper: { marginBottom: '8px', padding: '0 4px' }, nestedSection: { marginLeft: level > 0 ? '10px' : '0', paddingLeft: level > 0 ? '10px' : '0', borderLeft: level > 0 ? '2px solid #dee2e6' : 'none', marginBottom: '12px' }, label: (isRequired) => ({ fontSize: '11.5px', fontWeight: '600', color: isRequired ? '#dc3545' : '#555', marginBottom: '3px', display: 'block' }), input: { fontSize: '13px', borderRadius: '6px', border: '1px solid #ced4da', transition: 'border-color 0.15s ease-in-out' }, numberInput: { textAlign: 'right', fontWeight: '500' }, listCard: { backgroundColor: '#ffffff', border: '1px solid #e0e0e0', borderRadius: '8px', padding: '10px', marginBottom: '6px', position: 'relative', boxShadow: '0 1px 3px rgba(0,0,0,0.05)' }, deleteBtn: { position: 'absolute', top: '-8px', right: '-8px', padding: '0', width: '20px', height: '20px', fontSize: '12px', borderRadius: '50%', zIndex: 10 } }; const hasFieldError = (fieldKey, parentList, parentIndex) => { if (!errors) return false; if (parentList !== undefined && parentIndex !== undefined) { if (fieldKey === 'threshold_from') { return !!( errors[`tiers_${parentIndex}_from`] || errors[`tiers_${parentIndex}_from_neg`] || errors[`tiers_${parentIndex}_overlap`] || errors[`tiers_${parentIndex}_gap`] ); } if (fieldKey === 'threshold_to') { return !!( errors[`tiers_${parentIndex}_to`] || errors[`tiers_${parentIndex}_to_neg`] || errors[`tiers_${parentIndex}_to_compare_from`] ); } if (fieldKey === 'tier_value') { return !!errors[`tiers_${parentIndex}_value`]; } } else { return !!errors[fieldKey]; } return false; }; const getFieldErrorText = (fieldKey, parentList, parentIndex) => { if (!errors) return ''; if (parentList !== undefined && parentIndex !== undefined) { if (fieldKey === 'threshold_from') { return ( errors[`tiers_${parentIndex}_from`] || errors[`tiers_${parentIndex}_from_neg`] || errors[`tiers_${parentIndex}_overlap`] || errors[`tiers_${parentIndex}_gap`] || '' ); } if (fieldKey === 'threshold_to') { return ( errors[`tiers_${parentIndex}_to`] || errors[`tiers_${parentIndex}_to_neg`] || errors[`tiers_${parentIndex}_to_compare_from`] || '' ); } if (fieldKey === 'tier_value') { return errors[`tiers_${parentIndex}_value`] || ''; } } else { return errors[fieldKey] || ''; } return ''; }; const handleUpdate = (path, value) => { const updateDeep = (obj, pathArray, val) => { const [current, ...rest] = pathArray; if (rest.length === 0) { let finalVal = val; // Tự động chuẩn hóa liên kết mốc khi cập nhật mảng (ví dụ: sau khi xóa) if (Array.isArray(val) && val.length > 0 && typeof val[0] === 'object' && val[0] !== null && 'threshold_from' in val[0] && 'threshold_to' in val[0]) { finalVal = val.map((item, idx) => { if (idx === 0) return item; return { ...item, threshold_from: val[idx - 1].threshold_to }; }); } return { ...obj, [current]: finalVal }; } return { ...obj, [current]: updateDeep(obj[current] || {}, rest, val) }; }; const pathParts = path.split('.'); if (pathParts.length === 3 && pathParts[2] === 'threshold_to') { const listKey = pathParts[0]; const idx = parseInt(pathParts[1]); const nextIdx = idx + 1; setMeta(prev => { let updated = updateDeep(prev, pathParts, value); const list = updated[listKey]; if (Array.isArray(list) && nextIdx < list.length) { const nextPath = `${listKey}.${nextIdx}.threshold_from`; updated = updateDeep(updated, nextPath.split('.'), value); } return updated; }); } else { setMeta(prev => updateDeep(prev, pathParts, value)); } }; // Xử lý sự kiện nhấn phím Enter const handleKeyDown = (e, cfg, key, val) => { if (e.key === 'Enter' && cfg.action_callback && actionCallBack) { e.preventDefault(); // Ngăn submit form mặc định actionCallBack(key, val); } }; const handleAddListItem = (path, itemProperties, currentList) => { const newItem = {}; Object.keys(itemProperties).forEach(key => { if (itemProperties[key].type === 'list_object') newItem[key] = []; else if (itemProperties[key].type === 'object') newItem[key] = {}; else newItem[key] = itemProperties[key].defaultValue || (itemProperties[key].type === 'number' ? 0 : ''); }); // Tự động điền mốc Từ của dòng mới bằng mốc Đến của dòng cuối cùng trước đó if (currentList && currentList.length > 0) { const lastItem = currentList[currentList.length - 1]; if ('threshold_from' in newItem && lastItem && 'threshold_to' in lastItem) { newItem.threshold_from = lastItem.threshold_to; } } handleUpdate(path, [...(currentList || []), newItem]); }; const renderControl = (cfg, val, path, fieldKey) => { const isRequired = cfg.required === true; // Khóa cứng Từ mốc (threshold_from) của các dòng từ dòng thứ 2 trở đi (index > 0) let isControlDisabled = cfg.disabled; if (fieldKey === 'threshold_from' && parentIndex !== undefined && parentIndex > 0) { isControlDisabled = true; } const commonProps = { size: "sm", style: { ...styles.input, ...(cfg.type === 'number' ? styles.numberInput : {}) }, disabled: isControlDisabled, placeholder: cfg.placeholder || '', onKeyDown: (e) => handleKeyDown(e, cfg, fieldKey, val) // Gắn sự kiện Enter }; switch (cfg.type) { case 'dropdown': return ( handleUpdate(path, e.target.value)} > {cfg.options?.map(opt => )} ); case 'check': return (
{cfg.label} {isRequired && '*'}} checked={!!val} disabled={cfg.disabled} onChange={(e) => { const newValue = e.target.checked; handleUpdate(path, newValue); // Call nhanh nếu là checkbox và có action_callback (thường Enter cho text, nhưng trigger on change cho switch sẽ tiện hơn) if(cfg.action_callback && actionCallBack) actionCallBack(fieldKey, newValue); }} />
); case 'multicheck': const selectedList = Array.isArray(val) ? val : []; return (
{cfg.options?.map(opt => { const isChecked = selectedList.includes(opt.value); return ( { let newList; if (e.target.checked) { newList = [...selectedList, opt.value]; } else { newList = selectedList.filter(v => v !== opt.value); } handleUpdate(path, newList); }} style={{ fontSize: '11.5px', marginRight: '8px', marginBottom: '2px' }} /> ); })}
); case 'number': let customPlaceholder = cfg.placeholder || ''; if (fieldKey === 'threshold_to' && parentList && parentIndex === parentList.length - 1) { customPlaceholder = isVi ? "Trở đi" : "∞"; } return ( { let valStr = e.target.value; const stepStr = String(cfg.step || 'any'); let allowDecimals = stepStr !== '1'; if (fieldKey === 'threshold_from' || fieldKey === 'threshold_to') { allowDecimals = false; } let raw = valStr; if (!allowDecimals) { // Chỉ cho phép số nguyên raw = valStr.replace(/[^0-9-]/g, ''); } else { // Tự động phát hiện và xử lý dấu thập phân (dấu chấm hoặc phẩy) const dots = (valStr.match(/\./g) || []).length; const commas = (valStr.match(/,/g) || []).length; if (dots > 0 && commas > 0) { const lastDot = valStr.lastIndexOf('.'); const lastComma = valStr.lastIndexOf(','); if (lastDot > lastComma) { raw = valStr.split(',').join(''); } else { raw = valStr.split('.').join('').replace(',', '.'); } } else if (dots > 1) { raw = valStr.split('.').join(''); } else if (commas > 1) { raw = valStr.split(',').join(''); } else if (dots === 1) { raw = valStr; } else if (commas === 1) { raw = valStr.replace(',', '.'); } raw = raw.replace(/[^0-9.-]/g, ''); const hasMinus = raw.startsWith('-'); if (hasMinus) { raw = '-' + raw.substring(1).replace(/-/g, ''); } else { raw = raw.replace(/-/g, ''); } const dotIndex = raw.indexOf('.'); if (dotIndex !== -1) { raw = raw.substring(0, dotIndex + 1) + raw.substring(dotIndex + 1).replace(/\./g, ''); } } if (raw === '' || raw === '-') { handleUpdate(path, raw); return; } const parsed = parseFloat(raw); if (!isNaN(parsed)) { handleUpdate(path, raw); } }} /> ); case 'textarea': return ( handleUpdate(path, e.target.value)} /> ); default: return ( handleUpdate(path, e.target.value)} /> ); } }; const renderFields = (config, data, currentPath = "") => { if (!config) return null; const keys = Object.keys(config); return keys.map((key, index) => { const fieldConfig = config[key]; const fieldPath = currentPath ? `${currentPath}.${key}` : key; const fieldValue = data ? data[key] : undefined; const isRequired = fieldConfig.required === true; if (fieldConfig.visible === false) return null; if (fieldConfig.type === 'list_object') { const list = Array.isArray(fieldValue) ? fieldValue : []; return (
{fieldConfig.label} {isRequired && '*'}
{list.map((item, idx) => (
{ const newArray = [...list]; newArray[idx] = typeof updatedItem === 'function' ? updatedItem(item) : updatedItem; handleUpdate(fieldPath, newArray); }} level={level + 1} actionCallBack={actionCallBack} // Truyền callback xuống đệ quy parentList={list} parentIndex={idx} errors={errors} />
))} ); } if (fieldConfig.type === 'object') { return (
{fieldConfig.label} {isRequired && '*'}
{ const newVal = typeof updatedObj === 'function' ? updatedObj(fieldValue || {}) : updatedObj; handleUpdate(fieldPath, newVal); }} level={level + 1} actionCallBack={actionCallBack} // Truyền callback xuống đệ quy errors={errors} />
); } const errorText = getFieldErrorText(key, parentList, parentIndex); return (
{fieldConfig.type !== 'check' && ( {fieldConfig.label} {isRequired && *} )}
{renderControl(fieldConfig, fieldValue, fieldPath, key)} {errorText && (
!
{errorText}
)}
); }); }; return (
{renderFields(configStructureMeta, meta)}
); }; const JsonViewCard = ({ meta }) => { const { Card, Accordion, Badge, Button } = ReactBootstrap; const jsonStyle = { backgroundColor: '#1e1e1e', padding: '15px', borderRadius: '8px', fontSize: '13px', fontFamily: '"Fira Code", "Cascadia Code", Monaco, monospace', maxHeight: '400px', overflowY: 'auto', color: '#d4d4d4', border: '1px solid #333' }; const syntaxHighlight = (json) => { if (!json) return '// [Trống]: Không có dữ liệu'; let displayJson = json; try { if (typeof json === 'string') displayJson = JSON.parse(json); } catch (e) { displayJson = json; } const jsonString = JSON.stringify(displayJson, undefined, 2); return jsonString.replace(/("(\\u[a-zA-Z0-9]{4}|\\[^u]|[^\\"])*"(\s*:)?|\b(true|false|null)\b|-?\d+(?:\.\d*)?(?:[eE][+-]?\d+)?)/g, function (match) { let cls = 'color: #ce9178;'; if (/^"/.test(match)) { if (/:$/.test(match)) cls = 'color: #9cdcfe; font-weight: bold;'; } else if (/true|false/.test(match)) { cls = 'color: #569cd6;'; } else if (/null/.test(match)) { cls = 'color: #6c757d;'; } else { cls = 'color: #b5cea8;'; } return `${match}`; }); }; return ( {/* Không có defaultActiveKey = Mặc định đóng */}
DỮ LIỆU JSON META Click để xem chi tiết cấu hình raw
{/* Icon mũi tên sẽ được tự động thêm bởi class .accordion-button của Bootstrap */}
                    
Format: JSON Pretty
{/* CSS bổ sung để tinh chỉnh icon collapse của Bootstrap */}
); }; const DynamicFieldRenderer = ({ fieldKey, config, value, onChange }) => { const { type, label, options, placeholder } = config; const handleChange = (e) => { const { value, type, checked } = e.target; onChange(fieldKey, type === 'checkbox' ? checked : value); }; switch (type) { case 'number': case 'text': return ; case 'dropdown': return ; case 'switch': return ; default: return ( {label} ); } };