feat(election): Board of Directors module — appoint, manage, and track elected directors

- New director-board component: 2-step drawer (pick WON candidate → fill appointment details)
- Pending appointments section: highlights WON candidates not yet given a designation
- mat-datepicker for all date fields; tenure end auto-fills to +5 years from start
- Frontend validation: required field indicators + inline mat-error on submit attempt
- Past dates disabled via [min]="today" on all date pickers
- mat-select for Designation and Status (CBS outline style)
- Election service: appointDirector, updateDirector, deleteDirector, getDirectors, getPendingDirectors
- Route added: /election/directors; nav link in election dashboard
- Error handler: suppress 404 toast for /election/directors (backend not yet deployed guard)

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
Jaanu423
2026-07-02 12:47:33 +05:30
co-authored by Claude Sonnet 4.6
parent bc513e7c89
commit a44238774f
9 changed files with 1830 additions and 4 deletions
@@ -53,7 +53,8 @@ export class ErrorHandlerInterceptor implements HttpInterceptor {
const isClientImageRequest = request.url.includes('/clients/') && request.url.includes('/images');
const isClientKycRequest = request.url.match(/\/clients\/\d+\/kyc$/);
const isSilentRequest = isClientImageRequest || isClientKycRequest;
const isDirectorRequest = request.url.includes('/election/directors');
const isSilentRequest = isClientImageRequest || isClientKycRequest || isDirectorRequest;
if (!environment.production && !isSilentRequest) {
log.error(`Request Error: ${errorMessage}`);
@@ -0,0 +1,353 @@
<!--
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="board-page">
<!-- Header -->
<div class="board-header">
<div class="header-left">
<a routerLink="/election" class="back-btn">
<mat-icon>arrow_back</mat-icon>
</a>
<div>
<h2>Board of Directors</h2>
<p class="header-sub">Registry of elected directors, their designations and tenures</p>
</div>
</div>
<button class="btn-appoint" (click)="openAppoint()">
<mat-icon>person_add</mat-icon>
<span>Appoint Director</span>
</button>
</div>
<!-- Pending Appointments -->
@if (!pendingLoading && pendingCandidates.length > 0) {
<div class="pending-section">
<div class="pending-header">
<mat-icon class="pending-icon">pending_actions</mat-icon>
<div>
<h4>Pending Appointments</h4>
<p>{{ pendingCandidates.length }} won candidate(s) awaiting designation</p>
</div>
</div>
<div class="pending-list">
@for (p of pendingCandidates; track p.candidateId) {
<div class="pending-card">
<div class="pending-avatar">{{ p.candidateName?.charAt(0) }}</div>
<div class="pending-info">
<span class="pending-name">{{ p.candidateName }}</span>
<span class="pending-meta">{{ p.postName }} &nbsp;·&nbsp; {{ p.electionTitle }}</span>
</div>
<div class="pending-votes">
<mat-icon>emoji_events</mat-icon>
<span>{{ p.voteCount }} votes</span>
</div>
<button class="btn-appoint-now" (click)="pickPending(p)">
<mat-icon>person_add</mat-icon>
Appoint
</button>
</div>
}
</div>
</div>
}
<!-- Filters -->
<div class="filter-bar">
@for (s of statusOptions; track s) {
<button class="filter-chip" [class.active]="filterStatus === s" (click)="filterStatus = s; applyFilter()">
{{ statusLabels[s] }}
</button>
}
</div>
<!-- Loading -->
@if (loading) {
<div class="loading-state">
<div class="spinner"></div>
<p>Loading board registry...</p>
</div>
} @else if (directors.length === 0) {
<div class="empty-state">
<mat-icon class="empty-icon">groups</mat-icon>
<h3>No directors appointed yet</h3>
<p>Select a won candidate from election results to appoint as director.</p>
<button class="btn-appoint" (click)="openAppoint()">
<mat-icon>person_add</mat-icon>
<span>Appoint First Director</span>
</button>
</div>
} @else {
<!-- Director Cards Grid -->
<div class="directors-grid">
@for (d of directors; track d.id) {
<div class="director-card">
<div class="card-top">
<div class="director-avatar">{{ d.candidateName?.charAt(0) }}</div>
<div class="director-info">
<h4>{{ d.candidateName }}</h4>
<span class="post-badge">{{ d.postName }}</span>
</div>
<span
class="status-chip"
[style.background-color]="statusColor(d.status) + '20'"
[style.color]="statusColor(d.status)"
>
{{ d.status }}
</span>
</div>
<div class="card-field designation-field">
<mat-icon>badge</mat-icon>
<span>{{ d.designation || '—' }}</span>
</div>
<div class="tenure-row">
<div class="card-field">
<mat-icon>event_available</mat-icon>
<div>
<span class="field-label">Tenure Start</span>
<span class="field-val">{{ d.tenureStartDate ? (d.tenureStartDate | date: 'dd MMM yyyy') : '—' }}</span>
</div>
</div>
<div class="tenure-divider">→</div>
<div class="card-field">
<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>
</div>
</div>
</div>
<div class="meta-row">
@if (d.resolutionNo) {
<span class="meta-tag"><mat-icon>description</mat-icon>{{ d.resolutionNo }}</span>
}
@if (d.appointmentDate) {
<span class="meta-tag"
><mat-icon>calendar_today</mat-icon>{{ d.appointmentDate | date: 'dd MMM yyyy' }}</span
>
}
</div>
@if (d.remarks) {
<p class="remarks-text">{{ d.remarks }}</p>
}
<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>
{{ deleting === d.id ? 'Removing...' : 'Remove' }}
</button>
</div>
</div>
}
</div>
@if (totalPages > 1) {
<div class="pagination">
<button class="page-btn" (click)="prevPage()" [disabled]="page === 0">
<mat-icon>chevron_left</mat-icon>
</button>
<span class="page-info">Page {{ page + 1 }} of {{ totalPages }} &nbsp;·&nbsp; {{ totalElements }} records</span>
<button class="page-btn" (click)="nextPage()" [disabled]="page >= totalPages - 1">
<mat-icon>chevron_right</mat-icon>
</button>
</div>
}
}
</div>
<!-- ── Drawer Overlay ── -->
@if (showForm) {
<div class="overlay" (click)="closeForm()"></div>
<div class="form-drawer">
<!-- Drawer Header -->
<div class="drawer-header">
<div class="drawer-title-row">
@if (formStep === 2 && !editingId) {
<button class="icon-btn" (click)="backToStep1()">
<mat-icon>arrow_back</mat-icon>
</button>
}
<div>
<h3>{{ editingId ? 'Edit Director' : formStep === 1 ? 'Select Won Candidate' : 'Appointment Details' }}</h3>
@if (!editingId) {
<p class="step-hint">Step {{ formStep }} of 2</p>
}
</div>
</div>
<button class="close-btn" (click)="closeForm()"><mat-icon>close</mat-icon></button>
</div>
<!-- ── STEP 1: Select Won Candidate ── -->
@if (formStep === 1) {
<div class="drawer-body">
<!-- Election Selector -->
@if (electionsLoading) {
<div class="loading-state small">
<div class="spinner"></div>
<p>Loading elections...</p>
</div>
} @else {
<mat-form-field appearance="outline" class="full-field">
<mat-label>Select Election</mat-label>
<mat-select [(ngModel)]="selectedElectionId" (ngModelChange)="selectElection($event)">
<mat-option [value]="null">— Choose an election —</mat-option>
@for (e of elections; track e.id) {
<mat-option [value]="e.id">{{ e.title }} ({{ e.status }})</mat-option>
}
</mat-select>
</mat-form-field>
}
<!-- WON Candidates List -->
@if (wonLoading) {
<div class="loading-state small">
<div class="spinner"></div>
<p>Loading results...</p>
</div>
} @else if (selectedElectionId && wonCandidates.length === 0) {
<div class="empty-won">
<mat-icon>info</mat-icon>
<span>No won candidates found for this election.</span>
</div>
} @else if (wonCandidates.length > 0) {
<div class="form-section-title">Won Candidates — click to select</div>
<div class="won-list">
@for (c of wonCandidates; track c.candidateId) {
<button class="won-card" (click)="pickCandidate(c)">
<div class="won-avatar">{{ c.candidateName?.charAt(0) }}</div>
<div class="won-info">
<span class="won-name">{{ c.candidateName }}</span>
<span class="won-post">{{ c.postName }}</span>
</div>
<div class="won-votes">
<mat-icon class="trophy">emoji_events</mat-icon>
<span>{{ c.voteCount }} votes</span>
</div>
<mat-icon class="pick-arrow">chevron_right</mat-icon>
</button>
}
</div>
}
</div>
<div class="drawer-footer">
<button class="btn-cancel" (click)="closeForm()">Cancel</button>
</div>
}
<!-- ── STEP 2: Appointment Details ── -->
@if (formStep === 2) {
<div class="drawer-body">
<!-- Auto-filled candidate info banner -->
<div class="selected-candidate-banner">
<div class="selected-avatar">{{ form.candidateName?.charAt(0) }}</div>
<div>
<span class="selected-name">{{ form.candidateName }}</span>
<span class="selected-post">{{ form.postName }}</span>
</div>
<mat-icon class="check-icon">verified</mat-icon>
</div>
<!-- Appointment Details -->
<div class="form-section-title">Appointment Details</div>
<div class="form-row">
<mat-form-field appearance="outline">
<mat-label>Designation <span class="req">*</span></mat-label>
<mat-select [(ngModel)]="form.designation" #desig="ngModel" required>
<mat-option value="">— Select —</mat-option>
@for (opt of designationOptions; track opt) {
<mat-option [value]="opt">{{ opt }}</mat-option>
}
</mat-select>
@if (submitted && !form.designation) {
<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>
</div>
<div class="form-row">
<mat-form-field appearance="outline">
<mat-label>Appointment Date <span class="req">*</span></mat-label>
<input matInput [matDatepicker]="apptDate" [(ngModel)]="form.appointmentDate" [min]="today" required />
<mat-datepicker-toggle matSuffix [for]="apptDate"></mat-datepicker-toggle>
<mat-datepicker #apptDate></mat-datepicker>
@if (submitted && !form.appointmentDate) {
<mat-error>Appointment date is required</mat-error>
}
</mat-form-field>
<mat-form-field appearance="outline">
<mat-label>Resolution No.</mat-label>
<input matInput [(ngModel)]="form.resolutionNo" placeholder="e.g. AGM/2026/001" />
</mat-form-field>
</div>
<!-- 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>
<input
matInput
[matDatepicker]="tenureStart"
[(ngModel)]="form.tenureStartDate"
[min]="today"
(ngModelChange)="onTenureStartChange($event)"
required
/>
<mat-datepicker-toggle matSuffix [for]="tenureStart"></mat-datepicker-toggle>
<mat-datepicker #tenureStart></mat-datepicker>
@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>
<input
matInput
[matDatepicker]="tenureEnd"
[(ngModel)]="form.tenureEndDate"
[min]="form.tenureStartDate || today"
required
/>
<mat-datepicker-toggle matSuffix [for]="tenureEnd"></mat-datepicker-toggle>
<mat-datepicker #tenureEnd></mat-datepicker>
@if (submitted && !form.tenureEndDate) {
<mat-error>Tenure end date is required</mat-error>
}
</mat-form-field>
</div>
<mat-form-field appearance="outline" class="full-field">
<mat-label>Remarks</mat-label>
<textarea matInput [(ngModel)]="form.remarks" rows="3" placeholder="Additional notes..."></textarea>
</mat-form-field>
</div>
<div class="drawer-footer">
<button class="btn-cancel" (click)="closeForm()">Cancel</button>
<button class="btn-save" (click)="save()" [disabled]="saving">
<mat-icon>{{ saving ? 'hourglass_empty' : 'save' }}</mat-icon>
{{ saving ? 'Saving...' : editingId ? 'Update' : 'Appoint' }}
</button>
</div>
}
</div>
}
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,343 @@
/**
* 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/.
*/
import { Component, OnInit, inject } from '@angular/core';
import { CommonModule, DatePipe } from '@angular/common';
import { FormsModule } from '@angular/forms';
import { RouterModule } from '@angular/router';
import { MatIconModule } from '@angular/material/icon';
import { MatSnackBar, MatSnackBarModule } from '@angular/material/snack-bar';
import { MatDatepickerModule } from '@angular/material/datepicker';
import { MatNativeDateModule } from '@angular/material/core';
import { MatFormFieldModule } from '@angular/material/form-field';
import { MatInputModule } from '@angular/material/input';
import { MatSelectModule } from '@angular/material/select';
import { ElectionService } from '../../election.service';
@Component({
selector: 'mifosx-director-board',
standalone: true,
imports: [
CommonModule,
FormsModule,
RouterModule,
MatIconModule,
MatSnackBarModule,
MatDatepickerModule,
MatNativeDateModule,
MatFormFieldModule,
MatInputModule,
MatSelectModule,
DatePipe
],
templateUrl: './director-board.component.html',
styleUrls: ['./director-board.component.scss']
})
export class DirectorBoardComponent implements OnInit {
private service = inject(ElectionService);
private snackBar = inject(MatSnackBar);
// ── Pending appointments ───────────────────────────────────────
pendingCandidates: any[] = [];
pendingLoading = true;
// ── Director list ──────────────────────────────────────────────
directors: any[] = [];
loading = true;
totalElements = 0;
page = 0;
pageSize = 20;
filterStatus = '';
statusOptions = [
'',
'ACTIVE',
'RESIGNED',
'REMOVED',
'TENURE_ENDED'
];
statusLabels: Record<string, string> = {
'': 'All',
ACTIVE: 'Active',
RESIGNED: 'Resigned',
REMOVED: 'Removed',
TENURE_ENDED: 'Tenure Ended'
};
// ── Drawer state ───────────────────────────────────────────────
showForm = false;
/** 1 = pick candidate, 2 = fill details */
formStep: 1 | 2 = 1;
editingId: number | null = null;
saving = false;
deleting: number | null = null;
submitted = false;
// ── Step-1: election + WON candidates ─────────────────────────
elections: any[] = [];
electionsLoading = false;
selectedElectionId: number | null = null;
wonCandidates: any[] = [];
wonLoading = false;
// ── Step-2: form ───────────────────────────────────────────────
form: any = this.emptyForm();
readonly today: Date = new Date();
designationOptions = [
'Chairman',
'Vice Chairman',
'Secretary',
'Treasurer',
'Director',
'Managing Director',
'Chief Executive Officer'
];
ngOnInit(): void {
this.load();
this.loadPending();
}
loadPending(): void {
this.pendingLoading = true;
this.service.getPendingDirectors().subscribe((data: any[]) => {
this.pendingCandidates = data ?? [];
this.pendingLoading = false;
});
}
pickPending(p: any): void {
this.editingId = null;
this.form = {
...this.emptyForm(),
electionId: p.electionId,
candidateId: p.candidateId,
candidateName: p.candidateName,
postName: p.postName
};
this.selectedElectionId = p.electionId;
this.formStep = 2;
this.showForm = true;
}
load(): void {
this.loading = true;
this.service
.getDirectors({ status: this.filterStatus || undefined, page: this.page, size: this.pageSize })
.subscribe({
next: (data: any) => {
this.directors = data?.content ?? [];
this.totalElements = data?.totalElements ?? 0;
this.loading = false;
},
error: () => {
this.directors = [];
this.totalElements = 0;
this.loading = false;
}
});
}
applyFilter(): void {
this.page = 0;
this.load();
}
// ── Open appoint drawer (step 1) ───────────────────────────────
openAppoint(): void {
this.submitted = false;
this.editingId = null;
this.form = this.emptyForm();
this.formStep = 1;
this.selectedElectionId = null;
this.wonCandidates = [];
this.showForm = true;
this.loadElections();
}
loadElections(): void {
this.electionsLoading = true;
this.service.getAllElections(0, 50).subscribe((data: any) => {
this.elections = data?.content ?? [];
this.electionsLoading = false;
});
}
selectElection(electionId: number): void {
this.selectedElectionId = electionId;
this.wonCandidates = [];
this.wonLoading = true;
this.service.getResults(electionId).subscribe((results: any[]) => {
this.wonCandidates = (results ?? []).filter((r) => r.result === 'WON');
this.wonLoading = false;
});
}
pickCandidate(candidate: any): void {
this.form = {
...this.emptyForm(),
electionId: this.selectedElectionId,
candidateId: candidate.candidateId,
candidateName: candidate.candidateName,
postName: candidate.postName
};
this.formStep = 2;
}
backToStep1(): void {
this.formStep = 1;
}
onTenureStartChange(val: Date | null): void {
if (!val) return;
const end = new Date(val);
end.setFullYear(end.getFullYear() + 5);
this.form.tenureEndDate = end;
}
// ── Open edit drawer (skip step 1) ────────────────────────────
openEdit(d: any): void {
this.submitted = false;
this.editingId = d.id;
this.form = {
electionId: d.electionId,
candidateId: d.candidateId,
candidateName: d.candidateName,
postName: d.postName,
designation: d.designation ?? '',
tenureStartDate: d.tenureStartDate ? new Date(d.tenureStartDate) : null,
tenureEndDate: d.tenureEndDate ? new Date(d.tenureEndDate) : null,
resolutionNo: d.resolutionNo ?? '',
appointmentDate: d.appointmentDate ? new Date(d.appointmentDate) : null,
status: d.status ?? 'ACTIVE',
remarks: d.remarks ?? ''
};
this.formStep = 2;
this.showForm = true;
}
closeForm(): void {
this.showForm = false;
this.editingId = null;
this.form = this.emptyForm();
this.formStep = 1;
this.submitted = false;
}
save(): void {
this.submitted = true;
if (
!this.form.designation ||
!this.form.appointmentDate ||
!this.form.tenureStartDate ||
!this.form.tenureEndDate
) {
return;
}
this.saving = true;
const payload = {
...this.form,
appointmentDate: this.formatDate(this.form.appointmentDate),
tenureStartDate: this.formatDate(this.form.tenureStartDate),
tenureEndDate: this.formatDate(this.form.tenureEndDate)
};
const call =
this.editingId != null
? this.service.updateDirector(this.editingId, payload)
: this.service.appointDirector(payload);
call.subscribe({
next: () => {
this.saving = false;
this.closeForm();
this.load();
this.loadPending();
this.snackBar.open(
this.editingId != null ? 'Director updated successfully!' : 'Director appointed successfully!',
'Close',
{ duration: 3000 }
);
},
error: (err: any) => {
this.saving = false;
const msg = err?.error?.errors?.[0]?.defaultUserMessage ?? 'Operation failed';
this.snackBar.open(msg, 'Close', { duration: 5000 });
}
});
}
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 });
}
});
}
get totalPages(): number {
return Math.ceil(this.totalElements / this.pageSize);
}
prevPage(): void {
if (this.page > 0) {
this.page--;
this.load();
}
}
nextPage(): void {
if (this.page < this.totalPages - 1) {
this.page++;
this.load();
}
}
statusColor(status: string): string {
const map: Record<string, string> = {
ACTIVE: '#10b981',
RESIGNED: '#f59e0b',
REMOVED: '#ef4444',
TENURE_ENDED: '#94a3b8'
};
return map[status] ?? '#94a3b8';
}
private formatDate(date: Date | null): string | null {
if (!date) return null;
const y = date.getFullYear();
const m = String(date.getMonth() + 1).padStart(2, '0');
const d = String(date.getDate()).padStart(2, '0');
return `${y}-${m}-${d}`;
}
private emptyForm(): any {
return {
electionId: null,
candidateId: null,
candidateName: '',
postName: '',
designation: '',
tenureStartDate: null,
tenureEndDate: null,
resolutionNo: '',
appointmentDate: null,
status: 'ACTIVE',
remarks: ''
};
}
}
@@ -9,7 +9,13 @@
<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 class="header-actions">
<a routerLink="/election/directors" class="btn-directors">
<mat-icon>groups</mat-icon>
<span>Board of Directors</span>
</a>
<a routerLink="/election/config/new" class="btn-primary"> <mat-icon>add</mat-icon> Create Election </a>
</div>
</div>
@if (loading) {
@@ -15,6 +15,8 @@
justify-content: space-between;
align-items: center;
margin-bottom: 14px;
flex-wrap: wrap;
gap: 10px;
.page-title {
font-size: 18px;
@@ -24,6 +26,40 @@
}
}
.header-actions {
display: flex;
align-items: center;
gap: 10px;
flex-wrap: wrap;
}
.btn-directors {
display: inline-flex;
align-items: center;
gap: 6px;
padding: 8px 16px;
border-radius: 8px;
border: 1px solid #e2e8f0;
background: #fff;
color: #475569;
font-size: 13px;
font-weight: 600;
text-decoration: none;
transition: all 0.2s;
mat-icon {
font-size: 16px;
height: 16px;
width: 16px;
}
&:hover {
background: #f1f5f9;
color: #0f172a;
border-color: #cbd5e1;
}
}
.stats-row {
display: flex;
gap: 10px;
@@ -91,6 +91,12 @@ const routes: Routes = [
(m) => m.ShareholderAnalysisComponent
),
data: { title: 'Overall Shareholder Analysis' }
},
{
path: 'directors',
loadComponent: () =>
import('./components/director-board/director-board.component').then((m) => m.DirectorBoardComponent),
data: { title: 'Board of Directors' }
}
]
}
+30
View File
@@ -181,4 +181,34 @@ export class ElectionService {
.get<any>(`/election/config/${electionId}/audit-log`, { params: qp })
.pipe(catchError(() => of({ content: [], totalElements: 0, totalPages: 0, currentPage: 0, pageSize: 20 })));
}
// ── Board of Directors ─────────────────────────────────────────
getDirectors(params?: { status?: string; electionId?: number; page?: number; size?: number }): Observable<any> {
const qp: Record<string, string> = {};
if (params?.status) qp['status'] = params.status;
if (params?.electionId) qp['electionId'] = String(params.electionId);
if (params?.page !== undefined) qp['page'] = String(params.page);
if (params?.size !== undefined) qp['size'] = String(params.size);
return this.http.get<any>('/election/directors', { params: qp }).pipe(catchError(() => of({ content: [] })));
}
getDirector(id: number): Observable<any> {
return this.http.get<any>(`/election/directors/${id}`);
}
appointDirector(data: any): Observable<any> {
return this.http.post('/election/directors', data);
}
updateDirector(id: number, data: any): Observable<any> {
return this.http.put(`/election/directors/${id}`, data);
}
deleteDirector(id: number): Observable<any> {
return this.http.delete(`/election/directors/${id}`);
}
getPendingDirectors(): Observable<any[]> {
return this.http.get<any[]>('/election/directors/pending').pipe(catchError(() => of([])));
}
}
+2 -2
View File
@@ -9,8 +9,8 @@
/* tslint:disable */
export default {
'mifos_x': {
'version': '260626',
'hash': '9e33392b'
'version': '260701',
'hash': 'f2f2338c'
},
'allow_switching_backend_instance': true
};