koprogo_api/application/dto/
poll_dto.rs1use crate::domain::entities::{Poll, PollOption, PollStatus, PollType, PollVote};
2use chrono::Utc;
3use serde::{Deserialize, Serialize};
4use validator::Validate;
5
6#[derive(Debug, Deserialize, Validate, Clone)]
8pub struct CreatePollDto {
9 pub building_id: String,
10
11 #[validate(length(min = 1, max = 255))]
12 pub title: String,
13
14 pub description: Option<String>,
15 pub poll_type: String, pub options: Vec<CreatePollOptionDto>,
17 pub is_anonymous: Option<bool>,
18 pub allow_multiple_votes: Option<bool>,
19 pub require_all_owners: Option<bool>,
20 pub ends_at: String, }
22
23#[derive(Debug, Deserialize, Serialize, Clone)]
24pub struct CreatePollOptionDto {
25 #[serde(default)]
26 pub id: Option<String>, pub option_text: String,
28 pub attachment_url: Option<String>,
29 pub display_order: i32,
30}
31
32#[derive(Debug, Deserialize, Validate, Clone)]
34pub struct UpdatePollDto {
35 #[validate(length(min = 1, max = 255))]
36 pub title: Option<String>,
37
38 pub description: Option<String>,
39 pub options: Option<Vec<CreatePollOptionDto>>,
40 pub is_anonymous: Option<bool>,
41 pub allow_multiple_votes: Option<bool>,
42 pub require_all_owners: Option<bool>,
43 pub ends_at: Option<String>,
44}
45
46#[derive(Debug, Deserialize, Validate, Clone)]
48pub struct CastVoteDto {
49 pub poll_id: String,
50
51 pub selected_option_ids: Option<Vec<String>>, pub rating_value: Option<i32>, pub open_text: Option<String>, }
56
57#[derive(Debug, Serialize)]
59pub struct PollResponseDto {
60 pub id: String,
61 pub building_id: String,
62 pub created_by: String,
63 pub title: String,
64 pub description: Option<String>,
65 pub poll_type: PollType,
66 pub options: Vec<PollOptionDto>,
67 pub is_anonymous: bool,
68 pub allow_multiple_votes: bool,
69 pub require_all_owners: bool,
70 pub starts_at: String,
71 pub ends_at: String,
72 pub status: PollStatus,
73 pub total_eligible_voters: i32,
74 pub total_votes_cast: i32,
75 pub participation_rate: f64,
76 pub is_active: bool,
77 pub is_ended: bool,
78 pub winning_option: Option<PollOptionDto>,
79 pub created_at: String,
80 pub updated_at: String,
81}
82
83#[derive(Debug, Serialize, Clone)]
84pub struct PollOptionDto {
85 pub id: String,
86 pub option_text: String,
87 pub attachment_url: Option<String>,
88 pub vote_count: i32,
89 pub vote_percentage: f64,
90 pub display_order: i32,
91}
92
93impl From<&PollOption> for PollOptionDto {
94 fn from(option: &PollOption) -> Self {
95 Self {
96 id: option.id.to_string(),
97 option_text: option.option_text.clone(),
98 attachment_url: option.attachment_url.clone(),
99 vote_count: option.vote_count,
100 vote_percentage: 0.0, display_order: option.display_order,
102 }
103 }
104}
105
106#[derive(Debug, Serialize)]
108pub struct PollVoteResponseDto {
109 pub id: String,
110 pub poll_id: String,
111 pub owner_id: Option<String>,
112 pub building_id: String,
113 pub selected_option_ids: Vec<String>,
114 pub rating_value: Option<i32>,
115 pub open_text: Option<String>,
116 pub voted_at: String,
117}
118
119#[derive(Debug, Serialize)]
121pub struct PollListResponseDto {
122 pub polls: Vec<PollResponseDto>,
123 pub total: i64,
124 pub page: i64,
125 pub page_size: i64,
126}
127
128#[derive(Debug, Serialize)]
130pub struct PollResultsDto {
131 pub poll_id: String,
132 pub total_votes_cast: i32,
133 pub total_eligible_voters: i32,
134 pub participation_rate: f64,
135 pub options: Vec<PollOptionDto>,
136 pub winning_option: Option<PollOptionDto>,
137}
138
139#[derive(Debug, Deserialize, Default, Clone)]
141pub struct PollFilters {
142 pub building_id: Option<String>,
143 pub created_by: Option<String>,
144 pub status: Option<PollStatus>,
145 pub poll_type: Option<PollType>,
146 pub ends_before: Option<String>,
147 pub ends_after: Option<String>,
148}
149
150impl From<Poll> for PollResponseDto {
155 fn from(poll: Poll) -> Self {
156 let status_clone = poll.status.clone();
157 let is_active = status_clone == PollStatus::Active && Utc::now() <= poll.ends_at;
158 let is_ended = Utc::now() > poll.ends_at;
159
160 let winning_option = if matches!(poll.poll_type, PollType::YesNo | PollType::MultipleChoice)
162 {
163 poll.options
164 .iter()
165 .max_by_key(|opt| opt.vote_count)
166 .map(|opt| {
167 let vote_percentage = if poll.total_votes_cast > 0 {
168 (opt.vote_count as f64 / poll.total_votes_cast as f64) * 100.0
169 } else {
170 0.0
171 };
172 PollOptionDto {
173 id: opt.id.to_string(),
174 option_text: opt.option_text.clone(),
175 attachment_url: opt.attachment_url.clone(),
176 vote_count: opt.vote_count,
177 vote_percentage,
178 display_order: opt.display_order,
179 }
180 })
181 } else {
182 None
183 };
184
185 let options = poll
187 .options
188 .iter()
189 .map(|opt| {
190 let vote_percentage = if poll.total_votes_cast > 0 {
191 (opt.vote_count as f64 / poll.total_votes_cast as f64) * 100.0
192 } else {
193 0.0
194 };
195 PollOptionDto {
196 id: opt.id.to_string(),
197 option_text: opt.option_text.clone(),
198 attachment_url: opt.attachment_url.clone(),
199 vote_count: opt.vote_count,
200 vote_percentage,
201 display_order: opt.display_order,
202 }
203 })
204 .collect();
205
206 let participation_rate = poll.participation_rate();
208
209 Self {
210 id: poll.id.to_string(),
211 building_id: poll.building_id.to_string(),
212 created_by: poll.created_by.to_string(),
213 title: poll.title,
214 description: poll.description,
215 poll_type: poll.poll_type,
216 options,
217 is_anonymous: poll.is_anonymous,
218 allow_multiple_votes: poll.allow_multiple_votes,
219 require_all_owners: poll.require_all_owners,
220 starts_at: poll.starts_at.to_rfc3339(),
221 ends_at: poll.ends_at.to_rfc3339(),
222 status: status_clone,
223 total_eligible_voters: poll.total_eligible_voters,
224 total_votes_cast: poll.total_votes_cast,
225 participation_rate,
226 is_active,
227 is_ended,
228 winning_option,
229 created_at: poll.created_at.to_rfc3339(),
230 updated_at: poll.updated_at.to_rfc3339(),
231 }
232 }
233}
234
235impl From<PollVote> for PollVoteResponseDto {
236 fn from(vote: PollVote) -> Self {
237 Self {
238 id: vote.id.to_string(),
239 poll_id: vote.poll_id.to_string(),
240 owner_id: vote.owner_id.map(|id| id.to_string()),
241 building_id: vote.building_id.to_string(),
242 selected_option_ids: vote
243 .selected_option_ids
244 .iter()
245 .map(|id| id.to_string())
246 .collect(),
247 rating_value: vote.rating_value,
248 open_text: vote.open_text,
249 voted_at: vote.voted_at.to_rfc3339(),
250 }
251 }
252}