Overview

The Questionnaire Scoring Configuration system in @admin-lite provides advanced scoring capabilities for questionnaires, enabling organizations to implement sophisticated evaluation criteria and automated compliance checking. This feature is currently behind the QNR_SCORING_CONFIG feature flag and extends the base questionnaire functionality with enhanced scoring options.

For the foundation scoring concepts and formula system, refer to Formula Builder Admin Frontend and scoring-formulas.

Key Capabilities

  • Multiple Scoring Methods: Percentage, total score, weighted scoring
  • Pass/Fail Criteria: Configurable thresholds and conditions
  • Category Weights: Different importance levels per category
  • Deduction System: Negative scoring for critical failures
  • Scoring Questions: New question types specifically for scoring
  • Visual Configuration: Intuitive UI for scoring setup

Architecture

Implementation Structure

app/admin/questionnaires/
├── _components/
│   └── questionnaire-editor/
│       └── scoring-configuration.tsx    # Scoring settings UI
├── _domain/
│   └── questionnaire-dtos.ts           # Enhanced DTOs with scoring
└── _lib/
    ├── questionnaire-validation.ts      # Scoring validation rules
    └── questionnaire-payload-transformer.ts # Scoring transformations

Feature Flag Implementation

// Feature flag check
const ENABLE_SCORING = process.env.NEXT_PUBLIC_QNR_SCORING_CONFIG === 'true';
 
// Conditional rendering
{ENABLE_SCORING && (
  <ScoringConfiguration
    questionnaire={questionnaire}
    onChange={handleScoringChange}
  />
)}

Scoring Configuration Options

1. Pass Criteria Types

enum PassingType {
  TOTAL_SCORE = 'Total Score',
  TOTAL_PERCENTAGE = 'Total Percentage',
  TOTAL_FAILED = 'Total Failed',
  TOTAL_SCORE_BY_QUESTION = 'Total Score By Question',
  TOTAL_SCORE_BY_CATEGORY = 'Total Score By Category',
  TOTAL_PERCENTAGE_BY_QUESTION = 'Total Percentage By Question',
  TOTAL_PERCENTAGE_BY_CATEGORY = 'Total Percentage By Category'
}

2. Pass Criteria Operators

enum PassingCriteria {
  EQUAL_TO = 'Is equal to',
  NOT_EQUAL_TO = 'Not equal to',
  GREATER_THAN = 'Greater than',
  GREATER_THAN_OR_EQUAL = 'Greater than or equal to',
  LESS_THAN = 'Less than',
  LESS_THAN_OR_EQUAL = 'Less than or equal to',
  BETWEEN = 'Between' // Uses { lowerBound, upperBound }
}

3. Configuration Interface

interface ScoringConfig {
  // Pass/Fail Settings
  passingType?: PassingType;
  passingCriteria?: PassingCriteria;
  passingValue?: number | { lowerBound: number; upperBound: number };
  
  // Deduction Settings
  deductionToggle?: boolean;
  maxScore?: number;
  
  // Category Weights (per category)
  categoryWeights?: Record<string, number>;
}

Scoring Question Types

1. Score Question

Enhanced numeric scoring with advanced features:

interface ScoreQuestion extends BaseQuestion {
  type: 'score';
  score: number;              // Maximum score
  scoreWeight: number;        // Weight multiplier
  thresholdValue?: number;    // Pass threshold
  negativeScoreToggle?: boolean;
  topScoreLabel?: string;     // e.g., "Excellent"
  bottomScoreLabel?: string;  // e.g., "Poor"
  scaleType?: string;
}

2. Multiple Choice with Score

Each choice has an associated score:

interface MultipleChoiceScoreQuestion extends BaseQuestion {
  type: 'multiple-choice-score';
  multipleChoiceScore: Array<{
    choice: string;
    score: number;
    flag?: 'green' | 'yellow' | 'red';
  }>;
  scoreWeight: number;
}

3. Range with Flags

Score based on value ranges:

interface RangeFlagQuestion extends BaseQuestion {
  type: 'range-flag';
  minRange: number;
  maxRange: number;
  rangeOptions: Array<{
    rangeFrom: number;
    rangeTo: number;
    flag: 'green' | 'yellow' | 'red';
    score?: number;  // Optional score per range
  }>;
}

UI Components

Scoring Configuration Panel

<ScoringConfiguration>
  <PassCriteriaSection>
    <Select 
      label="Passing Type" 
      options={passingTypes}
      value={questionnaire.passingType}
    />
    <Select 
      label="Criteria" 
      options={passingCriteria}
      value={questionnaire.passingCriteria}
    />
    <NumberInput 
      label="Passing Value"
      value={questionnaire.passingValue}
    />
  </PassCriteriaSection>
  
  <DeductionSettings>
    <Switch 
      label="Enable Deductions"
      checked={questionnaire.deductionToggle}
    />
    <NumberInput 
      label="Maximum Score"
      value={questionnaire.maxScore}
      disabled={!questionnaire.deductionToggle}
    />
  </DeductionSettings>
  
  <CategoryWeights>
    {categories.map(category => (
      <CategoryWeight
        key={category.id}
        category={category}
        weight={categoryWeights[category.id]}
        onChange={handleWeightChange}
      />
    ))}
  </CategoryWeights>
</ScoringConfiguration>

Visual Scoring Preview

<ScoringPreview questionnaire={questionnaire}>
  <ScoringSummary>
    <TotalPossibleScore value={calculateMaxScore()} />
    <PassingScore value={calculatePassingScore()} />
    <ScoreBreakdown categories={categories} />
  </ScoringSummary>
</ScoringPreview>

Scoring Calculation Logic

1. Total Score Calculation

function calculateTotalScore(responses: QuestionResponse[]): number {
  return responses.reduce((total, response) => {
    const question = getQuestion(response.questionId);
    
    if (question.type === 'score') {
      return total + (response.score * question.scoreWeight);
    }
    
    if (question.type === 'multiple-choice-score') {
      const selectedChoice = question.multipleChoiceScore.find(
        choice => choice.choice === response.answer
      );
      return total + (selectedChoice?.score || 0) * question.scoreWeight;
    }
    
    return total;
  }, 0);
}

2. Pass/Fail Evaluation

function evaluatePassCriteria(
  score: number,
  config: ScoringConfig
): boolean {
  const { passingCriteria, passingValue, passingType } = config;
  
  // Convert to percentage if needed
  const evaluationValue = passingType.includes('Percentage') 
    ? (score / maxScore) * 100 
    : score;
  
  switch (passingCriteria) {
    case 'Greater than or equal to':
      return evaluationValue >= passingValue;
    case 'Between':
      return evaluationValue >= passingValue.lowerBound && 
             evaluationValue <= passingValue.upperBound;
    // ... other criteria
  }
}

3. Deduction Logic

function applyDeductions(
  baseScore: number,
  failedQuestions: Question[],
  config: ScoringConfig
): number {
  if (!config.deductionToggle) return baseScore;
  
  const deductions = failedQuestions.reduce((total, question) => {
    // Apply deduction based on question configuration
    return total + getDeductionForQuestion(question);
  }, 0);
  
  // Start from maxScore and apply deductions
  return Math.max(0, config.maxScore - deductions);
}

Integration with Formula Builder

The scoring configuration works in conjunction with the Formula Builder:

  1. Formula-based Scoring: Complex formulas can reference scoring questions
  2. Conditional Scoring: Different scores based on conditions
  3. Weighted Formulas: Incorporate category weights
  4. Grade Assignment: Automatic grade based on score

See Formula Builder Admin Frontend for formula creation details.


Validation Rules

Scoring-Specific Validations

const scoringValidations = {
  // Score questions must have weight > 0
  scoreWeight: (question: ScoreQuestion) => {
    if (question.scoreWeight <= 0) {
      return 'Score weight must be greater than 0';
    }
  },
  
  // Pass value must be valid for criteria
  passingValue: (config: ScoringConfig) => {
    if (config.passingCriteria === 'Between') {
      if (!config.passingValue?.lowerBound || !config.passingValue?.upperBound) {
        return 'Both bounds required for Between criteria';
      }
    }
  },
  
  // Category weights must total 100% (if used)
  categoryWeights: (weights: Record<string, number>) => {
    const total = Object.values(weights).reduce((sum, w) => sum + w, 0);
    if (Math.abs(total - 100) > 0.01) {
      return 'Category weights must total 100%';
    }
  }
};

Best Practices

Scoring Design Guidelines

  1. Clear Objectives: Define what constitutes success
  2. Balanced Weights: Distribute importance appropriately
  3. Reasonable Thresholds: Set achievable pass criteria
  4. Consistent Scales: Use similar scoring ranges
  5. Test Thoroughly: Validate scoring logic with examples

Implementation Tips

// Use constants for scoring configuration
const SCORING_DEFAULTS = {
  passingType: 'Total Percentage',
  passingCriteria: 'Greater than or equal to',
  passingValue: 80,
  deductionToggle: false,
  maxScore: 100
};
 
// Validate scoring configuration before save
const validateScoring = (config: ScoringConfig): ValidationResult => {
  const errors = [];
  
  if (config.passingType && !config.passingCriteria) {
    errors.push('Pass criteria required when pass type is set');
  }
  
  if (config.deductionToggle && !config.maxScore) {
    errors.push('Maximum score required when deductions enabled');
  }
  
  return { isValid: errors.length === 0, errors };
};

Examples

Example 1: Safety Audit with Deductions

const safetyAudit = {
  title: "Safety Compliance Audit",
  passingType: "Total Score",
  passingCriteria: "Greater than or equal to",
  passingValue: 80,
  deductionToggle: true,
  maxScore: 100,
  
  categories: [
    {
      name: "Critical Safety",
      weight: 50,
      questions: [
        {
          type: "binary",
          content: "Are emergency exits clear?",
          deductionValue: 20  // Major deduction
        }
      ]
    },
    {
      name: "General Safety",
      weight: 50,
      questions: [
        {
          type: "score",
          content: "Rate workplace cleanliness",
          score: 10,
          scoreWeight: 2
        }
      ]
    }
  ]
};

Example 2: Quality Assessment with Percentage

const qualityAssessment = {
  title: "Product Quality Check",
  passingType: "Total Percentage",
  passingCriteria: "Between",
  passingValue: { lowerBound: 75, upperBound: 100 },
  
  categories: [
    {
      name: "Visual Inspection",
      weight: 30,
      questions: [
        {
          type: "multiple-choice-score",
          content: "Product appearance",
          multipleChoiceScore: [
            { choice: "Excellent", score: 10 },
            { choice: "Good", score: 7 },
            { choice: "Acceptable", score: 5 },
            { choice: "Poor", score: 0 }
          ],
          scoreWeight: 1
        }
      ]
    }
  ]
};

Future Enhancements

Planned Improvements

  1. Score Templates: Pre-configured scoring patterns
  2. Dynamic Scoring: Adjust scores based on context
  3. Score History: Track score changes over time
  4. Benchmarking: Compare scores across locations
  5. AI Scoring: Machine learning-based scoring

Under Consideration

  • Custom scoring algorithms
  • Multi-dimensional scoring
  • Peer comparison scoring
  • Time-based score decay
  • Gamification elements

Core Systems

Implementation

  • @admin-lite - Frontend implementation
  • Scoring API Documentation - Backend details
  • Validation Rules - Complete validation reference