feat(election): Chairman/VC auto-resolution, member vote list on voting page
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Sonnet 4.6
parent
934a51c9ee
commit
87b308b527
+9
@@ -23,5 +23,14 @@ public class MeetingVotingPageData {
|
||||
private String agendaItem;
|
||||
private int displayOrder;
|
||||
private String currentVote; // null if not yet voted
|
||||
private List<MemberVoteStatus> memberVotes; // all committee members with their vote
|
||||
}
|
||||
|
||||
@Data
|
||||
@AllArgsConstructor
|
||||
public static class MemberVoteStatus {
|
||||
private String memberName;
|
||||
private String designation;
|
||||
private String vote; // APPROVED, REJECTED, ABSTAIN, or null if not voted
|
||||
}
|
||||
}
|
||||
|
||||
+3
@@ -7,12 +7,15 @@ import org.springframework.data.repository.query.Param;
|
||||
import org.springframework.stereotype.Repository;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.Optional;
|
||||
|
||||
@Repository
|
||||
public interface MeetingAgendaResolutionRepository extends JpaRepository<MeetingAgendaResolution, Long> {
|
||||
|
||||
List<MeetingAgendaResolution> findByMeetingId(Long meetingId);
|
||||
|
||||
Optional<MeetingAgendaResolution> findByAgendaId(Long agendaId);
|
||||
|
||||
@Modifying
|
||||
@Query("DELETE FROM MeetingAgendaResolution r WHERE r.meetingId = :meetingId")
|
||||
void deleteByMeetingId(@Param("meetingId") Long meetingId);
|
||||
|
||||
+6
-1
@@ -37,12 +37,17 @@ public class MeetingVoteToken {
|
||||
@Column(name = "created_at", nullable = false)
|
||||
private LocalDateTime createdAt;
|
||||
|
||||
public static MeetingVoteToken generate(Long meetingId, Long memberId, String memberName, int validDays) {
|
||||
@Column(name = "designation", length = 100)
|
||||
private String designation;
|
||||
|
||||
public static MeetingVoteToken generate(Long meetingId, Long memberId, String memberName,
|
||||
String designation, int validDays) {
|
||||
MeetingVoteToken t = new MeetingVoteToken();
|
||||
t.token = UUID.randomUUID().toString();
|
||||
t.meetingId = meetingId;
|
||||
t.memberId = memberId;
|
||||
t.memberName = memberName;
|
||||
t.designation = designation;
|
||||
t.used = false;
|
||||
t.createdAt = LocalDateTime.now();
|
||||
t.expiresAt = t.createdAt.plusDays(validDays);
|
||||
|
||||
+68
-4
@@ -29,6 +29,7 @@ public class MeetingVoteService {
|
||||
private final ElectionCandidateRepository electionCandidateRepository;
|
||||
private final ClientRepository clientRepository;
|
||||
private final EmailSenderService emailSenderService;
|
||||
private final DesignationService designationService;
|
||||
|
||||
@Value("${cbs.frontend.base-url:https://app.cbsnexora.com}")
|
||||
private String frontendBaseUrl;
|
||||
@@ -48,16 +49,50 @@ public class MeetingVoteService {
|
||||
|
||||
boolean closed = voteToken.isUsed() || voteToken.isExpired();
|
||||
|
||||
// Current voter's own votes
|
||||
Map<Long, String> existingVotes = voteRepository
|
||||
.findByMeetingIdAndMemberId(meeting.getId(), voteToken.getMemberId())
|
||||
.stream().collect(Collectors.toMap(
|
||||
MeetingMemberVote::getAgendaId,
|
||||
v -> v.getVote().name()));
|
||||
|
||||
// All committee members with designations for the member vote display
|
||||
List<CommitteeMember> allMembers = memberRepository.findByCommitteeId(meeting.getCommitteeId());
|
||||
Map<Long, MeetingVotingPageData.MemberVoteStatus> memberStatusTemplates = new HashMap<>();
|
||||
for (CommitteeMember m : allMembers) {
|
||||
DirectorAppointment appt = directorAppointmentRepository
|
||||
.findById(m.getDirectorAppointmentId()).orElse(null);
|
||||
String name = appt != null ? appt.getCandidateName() : "Member #" + m.getId();
|
||||
String desig = appt != null ? appt.getDesignation() : null;
|
||||
memberStatusTemplates.put(m.getId(),
|
||||
new MeetingVotingPageData.MemberVoteStatus(name, desig, null));
|
||||
}
|
||||
|
||||
// All votes for this meeting (all members)
|
||||
List<MeetingMemberVote> allMeetingVotes = voteRepository.findByMeetingId(meeting.getId());
|
||||
Map<Long, Map<Long, String>> votesByAgenda = new HashMap<>();
|
||||
for (MeetingMemberVote v : allMeetingVotes) {
|
||||
votesByAgenda.computeIfAbsent(v.getAgendaId(), k -> new HashMap<>())
|
||||
.put(v.getMemberId(), v.getVote().name());
|
||||
}
|
||||
|
||||
List<MeetingVotingPageData.AgendaItemVoteData> items = meeting.getAgendaItems().stream()
|
||||
.map(a -> new MeetingVotingPageData.AgendaItemVoteData(
|
||||
a.getId(), a.getAgendaItem(), a.getDisplayOrder(),
|
||||
existingVotes.get(a.getId())))
|
||||
.map(a -> {
|
||||
Map<Long, String> agendaVotes = votesByAgenda.getOrDefault(a.getId(), Map.of());
|
||||
List<MeetingVotingPageData.MemberVoteStatus> memberVotes = allMembers.stream()
|
||||
.map(m -> {
|
||||
MeetingVotingPageData.MemberVoteStatus tmpl = memberStatusTemplates.get(m.getId());
|
||||
String vote = agendaVotes.get(m.getId());
|
||||
return new MeetingVotingPageData.MemberVoteStatus(
|
||||
tmpl != null ? tmpl.getMemberName() : "Member #" + m.getId(),
|
||||
tmpl != null ? tmpl.getDesignation() : null,
|
||||
vote);
|
||||
})
|
||||
.toList();
|
||||
return new MeetingVotingPageData.AgendaItemVoteData(
|
||||
a.getId(), a.getAgendaItem(), a.getDisplayOrder(),
|
||||
existingVotes.get(a.getId()), memberVotes);
|
||||
})
|
||||
.toList();
|
||||
|
||||
String dateStr = meeting.getMeetingDate() != null
|
||||
@@ -102,6 +137,34 @@ public class MeetingVoteService {
|
||||
}
|
||||
|
||||
voteRepository.saveAll(toSave);
|
||||
voteRepository.flush();
|
||||
|
||||
// Auto-set resolution when Chairman or Vice Chairman votes
|
||||
String designation = voteToken.getDesignation();
|
||||
if (designation != null) {
|
||||
Set<String> mandatory = designationService.getMandatoryDesignations();
|
||||
if (mandatory.contains(designation)) {
|
||||
for (AgendaVoteRequest v : request.getVotes()) {
|
||||
if (v.getAgendaId() == null || v.getVote() == null) continue;
|
||||
AgendaVote agendaVote;
|
||||
try {
|
||||
agendaVote = AgendaVote.valueOf(v.getVote().toUpperCase());
|
||||
} catch (IllegalArgumentException e) {
|
||||
continue;
|
||||
}
|
||||
if (agendaVote == AgendaVote.ABSTAIN) continue;
|
||||
AgendaResolution resolution = agendaVote == AgendaVote.APPROVED
|
||||
? AgendaResolution.APPROVED : AgendaResolution.REJECTED;
|
||||
resolutionRepository.findByAgendaId(v.getAgendaId())
|
||||
.ifPresent(existing -> {
|
||||
resolutionRepository.delete(existing);
|
||||
resolutionRepository.flush();
|
||||
});
|
||||
resolutionRepository.save(
|
||||
MeetingAgendaResolution.of(meetingId, v.getAgendaId(), resolution, null));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
voteToken.setUsed(true);
|
||||
tokenRepository.save(voteToken);
|
||||
@@ -203,7 +266,8 @@ public class MeetingVoteService {
|
||||
if (email == null) continue;
|
||||
|
||||
MeetingVoteToken token = MeetingVoteToken.generate(
|
||||
meetingId, member.getId(), appointment.getCandidateName(), 7);
|
||||
meetingId, member.getId(), appointment.getCandidateName(),
|
||||
appointment.getDesignation(), 7);
|
||||
tokenRepository.save(token);
|
||||
|
||||
String html = buildVotingEmailHtml(
|
||||
|
||||
@@ -384,4 +384,5 @@
|
||||
<include file="parts/0354_meeting_vote_resolution.xml" relativeToChangelogFile="true"/>
|
||||
<include file="parts/0355_director_designation_code.xml" relativeToChangelogFile="true"/>
|
||||
<include file="parts/0356_meeting_vote_remark.xml" relativeToChangelogFile="true"/>
|
||||
<include file="parts/0357_meeting_vote_token_designation.xml" relativeToChangelogFile="true"/>
|
||||
</databaseChangeLog>
|
||||
|
||||
+15
@@ -0,0 +1,15 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<databaseChangeLog xmlns="http://www.liquibase.org/xml/ns/dbchangelog"
|
||||
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
|
||||
xsi:schemaLocation="http://www.liquibase.org/xml/ns/dbchangelog http://www.liquibase.org/xml/ns/dbchangelog/dbchangelog-4.1.xsd">
|
||||
|
||||
<changeSet id="0357-meeting-vote-token-designation" author="CBS">
|
||||
<preConditions onFail="MARK_RAN">
|
||||
<not><columnExists tableName="m_election_meeting_vote_token" columnName="designation"/></not>
|
||||
</preConditions>
|
||||
<addColumn tableName="m_election_meeting_vote_token">
|
||||
<column name="designation" type="VARCHAR(100)"/>
|
||||
</addColumn>
|
||||
</changeSet>
|
||||
|
||||
</databaseChangeLog>
|
||||
Reference in New Issue
Block a user