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:
- Formula-based Scoring: Complex formulas can reference scoring questions
- Conditional Scoring: Different scores based on conditions
- Weighted Formulas: Incorporate category weights
- 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
- Clear Objectives: Define what constitutes success
- Balanced Weights: Distribute importance appropriately
- Reasonable Thresholds: Set achievable pass criteria
- Consistent Scales: Use similar scoring ranges
- 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
- Score Templates: Pre-configured scoring patterns
- Dynamic Scoring: Adjust scores based on context
- Score History: Track score changes over time
- Benchmarking: Compare scores across locations
- AI Scoring: Machine learning-based scoring
Under Consideration
- Custom scoring algorithms
- Multi-dimensional scoring
- Peer comparison scoring
- Time-based score decay
- Gamification elements
Related Documentation
Core Systems
- Questionnaires - Main questionnaire system
- Formula Builder Admin Frontend - Formula creation
- Questionnaire Create Admin Frontend V2 - V2 editor
Implementation
- @admin-lite - Frontend implementation
- Scoring API Documentation - Backend details
- Validation Rules - Complete validation reference