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 <noreply@anthropic.com>
This commit is contained in:
Jaanu423
2026-07-03 12:24:50 +05:30
co-authored by Claude Sonnet 4.6
parent a44238774f
commit 228df2f32c
6 changed files with 201 additions and 39 deletions
@@ -106,7 +106,7 @@
<span>{{ d.designation || '—' }}</span>
</div>
<div class="tenure-row">
<div class="tenure-row" [class.tenure-expired]="d.isExpired">
<div class="card-field">
<mat-icon>event_available</mat-icon>
<div>
@@ -119,7 +119,12 @@
<mat-icon>event_busy</mat-icon>
<div>
<span class="field-label">Tenure End</span>
<span class="field-val">{{ d.tenureEndDate ? (d.tenureEndDate | date: 'dd MMM yyyy') : '—' }}</span>
<span class="field-val" [class.expired-date]="d.isExpired">
{{ d.tenureEndDate ? (d.tenureEndDate | date: 'dd MMM yyyy') : '—' }}
@if (d.isExpired) {
<span class="expired-tag">Expired</span>
}
</span>
</div>
</div>
</div>
@@ -141,8 +146,12 @@
<div class="card-actions">
<button class="btn-edit" (click)="openEdit(d)"><mat-icon>edit</mat-icon> Edit</button>
<button class="btn-delete" (click)="confirmDelete(d.id)" [disabled]="deleting === d.id">
<mat-icon>delete_outline</mat-icon>
<button
class="btn-delete"
(click)="confirmRemove(d)"
[disabled]="deleting === d.id || d.status === 'REMOVED'"
>
<mat-icon>person_remove</mat-icon>
{{ deleting === d.id ? 'Removing...' : 'Remove' }}
</button>
</div>
@@ -273,15 +282,17 @@
<mat-error>Designation is required</mat-error>
}
</mat-form-field>
<mat-form-field appearance="outline">
<mat-label>Status</mat-label>
<mat-select [(ngModel)]="form.status">
<mat-option value="ACTIVE">Active</mat-option>
<mat-option value="RESIGNED">Resigned</mat-option>
<mat-option value="REMOVED">Removed</mat-option>
<mat-option value="TENURE_ENDED">Tenure Ended</mat-option>
</mat-select>
</mat-form-field>
@if (editingId) {
<mat-form-field appearance="outline">
<mat-label>Status</mat-label>
<mat-select [(ngModel)]="form.status">
<mat-option value="ACTIVE">Active</mat-option>
<mat-option value="RESIGNED">Resigned</mat-option>
<mat-option value="REMOVED">Removed</mat-option>
<mat-option value="TENURE_ENDED">Tenure Ended</mat-option>
</mat-select>
</mat-form-field>
}
</div>
<div class="form-row">
<mat-form-field appearance="outline">
@@ -302,33 +313,59 @@
<!-- Tenure -->
<div class="form-section-title">Tenure Period</div>
<div class="form-row">
<mat-form-field appearance="outline">
<mat-label>Tenure Start Date <span class="req">*</span></mat-label>
<mat-form-field appearance="outline" [class.field-auto-filled]="isTenureStartAutoFilled">
<mat-label>
Tenure Start Date <span class="req">*</span>
@if (isTenureStartAutoFilled) {
<span class="auto-badge">Auto</span>
}
</mat-label>
<input
matInput
[matDatepicker]="tenureStart"
[(ngModel)]="form.tenureStartDate"
[min]="today"
[readonly]="isTenureStartAutoFilled"
(ngModelChange)="onTenureStartChange($event)"
required
/>
<mat-datepicker-toggle matSuffix [for]="tenureStart"></mat-datepicker-toggle>
<mat-datepicker #tenureStart></mat-datepicker>
<mat-datepicker-toggle
matSuffix
[for]="tenureStart"
[disabled]="isTenureStartAutoFilled"
></mat-datepicker-toggle>
<mat-datepicker #tenureStart [disabled]="isTenureStartAutoFilled"></mat-datepicker>
@if (isTenureStartAutoFilled) {
<mat-hint>Derived from election result date</mat-hint>
}
@if (submitted && !form.tenureStartDate) {
<mat-error>Tenure start date is required</mat-error>
}
</mat-form-field>
<mat-form-field appearance="outline">
<mat-label>Tenure End Date <span class="req">*</span></mat-label>
<mat-form-field appearance="outline" [class.field-auto-filled]="isTenureEndAutoFilled">
<mat-label>
Tenure End Date <span class="req">*</span>
@if (isTenureEndAutoFilled) {
<span class="auto-badge">Auto</span>
}
</mat-label>
<input
matInput
[matDatepicker]="tenureEnd"
[(ngModel)]="form.tenureEndDate"
[min]="form.tenureStartDate || today"
[readonly]="isTenureEndAutoFilled"
required
/>
<mat-datepicker-toggle matSuffix [for]="tenureEnd"></mat-datepicker-toggle>
<mat-datepicker #tenureEnd></mat-datepicker>
<mat-datepicker-toggle
matSuffix
[for]="tenureEnd"
[disabled]="isTenureEndAutoFilled"
></mat-datepicker-toggle>
<mat-datepicker #tenureEnd [disabled]="isTenureEndAutoFilled"></mat-datepicker>
@if (isTenureEndAutoFilled) {
<mat-hint>Derived from election duration ({{ selectedElection?.durationYears }} yr)</mat-hint>
}
@if (submitted && !form.tenureEndDate) {
<mat-error>Tenure end date is required</mat-error>
}
@@ -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;
@@ -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 {
@@ -91,6 +91,12 @@
<mat-datepicker #resDate></mat-datepicker>
<mat-error *ngIf="basicForm.errors?.['resultBeforeElection']">Must be after Election Date</mat-error>
</mat-form-field>
<mat-form-field appearance="outline">
<mat-label>Duration (Years)</mat-label>
<input matInput type="number" formControlName="durationYears" min="1" max="20" placeholder="e.g. 5" />
<mat-hint>Elected board tenure. No new election until this period expires.</mat-hint>
</mat-form-field>
</div>
<div class="step-actions">
@@ -190,6 +196,12 @@
{{ basicForm.value.resultDate ? (basicForm.value.resultDate | date: 'dd MMM yyyy') : '—' }}
</span>
</div>
@if (basicForm.value.durationYears) {
<div class="review-item">
<span class="review-label">Duration</span>
<span class="review-value">{{ basicForm.value.durationYears }} year(s)</span>
</div>
}
@if (basicForm.value.description) {
<div class="review-item full-span">
<span class="review-label">Description</span>
@@ -94,7 +94,8 @@ export class ElectionConfigComponent implements OnInit {
null,
Validators.required
],
resultDate: [null]
resultDate: [null],
durationYears: [null]
},
{ validators: this.dateOrderValidator() }
);
@@ -82,6 +82,15 @@
<span class="date-value">{{ election.electionDate | date: 'dd MMM yyyy' }}</span>
</div>
</div>
@if (election.durationEndDate) {
<div class="date-item">
<mat-icon>schedule</mat-icon>
<div>
<span class="date-label">Board Active Until</span>
<span class="date-value">{{ election.durationEndDate | date: 'dd MMM yyyy' }}</span>
</div>
</div>
}
</div>
@if (election.description) {