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:
co-authored by
Claude Sonnet 4.6
parent
93f4b9b03f
commit
3d65900fc4
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user