fix(exam): step the exam form instead of validating hidden tabs

Submitting from Basic Info complained that required fields on the Settings
tab were empty — a correct error the user could not act on, since Settings
had not been shown yet.

The primary action now reads "Next" while Basic Info is open and validates
only that tab before moving on; Create/Update appears on Settings, the final
step, alongside a Back button. Submit still checks both tabs, since the tab
headers stay clickable and a user can reach Settings without going through
Next.
This commit is contained in:
mihretue
2026-08-26 06:23:06 +00:00
parent b49de1c1a8
commit 5ad43ba760
3 changed files with 57 additions and 13 deletions

View File

@@ -79,21 +79,44 @@ function ExamForm({
const [status, setStatus] = useState<string | null>(editing?.status ?? null); const [status, setStatus] = useState<string | null>(editing?.status ?? null);
const [activeTab, setActiveTab] = useState<string | null>("basic"); const [activeTab, setActiveTab] = useState<string | null>("basic");
const handleSubmit = (e: React.FormEvent) => { /**
e.preventDefault(); * Split per tab so "Next" can check just the tab in front of the user.
* Submitting from Basic Info used to complain about Settings fields the
* user had not been shown yet — the error was correct and unactionable at
* the same time. Each returns the message key for what is missing, or null.
*/
const validateBasic = (): string | null => {
if (!certificationId || !titleEn || !titleAm || !date || !venue) { if (!certificationId || !titleEn || !titleAm || !date || !venue) {
setActiveTab("basic"); return "exam.form.fillRequiredBasic";
notify.error(t("exam.form.fillRequiredBasic"));
return;
} }
if ((directionEn || directionAm) && !(directionEn && directionAm)) { if ((directionEn || directionAm) && !(directionEn && directionAm)) {
setActiveTab("basic"); return "exam.form.directionBothLanguages";
notify.error(t("exam.form.directionBothLanguages")); }
return null;
};
const validateSettings = (): string | null =>
!type || !form || !adminMethod || !evalMethod || !cuttingPoint
? "exam.form.fillRequiredSettings"
: null;
const goNext = () => {
const error = validateBasic();
if (error) {
notify.error(t(error));
return; return;
} }
if (!type || !form || !adminMethod || !evalMethod || !cuttingPoint) { setActiveTab("settings");
setActiveTab("settings"); };
notify.error(t("exam.form.fillRequiredSettings"));
const handleSubmit = (e: React.FormEvent) => {
e.preventDefault();
// Still checks both: the tabs are clickable, so a user can reach Settings
// without going through Next.
const error = validateBasic() ?? validateSettings();
if (error) {
setActiveTab(error === "exam.form.fillRequiredSettings" ? "settings" : "basic");
notify.error(t(error));
return; return;
} }
onSubmit( onSubmit(
@@ -337,9 +360,26 @@ function ExamForm({
<Button variant="default" onClick={onCancel} size="sm"> <Button variant="default" onClick={onCancel} size="sm">
{t("exam.cancel")} {t("exam.cancel")}
</Button> </Button>
<Button type="submit" size="sm" loading={isSubmitting}> {activeTab === "basic" ? (
{editing ? t("exam.update") : t("exam.create")} /* Not type="submit": Basic Info is not the last step, so the
</Button> primary action advances rather than saves. */
<Button size="sm" onClick={goNext}>
{t("exam.form.next")}
</Button>
) : (
<>
<Button
variant="default"
size="sm"
onClick={() => setActiveTab("basic")}
>
{t("exam.form.back")}
</Button>
<Button type="submit" size="sm" loading={isSubmitting}>
{editing ? t("exam.update") : t("exam.create")}
</Button>
</>
)}
</ModalFooter> </ModalFooter>
</form> </form>
</Modal> </Modal>

View File

@@ -271,6 +271,8 @@ export const am: Translations = {
cuttingPointPercentageHint: "የመቶኛ ግምገማ — ከ100 አይበልጥም።", cuttingPointPercentageHint: "የመቶኛ ግምገማ — ከ100 አይበልጥም።",
fillRequiredBasic: "በመሠረታዊ መረጃ ውስጥ ያሉ አስፈላጊ መስኮችን ይሙሉ።", fillRequiredBasic: "በመሠረታዊ መረጃ ውስጥ ያሉ አስፈላጊ መስኮችን ይሙሉ።",
fillRequiredSettings: "በቅንብሮች ውስጥ ያሉ አስፈላጊ መስኮችን ይሙሉ — ዓይነት፣ ቅጽ፣ የአስተዳደር ዘዴ፣ የግምገማ ዘዴ እና የማለፊያ ነጥብ።", fillRequiredSettings: "በቅንብሮች ውስጥ ያሉ አስፈላጊ መስኮችን ይሙሉ — ዓይነት፣ ቅጽ፣ የአስተዳደር ዘዴ፣ የግምገማ ዘዴ እና የማለፊያ ነጥብ።",
next: "ቀጣይ",
back: "ተመለስ",
directionBothLanguages: "መመሪያ በሁለቱም እንግሊዝኛ እና አማርኛ ጽሑፍ ያስፈልገዋል፣ ወይም ሁለቱንም ባዶ ይተዉ።", directionBothLanguages: "መመሪያ በሁለቱም እንግሊዝኛ እና አማርኛ ጽሑፍ ያስፈልገዋል፣ ወይም ሁለቱንም ባዶ ይተዉ።",
status: "ሁኔታ", status: "ሁኔታ",
statusPlaceholder: "የፈተና ሁኔታ", statusPlaceholder: "የፈተና ሁኔታ",

View File

@@ -271,6 +271,8 @@ export const en = {
fillRequiredBasic: 'Please fill all required fields in Basic Info.', fillRequiredBasic: 'Please fill all required fields in Basic Info.',
fillRequiredSettings: 'Please fill all required fields in Settings — type, form, administration method, evaluation method, and cutting point.', fillRequiredSettings: 'Please fill all required fields in Settings — type, form, administration method, evaluation method, and cutting point.',
directionBothLanguages: 'Direction needs text in both English and Amharic, or leave both empty.', directionBothLanguages: 'Direction needs text in both English and Amharic, or leave both empty.',
next: 'Next',
back: 'Back',
status: 'Status', status: 'Status',
statusPlaceholder: 'Exam status', statusPlaceholder: 'Exam status',
pending: 'Pending', pending: 'Pending',