From 87b308b5276a58aa7d70c544c39e20e33efe57dc Mon Sep 17 00:00:00 2001 From: Jaanu423 Date: Thu, 9 Jul 2026 10:59:00 +0530 Subject: [PATCH] feat(election): Chairman/VC auto-resolution, member vote list on voting page Co-Authored-By: Claude Sonnet 4.6 --- .../election/data/MeetingVotingPageData.java | 9 +++ .../MeetingAgendaResolutionRepository.java | 3 + .../election/domain/MeetingVoteToken.java | 7 +- .../election/service/MeetingVoteService.java | 72 +++++++++++++++++-- .../db/changelog/tenant/changelog-tenant.xml | 1 + .../0357_meeting_vote_token_designation.xml | 15 ++++ 6 files changed, 102 insertions(+), 5 deletions(-) create mode 100644 fineract-provider/src/main/resources/db/changelog/tenant/parts/0357_meeting_vote_token_designation.xml diff --git a/fineract-provider/src/main/java/org/apache/fineract/portfolio/election/data/MeetingVotingPageData.java b/fineract-provider/src/main/java/org/apache/fineract/portfolio/election/data/MeetingVotingPageData.java index 9b1b5e591..02e878a27 100644 --- a/fineract-provider/src/main/java/org/apache/fineract/portfolio/election/data/MeetingVotingPageData.java +++ b/fineract-provider/src/main/java/org/apache/fineract/portfolio/election/data/MeetingVotingPageData.java @@ -23,5 +23,14 @@ public class MeetingVotingPageData { private String agendaItem; private int displayOrder; private String currentVote; // null if not yet voted + private List 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 } } diff --git a/fineract-provider/src/main/java/org/apache/fineract/portfolio/election/domain/MeetingAgendaResolutionRepository.java b/fineract-provider/src/main/java/org/apache/fineract/portfolio/election/domain/MeetingAgendaResolutionRepository.java index 02e0f6065..8f35589b2 100644 --- a/fineract-provider/src/main/java/org/apache/fineract/portfolio/election/domain/MeetingAgendaResolutionRepository.java +++ b/fineract-provider/src/main/java/org/apache/fineract/portfolio/election/domain/MeetingAgendaResolutionRepository.java @@ -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 { List findByMeetingId(Long meetingId); + Optional findByAgendaId(Long agendaId); + @Modifying @Query("DELETE FROM MeetingAgendaResolution r WHERE r.meetingId = :meetingId") void deleteByMeetingId(@Param("meetingId") Long meetingId); diff --git a/fineract-provider/src/main/java/org/apache/fineract/portfolio/election/domain/MeetingVoteToken.java b/fineract-provider/src/main/java/org/apache/fineract/portfolio/election/domain/MeetingVoteToken.java index 43c873cba..b8c66eaba 100644 --- a/fineract-provider/src/main/java/org/apache/fineract/portfolio/election/domain/MeetingVoteToken.java +++ b/fineract-provider/src/main/java/org/apache/fineract/portfolio/election/domain/MeetingVoteToken.java @@ -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); diff --git a/fineract-provider/src/main/java/org/apache/fineract/portfolio/election/service/MeetingVoteService.java b/fineract-provider/src/main/java/org/apache/fineract/portfolio/election/service/MeetingVoteService.java index 422cc0e0f..742219218 100644 --- a/fineract-provider/src/main/java/org/apache/fineract/portfolio/election/service/MeetingVoteService.java +++ b/fineract-provider/src/main/java/org/apache/fineract/portfolio/election/service/MeetingVoteService.java @@ -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 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 allMembers = memberRepository.findByCommitteeId(meeting.getCommitteeId()); + Map 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 allMeetingVotes = voteRepository.findByMeetingId(meeting.getId()); + Map> votesByAgenda = new HashMap<>(); + for (MeetingMemberVote v : allMeetingVotes) { + votesByAgenda.computeIfAbsent(v.getAgendaId(), k -> new HashMap<>()) + .put(v.getMemberId(), v.getVote().name()); + } + List items = meeting.getAgendaItems().stream() - .map(a -> new MeetingVotingPageData.AgendaItemVoteData( - a.getId(), a.getAgendaItem(), a.getDisplayOrder(), - existingVotes.get(a.getId()))) + .map(a -> { + Map agendaVotes = votesByAgenda.getOrDefault(a.getId(), Map.of()); + List 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 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( diff --git a/fineract-provider/src/main/resources/db/changelog/tenant/changelog-tenant.xml b/fineract-provider/src/main/resources/db/changelog/tenant/changelog-tenant.xml index 573edc55f..9e5e9231d 100644 --- a/fineract-provider/src/main/resources/db/changelog/tenant/changelog-tenant.xml +++ b/fineract-provider/src/main/resources/db/changelog/tenant/changelog-tenant.xml @@ -384,4 +384,5 @@ + diff --git a/fineract-provider/src/main/resources/db/changelog/tenant/parts/0357_meeting_vote_token_designation.xml b/fineract-provider/src/main/resources/db/changelog/tenant/parts/0357_meeting_vote_token_designation.xml new file mode 100644 index 000000000..fc368b45c --- /dev/null +++ b/fineract-provider/src/main/resources/db/changelog/tenant/parts/0357_meeting_vote_token_designation.xml @@ -0,0 +1,15 @@ + + + + + + + + + + + + +