diff --git a/infrastructure/eid-wallet/src/routes/(app)/scan-qr/+page.svelte b/infrastructure/eid-wallet/src/routes/(app)/scan-qr/+page.svelte index ea8aef92..e0fe7d25 100644 --- a/infrastructure/eid-wallet/src/routes/(app)/scan-qr/+page.svelte +++ b/infrastructure/eid-wallet/src/routes/(app)/scan-qr/+page.svelte @@ -1171,86 +1171,6 @@ {signingData?.pollId ?? "Unknown"}

- -
-

Your Vote

-
- {#if signingData?.voteData?.optionId !== undefined} - -

- You selected: Option {parseInt(signingData.voteData.optionId) + - 1} -

-

- (This is the option number from the poll) -

- {:else if signingData?.voteData?.ranks} - -

Your ranking order:

-
- {#each Object.entries(signingData.voteData.ranks).sort(([a], [b]) => parseInt(a) - parseInt(b)) as [rank, optionIndex]} -
- - {rank === "1" - ? "1st" - : rank === "2" - ? "2nd" - : rank === "3" - ? "3rd" - : `${rank}th`} - - Option {parseInt(String(optionIndex)) + - 1} -
- {/each} -
-

- (1st = most preferred, 2nd = second choice, etc.) -

- {:else if signingData?.voteData?.points} - -

Your point distribution:

-
- {#each Object.entries(signingData.voteData.points) - .filter(([_, points]) => (points as number) > 0) - .sort(([a], [b]) => parseInt(a) - parseInt(b)) as [optionIndex, points]} -
- - {points} pts - - Option {parseInt(String(optionIndex)) + - 1} -
- {/each} -
-

- (Total: {Object.values( - signingData.voteData.points, - ).reduce( - (sum, points) => - (sum as number) + ((points as number) || 0), - 0, - )}/100 points) -

- {:else} -

Vote data not available

- {/if} -
-
{:else if isBlindVotingRequest && signingData?.pollDetails}
diff --git a/platforms/eVoting/src/app/(app)/[id]/page.tsx b/platforms/eVoting/src/app/(app)/[id]/page.tsx index 9a503ccd..b9b057b4 100644 --- a/platforms/eVoting/src/app/(app)/[id]/page.tsx +++ b/platforms/eVoting/src/app/(app)/[id]/page.tsx @@ -331,329 +331,270 @@ export default function Vote({ params }: { params: Promise<{ id: string }> }) { )}
- {voteStatus?.hasVoted === true ? ( + {/* Show results if poll has ended, regardless of user's vote status */} + {!isVotingAllowed ? (
- {/* Vote Distribution */} -
-
- {resultsData?.results.map((option, index) => { - const percentage = - resultsData.totalVotes > 0 - ? ( - ((option.votes || 0) / - resultsData.totalVotes) * - 100 - ).toFixed(1) - : 0; - const isUserChoice = - option.option === selectedPoll.options[index]; - const isLeading = resultsData.results.every( - (r) => option.votes >= r.votes - ); - - return ( -
0 - ? "bg-red-50 border-red-200" - : isUserChoice - ? "bg-blue-50 border-blue-200" - : "bg-gray-50 border-gray-200" - }`} - > -
- 0 - ? "text-red-900" - : isUserChoice - ? "text-blue-900" - : "text-gray-900" - }`} - > - {option.option} - - 0 - ? "text-red-700" - : isUserChoice - ? "text-blue-700" - : "text-gray-600" - }`} - > - {selectedPoll.mode === "rank" - ? `${option.votes || 0} points` - : selectedPoll.mode === "point" - ? `${option.votes || 0} points` - : `${option.votes || 0} votes`} ( - {percentage}%) - -
-
-
0 - ? "bg-red-500" - : isUserChoice - ? "bg-blue-500" - : "bg-gray-400" - }`} - style={{ - width: `${percentage}%`, - }} - /> + {/* For private polls that have ended, show final results */} + {selectedPoll.visibility === "private" ? ( +
+ {/* Final Results for Private Polls */} +
+

+ + Final Results +

+
+ {blindVoteResults?.optionResults && blindVoteResults.optionResults.length > 0 ? ( + blindVoteResults.optionResults.map((result, index) => { + const isWinner = result.voteCount === Math.max(...blindVoteResults.optionResults.map(r => r.voteCount)); + const percentage = blindVoteResults.totalVotes > 0 ? (result.voteCount / blindVoteResults.totalVotes) * 100 : 0; + return ( +
+
+
+ + {result.optionText || `Option ${index + 1}`} + + {isWinner && ( + + 🏆 Winner + + )} +
+ + {result.voteCount} votes ({percentage.toFixed(1)}%) + +
+
+
+
+
+ ); + }) + ) : ( +
+ No blind vote results available for this private poll.
-
- ); - })} -
-
- - {/* Show results with user's choice highlighted - HIDE for private polls */} - {selectedPoll.visibility !== "private" && ( -
-
- -
-

- You voted for:{" "} - { - selectedPoll.options[ - parseInt(voteStatus.vote?.optionId || "0") - ] - } -

-

- Here are the current results for this - poll. -

+ )}
- )} - - {/* For private polls, show a different message */} - {selectedPoll.visibility === "private" && ( -
-
- -
-

- Private Poll - Your vote is hidden -

-

- This is a private poll. Your individual vote remains hidden until revealed. -

+ ) : selectedPoll.visibility !== "private" ? ( + /* For public polls that have ended, show final results */ +
+ {/* Final Results for Public Polls */} +
+

+ + Final Results +

+
+ {resultsData?.results && resultsData.results.length > 0 ? ( + resultsData.results.map((result, index) => { + // Handle different voting modes + let displayValue: string; + let isWinner: boolean; + let percentage: number; + + if (resultsData.mode === "point") { + // Point-based voting: show total points and average + displayValue = `${result.totalPoints} points (avg: ${result.averagePoints})`; + isWinner = result.totalPoints === Math.max(...resultsData.results.map(r => r.totalPoints)); + percentage = resultsData.totalVotes > 0 ? (result.totalPoints / resultsData.results.reduce((sum, r) => sum + r.totalPoints, 0)) * 100 : 0; + } else if (resultsData.mode === "rank") { + // Rank-based voting: show percentage only + displayValue = `${result.totalScore} points`; + isWinner = result.totalScore === Math.max(...resultsData.results.map(r => r.totalScore)); + percentage = resultsData.totalVotes > 0 ? (result.totalScore / resultsData.results.reduce((sum, r) => sum + r.totalScore, 0)) * 100 : 0; + // For rank voting, just show the percentage in the display + displayValue = `${percentage.toFixed(1)}%`; + } else { + // Normal voting: show votes and percentage + displayValue = `${result.votes} votes`; + isWinner = result.votes === Math.max(...resultsData.results.map(r => r.votes)); + percentage = resultsData.totalVotes > 0 ? (result.votes / resultsData.totalVotes) * 100 : 0; + } + + return ( +
+
+
+ + {result.option} + + {isWinner && ( + + 🏆 Winner + + )} +
+ + {displayValue} ({percentage.toFixed(1)}%) + +
+
+
+
+
+ ); + }) + ) : ( +
+ No results available for this poll. +
+ )}
- )} - -
- {/* Poll Statistics */} -
-
-
-
- -
-
-

- {selectedPoll.mode === "rank" ? "Points" : "Votes"} -

-

- {resultsData?.totalVotes || 0} -

+ ) : ( + <> + {/* For active polls, show user's vote choice */} + {selectedPoll.visibility !== "private" && ( +
+
+ +
+

+ You voted:{" "} + { + (() => { + if (voteStatus?.vote?.data?.mode === "normal" && Array.isArray(voteStatus.vote.data.data)) { + const optionIndex = parseInt(voteStatus.vote.data.data[0] || "0"); + return selectedPoll.options[optionIndex] || "Unknown option"; + } else if (voteStatus?.vote?.data?.mode === "point" && Array.isArray(voteStatus.vote.data.data)) { + const pointData = voteStatus.vote.data.data; + const totalPoints = pointData.reduce((sum, item) => sum + (item.points || 0), 0); + return `distributed ${totalPoints} points across options`; + } else if (voteStatus?.vote?.data?.mode === "rank" && Array.isArray(voteStatus.vote.data.data)) { + const rankData = voteStatus.vote.data.data; + const sortedRanks = [...rankData].sort((a, b) => a.points - b.points); + const topChoice = selectedPoll.options[parseInt(sortedRanks[0]?.option || "0")]; + return `ranked options (${topChoice} as 1st choice)`; + } + return "Unknown option"; + })() + } +

+

+ {isVotingAllowed + ? "Your vote has been submitted. Results will be shown when the poll ends." + : "Here are the final results for this poll." + } +

+
-
+ )} -
-
-
- -
-
-

- Status -

- +

+ Voting Options: +

+
+ {selectedPoll.options.map((option, index) => { + const isUserChoice = (() => { + if (voteStatus?.vote?.data?.mode === "normal" && Array.isArray(voteStatus.vote.data.data)) { + return voteStatus.vote.data.data.includes(index.toString()); + } else if (voteStatus?.vote?.data?.mode === "point" && Array.isArray(voteStatus.vote.data.data)) { + const pointData = voteStatus.vote.data.data; + const optionPoints = pointData.find(item => item.option === index.toString())?.points || 0; + return optionPoints > 0; + } else if (voteStatus?.vote?.data?.mode === "rank" && Array.isArray(voteStatus.vote.data.data)) { + const rankData = voteStatus.vote.data.data; + return rankData.some(item => item.option === index.toString()); } - className="text-lg px-4 py-2" - > - {isVotingAllowed - ? "Active" - : "Ended"} - -
+ return false; + })(); + + const userChoiceDetails = (() => { + if (voteStatus?.vote?.data?.mode === "normal" && Array.isArray(voteStatus.vote.data.data)) { + return voteStatus.vote.data.data.includes(index.toString()) ? "← You voted for this option" : null; + } else if (voteStatus?.vote?.data?.mode === "point" && Array.isArray(voteStatus.vote.data.data)) { + const pointData = voteStatus.vote.data.data; + const optionPoints = pointData.find(item => item.option === index.toString())?.points || 0; + return optionPoints > 0 ? `← You gave ${optionPoints} points` : null; + } else if (voteStatus?.vote?.data?.mode === "rank" && Array.isArray(voteStatus.vote.data.data)) { + const rankData = voteStatus.vote.data.data; + const optionRank = rankData.find(item => item.option === index.toString())?.points; + return optionRank ? `← You ranked this ${optionRank}${optionRank === 1 ? 'st' : optionRank === 2 ? 'nd' : optionRank === 3 ? 'rd' : 'th'}` : null; + } + return null; + })(); + + return ( +
+
+ + {userChoiceDetails && ( +
+ {userChoiceDetails} +
+ )} +
+
+ ); + })}
-
-
+ + )}
- ) : !isVotingAllowed ? ( -
- {/* Show results when voting is not allowed (deadline passed) */} -
+ ) : voteStatus?.hasVoted === true ? ( + // Show voting interface for active polls where user has already voted + <> + {/* Show that user has voted */} +
- +
-

- Voting has ended for this poll -

-

- The voting deadline has passed. Here are - the final results. -

+

Vote Submitted

+

You have already voted on this poll

+

Results will be shown when the poll ends

- -
- {/* Final Results */} -
-

- - Final Results -

-
- {/* For private polls, show blind vote results */} - {selectedPoll.visibility === "private" && blindVoteResults ? ( - <> - {blindVoteResults.optionResults && blindVoteResults.optionResults.length > 0 ? ( - blindVoteResults.optionResults.map((result, index) => { - const isWinner = result.voteCount === Math.max(...blindVoteResults.optionResults.map(r => r.voteCount)); - const percentage = blindVoteResults.totalVotes > 0 ? (result.voteCount / blindVoteResults.totalVotes) * 100 : 0; - return ( -
-
-
- - {result.optionText || `Option ${index + 1}`} - - {isWinner && ( - - 🏆 Winner - - )} -
- - {result.voteCount} votes ({percentage.toFixed(1)}%) - -
-
-
-
-
- ); - }) - ) : ( -
- No blind vote results available. -
- )} - - ) : resultsData ? ( - <> - {/* For public polls, show regular results */} - {resultsData.results && resultsData.results.length > 0 ? ( - resultsData.results.map((result, index) => { - const isWinner = result.votes === Math.max(...resultsData.results.map(r => r.votes)); - return ( -
-
-
- - {result.option || `Option ${index + 1}`} - - {isWinner && ( - - 🏆 Winner - - )} -
- - {selectedPoll.mode === "rank" - ? `${result.votes} points` - : `${result.votes} votes`} ({result.percentage.toFixed(1)}%) - -
-
-
-
-
- ); - }) - ) : ( -
- No results data available. -
- )} - - ) : ( -
- No results available yet. -
- )} -
-
- - {/* Voter Details for expired polls */} - {selectedPoll?.mode === "public" && - selectedPoll.totalVotes > 0 && ( -
-

- - Voter Details -

-
-

- Voter details are available for - active polls with results. -

-
-
- )} -
-
+ ) : (
@@ -667,228 +608,312 @@ export default function Vote({ params }: { params: Promise<{ id: string }> }) { /> ) : ( <> - {/* Regular Voting Interface based on poll mode */} - {selectedPoll.mode === "normal" && ( -
-

- Select your choice: -

- - setSelectedOption(Number.parseInt(value)) - } - disabled={!isVotingAllowed} - > + {/* For public polls, show different interface based on voting status */} + {hasVoted ? ( +
+ {/* Show that user has voted */} +
+
+ +
+

+ You voted for:{" "} + { + (() => { + if (voteStatus?.vote?.data?.mode === "normal" && Array.isArray(voteStatus.vote.data.data)) { + const optionIndex = parseInt(voteStatus.vote.data.data[0] || "0"); + return selectedPoll.options[optionIndex] || "Unknown option"; + } + return "Unknown option"; + })() + } +

+

+ Your vote has been submitted. Results will be shown when the poll ends. +

+
+
+
+ + {/* Show voting options with user's choice highlighted (grayed out, no results) */} +
+

+ Voting Options: +

- {selectedPoll.options.map((option, index) => ( -
- -
+ ); + })}
- -
- )} - - {selectedPoll.mode === "point" && ( -
-
-

- Distribute your points -

- -
-
-

- You have 100 points to distribute. Assign points to each option based on your preference. -

-
- {selectedPoll.options.map((option, index) => ( -
+ ) : ( + <> + {/* Regular Voting Interface based on poll mode */} + {selectedPoll.mode === "normal" && ( +
+

+ Select your choice: +

+ + setSelectedOption(Number.parseInt(value)) + } + disabled={!isVotingAllowed} > -
- -
-
- { - const value = parseInt(e.target.value) || 0; - setPointVotes(prev => ({ - ...prev, - [index]: value - })); - }} - className="w-20 px-3 py-2 border border-gray-300 rounded-md text-center" - disabled={!isVotingAllowed} - /> - points +
+ {selectedPoll.options.map((option, index) => ( +
+ + +
+ ))}
-
- ))} -
-
- - Total Points Used: - - - {totalPoints}/100 - -
+
-
-
- )} + )} - {selectedPoll.mode === "rank" && ( -
-
-

- {(() => { - const currentRank = Object.keys(rankVotes).length + 1; - const maxRanks = Math.min(selectedPoll.options.length, 3); - - if (currentRank > maxRanks) { - return "Ranking Complete"; - } - - const rankText = currentRank === 1 ? "1st" : currentRank === 2 ? "2nd" : currentRank === 3 ? "3rd" : `${currentRank}th`; - return `What's your ${rankText} choice?`; - })()} -

- -
-
-

- Rank your top 3 choices from most preferred (1) to least preferred (3). -

-
-
- {selectedPoll.options.map((option, index) => { - const rank = rankVotes[index]; - const isRanked = rank !== undefined; - return ( -
+
+

+ Distribute your points +

+
+ ))} +
+
+ + Total Points Used: + + + {totalPoints}/100 + +
+
+
+
+ )} + + {selectedPoll.mode === "rank" && ( +
+
+

+ {(() => { + const currentRank = Object.keys(rankVotes).length + 1; + const maxRanks = Math.min(selectedPoll.options.length, 3); + + if (currentRank > maxRanks) { + return "Ranking Complete"; + } + + return `Rank ${currentRank} of ${maxRanks}`; + })()} +

+ +
+
+

+ Rank your top 3 choices from most preferred (1) to least preferred (3). +

+
+
+ {selectedPoll.options.map((option, index) => { + const rank = rankVotes[index]; + const isRanked = rank !== undefined; + const usedRanks = Object.values(rankVotes); + const maxRanks = Math.min(selectedPoll.options.length, 3); + + return ( +
- - - - - - rank +
+ +
+
+ + rank +
+
+ ); + })} +
+
+ + Total Rankings Used: + + + {Object.keys(rankVotes).length}/{Math.min(selectedPoll.options.length, 3)} +
- ); - })} -
-
- - Total Rankings Used: - - - {Object.keys(rankVotes).length}/{Math.min(selectedPoll.options.length, 3)} -
+ )} + + {/* Submit button for regular voting */} +
+
-
+ )} - - {/* Submit button for regular voting */} -
- -
)}
diff --git a/platforms/eVoting/src/app/(app)/create/page.tsx b/platforms/eVoting/src/app/(app)/create/page.tsx index 5dc8a6d0..698222f2 100644 --- a/platforms/eVoting/src/app/(app)/create/page.tsx +++ b/platforms/eVoting/src/app/(app)/create/page.tsx @@ -35,9 +35,8 @@ const createPollSchema = z.object({ .min(2, "At least 2 options required"), deadline: z .string() - .optional() + .min(1, "Deadline is required") .refine((val) => { - if (!val) return true; // Allow empty deadline const date = new Date(val); return !Number.isNaN(date.getTime()) && date > new Date(); }, "Deadline must be a valid future date"), @@ -217,15 +216,16 @@ export default function CreatePoll() { {/* Vote Deadline */}

- Leave empty for no deadline. Voting will be open indefinitely. + Set a deadline for when voting will end.

{errors.deadline && (

@@ -234,6 +234,59 @@ export default function CreatePoll() { )}

+ {/* Vote Visibility */} +
+ +
+ + + +
+ {errors.visibility && ( +

+ {errors.visibility.message} +

+ )} +
+ {/* Vote Type */}