From 228df2f32cf73c82dc53da32c69320eb80f68f0d Mon Sep 17 00:00:00 2001 From: Jaanu423 Date: Fri, 3 Jul 2026 12:24:50 +0530 Subject: [PATCH] feat(election): election duration, director tenure auto-fill, and board UX improvements - Add durationYears field to election config with Board Active Until display on dashboard - Auto-fill director tenure start (from resultDate) and end (from durationEndDate) when election has duration set; both fields readonly with Auto badge - Hide Status field on new appointment form (only shown on edit) - Change Remove button from hard delete to soft delete (status REMOVED) with audit-safe confirm - Fix isTenureExpired() template call replaced with precomputed isExpired property Co-Authored-By: Claude Sonnet 4.6 --- .../director-board.component.html | 79 ++++++++++++----- .../director-board.component.scss | 50 +++++++++++ .../director-board.component.ts | 87 +++++++++++++++---- .../election-config.component.html | 12 +++ .../election-config.component.ts | 3 +- .../election-dashboard.component.html | 9 ++ 6 files changed, 201 insertions(+), 39 deletions(-) diff --git a/src/app/election/components/director-board/director-board.component.html b/src/app/election/components/director-board/director-board.component.html index 7fb2f4b64..13b37a547 100644 --- a/src/app/election/components/director-board/director-board.component.html +++ b/src/app/election/components/director-board/director-board.component.html @@ -106,7 +106,7 @@ {{ d.designation || '—' }} -
+
event_available
@@ -119,7 +119,12 @@ event_busy
Tenure End - {{ d.tenureEndDate ? (d.tenureEndDate | date: 'dd MMM yyyy') : '—' }} + + {{ d.tenureEndDate ? (d.tenureEndDate | date: 'dd MMM yyyy') : '—' }} + @if (d.isExpired) { + Expired + } +
@@ -141,8 +146,12 @@
-
@@ -273,15 +282,17 @@ Designation is required } - - Status - - Active - Resigned - Removed - Tenure Ended - - + @if (editingId) { + + Status + + Active + Resigned + Removed + Tenure Ended + + + }
@@ -302,33 +313,59 @@
Tenure Period
- - Tenure Start Date * + + + Tenure Start Date * + @if (isTenureStartAutoFilled) { + Auto + } + - - + + + @if (isTenureStartAutoFilled) { + Derived from election result date + } @if (submitted && !form.tenureStartDate) { Tenure start date is required } - - Tenure End Date * + + + Tenure End Date * + @if (isTenureEndAutoFilled) { + Auto + } + - - + + + @if (isTenureEndAutoFilled) { + Derived from election duration ({{ selectedElection?.durationYears }} yr) + } @if (submitted && !form.tenureEndDate) { Tenure end date is required } diff --git a/src/app/election/components/director-board/director-board.component.scss b/src/app/election/components/director-board/director-board.component.scss index 44eaa5fd7..337c6cc50 100644 --- a/src/app/election/components/director-board/director-board.component.scss +++ b/src/app/election/components/director-board/director-board.component.scss @@ -1032,6 +1032,56 @@ $font: font-style: normal; } +.auto-badge { + display: inline-block; + font-size: 9px; + font-weight: 700; + background: #d1fae5; + color: #065f46; + padding: 1px 6px; + border-radius: 6px; + letter-spacing: 0.3px; + text-transform: uppercase; + margin-left: 6px; + vertical-align: middle; +} + +.tenure-expired { + background: #fef2f2; + border: 1px solid #fecaca; +} + +.expired-date { + color: #dc2626; + display: flex; + align-items: center; + gap: 4px; +} + +.expired-tag { + display: inline-block; + font-size: 9px; + font-weight: 700; + background: #fee2e2; + color: #dc2626; + padding: 1px 6px; + border-radius: 6px; + letter-spacing: 0.3px; + text-transform: uppercase; +} + +.field-auto-filled { + ::ng-deep .mat-mdc-form-field-flex { + background: #f0fdf4 !important; + } + + ::ng-deep .mdc-notched-outline__leading, + ::ng-deep .mdc-notched-outline__notch, + ::ng-deep .mdc-notched-outline__trailing { + border-color: #86efac !important; + } +} + @media (width <= 600px) { .board-page { padding: 16px; diff --git a/src/app/election/components/director-board/director-board.component.ts b/src/app/election/components/director-board/director-board.component.ts index 37ff51968..8a7204990 100644 --- a/src/app/election/components/director-board/director-board.component.ts +++ b/src/app/election/components/director-board/director-board.component.ts @@ -77,6 +77,7 @@ export class DirectorBoardComponent implements OnInit { saving = false; deleting: number | null = null; submitted = false; + selectedElection: any = null; // ── Step-1: election + WON candidates ───────────────────────── elections: any[] = []; @@ -115,6 +116,8 @@ export class DirectorBoardComponent implements OnInit { pickPending(p: any): void { this.editingId = null; + this.selectedElectionId = p.electionId; + this.selectedElection = null; this.form = { ...this.emptyForm(), electionId: p.electionId, @@ -122,9 +125,17 @@ export class DirectorBoardComponent implements OnInit { candidateName: p.candidateName, postName: p.postName }; - this.selectedElectionId = p.electionId; this.formStep = 2; this.showForm = true; + this.service.getElection(p.electionId).subscribe((election: any) => { + this.selectedElection = election; + if (election?.durationEndDate) { + this.form.tenureEndDate = new Date(election.durationEndDate); + if (election?.resultDate) { + this.form.tenureStartDate = new Date(election.resultDate); + } + } + }); } load(): void { @@ -133,7 +144,11 @@ export class DirectorBoardComponent implements OnInit { .getDirectors({ status: this.filterStatus || undefined, page: this.page, size: this.pageSize }) .subscribe({ next: (data: any) => { - this.directors = data?.content ?? []; + const todayStr = new Date().toISOString().slice(0, 10); + this.directors = (data?.content ?? []).map((d: any) => ({ + ...d, + isExpired: d.tenureEndDate ? d.tenureEndDate < todayStr : false + })); this.totalElements = data?.totalElements ?? 0; this.loading = false; }, @@ -154,6 +169,7 @@ export class DirectorBoardComponent implements OnInit { openAppoint(): void { this.submitted = false; this.editingId = null; + this.selectedElection = null; this.form = this.emptyForm(); this.formStep = 1; this.selectedElectionId = null; @@ -172,6 +188,7 @@ export class DirectorBoardComponent implements OnInit { selectElection(electionId: number): void { this.selectedElectionId = electionId; + this.selectedElection = this.elections.find((e) => e.id === electionId) ?? null; this.wonCandidates = []; this.wonLoading = true; this.service.getResults(electionId).subscribe((results: any[]) => { @@ -181,12 +198,21 @@ export class DirectorBoardComponent implements OnInit { } pickCandidate(candidate: any): void { + const tenureEndDate = this.selectedElection?.durationEndDate + ? new Date(this.selectedElection.durationEndDate) + : null; + const tenureStartDate = + this.selectedElection?.durationEndDate && this.selectedElection?.resultDate + ? new Date(this.selectedElection.resultDate) + : null; this.form = { ...this.emptyForm(), electionId: this.selectedElectionId, candidateId: candidate.candidateId, candidateName: candidate.candidateName, - postName: candidate.postName + postName: candidate.postName, + tenureStartDate, + tenureEndDate }; this.formStep = 2; } @@ -197,15 +223,26 @@ export class DirectorBoardComponent implements OnInit { onTenureStartChange(val: Date | null): void { if (!val) return; + if (this.isTenureStartAutoFilled) return; const end = new Date(val); end.setFullYear(end.getFullYear() + 5); this.form.tenureEndDate = end; } + get isTenureStartAutoFilled(): boolean { + return !!this.selectedElection?.durationEndDate && !!this.selectedElection?.resultDate; + } + + get isTenureEndAutoFilled(): boolean { + return !!this.selectedElection?.durationEndDate; + } + // ── Open edit drawer (skip step 1) ──────────────────────────── openEdit(d: any): void { this.submitted = false; this.editingId = d.id; + this.selectedElectionId = d.electionId; + this.selectedElection = null; this.form = { electionId: d.electionId, candidateId: d.candidateId, @@ -221,6 +258,12 @@ export class DirectorBoardComponent implements OnInit { }; this.formStep = 2; this.showForm = true; + this.service.getElection(d.electionId).subscribe((election: any) => { + this.selectedElection = election; + if (election?.durationEndDate && !this.form.tenureEndDate) { + this.form.tenureEndDate = new Date(election.durationEndDate); + } + }); } closeForm(): void { @@ -229,6 +272,7 @@ export class DirectorBoardComponent implements OnInit { this.form = this.emptyForm(); this.formStep = 1; this.submitted = false; + this.selectedElection = null; } save(): void { @@ -273,20 +317,29 @@ export class DirectorBoardComponent implements OnInit { }); } - confirmDelete(id: number): void { - if (!confirm('Remove this director appointment?')) return; - this.deleting = id; - this.service.deleteDirector(id).subscribe({ - next: () => { - this.deleting = null; - this.load(); - this.snackBar.open('Appointment removed.', 'Close', { duration: 3000 }); - }, - error: () => { - this.deleting = null; - this.snackBar.open('Failed to remove appointment.', 'Close', { duration: 4000 }); - } - }); + confirmRemove(d: any): void { + if (!confirm(`Mark ${d.candidateName} as Removed? This will update their status but keep the record for audit.`)) + return; + this.deleting = d.id; + this.service + .updateDirector(d.id, { + ...d, + status: 'REMOVED', + appointmentDate: d.appointmentDate, + tenureStartDate: d.tenureStartDate, + tenureEndDate: d.tenureEndDate + }) + .subscribe({ + next: () => { + this.deleting = null; + this.load(); + this.snackBar.open('Director marked as Removed.', 'Close', { duration: 3000 }); + }, + error: () => { + this.deleting = null; + this.snackBar.open('Failed to remove director.', 'Close', { duration: 4000 }); + } + }); } get totalPages(): number { diff --git a/src/app/election/components/election-config/election-config.component.html b/src/app/election/components/election-config/election-config.component.html index 8de0e4557..d31aca9a3 100644 --- a/src/app/election/components/election-config/election-config.component.html +++ b/src/app/election/components/election-config/election-config.component.html @@ -91,6 +91,12 @@ Must be after Election Date + + + Duration (Years) + + Elected board tenure. No new election until this period expires. +
@@ -190,6 +196,12 @@ {{ basicForm.value.resultDate ? (basicForm.value.resultDate | date: 'dd MMM yyyy') : '—' }}
+ @if (basicForm.value.durationYears) { +
+ Duration + {{ basicForm.value.durationYears }} year(s) +
+ } @if (basicForm.value.description) {
Description diff --git a/src/app/election/components/election-config/election-config.component.ts b/src/app/election/components/election-config/election-config.component.ts index 53a6c1c2d..6005cd157 100644 --- a/src/app/election/components/election-config/election-config.component.ts +++ b/src/app/election/components/election-config/election-config.component.ts @@ -94,7 +94,8 @@ export class ElectionConfigComponent implements OnInit { null, Validators.required ], - resultDate: [null] + resultDate: [null], + durationYears: [null] }, { validators: this.dateOrderValidator() } ); diff --git a/src/app/election/components/election-dashboard/election-dashboard.component.html b/src/app/election/components/election-dashboard/election-dashboard.component.html index e35347808..55f36739a 100644 --- a/src/app/election/components/election-dashboard/election-dashboard.component.html +++ b/src/app/election/components/election-dashboard/election-dashboard.component.html @@ -82,6 +82,15 @@ {{ election.electionDate | date: 'dd MMM yyyy' }}
+ @if (election.durationEndDate) { +
+ schedule +
+ Board Active Until + {{ election.durationEndDate | date: 'dd MMM yyyy' }} +
+
+ }
@if (election.description) {