/* Schema-driven form engine: Field types + sections + Register/Trial/Survey forms */
function Field({ field: f, value, error, onChange }) {
const id = 'f_' + f.id;
const lbl =
;
if (['text', 'email', 'tel', 'number'].includes(f.type)) {
return (
{lbl}
onChange(e.target.value)} />
{error &&
⚠ {error}
}
);
}
if (f.type === 'textarea') {
return (
);
}
if (f.type === 'select') {
return (
{lbl}
{error &&
⚠ {error}
}
);
}
if (f.type === 'radio') {
return (
{lbl}
{f.options.map((o) =>
)}
{error &&
⚠ {error}
}
);
}
if (f.type === 'checkbox') {
const arr = Array.isArray(value) ? value : [];
const toggle = (o) => onChange(arr.includes(o) ? arr.filter((x) => x !== o) : [...arr, o]);
return (
{lbl}
{f.options.map((o) =>
)}
{error &&
⚠ {error}
}
);
}
if (f.type === 'rating') {
return (
{lbl}
{Array.from({ length: f.max || 5 }, (_, i) => i + 1).map((n) =>
)}
{f.lowLabel}{f.highLabel}
{error &&
⚠ {error}
}
);
}
if (f.type === 'matrix') {
const val = value || {};
const setRow = (key, patch) => onChange({ ...val, [key]: { ...(val[key] || {}), ...patch } });
return (
{lbl}
Vị trí / thiết bịSố lượngĐã có
{f.rows.map((r) => {
const cell = val[r.key] || {};
return (
);
})}
{error &&
⚠ {error}
}
);
}
return null;
}
function FormSection({ section: s, values, errors, onChange, showLetter = true }) {
// Sync the required (*) marker with validateSections logic for KSCT equipment matrices.
// Tier "Cần tư vấn" requires only a group ≥1; no individual stars under that path.
const tier = values.capDoMucTieu || '';
const equipRequired = {
thietBiStandard: tier.startsWith('Nhóm 1'),
thietBiAdvanced: tier.startsWith('Nhóm 2'),
thietBiPremium: tier.startsWith('Nhóm 3'),
};
return (
{showLetter &&
{s.letter}
}
{s.title}
{s.description &&
{s.description}
}
{s.fields.map((f) => {
const ef = (f.id in equipRequired) ? { ...f, required: equipRequired[f.id] } : f;
return
onChange(f.id, v)} />;
})}
);
}
// Validate a list of sections; return {errors, firstBadId}.
// Section A choice in `capDoMucTieu` decides which equipment matrix in Section C is required:
// "Nhóm 1 — Standard ..." → only thietBiStandard required
// "Nhóm 2 — Advanced ..." → only thietBiAdvanced required
// "Nhóm 3 — Premium ..." → only thietBiPremium required
// "Chưa xác định — cần tư vấn" → at least 1 of the 3 must be filled
function validateSections(sections, values) {
const errors = {};
let firstBad = null;
const tier = values.capDoMucTieu || '';
const isStandard = tier.startsWith('Nhóm 1');
const isAdvanced = tier.startsWith('Nhóm 2');
const isPremium = tier.startsWith('Nhóm 3');
const isAdvisory = tier.startsWith('Chưa xác định');
const equipIds = ['thietBiStandard', 'thietBiAdvanced', 'thietBiPremium'];
const equipIdSet = new Set(equipIds);
sections.forEach((s) => s.fields.forEach((f) => {
let effectiveField = f;
if (equipIdSet.has(f.id)) {
const requireThis =
(isStandard && f.id === 'thietBiStandard') ||
(isAdvanced && f.id === 'thietBiAdvanced') ||
(isPremium && f.id === 'thietBiPremium');
effectiveField = { ...f, required: requireThis };
}
const e = validateField(effectiveField, values[f.id]);
if (e) {errors[f.id] = e;if (!firstBad) firstBad = f.id;}
}));
if (isAdvisory) {
const matrixHasData = (v) =>
v && typeof v === 'object' && Object.keys(v).some((k) => {
const cell = v[k] || {};
return cell.have || (cell.qty !== undefined && cell.qty !== '');
});
if (!equipIds.some((id) => matrixHasData(values[id]))) {
equipIds.forEach((id) => {
if (!errors[id]) errors[id] = 'Vui lòng chọn ít nhất 1 nhóm thiết bị (Standard / Advanced / Premium).';
});
if (!firstBad) firstBad = 'thietBiStandard';
}
}
return { errors, firstBad };
}
const { useState } = React;
/* ---------- Full single-page schema form (used by Survey) ---------- */
function SchemaForm({ schema, type }) {
const [values, setValues] = useState({});
const [errors, setErrors] = useState({});
const [busy, setBusy] = useState(false);
const onChange = (id, v) => {setValues((p) => ({ ...p, [id]: v }));setErrors((p) => p[id] ? { ...p, [id]: '' } : p);};
async function submit(e) {
e.preventDefault();
const { errors: er, firstBad } = validateSections(schema.sections, values);
setErrors(er);
if (firstBad) {
window.toast({ kind: 'err', title: 'Còn mục chưa hoàn tất', sub: 'Vui lòng kiểm tra các ô được tô đỏ.' });
const el = document.getElementById('f_' + firstBad);
if (el) window.scrollTo({ top: el.getBoundingClientRect().top + window.scrollY - 120, behavior: 'smooth' });
return;
}
setBusy(true);
try {
await submitForm(schema.formName, { ...values, __type: type });
window.navigate('/cam-on?type=' + type);
} catch (err) {
setBusy(false);
window.toast({ kind: 'err', title: 'Gửi chưa thành công', sub: 'Vui lòng thử lại sau giây lát. Dữ liệu của bạn vẫn được giữ.' });
}
}
return (
);
}
/* ---------- "Not a robot" captcha (functional demo) ---------- */
function Captcha({ status, error, onVerify }) {
// status: 'idle' | 'checking' | 'done'
const cls = 'captcha' + (status === 'done' ? ' done' : '') + (error ? ' bad' : '');
return (
{status === 'done' ? 'Đã xác minh — cảm ơn bạn!' : status === 'checking' ? 'Đang xác minh…' : 'Tôi không phải là người máy'}
VietCanThi
{error &&
⚠ {error}
}
);
}
/* ---------- Short Register form (inline on landing) ---------- */
function RegisterForm({ compact = true }) {
const [v, setV] = useState({ consent: true });
const [errors, setErrors] = useState({});
const [busy, setBusy] = useState(false);
const [captcha, setCaptcha] = useState('idle'); // idle | checking | done
const set = (k, val) => {setV((p) => ({ ...p, [k]: val }));setErrors((p) => p[k] ? { ...p, [k]: '' } : p);};
const verifyCaptcha = () => {
setCaptcha('checking');
setErrors((p) => p.captcha ? { ...p, captcha: '' } : p);
setTimeout(() => setCaptcha('done'), 900);
};
async function submit(e) {
e.preventDefault();
const er = {};
if (!v.hoTen) er.hoTen = 'Vui lòng nhập họ tên.';
if (!isPhoneVN(v.sdt)) er.sdt = 'Số điện thoại chưa hợp lệ.';
if (!isEmail(v.email)) er.email = 'Email chưa hợp lệ.';
if (captcha !== 'done') er.captcha = 'Vui lòng xác minh bạn không phải người máy.';
setErrors(er);
if (Object.keys(er).length) {window.toast({ kind: 'err', title: 'Kiểm tra lại thông tin' });return;}
setBusy(true);
try {await submitForm('event_registrations', { ...v, captchaVerified: true, __type: 'dangky' });window.navigate('/cam-on?type=dangky');}
catch {setBusy(false);window.toast({ kind: 'err', title: 'Gửi chưa thành công', sub: 'Vui lòng thử lại.' });}
}
return (
);
}
/* ---------- Software trial registration ---------- */
function TrialForm() {
const [v, setV] = useState({});
const [errors, setErrors] = useState({});
const [busy, setBusy] = useState(false);
const [captcha, setCaptcha] = useState('idle');
const set = (k, val) => {setV((p) => ({ ...p, [k]: val }));setErrors((p) => p[k] ? { ...p, [k]: '' } : p);};
const verifyCaptcha = () => {setCaptcha('checking');setErrors((p) => p.captcha ? { ...p, captcha: '' } : p);setTimeout(() => setCaptcha('done'), 900);};
async function submit(e) {
e.preventDefault();
const er = {};
if (!v.hoTen) er.hoTen = 'Vui lòng nhập họ tên.';
if (!isEmail(v.email)) er.email = 'Email chưa hợp lệ.';
if (!isPhoneVN(v.sdt)) er.sdt = 'Số điện thoại chưa hợp lệ.';
if (!v.phongKham) er.phongKham = 'Vui lòng nhập tên phòng khám.';
if (!v.matKhau || v.matKhau.length < 6) er.matKhau = 'Mật khẩu tối thiểu 6 ký tự.';
if (captcha !== 'done') er.captcha = 'Vui lòng xác minh bạn không phải người máy.';
setErrors(er);
if (Object.keys(er).length) {window.toast({ kind: 'err', title: 'Kiểm tra lại thông tin' });return;}
setBusy(true);
try {await submitForm('software_trial_requests', { ...v, source: 'webinar', captchaVerified: true, __type: 'dungthu' });window.navigate('/cam-on?type=dungthu');}
catch {setBusy(false);window.toast({ kind: 'err', title: 'Gửi chưa thành công', sub: 'Vui lòng thử lại.' });}
}
return (
);
}
Object.assign(window, { Field, FormSection, validateSections, SchemaForm, RegisterForm, TrialForm, Captcha });