feat(election): Chairman/VC auto-resolution, member vote panel, read-only committee summary

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
Jaanu423
2026-07-09 11:02:32 +05:30
co-authored by Claude Sonnet 4.6
parent 93f4b9b03f
commit 3d65900fc4
6 changed files with 188 additions and 101 deletions
@@ -619,42 +619,29 @@
</div>
<div class="resolution-row">
<label class="res-label">Final Resolution:</label>
<div class="res-buttons">
<button
class="btn-res approve"
[class.active]="resolutionForm[item.agendaId]?.resolution === 'APPROVED'"
(click)="resolutionForm[item.agendaId].resolution = 'APPROVED'"
type="button"
<label class="res-label">Resolution:</label>
@if (item.resolution) {
<span
class="res-badge"
[class]="
item.resolution === 'APPROVED'
? 'res-badge-approved'
: item.resolution === 'REJECTED'
? 'res-badge-rejected'
: 'res-badge-deferred'
"
>
<mat-icon>check_circle</mat-icon> Approved
</button>
<button
class="btn-res reject"
[class.active]="resolutionForm[item.agendaId]?.resolution === 'REJECTED'"
(click)="resolutionForm[item.agendaId].resolution = 'REJECTED'"
type="button"
>
<mat-icon>cancel</mat-icon> Rejected
</button>
<button
class="btn-res defer"
[class.active]="resolutionForm[item.agendaId]?.resolution === 'DEFERRED'"
(click)="resolutionForm[item.agendaId].resolution = 'DEFERRED'"
type="button"
>
<mat-icon>pause_circle</mat-icon> Deferred
</button>
</div>
</div>
<div class="notes-row">
<textarea
class="notes-input"
placeholder="Discussion notes (optional)…"
rows="2"
[(ngModel)]="resolutionForm[item.agendaId].discussionNotes"
></textarea>
{{
item.resolution === 'APPROVED'
? '✅ Approved'
: item.resolution === 'REJECTED'
? '❌ Rejected'
: '⏸ Deferred'
}}
</span>
} @else {
<span class="res-badge res-badge-pending">Awaiting Chairman / Vice Chairman vote</span>
}
</div>
</div>
}
@@ -663,15 +650,7 @@
<!-- Modal footer -->
<div class="vm-footer">
<button class="btn-cancel" (click)="closeVotePanel()">Cancel</button>
<button
class="btn-save purple-btn"
(click)="saveResolutions()"
[disabled]="savingResolutions || voteSummaryLoading"
>
<mat-icon>{{ savingResolutions ? 'hourglass_empty' : 'send' }}</mat-icon>
{{ savingResolutions ? 'Saving...' : 'Save & Send Minutes' }}
</button>
<button class="btn-cancel" (click)="closeVotePanel()">Close</button>
</div>
</div>
}
@@ -1829,10 +1829,34 @@ $font-family:
margin-bottom: 8px;
}
.res-buttons {
display: flex;
gap: 6px;
flex-wrap: wrap;
.res-badge {
display: inline-block;
padding: 4px 12px;
border-radius: 20px;
font-size: 11px;
font-weight: 700;
&.res-badge-approved {
background: #dcfce7;
color: #15803d;
}
&.res-badge-rejected {
background: #fee2e2;
color: #b91c1c;
}
&.res-badge-deferred {
background: #fef3c7;
color: #b45309;
}
&.res-badge-pending {
background: #f1f5f9;
color: #94a3b8;
font-style: italic;
font-weight: 400;
}
}
}
@@ -77,8 +77,6 @@ export class CommitteeBoardComponent implements OnInit {
votePanelMeeting: any = null;
voteSummary: any[] = [];
voteSummaryLoading = false;
resolutionForm: Record<number, { resolution: string; discussionNotes: string }> = {};
savingResolutions = false;
ngOnInit(): void {
this.load();
@@ -653,18 +651,10 @@ export class CommitteeBoardComponent implements OnInit {
this.votePanelMeeting = meeting;
this.showVotePanel = true;
this.voteSummary = [];
this.resolutionForm = {};
this.voteSummaryLoading = true;
this.service.getVoteSummary(meeting.id).subscribe({
next: (data) => {
this.voteSummary = data ?? [];
this.resolutionForm = {};
for (const item of this.voteSummary) {
this.resolutionForm[item.agendaId] = {
resolution: item.resolution ?? '',
discussionNotes: item.discussionNotes ?? ''
};
}
this.voteSummaryLoading = false;
},
error: () => {
@@ -680,48 +670,6 @@ export class CommitteeBoardComponent implements OnInit {
this.voteSummary = [];
}
saveResolutions(): void {
if (this.savingResolutions) return;
const meetingId = this.votePanelMeeting?.id;
if (!meetingId) return;
const payload = Object.entries(this.resolutionForm)
.filter(
([
,
v
]) => v.resolution
)
.map(
([
agendaId,
v
]) => ({
agendaId: Number(agendaId),
resolution: v.resolution,
discussionNotes: v.discussionNotes
})
);
if (!payload.length) {
this.snackBar.open('Please set at least one resolution.', 'Close', { duration: 3000 });
return;
}
this.savingResolutions = true;
this.service.saveResolutions(meetingId, payload).subscribe({
next: () => {
this.savingResolutions = false;
this.snackBar.open('Resolutions saved and meeting minutes sent to all members.', 'Close', { duration: 4000 });
this.closeVotePanel();
},
error: () => {
this.savingResolutions = false;
this.snackBar.open('Failed to save resolutions.', 'Close', { duration: 3000 });
}
});
}
resolutionIcon(r: string): string {
return r === 'APPROVED' ? 'check_circle' : r === 'REJECTED' ? 'cancel' : 'pause_circle';
}
@@ -126,7 +126,36 @@
</button>
</div>
<!-- Remark row — shown only for Approve / Reject -->
<!-- Member votes panel — always visible -->
@if (item.memberVotes?.length) {
<div class="member-votes-panel">
<button class="mv-toggle" type="button" (click)="toggleMemberVotes(item.agendaId)">
<mat-icon>{{ showMemberVotes[item.agendaId] ? 'expand_less' : 'people' }}</mat-icon>
Committee Votes ({{ countVoted(item) }}/{{ item.memberVotes.length }})
</button>
@if (showMemberVotes[item.agendaId]) {
<div class="mv-list">
@for (mv of item.memberVotes; track mv.memberName) {
<div class="mv-row">
<span class="mv-name">{{ mv.memberName }}</span>
@if (mv.designation) {
<span class="mv-desig">{{ mv.designation }}</span>
}
@if (mv.vote) {
<span class="mv-badge" [class]="'badge-' + mv.vote.toLowerCase()">
{{ mv.vote === 'APPROVED' ? '✅' : mv.vote === 'REJECTED' ? '❌' : '—' }}
{{ mv.vote }}
</span>
} @else {
<span class="mv-badge badge-pending">Pending</span>
}
</div>
}
</div>
}
</div>
}
@if (selections[item.agendaId] === 'APPROVED' || selections[item.agendaId] === 'REJECTED') {
<div class="remark-row">
<textarea
@@ -196,6 +196,102 @@
}
}
// ── Member votes panel ────────────────────────────────────────────────────────
.member-votes-panel {
padding: 6px 0 0 28px;
}
.mv-toggle {
display: inline-flex;
align-items: center;
gap: 4px;
background: none;
border: 1.5px solid #e2e8f0;
border-radius: 6px;
padding: 3px 10px;
font-size: 11px;
font-weight: 600;
color: #64748b;
cursor: pointer;
transition: all 0.12s;
mat-icon {
font-size: 14px;
width: 14px;
height: 14px;
}
&:hover {
background: #f1f5f9;
border-color: #cbd5e1;
}
}
.mv-list {
margin-top: 6px;
border: 1px solid #e2e8f0;
border-radius: 6px;
overflow: hidden;
}
.mv-row {
display: flex;
align-items: center;
gap: 8px;
padding: 5px 10px;
border-bottom: 1px solid #f1f5f9;
font-size: 12px;
&:last-child {
border-bottom: none;
}
}
.mv-name {
flex: 1;
color: #1e293b;
font-weight: 500;
}
.mv-desig {
font-size: 10px;
color: #7c3aed;
background: #f5f3ff;
padding: 1px 6px;
border-radius: 10px;
font-weight: 600;
white-space: nowrap;
}
.mv-badge {
padding: 2px 8px;
border-radius: 20px;
font-size: 10px;
font-weight: 700;
white-space: nowrap;
flex-shrink: 0;
&.badge-approved {
background: #dcfce7;
color: #15803d;
}
&.badge-rejected {
background: #fee2e2;
color: #b91c1c;
}
&.badge-abstain {
background: #fef3c7;
color: #b45309;
}
&.badge-pending {
background: #f1f5f9;
color: #94a3b8;
}
}
// ── Remark row ────────────────────────────────────────────────────────────────
.remark-row {
display: flex;
@@ -43,6 +43,9 @@ export class MeetingVoteComponent implements OnInit, OnDestroy {
// agendaId → remark text
remarks: Record<number, string> = {};
// agendaId → whether member votes panel is expanded
showMemberVotes: Record<number, boolean> = {};
// Voice recording state
recording: number | null = null; // agendaId currently recording
recordingLang: 'hi-IN' | 'en-IN' = 'hi-IN';
@@ -206,4 +209,12 @@ export class MeetingVoteComponent implements OnInit, OnDestroy {
voteLabel(vote: string): string {
return vote === 'APPROVED' ? '✅ Approved' : vote === 'REJECTED' ? '❌ Rejected' : '— Abstain';
}
toggleMemberVotes(agendaId: number): void {
this.showMemberVotes[agendaId] = !this.showMemberVotes[agendaId];
}
countVoted(item: any): number {
return ((item.memberVotes ?? []) as any[]).filter((mv) => !!mv.vote).length;
}
}