Files
cbsnexora/src/app/election/components/election-dashboard/election-dashboard.component.html
T
Jaanu423andClaude Sonnet 4.6 960e4e181b feat(election): audit log UI with server-side filter, pagination, entity-aware actions
- New ElectionAuditLogComponent: timeline view, expandable JSON details
- Server-side filtering: entityType, action, full-text search (350ms debounce)
- Pagination bar with page size selector (10/20/50 per page)
- Action dropdown filtered by selected entity (CANDIDATE shows only
  candidate actions, NOMINATION shows nomination actions, etc.)
- Entity change resets action filter automatically
- election.service.ts: getAuditLog() sends query params to backend
- Audit Log button added to every election card in dashboard
- Route: /election/:electionId/audit-log

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-29 13:02:45 +05:30

185 lines
7.2 KiB
HTML
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<!--
Copyright since 2025 Mifos Initiative
This Source Code Form is subject to the terms of the Mozilla Public
License, v. 2.0. If a copy of the MPL was not distributed with this
file, You can obtain one at http://mozilla.org/MPL/2.0/.
-->
<div class="election-dashboard">
<div class="page-header">
<h2 class="page-title">Election Management</h2>
<a routerLink="/election/config/new" class="btn-primary"> <mat-icon>add</mat-icon> Create Election </a>
</div>
@if (loading) {
<div class="loading-state">
<div class="spinner"></div>
<p>Loading elections...</p>
</div>
} @else if (elections.length === 0) {
<div class="empty-state">
<mat-icon class="empty-icon">how_to_vote</mat-icon>
<h3>No elections found</h3>
<p>Create a new election to get started.</p>
<a routerLink="/election/config/new" class="btn-primary">Create Election</a>
</div>
} @else {
<div class="stats-row">
<div class="stat-card">
<span class="stat-value">{{ totalElements }}</span>
<span class="stat-label">Total</span>
</div>
<div class="stat-card">
<span class="stat-value">{{ countByStatus('ACTIVE') }}</span>
<span class="stat-label">Active</span>
</div>
<div class="stat-card">
<span class="stat-value">{{ countByStatus('VOTING_OPEN') }}</span>
<span class="stat-label">Voting Open</span>
</div>
<div class="stat-card">
<span class="stat-value">{{ countByStatus('RESULT_DECLARED') }}</span>
<span class="stat-label">Completed</span>
</div>
</div>
<div class="elections-grid">
@for (election of paginatedElections; track election.id) {
<div class="election-card">
<div class="card-header">
<div>
<h3 class="election-title">{{ election.title }}</h3>
<span class="election-type">{{ election.electionType }}</span>
</div>
<span class="status-chip" [ngClass]="statusClass(election.status)">
{{ statusLabel(election.status) }}
</span>
</div>
<div class="card-body">
<div class="date-row">
<div class="date-item">
<mat-icon>event_note</mat-icon>
<div>
<span class="date-label">Nominations</span>
<span class="date-value">
{{ election.nominationStartDate | date: 'dd MMM' }} –
{{ election.nominationEndDate | date: 'dd MMM yyyy' }}
</span>
</div>
</div>
<div class="date-item">
<mat-icon>how_to_vote</mat-icon>
<div>
<span class="date-label">Election Date</span>
<span class="date-value">{{ election.electionDate | date: 'dd MMM yyyy' }}</span>
</div>
</div>
</div>
@if (election.description) {
<p class="description">{{ election.description }}</p>
}
@if (voteSummaryMap[election.id]) {
<div class="voter-count-row">
<span class="voter-count-chip">
<mat-icon>how_to_vote</mat-icon>
{{ voteSummaryMap[election.id].totalVoters }} members voted
</span>
<span class="vote-total-chip"> {{ voteSummaryMap[election.id].totalVotes }} total votes </span>
</div>
}
</div>
<div class="card-actions">
<a [routerLink]="['/election', election.id, 'nominations']" class="btn-outline">
<mat-icon>people</mat-icon> Nominations
</a>
<a [routerLink]="['/election', election.id, 'candidates']" class="btn-outline">
<mat-icon>person_pin</mat-icon> Candidates
</a>
@if (election.status === 'DRAFT') {
<button class="btn-activate" [disabled]="actionLoading[election.id]" (click)="activate(election)">
<mat-icon>play_circle</mat-icon>
{{ actionLoading[election.id] ? 'Activating...' : 'Activate' }}
</button>
}
@if (canNominate(election)) {
<a [routerLink]="['/election', election.id, 'nomination', 'apply']" class="btn-primary">
<mat-icon>person_add</mat-icon> Nominate Member
</a>
}
@if (election.status === 'ACTIVE') {
<button class="btn-warn" [disabled]="actionLoading[election.id]" (click)="closeNominations(election)">
<mat-icon>lock</mat-icon> Close Nominations
</button>
}
@if (election.status === 'NOMINATION_CLOSED') {
<button class="btn-activate" [disabled]="actionLoading[election.id]" (click)="openVoting(election)">
<mat-icon>how_to_vote</mat-icon> Open Voting
</button>
}
@if (canVote(election)) {
<a [routerLink]="['/election', election.id, 'vote']" class="btn-vote">
<mat-icon>ballot</mat-icon> Vote Now
</a>
}
@if (canViewResults(election)) {
<a [routerLink]="['/election', election.id, 'results']" class="btn-results">
<mat-icon>bar_chart</mat-icon> Results
</a>
}
<a [routerLink]="['/election', election.id, 'audit-log']" class="btn-audit">
<mat-icon>history</mat-icon> Audit Log
</a>
</div>
</div>
}
</div>
@if (totalPages > 1 || totalElements > pageSizeOptions[0]) {
<div class="pagination-bar">
<div class="pg-info">Showing {{ startItem }}–{{ endItem }} of {{ totalElements }}</div>
<div class="pg-controls">
<button class="pg-btn" [disabled]="currentPage === 1" (click)="goToPage(1)" title="First">
<mat-icon>first_page</mat-icon>
</button>
<button class="pg-btn" [disabled]="currentPage === 1" (click)="goToPage(currentPage - 1)" title="Previous">
<mat-icon>chevron_left</mat-icon>
</button>
@for (p of pageNumbers; track $index) {
@if (isNumber(p)) {
<button class="pg-btn pg-num" [class.pg-num--active]="p === currentPage" (click)="goToPage(p)">
{{ p }}
</button>
} @else {
<span class="pg-ellipsis">…</span>
}
}
<button
class="pg-btn"
[disabled]="currentPage === totalPages"
(click)="goToPage(currentPage + 1)"
title="Next"
>
<mat-icon>chevron_right</mat-icon>
</button>
<button class="pg-btn" [disabled]="currentPage === totalPages" (click)="goToPage(totalPages)" title="Last">
<mat-icon>last_page</mat-icon>
</button>
</div>
<div class="pg-size">
<select [(ngModel)]="pageSize" (change)="onPageSizeChange()">
@for (n of pageSizeOptions; track n) {
<option [value]="n">{{ n }} / page</option>
}
</select>
</div>
</div>
}
}
</div>