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 String agendaItem;
|
||||||
private int displayOrder;
|
private int displayOrder;
|
||||||
private String currentVote; // null if not yet voted
|
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 org.springframework.stereotype.Repository;
|
||||||
|
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
|
import java.util.Optional;
|
||||||
|
|
||||||
@Repository
|
@Repository
|
||||||
public interface MeetingAgendaResolutionRepository extends JpaRepository<MeetingAgendaResolution, Long> {
|
public interface MeetingAgendaResolutionRepository extends JpaRepository<MeetingAgendaResolution, Long> {
|
||||||
|
|
||||||
List<MeetingAgendaResolution> findByMeetingId(Long meetingId);
|
List<MeetingAgendaResolution> findByMeetingId(Long meetingId);
|
||||||
|
|
||||||
|
Optional<MeetingAgendaResolution> findByAgendaId(Long agendaId);
|
||||||
|
|
||||||
@Modifying
|
@Modifying
|
||||||
@Query("DELETE FROM MeetingAgendaResolution r WHERE r.meetingId = :meetingId")
|
@Query("DELETE FROM MeetingAgendaResolution r WHERE r.meetingId = :meetingId")
|
||||||
void deleteByMeetingId(@Param("meetingId") Long meetingId);
|
void deleteByMeetingId(@Param("meetingId") Long meetingId);
|
||||||
|
|||||||
+6
-1
@@ -37,12 +37,17 @@ public class MeetingVoteToken {
|
|||||||
@Column(name = "created_at", nullable = false)
|
@Column(name = "created_at", nullable = false)
|
||||||
private LocalDateTime createdAt;
|
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();
|
MeetingVoteToken t = new MeetingVoteToken();
|
||||||
t.token = UUID.randomUUID().toString();
|
t.token = UUID.randomUUID().toString();
|
||||||
t.meetingId = meetingId;
|
t.meetingId = meetingId;
|
||||||
t.memberId = memberId;
|
t.memberId = memberId;
|
||||||
t.memberName = memberName;
|
t.memberName = memberName;
|
||||||
|
t.designation = designation;
|
||||||
t.used = false;
|
t.used = false;
|
||||||
t.createdAt = LocalDateTime.now();
|
t.createdAt = LocalDateTime.now();
|
||||||
t.expiresAt = t.createdAt.plusDays(validDays);
|
t.expiresAt = t.createdAt.plusDays(validDays);
|
||||||
|
|||||||
+68
-4
@@ -29,6 +29,7 @@ public class MeetingVoteService {
|
|||||||
private final ElectionCandidateRepository electionCandidateRepository;
|
private final ElectionCandidateRepository electionCandidateRepository;
|
||||||
private final ClientRepository clientRepository;
|
private final ClientRepository clientRepository;
|
||||||
private final EmailSenderService emailSenderService;
|
private final EmailSenderService emailSenderService;
|
||||||
|
private final DesignationService designationService;
|
||||||
|
|
||||||
@Value("${cbs.frontend.base-url:https://app.cbsnexora.com}")
|
@Value("${cbs.frontend.base-url:https://app.cbsnexora.com}")
|
||||||
private String frontendBaseUrl;
|
private String frontendBaseUrl;
|
||||||
@@ -48,16 +49,50 @@ public class MeetingVoteService {
|
|||||||
|
|
||||||
boolean closed = voteToken.isUsed() || voteToken.isExpired();
|
boolean closed = voteToken.isUsed() || voteToken.isExpired();
|
||||||
|
|
||||||
|
// Current voter's own votes
|
||||||
Map<Long, String> existingVotes = voteRepository
|
Map<Long, String> existingVotes = voteRepository
|
||||||
.findByMeetingIdAndMemberId(meeting.getId(), voteToken.getMemberId())
|
.findByMeetingIdAndMemberId(meeting.getId(), voteToken.getMemberId())
|
||||||
.stream().collect(Collectors.toMap(
|
.stream().collect(Collectors.toMap(
|
||||||
MeetingMemberVote::getAgendaId,
|
MeetingMemberVote::getAgendaId,
|
||||||
v -> v.getVote().name()));
|
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()
|
List<MeetingVotingPageData.AgendaItemVoteData> items = meeting.getAgendaItems().stream()
|
||||||
.map(a -> new MeetingVotingPageData.AgendaItemVoteData(
|
.map(a -> {
|
||||||
a.getId(), a.getAgendaItem(), a.getDisplayOrder(),
|
Map<Long, String> agendaVotes = votesByAgenda.getOrDefault(a.getId(), Map.of());
|
||||||
existingVotes.get(a.getId())))
|
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();
|
.toList();
|
||||||
|
|
||||||
String dateStr = meeting.getMeetingDate() != null
|
String dateStr = meeting.getMeetingDate() != null
|
||||||
@@ -102,6 +137,34 @@ public class MeetingVoteService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
voteRepository.saveAll(toSave);
|
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);
|
voteToken.setUsed(true);
|
||||||
tokenRepository.save(voteToken);
|
tokenRepository.save(voteToken);
|
||||||
@@ -203,7 +266,8 @@ public class MeetingVoteService {
|
|||||||
if (email == null) continue;
|
if (email == null) continue;
|
||||||
|
|
||||||
MeetingVoteToken token = MeetingVoteToken.generate(
|
MeetingVoteToken token = MeetingVoteToken.generate(
|
||||||
meetingId, member.getId(), appointment.getCandidateName(), 7);
|
meetingId, member.getId(), appointment.getCandidateName(),
|
||||||
|
appointment.getDesignation(), 7);
|
||||||
tokenRepository.save(token);
|
tokenRepository.save(token);
|
||||||
|
|
||||||
String html = buildVotingEmailHtml(
|
String html = buildVotingEmailHtml(
|
||||||
|
|||||||
@@ -384,4 +384,5 @@
|
|||||||
<include file="parts/0354_meeting_vote_resolution.xml" relativeToChangelogFile="true"/>
|
<include file="parts/0354_meeting_vote_resolution.xml" relativeToChangelogFile="true"/>
|
||||||
<include file="parts/0355_director_designation_code.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/0356_meeting_vote_remark.xml" relativeToChangelogFile="true"/>
|
||||||
|
<include file="parts/0357_meeting_vote_token_designation.xml" relativeToChangelogFile="true"/>
|
||||||
</databaseChangeLog>
|
</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