From 337e96bf19e3ab026cdfad5e859ee8ff283e31c8 Mon Sep 17 00:00:00 2001
From: Roy Jeong <roy.jeong@gov.bc.ca>
Date: Fri, 17 Jan 2025 17:03:55 -0800
Subject: [PATCH 1/2] Refactoring FileUpload view to component and input-param
 components moving and renaming

---
 frontend/src/components/index.ts              |   7 +
 .../input/file-upload}/FileUpload.vue         | 213 ++++++++----------
 .../report-info/ReportInfoPanel.vue}          | 105 ++++-----
 .../site-info/SiteInfoPanel.vue}              |  72 +++---
 .../species-info/SpeciesInfoPanel.vue}        | 157 +++----------
 .../stand-density/StandDensityPanel.vue}      |  68 ++----
 frontend/src/views/ModelParameterInput.vue    |  18 +-
 .../src/views/ModelParameterSelection.vue     |   9 -
 8 files changed, 232 insertions(+), 417 deletions(-)
 rename frontend/src/{views => components/input/file-upload}/FileUpload.vue (64%)
 rename frontend/src/components/{model-param-selection-panes/ReportInfo.vue => input/report-info/ReportInfoPanel.vue} (72%)
 rename frontend/src/components/{model-param-selection-panes/SiteInfo.vue => input/site-info/SiteInfoPanel.vue} (93%)
 rename frontend/src/components/{model-param-selection-panes/SpeciesInfo.vue => input/species-info/SpeciesInfoPanel.vue} (63%)
 rename frontend/src/components/{model-param-selection-panes/StandDensity.vue => input/stand-density/StandDensityPanel.vue} (73%)
 delete mode 100644 frontend/src/views/ModelParameterSelection.vue

diff --git a/frontend/src/components/index.ts b/frontend/src/components/index.ts
index f378e524d..659bee90a 100644
--- a/frontend/src/components/index.ts
+++ b/frontend/src/components/index.ts
@@ -12,7 +12,14 @@ export { default as AppTabs } from './core/AppTabs.vue'
 export { default as ReportConfiguration } from './input/ReportConfiguration.vue'
 
 export { default as SpeciesListInput } from './input/species-info/SpeciesListInput.vue'
+export { default as SpeciesGroupsDisplay } from './input/species-info/SpeciesGroupsDisplay.vue'
+export { default as SpeciesInfoPanel } from './input/species-info/SpeciesInfoPanel.vue'
 
+export { default as SiteInfoPanel } from './input/site-info/SiteInfoPanel.vue'
+export { default as StandDensityPanel } from './input/stand-density/StandDensityPanel.vue'
+export { default as ReportInfoPanel } from './input/report-info/ReportInfoPanel.vue'
+
+export { default as FileUpload } from './input/file-upload/FileUpload.vue'
 export { default as BCLogo } from './layout/BCLogo.vue'
 export { default as HeaderTitle } from './layout/HeaderTitle.vue'
 export { default as TheHeader } from './layout/TheHeader.vue'
diff --git a/frontend/src/views/FileUpload.vue b/frontend/src/components/input/file-upload/FileUpload.vue
similarity index 64%
rename from frontend/src/views/FileUpload.vue
rename to frontend/src/components/input/file-upload/FileUpload.vue
index 50b657a48..8202f53de 100644
--- a/frontend/src/views/FileUpload.vue
+++ b/frontend/src/components/input/file-upload/FileUpload.vue
@@ -93,7 +93,7 @@ import {
 } from '@/components'
 import type { MessageDialog } from '@/interfaces/interfaces'
 import { CONSTANTS, MESSAGE, DEFAULTS } from '@/constants'
-import { FileUploadValidation } from '@/validation/fileUploadValidation'
+import { fileUploadValidation } from '@/validation'
 import { Util } from '@/utils/util'
 import { logSuccessMessage } from '@/utils/messageHandler'
 
@@ -102,8 +102,6 @@ const form = ref<HTMLFormElement>()
 const isProgressVisible = ref(false)
 const progressMessage = ref('')
 
-const fileUploadValidator = new FileUploadValidation()
-
 const startingAge = ref<number | null>(DEFAULTS.DEFAULT_VALUES.STARTING_AGE)
 const finishingAge = ref<number | null>(DEFAULTS.DEFAULT_VALUES.FINISHING_AGE)
 const ageIncrement = ref<number | null>(DEFAULTS.DEFAULT_VALUES.AGE_INCREMENT)
@@ -151,140 +149,105 @@ const handleReportTitleUpdate = (value: string | null) => {
   reportTitle.value = value
 }
 
-const validateComparison = (): boolean => {
-  if (
-    !fileUploadValidator.validateAgeComparison(
-      finishingAge.value,
+const runModelHandler = async () => {
+  try {
+    // validation - comparison
+    const comparisonResult = fileUploadValidation.validateComparison(
       startingAge.value,
+      finishingAge.value,
     )
-  ) {
-    messageDialog.value = {
-      dialog: true,
-      title: MESSAGE.MSG_DIALOG_TITLE.INVALID_INPUT,
-      message: MESSAGE.MDL_PRM_INPUT_ERR.RPT_VLD_COMP_FNSH_AGE,
-      btnLabel: CONSTANTS.BUTTON_LABEL.CONT_EDIT,
-    }
-    return false
-  }
-
-  return true
-}
-
-const validateRange = (): boolean => {
-  if (!fileUploadValidator.validateStartingAgeRange(startingAge.value)) {
-    messageDialog.value = {
-      dialog: true,
-      title: MESSAGE.MSG_DIALOG_TITLE.INVALID_INPUT,
-      message: MESSAGE.MDL_PRM_INPUT_ERR.RPT_VLD_START_AGE_RNG(
-        CONSTANTS.NUM_INPUT_LIMITS.STARTING_AGE_MIN,
-        CONSTANTS.NUM_INPUT_LIMITS.STARTING_AGE_MAX,
-      ),
-      btnLabel: CONSTANTS.BUTTON_LABEL.CONT_EDIT,
-    }
-    return false
-  }
-
-  if (!fileUploadValidator.validateFinishingAgeRange(finishingAge.value)) {
-    messageDialog.value = {
-      dialog: true,
-      title: MESSAGE.MSG_DIALOG_TITLE.INVALID_INPUT,
-      message: MESSAGE.MDL_PRM_INPUT_ERR.RPT_VLD_START_FNSH_RNG(
-        CONSTANTS.NUM_INPUT_LIMITS.FINISHING_AGE_MIN,
-        CONSTANTS.NUM_INPUT_LIMITS.FINISHING_AGE_MAX,
-      ),
-      btnLabel: CONSTANTS.BUTTON_LABEL.CONT_EDIT,
-    }
-    return false
-  }
-
-  if (!fileUploadValidator.validateAgeIncrementRange(ageIncrement.value)) {
-    messageDialog.value = {
-      dialog: true,
-      title: MESSAGE.MSG_DIALOG_TITLE.INVALID_INPUT,
-      message: MESSAGE.MDL_PRM_INPUT_ERR.RPT_VLD_AGE_INC_RNG(
-        CONSTANTS.NUM_INPUT_LIMITS.AGE_INC_MIN,
-        CONSTANTS.NUM_INPUT_LIMITS.AGE_INC_MAX,
-      ),
-      btnLabel: CONSTANTS.BUTTON_LABEL.CONT_EDIT,
-    }
-    return false
-  }
-
-  return true
-}
-
-const validateFiles = async () => {
-  if (!layerFile.value) {
-    messageDialog.value = {
-      dialog: true,
-      title: MESSAGE.MSG_DIALOG_TITLE.MISSING_FILE,
-      message: MESSAGE.FILE_UPLOAD_ERR.LAYER_FILE_MISSING,
-      btnLabel: CONSTANTS.BUTTON_LABEL.CONT_EDIT,
-    }
-    return false
-  }
-
-  if (!polygonFile.value) {
-    messageDialog.value = {
-      dialog: true,
-      title: MESSAGE.MSG_DIALOG_TITLE.MISSING_FILE,
-      message: MESSAGE.FILE_UPLOAD_ERR.POLYGON_FILE_MISSING,
-      btnLabel: CONSTANTS.BUTTON_LABEL.CONT_EDIT,
+    if (!comparisonResult.isValid) {
+      messageDialog.value = {
+        dialog: true,
+        title: MESSAGE.MSG_DIALOG_TITLE.INVALID_INPUT,
+        message: MESSAGE.MDL_PRM_INPUT_ERR.RPT_VLD_COMP_FNSH_AGE,
+        btnLabel: CONSTANTS.BUTTON_LABEL.CONT_EDIT,
+      }
+      return
     }
-    return false
-  }
 
-  if (!(await fileUploadValidator.isCSVFile(layerFile.value))) {
-    messageDialog.value = {
-      dialog: true,
-      title: MESSAGE.MSG_DIALOG_TITLE.INVALID_FILE,
-      message: MESSAGE.FILE_UPLOAD_ERR.LAYER_FILE_NOT_CSV_FORMAT,
-      btnLabel: CONSTANTS.BUTTON_LABEL.CONT_EDIT,
-    }
-    return false
-  }
-
-  if (!(await fileUploadValidator.isCSVFile(polygonFile.value))) {
-    messageDialog.value = {
-      dialog: true,
-      title: MESSAGE.MSG_DIALOG_TITLE.INVALID_FILE,
-      message: MESSAGE.FILE_UPLOAD_ERR.POLYGON_FILE_NOT_CSV_FORMAT,
-      btnLabel: CONSTANTS.BUTTON_LABEL.CONT_EDIT,
+    // validation - required fields
+    const requiredFieldsResult = fileUploadValidation.validateRequiredFields(
+      startingAge.value,
+      finishingAge.value,
+      ageIncrement.value,
+    )
+    if (!requiredFieldsResult.isValid) {
+      messageDialog.value = {
+        dialog: true,
+        title: MESSAGE.MSG_DIALOG_TITLE.INVALID_INPUT,
+        message: MESSAGE.FILE_UPLOAD_ERR.RPT_VLD_REQUIRED_FIELDS,
+        btnLabel: CONSTANTS.BUTTON_LABEL.CONT_EDIT,
+      }
+      return
     }
-    return false
-  }
-
-  return true
-}
 
-const validateRequiredFields = (): boolean => {
-  if (
-    !fileUploadValidator.validateRequiredFields(
+    // validation - range
+    const rangeResult = fileUploadValidation.validateRange(
       startingAge.value,
       finishingAge.value,
       ageIncrement.value,
     )
-  ) {
-    messageDialog.value = {
-      dialog: true,
-      title: MESSAGE.MSG_DIALOG_TITLE.INVALID_INPUT,
-      message: MESSAGE.FILE_UPLOAD_ERR.RPT_VLD_REQUIRED_FIELDS,
-      btnLabel: CONSTANTS.BUTTON_LABEL.CONT_EDIT,
+    if (!rangeResult.isValid) {
+      let message = ''
+      switch (rangeResult.errorType) {
+        case 'startingAge':
+          message = MESSAGE.MDL_PRM_INPUT_ERR.RPT_VLD_START_AGE_RNG(
+            CONSTANTS.NUM_INPUT_LIMITS.STARTING_AGE_MIN,
+            CONSTANTS.NUM_INPUT_LIMITS.STARTING_AGE_MAX,
+          )
+          break
+        case 'finishingAge':
+          message = MESSAGE.MDL_PRM_INPUT_ERR.RPT_VLD_START_FNSH_RNG(
+            CONSTANTS.NUM_INPUT_LIMITS.FINISHING_AGE_MIN,
+            CONSTANTS.NUM_INPUT_LIMITS.FINISHING_AGE_MAX,
+          )
+          break
+        case 'ageIncrement':
+          message = MESSAGE.MDL_PRM_INPUT_ERR.RPT_VLD_AGE_INC_RNG(
+            CONSTANTS.NUM_INPUT_LIMITS.AGE_INC_MIN,
+            CONSTANTS.NUM_INPUT_LIMITS.AGE_INC_MAX,
+          )
+          break
+      }
+
+      messageDialog.value = {
+        dialog: true,
+        title: MESSAGE.MSG_DIALOG_TITLE.INVALID_INPUT,
+        message: message,
+        btnLabel: CONSTANTS.BUTTON_LABEL.CONT_EDIT,
+      }
+      return
     }
-    return false
-  }
-  return true
-}
 
-const runModelHandler = async () => {
-  try {
-    const isValidationSuccessful =
-      validateRequiredFields() &&
-      validateComparison() &&
-      validateRange() &&
-      (await validateFiles())
-
-    if (!isValidationSuccessful) {
+    // validation - files
+    const filesResult = await fileUploadValidation.validateFiles(
+      layerFile.value,
+      polygonFile.value,
+    )
+    if (!filesResult.isValid) {
+      let message = ''
+      switch (filesResult.errorType) {
+        case 'layerFileMissing':
+          message = MESSAGE.FILE_UPLOAD_ERR.LAYER_FILE_MISSING
+          break
+        case 'polygonFileMissing':
+          message = MESSAGE.FILE_UPLOAD_ERR.POLYGON_FILE_MISSING
+          break
+        case 'layerFileNotCSVFormat':
+          message = MESSAGE.FILE_UPLOAD_ERR.LAYER_FILE_NOT_CSV_FORMAT
+          break
+        case 'polygonFileNotCSVFormat':
+          message = MESSAGE.FILE_UPLOAD_ERR.POLYGON_FILE_NOT_CSV_FORMAT
+          break
+      }
+
+      messageDialog.value = {
+        dialog: true,
+        title: MESSAGE.MSG_DIALOG_TITLE.INVALID_FILE,
+        message: message,
+        btnLabel: CONSTANTS.BUTTON_LABEL.CONT_EDIT,
+      }
       return
     }
 
diff --git a/frontend/src/components/model-param-selection-panes/ReportInfo.vue b/frontend/src/components/input/report-info/ReportInfoPanel.vue
similarity index 72%
rename from frontend/src/components/model-param-selection-panes/ReportInfo.vue
rename to frontend/src/components/input/report-info/ReportInfoPanel.vue
index 30842969a..326998f64 100644
--- a/frontend/src/components/model-param-selection-panes/ReportInfo.vue
+++ b/frontend/src/components/input/report-info/ReportInfoPanel.vue
@@ -68,12 +68,10 @@ import {
 } from '@/components'
 import { CONSTANTS, DEFAULTS, MESSAGE } from '@/constants'
 import type { MessageDialog } from '@/interfaces/interfaces'
-import { ReportInfoValidation } from '@/validation/reportInfoValidation'
+import { reportInfoValidation } from '@/validation'
 
 const form = ref<HTMLFormElement>()
 
-const reportInfoValidator = new ReportInfoValidation()
-
 const modelParameterStore = useModelParameterStore()
 
 const messageDialog = ref<MessageDialog>({
@@ -129,81 +127,70 @@ const handleReportTitleUpdate = (value: string | null) => {
   reportTitle.value = value
 }
 
-const validateComparison = (): boolean => {
-  if (
-    !reportInfoValidator.validateAgeComparison(
-      finishingAge.value,
-      startingAge.value,
-    )
-  ) {
+const onConfirm = () => {
+  // validation - comparison
+  const comparisonResult = reportInfoValidation.validateComparison(
+    startingAge.value,
+    finishingAge.value,
+  )
+  if (!comparisonResult.isValid) {
     messageDialog.value = {
       dialog: true,
       title: MESSAGE.MSG_DIALOG_TITLE.INVALID_INPUT,
       message: MESSAGE.MDL_PRM_INPUT_ERR.RPT_VLD_COMP_FNSH_AGE,
       btnLabel: CONSTANTS.BUTTON_LABEL.CONT_EDIT,
     }
-
-    return false
+    return
   }
 
-  return true
-}
-
-const validateRange = (): boolean => {
-  if (!reportInfoValidator.validateStartingAgeRange(startingAge.value)) {
-    messageDialog.value = {
-      dialog: true,
-      title: MESSAGE.MSG_DIALOG_TITLE.INVALID_INPUT,
-      message: MESSAGE.MDL_PRM_INPUT_ERR.RPT_VLD_START_AGE_RNG(
-        CONSTANTS.NUM_INPUT_LIMITS.STARTING_AGE_MIN,
-        CONSTANTS.NUM_INPUT_LIMITS.STARTING_AGE_MAX,
-      ),
-      btnLabel: CONSTANTS.BUTTON_LABEL.CONT_EDIT,
+  // validation - range
+  const rangeResult = reportInfoValidation.validateRange(
+    startingAge.value,
+    finishingAge.value,
+    ageIncrement.value,
+  )
+
+  if (!rangeResult.isValid) {
+    let message = ''
+    switch (rangeResult.errorType) {
+      case 'startingAge':
+        message = MESSAGE.MDL_PRM_INPUT_ERR.RPT_VLD_START_AGE_RNG(
+          CONSTANTS.NUM_INPUT_LIMITS.STARTING_AGE_MIN,
+          CONSTANTS.NUM_INPUT_LIMITS.STARTING_AGE_MAX,
+        )
+        break
+      case 'finishingAge':
+        message = MESSAGE.MDL_PRM_INPUT_ERR.RPT_VLD_START_FNSH_RNG(
+          CONSTANTS.NUM_INPUT_LIMITS.FINISHING_AGE_MIN,
+          CONSTANTS.NUM_INPUT_LIMITS.FINISHING_AGE_MAX,
+        )
+        break
+      case 'ageIncrement':
+        message = MESSAGE.MDL_PRM_INPUT_ERR.RPT_VLD_AGE_INC_RNG(
+          CONSTANTS.NUM_INPUT_LIMITS.AGE_INC_MIN,
+          CONSTANTS.NUM_INPUT_LIMITS.AGE_INC_MAX,
+        )
+        break
     }
-    return false
-  }
 
-  if (!reportInfoValidator.validateFinishingAgeRange(finishingAge.value)) {
     messageDialog.value = {
       dialog: true,
       title: MESSAGE.MSG_DIALOG_TITLE.INVALID_INPUT,
-      message: MESSAGE.MDL_PRM_INPUT_ERR.RPT_VLD_START_FNSH_RNG(
-        CONSTANTS.NUM_INPUT_LIMITS.FINISHING_AGE_MIN,
-        CONSTANTS.NUM_INPUT_LIMITS.FINISHING_AGE_MAX,
-      ),
+      message: message,
       btnLabel: CONSTANTS.BUTTON_LABEL.CONT_EDIT,
     }
-    return false
+    return
   }
 
-  if (!reportInfoValidator.validateAgeIncrementRange(ageIncrement.value)) {
-    messageDialog.value = {
-      dialog: true,
-      title: MESSAGE.MSG_DIALOG_TITLE.INVALID_INPUT,
-      message: MESSAGE.MDL_PRM_INPUT_ERR.RPT_VLD_AGE_INC_RNG(
-        CONSTANTS.NUM_INPUT_LIMITS.AGE_INC_MIN,
-        CONSTANTS.NUM_INPUT_LIMITS.AGE_INC_MAX,
-      ),
-      btnLabel: CONSTANTS.BUTTON_LABEL.CONT_EDIT,
-    }
-    return false
+  if (form.value) {
+    form.value.validate()
+  } else {
+    console.warn('Form reference is null. Validation skipped.')
   }
 
-  return true
-}
-
-const onConfirm = () => {
-  if (validateComparison() && validateRange()) {
-    if (form.value) {
-      form.value.validate()
-    } else {
-      console.warn('Form reference is null. Validation skipped.')
-    }
-
-    // this panel is not in a confirmed state
-    if (!isConfirmed.value) {
-      modelParameterStore.confirmPanel(panelName)
-    }
+  // this panel is not in a confirmed state
+  if (!isConfirmed.value) {
+    modelParameterStore.confirmPanel(panelName)
   }
 }
 
diff --git a/frontend/src/components/model-param-selection-panes/SiteInfo.vue b/frontend/src/components/input/site-info/SiteInfoPanel.vue
similarity index 93%
rename from frontend/src/components/model-param-selection-panes/SiteInfo.vue
rename to frontend/src/components/input/site-info/SiteInfoPanel.vue
index 5e59f0aed..94e5eba21 100644
--- a/frontend/src/components/model-param-selection-panes/SiteInfo.vue
+++ b/frontend/src/components/input/site-info/SiteInfoPanel.vue
@@ -188,13 +188,11 @@ import { useModelParameterStore } from '@/stores/modelParameterStore'
 import { AppMessageDialog, AppPanelActions, AppSpinField } from '@/components'
 import type { SpeciesGroup, MessageDialog } from '@/interfaces/interfaces'
 import { CONSTANTS, OPTIONS, DEFAULTS, MESSAGE } from '@/constants'
-import { SiteInfoValidation } from '@/validation/siteInfoValidation'
+import { siteInfoValidation } from '@/validation'
 import { Util } from '@/utils/util'
 
 const form = ref<HTMLFormElement>()
 
-const siteInfoValidator = new SiteInfoValidation()
-
 const modelParameterStore = useModelParameterStore()
 
 const messageDialog = ref<MessageDialog>({
@@ -264,23 +262,20 @@ const handleBha50SiteIndexUpdate = (value: string | null) => {
   bha50SiteIndex.value = value
 }
 
-const validateRange = (): boolean => {
-  if (!siteInfoValidator.validateBha50SiteIndexRange(bha50SiteIndex.value)) {
-    messageDialog.value = {
-      dialog: true,
-      title: MESSAGE.MSG_DIALOG_TITLE.INVALID_INPUT,
-      message: MESSAGE.MDL_PRM_INPUT_ERR.SITE_VLD_SI_RNG,
-      btnLabel: CONSTANTS.BUTTON_LABEL.CONT_EDIT,
-    }
-
-    return false
+const formattingValues = (): void => {
+  if (bha50SiteIndex.value) {
+    bha50SiteIndex.value = parseFloat(bha50SiteIndex.value).toFixed(
+      CONSTANTS.NUM_INPUT_LIMITS.BHA50_SITE_INDEX_DECIMAL_NUM,
+    )
   }
-
-  return true
 }
 
-const validateRequiredFields = (): boolean => {
-  if (!siteInfoValidator.validateRequiredFields(bha50SiteIndex.value)) {
+const onConfirm = () => {
+  // validation - required fields
+  const requiredResult = siteInfoValidation.validateRequiredFields(
+    bha50SiteIndex.value,
+  )
+  if (!requiredResult.isValid) {
     messageDialog.value = {
       dialog: true,
       title: MESSAGE.MSG_DIALOG_TITLE.MISSING_INFO,
@@ -289,35 +284,32 @@ const validateRequiredFields = (): boolean => {
       ),
       btnLabel: CONSTANTS.BUTTON_LABEL.CONT_EDIT,
     }
-
-    return false
+    return
   }
 
-  return true
-}
-
-const formattingValues = (): void => {
-  if (bha50SiteIndex.value) {
-    bha50SiteIndex.value = parseFloat(bha50SiteIndex.value).toFixed(
-      CONSTANTS.NUM_INPUT_LIMITS.BHA50_SITE_INDEX_DECIMAL_NUM,
-    )
+  // validation - range
+  const rangeResult = siteInfoValidation.validateRange(bha50SiteIndex.value)
+  if (!rangeResult.isValid) {
+    messageDialog.value = {
+      dialog: true,
+      title: MESSAGE.MSG_DIALOG_TITLE.INVALID_INPUT,
+      message: MESSAGE.MDL_PRM_INPUT_ERR.SITE_VLD_SI_RNG,
+      btnLabel: CONSTANTS.BUTTON_LABEL.CONT_EDIT,
+    }
+    return
   }
-}
 
-const onConfirm = () => {
-  if (validateRequiredFields() && validateRange()) {
-    if (form.value) {
-      form.value.validate()
-    } else {
-      console.warn('Form reference is null. Validation skipped.')
-    }
+  if (form.value) {
+    form.value.validate()
+  } else {
+    console.warn('Form reference is null. Validation skipped.')
+  }
 
-    formattingValues()
+  formattingValues()
 
-    // this panel is not in a confirmed state
-    if (!isConfirmed.value) {
-      modelParameterStore.confirmPanel(panelName)
-    }
+  // this panel is not in a confirmed state
+  if (!isConfirmed.value) {
+    modelParameterStore.confirmPanel(panelName)
   }
 }
 
diff --git a/frontend/src/components/model-param-selection-panes/SpeciesInfo.vue b/frontend/src/components/input/species-info/SpeciesInfoPanel.vue
similarity index 63%
rename from frontend/src/components/model-param-selection-panes/SpeciesInfo.vue
rename to frontend/src/components/input/species-info/SpeciesInfoPanel.vue
index 984a7da2a..9e53f199e 100644
--- a/frontend/src/components/model-param-selection-panes/SpeciesInfo.vue
+++ b/frontend/src/components/input/species-info/SpeciesInfoPanel.vue
@@ -69,85 +69,7 @@
                   />
                 </v-col>
                 <v-col class="vertical-line pb-0" />
-                <!-- output -->
-                <v-col cols="6" v-if="speciesGroups.length > 0">
-                  <div
-                    v-for="(group, index) in speciesGroups"
-                    :key="index"
-                    class="mt-2"
-                  >
-                    <v-row>
-                      <v-col cols="4" sm="4" md="4">
-                        <v-text-field
-                          label="Species Group"
-                          :model-value="group.group"
-                          variant="underlined"
-                          disabled
-                          density="compact"
-                          dense
-                        ></v-text-field>
-                      </v-col>
-                      <v-col cols="4" sm="4" md="4">
-                        <v-text-field
-                          label="Species Group Percent"
-                          :model-value="group.percent"
-                          variant="underlined"
-                          disabled
-                          density="compact"
-                          dense
-                        ></v-text-field>
-                      </v-col>
-                      <v-col cols="4" sm="4" md="4">
-                        <v-text-field
-                          label="Site Species"
-                          :model-value="group.siteSpecies"
-                          variant="underlined"
-                          disabled
-                          density="compact"
-                          dense
-                        ></v-text-field>
-                      </v-col>
-                    </v-row>
-                    <div class="hr-line mb-3"></div>
-                  </div>
-                </v-col>
-                <v-col cols="6" v-else>
-                  <div class="mt-2">
-                    <v-row
-                      ><v-col cols="4" sm="4" md="4">
-                        <v-text-field
-                          label="Species Group"
-                          variant="underlined"
-                          disabled
-                          persistent-placeholder
-                          placeholder=""
-                          density="compact"
-                          dense
-                        ></v-text-field></v-col
-                      ><v-col cols="4" sm="4" md="4">
-                        <v-text-field
-                          label="Species Group Percent"
-                          variant="underlined"
-                          disabled
-                          persistent-placeholder
-                          placeholder=""
-                          density="compact"
-                          dense
-                        ></v-text-field></v-col
-                      ><v-col cols="4" sm="4" md="4">
-                        <v-text-field
-                          label="Site Species"
-                          variant="underlined"
-                          disabled
-                          persistent-placeholder
-                          placeholder=""
-                          density="compact"
-                          dense
-                        ></v-text-field
-                      ></v-col>
-                    </v-row>
-                  </div>
-                </v-col>
+                <SpeciesGroupsDisplay :speciesGroups="speciesGroups" />
               </v-row>
             </div>
             <div>
@@ -194,16 +116,15 @@ import {
   AppMessageDialog,
   AppPanelActions,
   SpeciesListInput,
+  SpeciesGroupsDisplay,
 } from '@/components'
 import { CONSTANTS, DEFAULTS, MAPPINGS, MESSAGE, OPTIONS } from '@/constants'
 import type { SpeciesList, MessageDialog } from '@/interfaces/interfaces'
-import { SpeciesInfoValidation } from '@/validation/speciesInfoValidation'
+import { speciesInfoValidation } from '@/validation'
 import { cloneDeep } from 'lodash'
 
 const form = ref<HTMLFormElement>()
 
-const speciesInfoValidator = new SpeciesInfoValidation()
-
 const modelParameterStore = useModelParameterStore()
 
 const messageDialog = ref<MessageDialog>({
@@ -264,19 +185,20 @@ const handleSpeciesListUpdate = (updatedList: SpeciesList[]) => {
   }
 }
 
-const validateDuplicateSpecies = () => {
-  const duplicateSpecies = speciesInfoValidator.validateDuplicateSpecies(
+const onConfirm = () => {
+  // validation - duplicate
+  const duplicateSpeciesResult = speciesInfoValidation.validateDuplicateSpecies(
     speciesList.value,
   )
-  if (duplicateSpecies) {
+  if (!duplicateSpeciesResult.isValid) {
+    const duplicateSpecies =
+      duplicateSpeciesResult.duplicateSpecies as keyof typeof MAPPINGS.SPECIES_MAP
     const speciesLabel = (
       Object.keys(MAPPINGS.SPECIES_MAP) as Array<
         keyof typeof MAPPINGS.SPECIES_MAP
       >
-    ).find((key) => key === duplicateSpecies)
-      ? MAPPINGS.SPECIES_MAP[
-          duplicateSpecies as keyof typeof MAPPINGS.SPECIES_MAP
-        ]
+    ).find((key) => key === duplicateSpeciesResult.duplicateSpecies)
+      ? MAPPINGS.SPECIES_MAP[duplicateSpecies]
       : ''
 
     const message = speciesLabel
@@ -292,62 +214,45 @@ const validateDuplicateSpecies = () => {
       message: message,
       btnLabel: CONSTANTS.BUTTON_LABEL.CONT_EDIT,
     }
-
-    return false
+    return
   }
 
-  return true
-}
-
-const validateTotalSpeciesPercent = () => {
-  if (
-    !speciesInfoValidator.validateTotalSpeciesPercent(
-      totalSpeciesPercent.value,
-      totalSpeciesGroupPercent.value,
-    )
-  ) {
+  // validation - total percent
+  const totalPercentResult = speciesInfoValidation.validateTotalSpeciesPercent(
+    totalSpeciesPercent.value,
+    totalSpeciesGroupPercent.value,
+  )
+  if (!totalPercentResult.isValid) {
     messageDialog.value = {
       dialog: true,
       title: MESSAGE.MSG_DIALOG_TITLE.DATA_INCOMPLETE,
       message: MESSAGE.MDL_PRM_INPUT_ERR.SPCZ_VLD_TOTAL_PCT,
       btnLabel: CONSTANTS.BUTTON_LABEL.CONT_EDIT,
     }
-
-    return false
+    return
   }
-  return true
-}
 
-const validateRequired = () => {
-  if (!speciesInfoValidator.validateRequired(derivedBy.value)) {
+  // validation - required fields
+  const requiredResult = speciesInfoValidation.validateRequired(derivedBy.value)
+  if (!requiredResult.isValid) {
     messageDialog.value = {
       dialog: true,
       title: MESSAGE.MSG_DIALOG_TITLE.MISSING_INFO,
       message: MESSAGE.MDL_PRM_INPUT_ERR.SPCZ_VLD_MISSING_DERIVED_BY,
       btnLabel: CONSTANTS.BUTTON_LABEL.CONT_EDIT,
     }
-
-    return false
+    return
   }
-  return true
-}
 
-const onConfirm = () => {
-  if (
-    validateDuplicateSpecies() &&
-    validateTotalSpeciesPercent() &&
-    validateRequired()
-  ) {
-    if (form.value) {
-      form.value.validate()
-    } else {
-      console.warn('Form reference is null. Validation skipped.')
-    }
+  if (form.value) {
+    form.value.validate()
+  } else {
+    console.warn('Form reference is null. Validation skipped.')
+  }
 
-    // this panel is not in a confirmed state
-    if (!isConfirmed.value) {
-      modelParameterStore.confirmPanel(panelName)
-    }
+  // this panel is not in a confirmed state
+  if (!isConfirmed.value) {
+    modelParameterStore.confirmPanel(panelName)
   }
 }
 
diff --git a/frontend/src/components/model-param-selection-panes/StandDensity.vue b/frontend/src/components/input/stand-density/StandDensityPanel.vue
similarity index 73%
rename from frontend/src/components/model-param-selection-panes/StandDensity.vue
rename to frontend/src/components/input/stand-density/StandDensityPanel.vue
index 701d6d279..73c3def8a 100644
--- a/frontend/src/components/model-param-selection-panes/StandDensity.vue
+++ b/frontend/src/components/input/stand-density/StandDensityPanel.vue
@@ -16,7 +16,7 @@
             <!-- Place an arrow icon to the left of the title -->
             <v-col cols="auto" class="expansion-panel-icon-col">
               <v-icon class="expansion-panel-icon">{{
-                panelOpenStates.standDensity === PANEL.OPEN
+                panelOpenStates.standDensity === CONSTANTS.PANEL.OPEN
                   ? 'mdi-chevron-up'
                   : 'mdi-chevron-down'
               }}</v-icon>
@@ -34,9 +34,9 @@
                   label="% Stockable Area"
                   type="number"
                   v-model.number="percentStockableArea"
-                  :max="NUM_INPUT_LIMITS.PERCENT_STOCKABLE_AREA_MAX"
-                  :min="NUM_INPUT_LIMITS.PERCENT_STOCKABLE_AREA_MIN"
-                  :step="NUM_INPUT_LIMITS.PERCENT_STOCKABLE_AREA_STEP"
+                  :max="CONSTANTS.NUM_INPUT_LIMITS.PERCENT_STOCKABLE_AREA_MAX"
+                  :min="CONSTANTS.NUM_INPUT_LIMITS.PERCENT_STOCKABLE_AREA_MIN"
+                  :step="CONSTANTS.NUM_INPUT_LIMITS.PERCENT_STOCKABLE_AREA_STEP"
                   placeholder=""
                   persistent-placeholder
                   hide-details
@@ -47,7 +47,7 @@
                 <v-label
                   v-show="Util.isZeroValue(percentStockableArea)"
                   style="font-size: 12px"
-                  >{{ MDL_PRM_INPUT_HINT.SITE_DFT_COMPUTED }}</v-label
+                  >{{ MESSAGE.MDL_PRM_INPUT_HINT.SITE_DFT_COMPUTED }}</v-label
                 >
               </v-col>
             </v-row>
@@ -67,28 +67,16 @@
 
 <script setup lang="ts">
 import { ref, computed } from 'vue'
-import { Util } from '@/utils/util'
+import { storeToRefs } from 'pinia'
 import { useModelParameterStore } from '@/stores/modelParameterStore'
 import { AppMessageDialog, AppPanelActions } from '@/components'
-import { storeToRefs } from 'pinia'
-import {
-  PANEL,
-  MODEL_PARAMETER_PANEL,
-  NUM_INPUT_LIMITS,
-  BUTTON_LABEL,
-} from '@/constants/constants'
-import {
-  MDL_PRM_INPUT_ERR,
-  MSG_DIALOG_TITLE,
-  MDL_PRM_INPUT_HINT,
-} from '@/constants/message'
+import { CONSTANTS, MESSAGE } from '@/constants'
 import type { MessageDialog } from '@/interfaces/interfaces'
-import { StandDensityValidation } from '@/validation/standDensityValidation'
+import { standDensityValidation } from '@/validation'
+import { Util } from '@/utils/util'
 
 const form = ref<HTMLFormElement>()
 
-const standDensityValidator = new StandDensityValidation()
-
 const modelParameterStore = useModelParameterStore()
 
 const messageDialog = ref<MessageDialog>({
@@ -100,7 +88,7 @@ const messageDialog = ref<MessageDialog>({
 const { panelOpenStates, percentStockableArea } =
   storeToRefs(modelParameterStore)
 
-const panelName = MODEL_PARAMETER_PANEL.STAND_DENSITY
+const panelName = CONSTANTS.MODEL_PARAMETER_PANEL.STAND_DENSITY
 const isConfirmEnabled = computed(
   () => modelParameterStore.panelState[panelName].editable,
 )
@@ -108,36 +96,18 @@ const isConfirmed = computed(
   () => modelParameterStore.panelState[panelName].confirmed,
 )
 
-const validateRange = (): boolean => {
-  if (
-    !standDensityValidator.validatePercentStockableAreaRange(
-      percentStockableArea.value,
-    )
-  ) {
+const onConfirm = () => {
+  // validation - range
+  const rangeResult = standDensityValidation.validateRange(
+    percentStockableArea.value,
+  )
+  if (!rangeResult.isValid) {
     messageDialog.value = {
       dialog: true,
-      title: MSG_DIALOG_TITLE.INVALID_INPUT,
-      message: MDL_PRM_INPUT_ERR.DENSITY_VLD_PCT_STCB_AREA_RNG,
-      btnLabel: BUTTON_LABEL.CONT_EDIT,
+      title: MESSAGE.MSG_DIALOG_TITLE.INVALID_INPUT,
+      message: MESSAGE.MDL_PRM_INPUT_ERR.DENSITY_VLD_PCT_STCB_AREA_RNG,
+      btnLabel: CONSTANTS.BUTTON_LABEL.CONT_EDIT,
     }
-    return false
-  }
-
-  return true
-}
-
-const validateFormInputs = async (): Promise<boolean> => {
-  if (!validateRange()) {
-    return false
-  }
-
-  return true
-}
-
-const onConfirm = async () => {
-  const isFormValid = await validateFormInputs()
-
-  if (!isFormValid) {
     return
   }
 
diff --git a/frontend/src/views/ModelParameterInput.vue b/frontend/src/views/ModelParameterInput.vue
index 826864aaf..015d7519e 100644
--- a/frontend/src/views/ModelParameterInput.vue
+++ b/frontend/src/views/ModelParameterInput.vue
@@ -19,11 +19,11 @@
       <AppTabs v-model:currentTab="activeTab" :tabs="tabs" />
       <template v-if="isModelParameterPanelsVisible">
         <v-spacer class="space"></v-spacer>
-        <SiteInfo />
+        <SiteInfoPanel />
         <v-spacer class="space"></v-spacer>
-        <StandDensity />
+        <StandDensityPanel />
         <v-spacer class="space"></v-spacer>
-        <ReportInfo />
+        <ReportInfoPanel />
         <AppRunModelButton
           :isDisabled="!modelParameterStore.runModelEnabled"
           cardClass="input-model-param-run-model-card"
@@ -48,12 +48,12 @@ import {
   ModelSelectionContainer,
   TopProjectYear,
   ReportingContainer,
+  SpeciesInfoPanel,
+  SiteInfoPanel,
+  StandDensityPanel,
+  ReportInfoPanel,
+  FileUpload,
 } from '@/components'
-import SiteInfo from '@/components/model-param-selection-panes/SiteInfo.vue'
-import StandDensity from '@/components/model-param-selection-panes/StandDensity.vue'
-import ReportInfo from '@/components/model-param-selection-panes/ReportInfo.vue'
-import ModelParameterSelection from '@/views/ModelParameterSelection.vue'
-import FileUpload from '@/views/FileUpload.vue'
 import type { Tab } from '@/interfaces/interfaces'
 import { CONSTANTS, MESSAGE, DEFAULTS } from '@/constants'
 import { handleApiError } from '@/services/apiErrorHandler'
@@ -72,7 +72,7 @@ const projectionStore = useProjectionStore()
 const tabs: Tab[] = [
   {
     label: CONSTANTS.MODEL_PARAM_TAB_NAME.MODEL_PARAM_SELECTION,
-    component: ModelParameterSelection,
+    component: SpeciesInfoPanel,
     tabname: null,
   },
   {
diff --git a/frontend/src/views/ModelParameterSelection.vue b/frontend/src/views/ModelParameterSelection.vue
deleted file mode 100644
index 03cd11581..000000000
--- a/frontend/src/views/ModelParameterSelection.vue
+++ /dev/null
@@ -1,9 +0,0 @@
-<template>
-  <SpeciesInfo />
-</template>
-
-<script setup lang="ts">
-import SpeciesInfo from '@/components/model-param-selection-panes/SpeciesInfo.vue'
-</script>
-
-<style scoped />

From 92aa916eb71ad552ad4a887794519e21b1755280 Mon Sep 17 00:00:00 2001
From: Roy Jeong <roy.jeong@gov.bc.ca>
Date: Fri, 17 Jan 2025 17:26:52 -0800
Subject: [PATCH 2/2] SpeciesGroupsDisplay component

---
 .../species-info/SpeciesGroupsDisplay.vue     | 106 ++++++++++++++++++
 .../input/species-info/SpeciesListInput.vue   |  10 +-
 2 files changed, 110 insertions(+), 6 deletions(-)
 create mode 100644 frontend/src/components/input/species-info/SpeciesGroupsDisplay.vue

diff --git a/frontend/src/components/input/species-info/SpeciesGroupsDisplay.vue b/frontend/src/components/input/species-info/SpeciesGroupsDisplay.vue
new file mode 100644
index 000000000..70a478ad4
--- /dev/null
+++ b/frontend/src/components/input/species-info/SpeciesGroupsDisplay.vue
@@ -0,0 +1,106 @@
+<template>
+  <v-col
+    cols="6"
+    v-if="speciesGroups.length > 0"
+    data-testid="species-groups-container"
+  >
+    <div
+      v-for="(group, index) in speciesGroups"
+      :key="index"
+      class="mt-2"
+      data-testid="species-group-row"
+    >
+      <v-row>
+        <v-col cols="4" sm="4" md="4" data-testid="species-group-column">
+          <v-text-field
+            label="Species Group"
+            :model-value="group.group"
+            variant="underlined"
+            disabled
+            density="compact"
+            dense
+          ></v-text-field>
+        </v-col>
+        <v-col
+          cols="4"
+          sm="4"
+          md="4"
+          data-testid="species-group-percent-column"
+        >
+          <v-text-field
+            label="Species Group Percent"
+            :model-value="group.percent"
+            variant="underlined"
+            disabled
+            density="compact"
+            dense
+          ></v-text-field>
+        </v-col>
+        <v-col cols="4" sm="4" md="4" data-testid="site-species-column">
+          <v-text-field
+            label="Site Species"
+            :model-value="group.siteSpecies"
+            variant="underlined"
+            disabled
+            density="compact"
+            dense
+          ></v-text-field>
+        </v-col>
+      </v-row>
+      <div class="hr-line mb-3"></div>
+    </div>
+  </v-col>
+  <v-col cols="6" v-else>
+    <div class="mt-2">
+      <v-row>
+        <v-col cols="4" sm="4" md="4">
+          <v-text-field
+            label="Species Group"
+            variant="underlined"
+            disabled
+            persistent-placeholder
+            placeholder=""
+            density="compact"
+            dense
+          ></v-text-field>
+        </v-col>
+        <v-col cols="4" sm="4" md="4">
+          <v-text-field
+            label="Species Group Percent"
+            variant="underlined"
+            disabled
+            persistent-placeholder
+            placeholder=""
+            density="compact"
+            dense
+          ></v-text-field>
+        </v-col>
+        <v-col cols="4" sm="4" md="4">
+          <v-text-field
+            label="Site Species"
+            variant="underlined"
+            disabled
+            persistent-placeholder
+            placeholder=""
+            density="compact"
+            dense
+          ></v-text-field>
+        </v-col>
+      </v-row>
+    </div>
+  </v-col>
+</template>
+
+<script setup lang="ts">
+import { defineProps } from 'vue'
+import type { SpeciesGroup } from '@/interfaces/interfaces'
+
+defineProps({
+  speciesGroups: {
+    type: Array as () => SpeciesGroup[],
+    required: true,
+  },
+})
+</script>
+
+<style scoped></style>
diff --git a/frontend/src/components/input/species-info/SpeciesListInput.vue b/frontend/src/components/input/species-info/SpeciesListInput.vue
index 94538014f..7b0ca0b51 100644
--- a/frontend/src/components/input/species-info/SpeciesListInput.vue
+++ b/frontend/src/components/input/species-info/SpeciesListInput.vue
@@ -71,7 +71,7 @@
 import { ref, onBeforeUnmount, watch, type PropType } from 'vue'
 import { CONSTANTS, MESSAGE } from '@/constants'
 import type { SpeciesList } from '@/interfaces/interfaces'
-import { SpeciesInfoValidation } from '@/validation/speciesInfoValidation'
+import { speciesInfoValidation } from '@/validation'
 import { Util } from '@/utils/util'
 import { cloneDeep } from 'lodash'
 
@@ -106,8 +106,6 @@ const emit = defineEmits(['update:speciesList'])
 
 const localSpeciesList = ref(cloneDeep(props.speciesList))
 
-const speciesInfoValidator = new SpeciesInfoValidation()
-
 // Watch for external speciesList changes
 watch(
   () => props.speciesList,
@@ -219,9 +217,9 @@ const triggerSpeciesSortByPercent = () => {
   })
 }
 
-const validatePercent = (percent: any): boolean | string => {
-  const isValid = speciesInfoValidator.validatePercent(percent)
-  if (!isValid) {
+const validatePercent = (percent: string | null): boolean | string => {
+  const validationResult = speciesInfoValidation.validatePercent(percent)
+  if (!validationResult.isValid) {
     return MESSAGE.MDL_PRM_INPUT_ERR.SPCZ_VLD_INPUT_RANGE(
       CONSTANTS.NUM_INPUT_LIMITS.SPECIES_PERCENT_MIN,
       CONSTANTS.NUM_INPUT_LIMITS.SPECIES_PERCENT_MAX,