22// SPDX-License-Identifier: MIT
33
44import { DatePipe } from '@angular/common' ;
5- import { Component , computed , effect , input , model , signal , Signal } from '@angular/core' ;
5+ import { Component , computed , inject , input , model , signal , Signal } from '@angular/core' ;
6+ import { toObservable , toSignal } from '@angular/core/rxjs-interop' ;
67import { ButtonComponent } from '@components/button/button.component' ;
78import { TagComponent } from '@components/tag/tag.component' ;
89import { environment } from '@environments/environment' ;
910import { PollStatus , PollType } from '@lfx-one/shared' ;
10- import { Vote , VoteParticipationStats , VoteResultsOption , VoteResultsQuestion } from '@lfx-one/shared/interfaces' ;
11+ import { PollCommentResult , Vote , VoteParticipationStats , VoteResultsOption , VoteResultsQuestion , VoteResultsResponse } from '@lfx-one/shared/interfaces' ;
1112import { PollStatusLabelPipe } from '@pipes/poll-status-label.pipe' ;
1213import { PollStatusSeverityPipe } from '@pipes/poll-status-severity.pipe' ;
14+ import { VoteService } from '@services/vote.service' ;
1315import { DrawerModule } from 'primeng/drawer' ;
1416import { SkeletonModule } from 'primeng/skeleton' ;
17+ import { catchError , finalize , of , shareReplay , startWith , switchMap } from 'rxjs' ;
1518
1619@Component ( {
1720 selector : 'lfx-vote-results-drawer' ,
@@ -20,42 +23,86 @@ import { SkeletonModule } from 'primeng/skeleton';
2023 styleUrl : './vote-results-drawer.component.scss' ,
2124} )
2225export class VoteResultsDrawerComponent {
26+ // === Services ===
27+ private readonly voteService = inject ( VoteService ) ;
28+
2329 // === Inputs ===
24- public readonly vote = input < Vote | null > ( null ) ;
30+ public readonly voteId = input < string | null > ( null ) ;
31+ public readonly listVote = input < Vote | null > ( null ) ;
2532
2633 // === Model Signals (two-way binding) ===
2734 public readonly visible = model < boolean > ( false ) ;
2835
2936 // === Writable Signals ===
30- protected readonly loading = signal < boolean > ( false ) ;
37+ protected readonly loadingVoteDetails = signal < boolean > ( false ) ;
38+ protected readonly loadingVoteResults = signal < boolean > ( false ) ;
39+
40+ // === Shared Observables ===
41+ private readonly voteId$ = toObservable ( this . voteId ) . pipe ( shareReplay ( { bufferSize : 1 , refCount : true } ) ) ;
42+
43+ // === Derived Signals (from API) ===
44+ protected readonly vote : Signal < Vote | null > = this . initVote ( ) ;
45+ protected readonly voteResults : Signal < VoteResultsResponse | null > = this . initVoteResults ( ) ;
3146
3247 // === Computed Signals ===
3348 protected readonly isGenericPoll : Signal < boolean > = this . initIsGenericPoll ( ) ;
3449 protected readonly pccVotingUrl : Signal < string > = this . initPccVotingUrl ( ) ;
3550 protected readonly isVoteClosed : Signal < boolean > = this . initIsVoteClosed ( ) ;
51+ protected readonly isLoading : Signal < boolean > = computed ( ( ) => this . loadingVoteDetails ( ) || this . loadingVoteResults ( ) ) ;
3652 protected readonly participationStats : Signal < VoteParticipationStats > = this . initParticipationStats ( ) ;
3753 protected readonly questionsWithResults : Signal < VoteResultsQuestion [ ] > = this . initQuestionsWithResults ( ) ;
54+ protected readonly commentResults : Signal < PollCommentResult [ ] > = this . initCommentResults ( ) ;
3855 protected readonly votingMethodText : Signal < string > = this . initVotingMethodText ( ) ;
3956
40- // === Constructor ===
41- public constructor ( ) {
42- // Simulate loading when vote changes
43- effect ( ( ) => {
44- const v = this . vote ( ) ;
45- if ( v && this . visible ( ) ) {
46- this . loading . set ( true ) ;
47- // Simulate API fetch delay
48- setTimeout ( ( ) => this . loading . set ( false ) , 500 ) ;
49- }
50- } ) ;
51- }
52-
5357 // === Protected Methods ===
5458 protected onClose ( ) : void {
5559 this . visible . set ( false ) ;
5660 }
5761
5862 // === Private Initializers ===
63+ private initVote ( ) : Signal < Vote | null > {
64+ return toSignal (
65+ this . voteId$ . pipe (
66+ switchMap ( ( id ) => {
67+ if ( ! id ) {
68+ this . loadingVoteDetails . set ( false ) ;
69+ return of ( null ) ;
70+ }
71+
72+ this . loadingVoteDetails . set ( true ) ;
73+ const listVote = this . listVote ( ) ;
74+
75+ return this . voteService . getVote ( id ) . pipe (
76+ catchError ( ( ) => of ( listVote ) ) ,
77+ finalize ( ( ) => this . loadingVoteDetails . set ( false ) ) ,
78+ startWith ( listVote )
79+ ) ;
80+ } )
81+ ) ,
82+ { initialValue : null }
83+ ) ;
84+ }
85+
86+ private initVoteResults ( ) : Signal < VoteResultsResponse | null > {
87+ return toSignal (
88+ this . voteId$ . pipe (
89+ switchMap ( ( id ) => {
90+ if ( ! id ) {
91+ this . loadingVoteResults . set ( false ) ;
92+ return of ( null ) ;
93+ }
94+
95+ this . loadingVoteResults . set ( true ) ;
96+ return this . voteService . getVoteResults ( id ) . pipe (
97+ catchError ( ( ) => of ( null ) ) ,
98+ finalize ( ( ) => this . loadingVoteResults . set ( false ) )
99+ ) ;
100+ } )
101+ ) ,
102+ { initialValue : null }
103+ ) ;
104+ }
105+
59106 private initIsGenericPoll ( ) : Signal < boolean > {
60107 return computed ( ( ) => {
61108 const v = this . vote ( ) ;
@@ -84,13 +131,13 @@ export class VoteResultsDrawerComponent {
84131
85132 private initParticipationStats ( ) : Signal < VoteParticipationStats > {
86133 return computed ( ( ) => {
87- const v = this . vote ( ) ;
88- if ( ! v ) {
134+ const results = this . voteResults ( ) ;
135+ if ( ! results ) {
89136 return { eligibleVoters : 0 , totalResponses : 0 , participationRate : 0 } ;
90137 }
91138
92- const eligibleVoters = v . total_voting_request_invitations || 0 ;
93- const totalResponses = v . num_response_received || 0 ;
139+ const eligibleVoters = results . num_recipients || 0 ;
140+ const totalResponses = results . num_votes_cast || 0 ;
94141 const participationRate = eligibleVoters > 0 ? Math . round ( ( totalResponses / eligibleVoters ) * 100 ) : 0 ;
95142
96143 return { eligibleVoters, totalResponses, participationRate } ;
@@ -99,59 +146,69 @@ export class VoteResultsDrawerComponent {
99146
100147 private initQuestionsWithResults ( ) : Signal < VoteResultsQuestion [ ] > {
101148 return computed ( ( ) => {
102- const v = this . vote ( ) ;
149+ const results = this . voteResults ( ) ;
103150 const isClosed = this . isVoteClosed ( ) ;
104151
105- if ( ! v ?. poll_questions ?. length ) {
152+ if ( ! results ?. poll_results ?. length ) {
106153 return [ ] ;
107154 }
108155
109- return v . poll_questions . map ( ( question ) => {
110- // Get vote counts from generic_choice_votes or default to 0
111- const choiceVotes = v . generic_choice_votes || { } ;
112-
113- // Calculate total votes for this question
114- let totalVotes = 0 ;
115- const optionsWithCounts : VoteResultsOption [ ] = question . choices . map ( ( choice ) => {
116- const voteCount = choiceVotes [ choice . choice_id ] || 0 ;
117- totalVotes += voteCount ;
118- return {
119- choiceId : choice . choice_id ,
120- text : choice . choice_text ,
121- voteCount,
122- percentage : 0 , // Will calculate after we have total
123- isWinner : false ,
124- isTied : false ,
125- isLeading : false , // Will calculate after we have max votes
126- } ;
127- } ) ;
128-
129- // Calculate percentages and determine winner/ties
156+ return results . poll_results . map ( ( pollResult ) => {
157+ const choiceVotes = pollResult . generic_choice_votes || [ ] ;
158+
159+ // Compute total votes first for percentage calculation
160+ const totalVotes = choiceVotes . reduce ( ( sum , cv ) => sum + cv . vote_count , 0 ) ;
161+
162+ // Build options with vote counts from the results API
163+ const optionsWithCounts : VoteResultsOption [ ] = choiceVotes . map ( ( cv ) => ( {
164+ choiceId : cv . choice_id ,
165+ text : pollResult . question . choices . find ( ( c ) => c . choice_id === cv . choice_id ) ?. choice_text || cv . choice_id ,
166+ voteCount : cv . vote_count ,
167+ percentage : this . computePercentage ( cv . percentage , cv . vote_count , totalVotes ) ,
168+ isWinner : false ,
169+ isTied : false ,
170+ isLeading : false ,
171+ } ) ) ;
172+
173+ // Determine winner/ties based on max votes
130174 const maxVotes = Math . max ( ...optionsWithCounts . map ( ( o ) => o . voteCount ) , 0 ) ;
131175 const optionsWithMaxVotes = optionsWithCounts . filter ( ( o ) => o . voteCount === maxVotes ) ;
132176 const isTied = optionsWithMaxVotes . length > 1 && maxVotes > 0 ;
133177
134178 const processedOptions = optionsWithCounts . map ( ( option ) => ( {
135179 ...option ,
136- percentage : totalVotes > 0 ? Math . round ( ( option . voteCount / totalVotes ) * 100 ) : 0 ,
137- // Only show winner if vote is closed and there's no tie
138180 isWinner : isClosed && ! isTied && option . voteCount === maxVotes && maxVotes > 0 ,
139- // Show tied status for all options with max votes (only for closed votes)
140181 isTied : isClosed && isTied && option . voteCount === maxVotes ,
141- // Show leading status for live votes (options with max votes)
142182 isLeading : option . voteCount === maxVotes && maxVotes > 0 ,
143183 } ) ) ;
144184
145185 return {
146- questionId : question . question_id ,
147- question : question . prompt ,
186+ questionId : pollResult . question . question_id ,
187+ question : pollResult . question . prompt ,
148188 options : processedOptions ,
149189 totalVotes,
150190 } ;
151191 } ) ;
152192 } ) ;
153193 }
154194
195+ private initCommentResults ( ) : Signal < PollCommentResult [ ] > {
196+ return computed ( ( ) => {
197+ const results = this . voteResults ( ) ;
198+ if ( ! results ?. comment_results ?. length ) {
199+ return [ ] ;
200+ }
201+
202+ return results . comment_results . filter ( ( cr ) => cr . comments . length > 0 ) ;
203+ } ) ;
204+ }
205+
206+ private computePercentage ( apiPercentage : number , voteCount : number , totalVotes : number ) : number {
207+ if ( apiPercentage > 0 ) return apiPercentage ;
208+ if ( totalVotes <= 0 ) return 0 ;
209+ return Math . round ( ( voteCount / totalVotes ) * 100 ) ;
210+ }
211+
155212 private initVotingMethodText ( ) : Signal < string > {
156213 return computed ( ( ) => {
157214 const v = this . vote ( ) ;
0 commit comments