Skip to content

Commit 99bbf5d

Browse files
committed
fix: evoting tie logic
1 parent e07e51e commit 99bbf5d

File tree

3 files changed

+68
-11
lines changed

3 files changed

+68
-11
lines changed

platforms/eVoting/src/app/(app)/[id]/page.tsx

Lines changed: 22 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -403,7 +403,12 @@ export default function Vote({ params }: { params: Promise<{ id: string }> }) {
403403
<span className="font-medium text-gray-900">
404404
{result.optionText || `Option ${index + 1}`}
405405
</span>
406-
{isWinner && (
406+
{result.isTied && (
407+
<Badge variant="success" className="bg-blue-500 text-white">
408+
🏆 Tied
409+
</Badge>
410+
)}
411+
{isWinner && !result.isTied && (
407412
<Badge variant="success" className="bg-green-500 text-white">
408413
🏆 Winner
409414
</Badge>
@@ -416,7 +421,7 @@ export default function Vote({ params }: { params: Promise<{ id: string }> }) {
416421
<div className="w-full bg-gray-200 rounded-full h-2">
417422
<div
418423
className={`h-2 rounded-full ${
419-
isWinner ? 'bg-green-500' : 'bg-red-500'
424+
result.isTied ? 'bg-blue-500' : isWinner ? 'bg-green-500' : 'bg-red-500'
420425
}`}
421426
style={{
422427
width: `${percentage}%`,
@@ -478,8 +483,14 @@ export default function Vote({ params }: { params: Promise<{ id: string }> }) {
478483
percentage = resultsData.totalVotes > 0 ? (result.totalPoints / resultsData.results.reduce((sum, r) => sum + r.totalPoints, 0)) * 100 : 0;
479484
} else if (resultsData.mode === "rank") {
480485
// Rank-based voting: show winner status instead of misleading vote counts
481-
displayValue = result.isWinner ? "🏆 Winner" : "Eliminated";
482-
isWinner = result.isWinner || false; // Use the isWinner flag from backend
486+
if (result.isTied) {
487+
displayValue = "🏆 Tied Winner";
488+
} else if (result.isWinner) {
489+
displayValue = "🏆 Winner";
490+
} else {
491+
displayValue = "Eliminated";
492+
}
493+
isWinner = result.isWinner || result.isTied || false; // Both winners and tied winners are "winners"
483494
percentage = result.percentage || 0; // Use the percentage from backend
484495

485496
// Check if there might have been a tie situation
@@ -508,7 +519,12 @@ export default function Vote({ params }: { params: Promise<{ id: string }> }) {
508519
<span className="font-medium text-gray-900">
509520
{result.option}
510521
</span>
511-
{isWinner && (
522+
{result.isTied && (
523+
<Badge variant="success" className="bg-blue-500 text-white">
524+
🏆 Tied
525+
</Badge>
526+
)}
527+
{isWinner && !result.isTied && (
512528
<Badge variant="success" className="bg-green-500 text-white">
513529
🏆 Winner
514530
</Badge>
@@ -521,7 +537,7 @@ export default function Vote({ params }: { params: Promise<{ id: string }> }) {
521537
<div className="w-full bg-gray-200 rounded-full h-2">
522538
<div
523539
className={`h-2 rounded-full ${
524-
isWinner ? 'bg-green-500' : 'bg-red-500'
540+
result.isTied ? 'bg-blue-500' : isWinner ? 'bg-green-500' : 'bg-red-500'
525541
}`}
526542
style={{
527543
width: `${percentage}%`,

platforms/eVoting/src/lib/pollApi.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -72,6 +72,7 @@ export interface PollResults {
7272
totalPoints?: number;
7373
averagePoints?: number;
7474
isWinner?: boolean;
75+
isTied?: boolean;
7576
finalRound?: number;
7677
}[];
7778
// Detailed IRV info for rank mode

platforms/evoting-api/src/services/VoteService.ts

Lines changed: 45 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -298,8 +298,28 @@ export class VoteService {
298298

299299
// Convert IRV results to the same format as other voting modes for consistent frontend rendering
300300
let results;
301-
if (irvResult.winnerIndex !== null) {
302-
// Create results array with winner first, then others in order
301+
if (irvResult.isTie) {
302+
// Handle tie situation - mark tied candidates as winners
303+
results = poll.options.map((option, index) => {
304+
const isTiedWinner = irvResult.tiedCandidates && irvResult.tiedCandidates.includes(index);
305+
return {
306+
option,
307+
votes: isTiedWinner ? votes.length : 0, // Tied winners share all votes
308+
percentage: isTiedWinner ? (100 / (irvResult.tiedCandidates?.length || 1)) : 0, // Split percentage among tied winners
309+
isWinner: isTiedWinner,
310+
isTied: isTiedWinner,
311+
finalRound: irvResult.rounds.length
312+
};
313+
});
314+
315+
// Sort: tied winners first, then by original option order
316+
results.sort((a, b) => {
317+
if (a.isWinner && !b.isWinner) return -1;
318+
if (!a.isWinner && b.isWinner) return 1;
319+
return poll.options.indexOf(a.option) - poll.options.indexOf(b.option);
320+
});
321+
} else if (irvResult.winnerIndex !== null) {
322+
// Single winner case
303323
results = poll.options.map((option, index) => {
304324
if (index === irvResult.winnerIndex) {
305325
return {
@@ -327,7 +347,7 @@ export class VoteService {
327347
return poll.options.indexOf(a.option) - poll.options.indexOf(b.option);
328348
});
329349
} else {
330-
// No winner determined, show all options with 0 votes
350+
// No winner determined (exhausted), show all options with 0 votes
331351
results = poll.options.map((option, index) => ({
332352
option,
333353
votes: 0,
@@ -782,7 +802,7 @@ export class VoteService {
782802
const allSame = firstChoiceValues.length > 0 && firstChoiceValues.every(v => v === firstChoiceValues[0]);
783803

784804
if (allSame && firstChoiceValues.length > 1) {
785-
console.warn(`⚠️ IRV Initial Tie: All candidates have the same number of first-choice votes (${firstChoiceValues[0]}). This will result in arbitrary elimination.`);
805+
console.log(`ℹ️ IRV Initial Tie: All candidates have the same number of first-choice votes (${firstChoiceValues[0]}). Will declare a tie.`);
786806
}
787807

788808
// Initialize IRV process
@@ -841,6 +861,24 @@ export class VoteService {
841861
rejectedReasons
842862
};
843863
}
864+
865+
// Check for tie (all remaining candidates have same votes)
866+
const voteCounts = activeCandidates.map(c => counts[c] || 0);
867+
const allSameVotes = voteCounts.length > 0 && voteCounts.every(v => v === voteCounts[0]);
868+
869+
if (allSameVotes && activeCandidates.length > 1) {
870+
console.log(`ℹ️ IRV Tie detected: All remaining candidates have ${voteCounts[0]} votes. Declaring tie.`);
871+
return {
872+
winnerIndex: null,
873+
winnerOption: undefined,
874+
tiedCandidates: activeCandidates,
875+
tiedOptions: activeCandidates.map(i => options[i]),
876+
rounds,
877+
rejectedBallots,
878+
rejectedReasons,
879+
isTie: true
880+
};
881+
}
844882

845883
// If only one candidate remains, they win
846884
if (activeCandidates.length === 1) {
@@ -860,7 +898,9 @@ export class VoteService {
860898
winnerOption: undefined,
861899
rounds,
862900
rejectedBallots,
863-
rejectedReasons
901+
rejectedReasons,
902+
isTie: false,
903+
exhausted: true
864904
};
865905
}
866906

0 commit comments

Comments
 (0)