Add defensive null guards to all chart components and analytics
All checks were successful
Build and Push Docker Image / build (push) Successful in 8m59s
All checks were successful
Build and Push Docker Image / build (push) Successful in 8m59s
All 9 chart components now have early-return null/empty checks before calling .map() on data props. The diversity-metrics chart guards all nested array fields (byCountry, byCategory, byOceanIssue, byTag). Analytics backend guards p.tags in getDiversityMetrics. This prevents any "Cannot read properties of null (reading 'map')" crashes even if upstream data shapes are unexpected. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -23,6 +23,8 @@ type CriterionBarDatum = {
|
||||
}
|
||||
|
||||
export function CriteriaScoresChart({ data }: CriteriaScoresProps) {
|
||||
if (!data?.length) return null
|
||||
|
||||
const overallAverage =
|
||||
data.length > 0
|
||||
? data.reduce((sum, d) => sum + d.averageScore, 0) / data.length
|
||||
|
||||
@@ -21,6 +21,16 @@ interface CrossStageComparisonProps {
|
||||
export function CrossStageComparisonChart({
|
||||
data,
|
||||
}: CrossStageComparisonProps) {
|
||||
if (!data?.length) {
|
||||
return (
|
||||
<Card>
|
||||
<CardContent className="flex items-center justify-center py-12">
|
||||
<p className="text-muted-foreground">No comparison data available</p>
|
||||
</CardContent>
|
||||
</Card>
|
||||
)
|
||||
}
|
||||
|
||||
const baseData = data.map((round) => ({
|
||||
name:
|
||||
round.roundName.length > 20
|
||||
|
||||
@@ -39,7 +39,7 @@ function formatLabel(value: string): string {
|
||||
}
|
||||
|
||||
export function DiversityMetricsChart({ data }: DiversityMetricsProps) {
|
||||
if (data.total === 0) {
|
||||
if (!data || data.total === 0) {
|
||||
return (
|
||||
<Card>
|
||||
<CardContent className="flex items-center justify-center py-12">
|
||||
@@ -50,8 +50,8 @@ export function DiversityMetricsChart({ data }: DiversityMetricsProps) {
|
||||
}
|
||||
|
||||
// Top countries for pie chart (max 10, others grouped)
|
||||
const topCountries = data.byCountry.slice(0, 10)
|
||||
const otherCountries = data.byCountry.slice(10)
|
||||
const topCountries = (data.byCountry || []).slice(0, 10)
|
||||
const otherCountries = (data.byCountry || []).slice(10)
|
||||
const countryPieData = otherCountries.length > 0
|
||||
? [...topCountries, {
|
||||
country: 'Others',
|
||||
@@ -67,12 +67,12 @@ export function DiversityMetricsChart({ data }: DiversityMetricsProps) {
|
||||
}))
|
||||
|
||||
// Pre-format category and ocean issue data for display
|
||||
const formattedCategories = data.byCategory.slice(0, 10).map((c) => ({
|
||||
const formattedCategories = (data.byCategory || []).slice(0, 10).map((c) => ({
|
||||
category: formatLabel(c.category),
|
||||
count: c.count,
|
||||
}))
|
||||
|
||||
const formattedOceanIssues = data.byOceanIssue.slice(0, 15).map((o) => ({
|
||||
const formattedOceanIssues = (data.byOceanIssue || []).slice(0, 15).map((o) => ({
|
||||
issue: formatLabel(o.issue),
|
||||
count: o.count,
|
||||
}))
|
||||
@@ -89,19 +89,19 @@ export function DiversityMetricsChart({ data }: DiversityMetricsProps) {
|
||||
</Card>
|
||||
<Card>
|
||||
<CardContent className="pt-6">
|
||||
<div className="text-2xl font-bold">{data.byCountry.length}</div>
|
||||
<div className="text-2xl font-bold">{(data.byCountry || []).length}</div>
|
||||
<p className="text-sm text-muted-foreground">Countries Represented</p>
|
||||
</CardContent>
|
||||
</Card>
|
||||
<Card>
|
||||
<CardContent className="pt-6">
|
||||
<div className="text-2xl font-bold">{data.byCategory.length}</div>
|
||||
<div className="text-2xl font-bold">{(data.byCategory || []).length}</div>
|
||||
<p className="text-sm text-muted-foreground">Categories</p>
|
||||
</CardContent>
|
||||
</Card>
|
||||
<Card>
|
||||
<CardContent className="pt-6">
|
||||
<div className="text-2xl font-bold">{data.byTag.length}</div>
|
||||
<div className="text-2xl font-bold">{(data.byTag || []).length}</div>
|
||||
<p className="text-sm text-muted-foreground">Unique Tags</p>
|
||||
</CardContent>
|
||||
</Card>
|
||||
@@ -228,14 +228,14 @@ export function DiversityMetricsChart({ data }: DiversityMetricsProps) {
|
||||
)}
|
||||
|
||||
{/* Tags Cloud */}
|
||||
{data.byTag.length > 0 && (
|
||||
{(data.byTag || []).length > 0 && (
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle>Project Tags</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<div className="flex flex-wrap gap-2">
|
||||
{data.byTag.slice(0, 30).map((tag) => (
|
||||
{(data.byTag || []).slice(0, 30).map((tag) => (
|
||||
<Badge
|
||||
key={tag.tag}
|
||||
variant="secondary"
|
||||
|
||||
@@ -15,6 +15,8 @@ interface EvaluationTimelineProps {
|
||||
}
|
||||
|
||||
export function EvaluationTimelineChart({ data }: EvaluationTimelineProps) {
|
||||
if (!data?.length) return null
|
||||
|
||||
const formattedData = data.map((d) => ({
|
||||
...d,
|
||||
dateFormatted: new Date(d.date).toLocaleDateString('en-US', {
|
||||
|
||||
@@ -91,6 +91,16 @@ function CustomNode({
|
||||
}
|
||||
|
||||
export function JurorConsistencyChart({ data }: JurorConsistencyProps) {
|
||||
if (!data?.jurors?.length) {
|
||||
return (
|
||||
<Card>
|
||||
<CardContent className="flex items-center justify-center py-12">
|
||||
<p className="text-muted-foreground">No juror consistency data available</p>
|
||||
</CardContent>
|
||||
</Card>
|
||||
)
|
||||
}
|
||||
|
||||
const scatterData = [
|
||||
{
|
||||
id: 'Jurors',
|
||||
|
||||
@@ -25,6 +25,8 @@ type WorkloadBarDatum = {
|
||||
}
|
||||
|
||||
export function JurorWorkloadChart({ data }: JurorWorkloadProps) {
|
||||
if (!data?.length) return null
|
||||
|
||||
const totalAssigned = data.reduce((sum, d) => sum + d.assigned, 0)
|
||||
const totalCompleted = data.reduce((sum, d) => sum + d.completed, 0)
|
||||
const overallRate =
|
||||
|
||||
@@ -30,6 +30,8 @@ export function ProjectRankingsChart({
|
||||
data,
|
||||
limit = 20,
|
||||
}: ProjectRankingsProps) {
|
||||
if (!data?.length) return null
|
||||
|
||||
const scoredData = data.filter(
|
||||
(d): d is ProjectRankingData & { averageScore: number } =>
|
||||
d.averageScore !== null,
|
||||
|
||||
@@ -15,6 +15,8 @@ export function ScoreDistributionChart({
|
||||
averageScore,
|
||||
totalScores,
|
||||
}: ScoreDistributionProps) {
|
||||
if (!data?.length) return null
|
||||
|
||||
const chartData = data.map((d) => ({
|
||||
score: String(d.score),
|
||||
count: d.count,
|
||||
|
||||
@@ -14,6 +14,8 @@ interface StatusBreakdownProps {
|
||||
}
|
||||
|
||||
export function StatusBreakdownChart({ data }: StatusBreakdownProps) {
|
||||
if (!data?.length) return null
|
||||
|
||||
const total = data.reduce((sum, item) => sum + item.count, 0)
|
||||
|
||||
const pieData = data.map((d) => ({
|
||||
|
||||
@@ -615,7 +615,7 @@ export const analyticsRouter = router({
|
||||
// By tag
|
||||
const tagCounts: Record<string, number> = {}
|
||||
projects.forEach((p) => {
|
||||
p.tags.forEach((tag) => {
|
||||
(p.tags || []).forEach((tag) => {
|
||||
tagCounts[tag] = (tagCounts[tag] || 0) + 1
|
||||
})
|
||||
})
|
||||
|
||||
Reference in New Issue
Block a user