supportServices = {
const wait = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
const waitFor = async (getter, timeoutMs = 5000) => {
const deadline = Date.now() + timeoutMs;
while (Date.now() < deadline) {
const value = getter();
if (value) {
return value;
}
await wait(100);
}
return null;
};
const auth = await waitFor(() => {
const candidate = window.Auth;
return candidate &&
typeof candidate.getCurrentUser === 'function' &&
typeof candidate.isUserAuthenticated === 'function' &&
typeof candidate.isUserAdmin === 'function'
? candidate
: null;
});
const api = await waitFor(() => {
const candidate = window.IoTClassAPI;
return candidate && candidate.Badge && candidate.Admin ? candidate : null;
});
const badgeSystem = await waitFor(() => {
const candidate = window.BadgeSystem;
return candidate && (candidate.badgeCatalog || typeof candidate.init === 'function')
? candidate
: null;
}, 2000);
return {
auth,
api,
badgeSystem,
isAuthenticated: Boolean(auth?.isUserAuthenticated?.()),
};
}
// Colors
colors = ({
navy: '#2C3E50',
teal: '#16A085',
gray: '#7F8C8D'
})
// Check authentication and admin status
currentUser = {
const auth = supportServices.auth;
if (!auth) return null;
const user = auth.getCurrentUser();
if (!user) return null;
const isAdmin = auth.isUserAdmin();
return { ...user, isAdmin };
}
// Load badge configuration
badgeConfig = {
try {
const badgeSystem = supportServices.badgeSystem;
const badgeApi = supportServices.api?.Badge;
// Try to load from BadgeSystem if available
if (badgeSystem?.badgeCatalog) {
return badgeSystem.badgeCatalog;
}
// Fallback: try to load badge definitions from Supabase
if (!badgeApi?.getAllBadgeDefinitions) {
return [];
}
const definitions = await badgeApi.getAllBadgeDefinitions();
if (definitions && definitions.length > 0) {
return definitions;
}
return [];
} catch (error) {
console.error('Error loading badge config:', error);
return [];
}
}
// Load all badge awards via Supabase
allBadgeAwards = {
if (!currentUser || !currentUser.isAdmin) {
return null;
}
try {
const adminApi = supportServices.api?.Admin;
if (!adminApi?.getAllBadges) {
return null;
}
const badgeAwards = await adminApi.getAllBadges();
// Count awards per badge
const awardCounts = {};
badgeAwards.forEach(award => {
const badgeId = award.badge_id || 'unknown';
awardCounts[badgeId] = (awardCounts[badgeId] || 0) + 1;
});
return {
awards: badgeAwards,
counts: awardCounts,
total: badgeAwards.length,
uniqueRecipients: new Set(badgeAwards.map(a => a.user_id)).size
};
} catch (error) {
console.error('Error loading badge awards:', error);
return null;
}
}
// Calculate badge statistics
badgeStats = {
if (!badgeConfig || !allBadgeAwards) return null;
const totalBadges = badgeConfig.length;
const totalAwarded = allBadgeAwards.total;
const avgAwardsPerBadge = totalBadges > 0 ? (totalAwarded / totalBadges).toFixed(1) : 0;
// Most awarded badge
const sortedBadges = Object.entries(allBadgeAwards.counts)
.sort((a, b) => b[1] - a[1]);
const mostAwardedBadgeId = sortedBadges.length > 0 ? sortedBadges[0][0] : null;
const mostAwardedBadge = badgeConfig.find(b => b.id === mostAwardedBadgeId);
const mostAwardedCount = sortedBadges.length > 0 ? sortedBadges[0][1] : 0;
// Least awarded (but awarded at least once)
const awardedBadges = sortedBadges.filter(([id, count]) => count > 0);
const leastAwardedBadgeId = awardedBadges.length > 0 ? awardedBadges[awardedBadges.length - 1][0] : null;
const leastAwardedBadge = badgeConfig.find(b => b.id === leastAwardedBadgeId);
const leastAwardedCount = awardedBadges.length > 0 ? awardedBadges[awardedBadges.length - 1][1] : 0;
// Badges never awarded
const neverAwarded = badgeConfig.filter(badge => !allBadgeAwards.counts[badge.id]);
return {
totalBadges,
totalAwarded,
avgAwardsPerBadge,
mostAwardedBadge: mostAwardedBadge ? mostAwardedBadge.name : 'None',
mostAwardedCount,
leastAwardedBadge: leastAwardedBadge ? leastAwardedBadge.name : 'None',
leastAwardedCount,
neverAwarded: neverAwarded.length,
uniqueRecipients: allBadgeAwards.uniqueRecipients
};
}
// Filter state
viewState = ({ filterCategory: 'all', sortBy: 'awarded-desc' })
// Filtered and sorted badges
filteredBadges = {
if (!badgeConfig) return [];
let filtered = [...badgeConfig];
// Apply category filter
if (viewState.filterCategory !== 'all') {
filtered = filtered.filter(badge => badge.category === viewState.filterCategory);
}
// Add award counts
filtered = filtered.map(badge => ({
...badge,
awardCount: (allBadgeAwards?.counts[badge.id] || 0)
}));
// Apply sorting
filtered.sort((a, b) => {
switch (viewState.sortBy) {
case 'awarded-desc':
return b.awardCount - a.awardCount;
case 'awarded-asc':
return a.awardCount - b.awardCount;
case 'name-asc':
return a.name.localeCompare(b.name);
case 'name-desc':
return b.name.localeCompare(a.name);
case 'xp-desc':
return (b.xp_reward || 0) - (a.xp_reward || 0);
case 'xp-asc':
return (a.xp_reward || 0) - (b.xp_reward || 0);
default:
return 0;
}
});
return filtered;
}🏆 Badge Management
View Badge Distribution and Awards
TipPutting Numbers to It
Operational dashboards should track completion velocity:
\[ V_{\text{week}} = \frac{N_{\text{completed}}}{\text{week}}, \qquad P_{\text{complete}} = \frac{N_{\text{completed}}}{N_{\text{total}}} \]
Worked example: If 96 chapters are completed out of 120 total, then \(P_{\text{complete}}=80\%\). At 8 chapters/week, remaining effort is \((120-96)/8=3\) weeks.
🏆 Badge Management
View badge distribution, track awards, and manage achievement system.
Badge Management Tips
TipManaging Badges
Badge Categories:
- Learning Path: Awarded for completing parts or achieving learning milestones
- Engagement: Awarded for active participation (games, labs, quizzes)
- Milestone: Awarded for reaching XP or streak milestones
Rarity Levels:
- Common: Easy to achieve, frequent awards
- Uncommon: Moderate difficulty
- Rare: Difficult to achieve
- Legendary: Very rare, exceptional achievement
Future Features (Coming Soon): - Manual badge award tool for admins - Badge revocation (if awarded in error) - Custom badge creation - Badge expiration/renewal system