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:
Jaanu423
2026-07-09 10:59:00 +05:30
co-authored by Claude Sonnet 4.6
parent 934a51c9ee
commit 87b308b527
6 changed files with 102 additions and 5 deletions
@@ -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
}
}
@@ -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);
@@ -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);
@@ -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>
@@ -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>