feat(election): candidate photos/symbols, voting interface, withdrawal, live vote counts

- Candidate board: show client profile photos and election symbols via CBS ContentStore
  (lazy-fetch base64 via GET /election/{id}/candidate/{id}/symbol), zoom/drag viewer
- Candidate board: live vote count badge per candidate (VOTING_OPEN/CLOSED only)
- Election dashboard: total voters/votes chips on VOTING_OPEN election cards
- Voting interface: fix candidates not loading (paged response .content extraction),
  lazy-load candidate photos and symbols, gold-bordered symbol on ballot card
- Nomination withdrawal: implement withdraw button in nomination-detail and nomination-list
- ImagePreviewDialogComponent: DomSanitizer support for data: URLs
- election.service: uploadCandidateSymbol (FormData), getCandidateSymbol, getClientImage,
  getVoteSummary methods
- main.scss: ipd-dialog / ipd-backdrop styles for image preview dialog

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
Jaanu423
2026-06-26 18:38:11 +05:30
co-authored by Claude Sonnet 4.6
parent 9e33392b01
commit f1b829a100
19 changed files with 1217 additions and 182 deletions
+8 -2
View File
@@ -57,10 +57,16 @@
<!-- Toolbar always visible (shows CBSNexora, Branch, Session info) -->
<mifosx-toolbar [sidenav]="sidenav" (collapse)="toggleCollapse($event)"></mifosx-toolbar>
<!-- Simplified menu bar — cashier OR clerk fullscreen mode. The review screen renders its own nav. -->
<!-- Simplified menu bar — cashier OR clerk OR election fullscreen mode. The review screen renders its own nav. -->
@if (hideShellLayout && !isWithdrawalReviewUrl) {
<!-- Election fullscreen — just an ESC hint bar -->
@if (currentUrl.includes('/election')) {
<nav class="cashier-nav cashier-nav--election">
<span class="cashier-nav__esc-hint" (click)="onEscapeKey()"> <kbd>ESC</kbd> Show Header </span>
</nav>
}
<!-- Clerk Dashboard fullscreen nav — only visible at /clerk/* -->
@if (currentUrl.includes('/clerk/')) {
@else if (currentUrl.includes('/clerk/')) {
<nav class="cashier-nav cashier-nav--clerk">
<a
class="cashier-nav__item cashier-nav__item--primary"
+7 -3
View File
@@ -137,7 +137,8 @@ export class ShellComponent implements OnInit, OnDestroy {
url.includes('/clerk/today-activity') ||
url.includes('/clerk/withdrawal-request') ||
url.includes('/clerk/my-disbursement-requests');
this.hideShellLayout = isCashierOnly || isClerkRoute || this.isWithdrawalReviewUrl;
const isElectionRoute = url.includes('/election');
this.hideShellLayout = isCashierOnly || isClerkRoute || isElectionRoute || this.isWithdrawalReviewUrl;
this.cdr.detectChanges();
};
this.routeSub = this.router.events.pipe(filter((e) => e instanceof NavigationEnd)).subscribe(() => checkLayout());
@@ -174,10 +175,13 @@ export class ShellComponent implements OnInit, OnDestroy {
return /\/checker-inbox-and-tasks\/withdrawal-requests\/\d+\/review/.test(this.router.url);
}
/** ESC key — exit fullscreen at Clerk Dashboard / review screen, restoring full shell header. */
/** ESC key — exit fullscreen at Clerk Dashboard / Election / review screen, restoring full shell header. */
@HostListener('document:keydown.escape')
onEscapeKey(): void {
if (this.hideShellLayout && (this.router.url.includes('/clerk/') || this.isWithdrawalReviewUrl)) {
if (
this.hideShellLayout &&
(this.router.url.includes('/clerk/') || this.router.url.includes('/election') || this.isWithdrawalReviewUrl)
) {
this.hideShellLayout = false;
this.cdr.detectChanges();
}