diff --git a/src/actions/challenges.js b/src/actions/challenges.js
index 74a7252d..187d1f1e 100644
--- a/src/actions/challenges.js
+++ b/src/actions/challenges.js
@@ -23,6 +23,7 @@ import {
updateChallengeSkillsApi,
fetchDefaultReviewers,
fetchScorecards,
+ fetchScorecardById,
fetchWorkflows
} from '../services/challenges'
import { searchProfilesByUserIds } from '../services/user'
@@ -794,6 +795,34 @@ export function loadScorecards (filters = {}) {
}
}
+/**
+ * Load a specific scorecard by ID
+ * @param {string} scorecardId the scorecard ID
+ */
+export function loadScorecardById (scorecardId) {
+ return async (dispatch) => {
+ try {
+ const scorecard = await fetchScorecardById(scorecardId)
+ dispatch({
+ type: LOAD_CHALLENGE_METADATA_SUCCESS,
+ metadataKey: 'scorecardById',
+ metadataValue: {
+ ...scorecard,
+ id: scorecardId
+ }
+ })
+ } catch (error) {
+ console.error('Error loading scorecard by ID:', error)
+ // Return null on error
+ dispatch({
+ type: LOAD_CHALLENGE_METADATA_SUCCESS,
+ metadataKey: 'scorecardById',
+ metadataValue: null
+ })
+ }
+ }
+}
+
/**
* Load default reviewers
* @param {Object} filters filters for default reviewers
diff --git a/src/components/ChallengeEditor/ChallengeReviewer-Field/ChallengeReviewer-Field.module.scss b/src/components/ChallengeEditor/ChallengeReviewer-Field/ChallengeReviewer-Field.module.scss
index 98c5d72d..3dfcc85e 100644
--- a/src/components/ChallengeEditor/ChallengeReviewer-Field/ChallengeReviewer-Field.module.scss
+++ b/src/components/ChallengeEditor/ChallengeReviewer-Field/ChallengeReviewer-Field.module.scss
@@ -186,11 +186,7 @@
.error {
color: $tc-red;
- background-color: #ffebee;
- padding: 15px;
- border-radius: 4px;
- border: 1px solid #ffcdd2;
- margin-bottom: 20px;
+ padding: 5px;
}
.validationErrors {
@@ -211,13 +207,6 @@
margin-bottom: 0;
}
-.fieldError {
- color: #DC3545;
- font-size: 12px;
- margin-top: 4px;
- display: block;
-}
-
// Responsive adjustments
@media (max-width: 768px) {
.formRow {
diff --git a/src/components/ChallengeEditor/ChallengeReviewer-Field/index.js b/src/components/ChallengeEditor/ChallengeReviewer-Field/index.js
index 175fcc3b..7befe679 100644
--- a/src/components/ChallengeEditor/ChallengeReviewer-Field/index.js
+++ b/src/components/ChallengeEditor/ChallengeReviewer-Field/index.js
@@ -4,7 +4,7 @@ import { connect } from 'react-redux'
import cn from 'classnames'
import { PrimaryButton, OutlineButton } from '../../Buttons'
import { REVIEW_OPPORTUNITY_TYPE_LABELS, REVIEW_OPPORTUNITY_TYPES, VALIDATION_VALUE_TYPE } from '../../../config/constants'
-import { loadScorecards, loadDefaultReviewers, loadWorkflows } from '../../../actions/challenges'
+import { loadScorecards, loadDefaultReviewers, loadWorkflows, loadScorecardById } from '../../../actions/challenges'
import styles from './ChallengeReviewer-Field.module.scss'
import { convertDollarToInteger, validateValue } from '../../../util/input-check'
@@ -31,18 +31,28 @@ class ChallengeReviewerField extends Component {
}
componentDidMount () {
- this.loadScorecards()
+ if (this.props.readOnly) {
+ // In read-only mode, only load specific scorecards for existing reviewers
+ this.loadSpecificScorecards()
+ } else {
+ // In edit mode, load all scorecards for dropdown
+ this.loadScorecards()
+ }
this.loadDefaultReviewers()
this.loadWorkflows()
}
componentDidUpdate (prevProps) {
- const { challenge } = this.props
+ const { challenge, readOnly } = this.props
const prevChallenge = prevProps.challenge
if (challenge && prevChallenge &&
(challenge.type !== prevChallenge.type || challenge.track !== prevChallenge.track)) {
- this.loadScorecards()
+ if (readOnly) {
+ this.loadSpecificScorecards()
+ } else {
+ this.loadScorecards()
+ }
}
if (challenge && prevChallenge &&
@@ -69,6 +79,27 @@ class ChallengeReviewerField extends Component {
loadScorecards(filters)
}
+ loadSpecificScorecards () {
+ const { challenge, loadScorecardById } = this.props
+ const reviewers = challenge.reviewers || []
+
+ // Get unique scorecard IDs from reviewers
+ const scorecardIds = [...new Set(
+ reviewers
+ .filter(reviewer => reviewer.scorecardId)
+ .map(reviewer => reviewer.scorecardId)
+ )]
+
+ if (scorecardIds.length === 0) {
+ return
+ }
+
+ // Load each scorecard individually
+ scorecardIds.forEach(scorecardId => {
+ loadScorecardById(scorecardId)
+ })
+ }
+
loadDefaultReviewers () {
const { challenge, loadDefaultReviewers } = this.props
@@ -240,7 +271,7 @@ class ChallengeReviewerField extends Component {
renderReviewerForm (reviewer, index) {
const { challenge, metadata = {}, readOnly = false } = this.props
const { scorecards = [], workflows = [] } = metadata
- const validationErrors = this.validateReviewer(reviewer)
+ const validationErrors = challenge.submitTriggered ? this.validateReviewer(reviewer) : {}
return (
@@ -337,8 +368,10 @@ class ChallengeReviewerField extends Component {
))}
)}
- {validationErrors.aiWorkflowId && (
-
{validationErrors.aiWorkflowId}
+ {!readOnly && challenge.submitTriggered && validationErrors.aiWorkflowId && (
+
+ {validationErrors.aiWorkflowId}
+
)}
) : (
@@ -347,8 +380,15 @@ class ChallengeReviewerField extends Component {
{readOnly ? (
{(() => {
+ const { metadata = {} } = this.props
+ const specificScorecard = metadata.scorecardById
+ if (specificScorecard && specificScorecard.id === reviewer.scorecardId) {
+ return `${specificScorecard.name || 'Unknown'} - ${specificScorecard.type || 'Unknown'} (${specificScorecard.challengeTrack || 'Unknown'}) v${specificScorecard.version || 'Unknown'}`
+ }
+
+ // Fallback to searching in the general scorecards array
const scorecard = scorecards.find(s => s.id === reviewer.scorecardId)
- return scorecard ? `${scorecard.name} - ${scorecard.type} (${scorecard.challengeTrack}) v${scorecard.version}` : 'Not selected'
+ return scorecard ? `${scorecard.name || 'Unknown'} - ${scorecard.type || 'Unknown'} (${scorecard.challengeTrack || 'Unknown'}) v${scorecard.version || 'Unknown'}` : 'Not selected'
})()}
) : (
@@ -359,13 +399,15 @@ class ChallengeReviewerField extends Component {
{scorecards.map(scorecard => (
))}
)}
- {validationErrors.scorecardId && (
- {validationErrors.scorecardId}
+ {!readOnly && challenge.submitTriggered && validationErrors.scorecardId && (
+
+ {validationErrors.scorecardId}
+
)}
)}
@@ -409,8 +451,10 @@ class ChallengeReviewerField extends Component {
))}
)}
- {validationErrors.phaseId && (
- {validationErrors.phaseId}
+ {!readOnly && challenge.submitTriggered && validationErrors.phaseId && (
+
+ {validationErrors.phaseId}
+
)}
@@ -433,8 +477,10 @@ class ChallengeReviewerField extends Component {
}}
/>
)}
- {validationErrors.memberReviewerCount && (
- {validationErrors.memberReviewerCount}
+ {!readOnly && challenge.submitTriggered && validationErrors.memberReviewerCount && (
+
+ {validationErrors.memberReviewerCount}
+
)}
@@ -453,8 +499,10 @@ class ChallengeReviewerField extends Component {
}}
/>
)}
- {validationErrors.basePayment && (
- {validationErrors.basePayment}
+ {!readOnly && challenge.submitTriggered && validationErrors.basePayment && (
+
+ {validationErrors.basePayment}
+
)}
@@ -617,13 +665,15 @@ ChallengeReviewerField.propTypes = {
metadata: PropTypes.shape({
scorecards: PropTypes.array,
defaultReviewers: PropTypes.array,
- workflows: PropTypes.array
+ workflows: PropTypes.array,
+ scorecardById: PropTypes.object
}),
isLoading: PropTypes.bool,
readOnly: PropTypes.bool,
loadScorecards: PropTypes.func.isRequired,
loadDefaultReviewers: PropTypes.func.isRequired,
- loadWorkflows: PropTypes.func.isRequired
+ loadWorkflows: PropTypes.func.isRequired,
+ loadScorecardById: PropTypes.func.isRequired
}
const mapStateToProps = (state) => ({
@@ -634,7 +684,8 @@ const mapStateToProps = (state) => ({
const mapDispatchToProps = {
loadScorecards,
loadDefaultReviewers,
- loadWorkflows
+ loadWorkflows,
+ loadScorecardById
}
export default connect(mapStateToProps, mapDispatchToProps)(ChallengeReviewerField)
diff --git a/src/components/ChallengeEditor/index.js b/src/components/ChallengeEditor/index.js
index b8d2cd87..913cc829 100644
--- a/src/components/ChallengeEditor/index.js
+++ b/src/components/ChallengeEditor/index.js
@@ -769,6 +769,46 @@ class ChallengeEditor extends Component {
return true
}
+ isValidReviewers () {
+ const { challenge } = this.state
+ const reviewers = challenge.reviewers || []
+
+ if (reviewers.length === 0) {
+ return true
+ }
+
+ // Validate each reviewer
+ for (const reviewer of reviewers) {
+ const isAI = (reviewer.aiWorkflowId && reviewer.aiWorkflowId.trim() !== '') || !reviewer.isMemberReview
+
+ if (isAI) {
+ if (!reviewer.aiWorkflowId || reviewer.aiWorkflowId.trim() === '') {
+ return false
+ }
+ } else {
+ if (!reviewer.scorecardId) {
+ return false
+ }
+
+ const memberCount = parseInt(reviewer.memberReviewerCount) || 1
+ if (memberCount < 1 || !Number.isInteger(memberCount)) {
+ return false
+ }
+
+ const basePayment = convertDollarToInteger(reviewer.basePayment || '0', '')
+ if (basePayment < 0) {
+ return false
+ }
+ }
+
+ if (!reviewer.phaseId) {
+ return false
+ }
+ }
+
+ return true
+ }
+
isValidChallenge () {
const { challenge } = this.state
if (this.props.isNew) {
@@ -790,6 +830,10 @@ class ChallengeEditor extends Component {
return false
}
+ if (!this.isValidReviewers()) {
+ return false
+ }
+
const requiredFields = [
'trackId',
'typeId',
diff --git a/src/services/challenges.js b/src/services/challenges.js
index 5363dfee..6a019f5e 100644
--- a/src/services/challenges.js
+++ b/src/services/challenges.js
@@ -335,6 +335,15 @@ export async function fetchScorecards (filters = {}) {
const response = await axiosInstance.get(`${SCORECARDS_API_URL}?${qs.stringify(query, { encode: false })}`)
return _.get(response, 'data', {})
}
+/**
+ * Api request for fetching a specific scorecard by ID
+ * @param {string} scorecardId the scorecard ID
+ * @returns {Promise<*>}
+ */
+export async function fetchScorecardById (scorecardId) {
+ const response = await axiosInstance.get(`${SCORECARDS_API_URL}/${scorecardId}`)
+ return _.get(response, 'data', {})
+}
/**
* Api request for fetching default reviewers
* @param {Object} filters filters for default reviewers