feat: add Current Account module, DIP verification, charges integration, and translations
- Current Account module with full CRUD, transactions, charges, hold/release, block/unblock - Current Products management under Products module - DIP mobile number verification with real API call to /lookup/check - Sanctions verification dialog - Charges support for Current Account (chargeAppliesTo=5) in create/edit - Fixed permissions from SAVINGSACCOUNT to CURRENTACCOUNT across all current account components - Added all missing translation keys for Current Account in en-US.json Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 4.6
parent
73aa69ee02
commit
0a32d7135e
@@ -0,0 +1,52 @@
|
||||
/**
|
||||
* 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 { Injectable, inject } from '@angular/core';
|
||||
import { HttpClient, HttpParams } from '@angular/common/http';
|
||||
import { Observable } from 'rxjs';
|
||||
import { environment } from 'environments/environment';
|
||||
|
||||
export interface OpenProjectWorkPackage {
|
||||
id: number;
|
||||
subject: string;
|
||||
description?: string;
|
||||
status: string;
|
||||
type: string;
|
||||
priority: string;
|
||||
assignee?: string;
|
||||
project: string;
|
||||
createdAt: string;
|
||||
updatedAt: string;
|
||||
dueDate?: string;
|
||||
percentageDone?: number;
|
||||
openProjectUrl?: string;
|
||||
}
|
||||
|
||||
@Injectable({
|
||||
providedIn: 'root'
|
||||
})
|
||||
export class OpenProjectService {
|
||||
private http = inject(HttpClient);
|
||||
|
||||
get isEnabled(): boolean {
|
||||
return environment.openProjectEnabled;
|
||||
}
|
||||
|
||||
getWorkPackages(pageSize = 50, offset = 1): Observable<OpenProjectWorkPackage[]> {
|
||||
let params = new HttpParams().set('pageSize', pageSize).set('offset', offset);
|
||||
return this.http.get<OpenProjectWorkPackage[]>('/openproject/work-packages', { params });
|
||||
}
|
||||
|
||||
getWorkPackageById(id: number): Observable<OpenProjectWorkPackage> {
|
||||
return this.http.get<OpenProjectWorkPackage>(`/openproject/work-packages/${id}`);
|
||||
}
|
||||
|
||||
triggerSync(): Observable<any> {
|
||||
return this.http.post('/openproject/sync', {});
|
||||
}
|
||||
}
|
||||
@@ -132,11 +132,252 @@
|
||||
</td>
|
||||
</ng-container>
|
||||
|
||||
<tr mat-header-row *matHeaderRowDef="displayedColumns"></tr>
|
||||
<tr mat-row *matRowDef="let row; columns: displayedColumns" class="feed-row"></tr>
|
||||
<!-- OpenProject Details Column (Notification tab only) -->
|
||||
<ng-container matColumnDef="opDetails">
|
||||
<th mat-header-cell *matHeaderCellDef class="col-op-details">OpenProject</th>
|
||||
<td mat-cell *matCellDef="let row" class="col-op-details">
|
||||
<!-- No work package linked -->
|
||||
<span *ngIf="!row.openProjectWpId" class="op-no-link">—</span>
|
||||
|
||||
<!-- Fetch button (only when openProjectWpId exists) -->
|
||||
<button
|
||||
*ngIf="row.openProjectWpId && !opRowDetails[row.id] && !opRowLoading[row.id]"
|
||||
mat-stroked-button
|
||||
color="accent"
|
||||
class="op-fetch-btn"
|
||||
(click)="fetchOpDetailsForRow(row)"
|
||||
[matTooltip]="'Fetch OpenProject work item #' + row.openProjectWpId"
|
||||
>
|
||||
<fa-icon icon="tasks" class="mr-05"></fa-icon>
|
||||
#{{ row.openProjectWpId }}
|
||||
</button>
|
||||
|
||||
<!-- Loading spinner -->
|
||||
<div *ngIf="opRowLoading[row.id]" class="op-row-loading">
|
||||
<mat-progress-spinner diameter="20" mode="indeterminate"></mat-progress-spinner>
|
||||
</div>
|
||||
|
||||
<!-- Details result -->
|
||||
<div *ngIf="opRowDetails[row.id]" class="op-row-result">
|
||||
<div class="op-row-summary">
|
||||
<span class="op-row-id">#{{ opRowDetails[row.id].id }}</span>
|
||||
<span class="op-status-chip" [style.background-color]="getStatusColor(opRowDetails[row.id].status)">
|
||||
{{ opRowDetails[row.id].status }}
|
||||
</span>
|
||||
</div>
|
||||
<div class="op-row-subject">{{ opRowDetails[row.id].subject }}</div>
|
||||
<div class="op-row-meta">
|
||||
<span *ngIf="opRowDetails[row.id].assignee">
|
||||
<fa-icon icon="user" class="op-info-icon"></fa-icon> {{ opRowDetails[row.id].assignee }}
|
||||
</span>
|
||||
<span *ngIf="opRowDetails[row.id].priority">
|
||||
<fa-icon icon="flag" class="op-info-icon"></fa-icon> {{ opRowDetails[row.id].priority }}
|
||||
</span>
|
||||
</div>
|
||||
<div class="op-row-actions">
|
||||
<button
|
||||
mat-button
|
||||
color="primary"
|
||||
class="op-view-detail-btn"
|
||||
(click)="viewWorkPackageDetails(opRowDetails[row.id].id)"
|
||||
matTooltip="View full details"
|
||||
>
|
||||
<fa-icon icon="info-circle" class="mr-05"></fa-icon> Full Details
|
||||
</button>
|
||||
<button
|
||||
*ngIf="opRowDetails[row.id].openProjectUrl"
|
||||
mat-button
|
||||
color="primary"
|
||||
class="op-view-detail-btn"
|
||||
(click)="openInOpenProject(opRowDetails[row.id])"
|
||||
matTooltip="Open in OpenProject"
|
||||
>
|
||||
<fa-icon icon="external-link-alt" class="mr-05"></fa-icon> Open
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</td>
|
||||
</ng-container>
|
||||
|
||||
<tr mat-header-row *matHeaderRowDef="getColumnsForTab(tab)"></tr>
|
||||
<tr mat-row *matRowDef="let row; columns: getColumnsForTab(tab)" class="feed-row"></tr>
|
||||
</table>
|
||||
|
||||
<div class="results-info">Showing {{ tab.dataSource.filteredData.length }} items</div>
|
||||
<mat-paginator
|
||||
[pageSize]="10"
|
||||
[pageSizeOptions]="[5, 10, 25, 50]"
|
||||
showFirstLastButtons
|
||||
></mat-paginator>
|
||||
</div>
|
||||
</div>
|
||||
</mat-tab>
|
||||
|
||||
<!-- OpenProject Work Items Tab -->
|
||||
<mat-tab *ngIf="openProjectEnabled">
|
||||
<ng-template mat-tab-label>
|
||||
<div class="tab-label">
|
||||
<fa-icon icon="tasks" class="tab-icon" style="color: #6a1b9a"></fa-icon>
|
||||
<span>OpenProject</span>
|
||||
<span class="tab-badge" style="background: #6a1b9a">{{ opFilteredWorkPackages.length }}</span>
|
||||
</div>
|
||||
</ng-template>
|
||||
|
||||
<div class="tab-content">
|
||||
<!-- Not loaded yet -->
|
||||
<div *ngIf="!opLoading && opWorkPackages.length === 0 && !opError" class="op-start-section">
|
||||
<fa-icon icon="project-diagram" class="op-start-icon"></fa-icon>
|
||||
<h3>OpenProject Work Items</h3>
|
||||
<p>Fetch work items from your OpenProject instance to view and track project tasks.</p>
|
||||
<button mat-raised-button color="primary" (click)="loadWorkPackages()">
|
||||
<fa-icon icon="download" class="mr-05"></fa-icon>
|
||||
Fetch Work Items
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<!-- Loading -->
|
||||
<div *ngIf="opLoading" class="loading-state">
|
||||
<mat-spinner diameter="40"></mat-spinner>
|
||||
<p>Loading OpenProject work items...</p>
|
||||
</div>
|
||||
|
||||
<!-- Error -->
|
||||
<div *ngIf="opError && !opLoading" class="error-state">
|
||||
<fa-icon icon="exclamation-triangle" class="error-icon"></fa-icon>
|
||||
<p>{{ opError }}</p>
|
||||
<button mat-raised-button color="primary" (click)="loadWorkPackages()">
|
||||
<fa-icon icon="sync" class="mr-05"></fa-icon> Retry
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<!-- Work Packages List -->
|
||||
<div *ngIf="!opLoading && !opError && opWorkPackages.length > 0">
|
||||
<!-- Search within OpenProject -->
|
||||
<div class="op-toolbar">
|
||||
<mat-form-field class="op-search-field" appearance="outline">
|
||||
<mat-label>Search work items...</mat-label>
|
||||
<input matInput (input)="filterWorkPackages($any($event.target).value)" placeholder="Search by subject, status, or type" />
|
||||
<fa-icon matSuffix icon="search" class="search-icon"></fa-icon>
|
||||
</mat-form-field>
|
||||
<button mat-stroked-button color="primary" (click)="loadWorkPackages()" matTooltip="Refresh work items">
|
||||
<fa-icon icon="sync" class="mr-05"></fa-icon> Refresh
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<!-- Work Packages Cards -->
|
||||
<div class="op-grid">
|
||||
<div *ngFor="let wp of opFilteredWorkPackages" class="op-card" (click)="viewWorkPackageDetails(wp.id)">
|
||||
<div class="op-card-header">
|
||||
<span class="op-card-id">#{{ wp.id }}</span>
|
||||
<span class="op-card-type" [style.color]="getPriorityColor(wp.priority)">{{ wp.type }}</span>
|
||||
</div>
|
||||
<h4 class="op-card-subject">{{ wp.subject }}</h4>
|
||||
<div class="op-card-meta">
|
||||
<span class="op-status-chip" [style.background-color]="getStatusColor(wp.status)">
|
||||
{{ wp.status }}
|
||||
</span>
|
||||
<span class="op-priority-chip" [style.border-color]="getPriorityColor(wp.priority)" [style.color]="getPriorityColor(wp.priority)">
|
||||
{{ wp.priority }}
|
||||
</span>
|
||||
</div>
|
||||
<div class="op-card-info">
|
||||
<span *ngIf="wp.assignee"><fa-icon icon="user" class="op-info-icon"></fa-icon> {{ wp.assignee }}</span>
|
||||
<span *ngIf="wp.dueDate"><fa-icon icon="calendar-alt" class="op-info-icon"></fa-icon> {{ formatOpDate(wp.dueDate) }}</span>
|
||||
<span><fa-icon icon="folder" class="op-info-icon"></fa-icon> {{ wp.project }}</span>
|
||||
</div>
|
||||
<div *ngIf="wp.percentageDone !== undefined && wp.percentageDone > 0" class="op-progress">
|
||||
<mat-progress-bar mode="determinate" [value]="wp.percentageDone"></mat-progress-bar>
|
||||
<span class="op-progress-label">{{ wp.percentageDone }}%</span>
|
||||
</div>
|
||||
<div class="op-card-actions">
|
||||
<button mat-button color="primary" (click)="openInOpenProject(wp); $event.stopPropagation()" matTooltip="Open in OpenProject">
|
||||
<fa-icon icon="external-link-alt" class="mr-05"></fa-icon> Open in OpenProject
|
||||
</button>
|
||||
<button mat-button color="accent" (click)="viewWorkPackageDetails(wp.id); $event.stopPropagation()" matTooltip="Fetch full details via API">
|
||||
<fa-icon icon="info-circle" class="mr-05"></fa-icon> Fetch Details
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="results-info">Showing {{ opFilteredWorkPackages.length }} of {{ opWorkPackages.length }} work items</div>
|
||||
</div>
|
||||
|
||||
<!-- Work Package Detail Panel -->
|
||||
<div *ngIf="opSelectedWorkPackage" class="op-detail-overlay" (click)="closeWorkPackageDetails()">
|
||||
<div class="op-detail-panel" (click)="$event.stopPropagation()">
|
||||
<div class="op-detail-header">
|
||||
<h3>
|
||||
<span class="op-card-id">#{{ opSelectedWorkPackage.id }}</span>
|
||||
{{ opSelectedWorkPackage.subject }}
|
||||
</h3>
|
||||
<button mat-icon-button (click)="closeWorkPackageDetails()">
|
||||
<mat-icon>close</mat-icon>
|
||||
</button>
|
||||
</div>
|
||||
<div class="op-detail-body">
|
||||
<div class="op-detail-row">
|
||||
<span class="op-detail-label">Status</span>
|
||||
<span class="op-status-chip" [style.background-color]="getStatusColor(opSelectedWorkPackage.status)">
|
||||
{{ opSelectedWorkPackage.status }}
|
||||
</span>
|
||||
</div>
|
||||
<div class="op-detail-row">
|
||||
<span class="op-detail-label">Type</span>
|
||||
<span class="op-detail-value">{{ opSelectedWorkPackage.type }}</span>
|
||||
</div>
|
||||
<div class="op-detail-row">
|
||||
<span class="op-detail-label">Priority</span>
|
||||
<span class="op-priority-chip" [style.border-color]="getPriorityColor(opSelectedWorkPackage.priority)" [style.color]="getPriorityColor(opSelectedWorkPackage.priority)">
|
||||
{{ opSelectedWorkPackage.priority }}
|
||||
</span>
|
||||
</div>
|
||||
<div class="op-detail-row">
|
||||
<span class="op-detail-label">Project</span>
|
||||
<span class="op-detail-value">{{ opSelectedWorkPackage.project }}</span>
|
||||
</div>
|
||||
<div *ngIf="opSelectedWorkPackage.assignee" class="op-detail-row">
|
||||
<span class="op-detail-label">Assignee</span>
|
||||
<span class="op-detail-value">{{ opSelectedWorkPackage.assignee }}</span>
|
||||
</div>
|
||||
<div class="op-detail-row">
|
||||
<span class="op-detail-label">Created</span>
|
||||
<span class="op-detail-value">{{ formatOpDate(opSelectedWorkPackage.createdAt) }}</span>
|
||||
</div>
|
||||
<div class="op-detail-row">
|
||||
<span class="op-detail-label">Updated</span>
|
||||
<span class="op-detail-value">{{ formatOpDate(opSelectedWorkPackage.updatedAt) }}</span>
|
||||
</div>
|
||||
<div *ngIf="opSelectedWorkPackage.dueDate" class="op-detail-row">
|
||||
<span class="op-detail-label">Due Date</span>
|
||||
<span class="op-detail-value">{{ formatOpDate(opSelectedWorkPackage.dueDate) }}</span>
|
||||
</div>
|
||||
<div *ngIf="opSelectedWorkPackage.percentageDone !== undefined" class="op-detail-row">
|
||||
<span class="op-detail-label">Progress</span>
|
||||
<div class="op-detail-progress">
|
||||
<mat-progress-bar mode="determinate" [value]="opSelectedWorkPackage.percentageDone"></mat-progress-bar>
|
||||
<span>{{ opSelectedWorkPackage.percentageDone }}%</span>
|
||||
</div>
|
||||
</div>
|
||||
<div *ngIf="opSelectedWorkPackage.description" class="op-detail-description">
|
||||
<span class="op-detail-label">Description</span>
|
||||
<div class="op-description-content">{{ opSelectedWorkPackage.description }}</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="op-detail-footer">
|
||||
<button mat-raised-button color="primary" (click)="openInOpenProject(opSelectedWorkPackage)">
|
||||
<fa-icon icon="external-link-alt" class="mr-05"></fa-icon> Open in OpenProject
|
||||
</button>
|
||||
<button mat-stroked-button (click)="closeWorkPackageDetails()">Close</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Detail Loading Overlay -->
|
||||
<div *ngIf="opDetailLoading" class="op-detail-overlay">
|
||||
<div class="op-detail-loading">
|
||||
<mat-spinner diameter="40"></mat-spinner>
|
||||
<p>Fetching work item details...</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</mat-tab>
|
||||
|
||||
@@ -293,6 +293,365 @@
|
||||
}
|
||||
}
|
||||
|
||||
// OpenProject column in Notification tab
|
||||
.col-op-details {
|
||||
width: 240px;
|
||||
vertical-align: top;
|
||||
}
|
||||
|
||||
.op-no-link {
|
||||
color: #ccc;
|
||||
font-size: 0.85rem;
|
||||
}
|
||||
|
||||
.op-fetch-btn {
|
||||
font-size: 0.75rem;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.op-row-loading {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
padding: 4px 0;
|
||||
}
|
||||
|
||||
.op-row-result {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 4px;
|
||||
font-size: 0.8rem;
|
||||
}
|
||||
|
||||
.op-row-summary {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
}
|
||||
|
||||
.op-row-id {
|
||||
font-weight: 700;
|
||||
color: #6a1b9a;
|
||||
font-size: 0.75rem;
|
||||
}
|
||||
|
||||
.op-row-subject {
|
||||
font-weight: 500;
|
||||
color: #333;
|
||||
line-height: 1.3;
|
||||
display: -webkit-box;
|
||||
-webkit-line-clamp: 2;
|
||||
-webkit-box-orient: vertical;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.op-row-meta {
|
||||
display: flex;
|
||||
gap: 10px;
|
||||
color: #777;
|
||||
font-size: 0.75rem;
|
||||
|
||||
.op-info-icon {
|
||||
font-size: 0.65rem;
|
||||
color: #aaa;
|
||||
margin-right: 3px;
|
||||
}
|
||||
}
|
||||
|
||||
.op-row-actions {
|
||||
display: flex;
|
||||
gap: 2px;
|
||||
margin-top: 2px;
|
||||
}
|
||||
|
||||
.op-view-detail-btn {
|
||||
font-size: 0.7rem;
|
||||
padding: 0 6px;
|
||||
line-height: 24px;
|
||||
height: 24px;
|
||||
}
|
||||
|
||||
// OpenProject Tab
|
||||
.op-start-section {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
padding: 48px 0;
|
||||
text-align: center;
|
||||
gap: 8px;
|
||||
|
||||
.op-start-icon {
|
||||
font-size: 2.5rem;
|
||||
color: #6a1b9a;
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
|
||||
h3 {
|
||||
margin: 0;
|
||||
color: #333;
|
||||
}
|
||||
|
||||
p {
|
||||
color: #888;
|
||||
font-size: 0.85rem;
|
||||
margin: 0 0 16px;
|
||||
}
|
||||
}
|
||||
|
||||
.op-toolbar {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 12px;
|
||||
margin-bottom: 16px;
|
||||
flex-wrap: wrap;
|
||||
|
||||
.op-search-field {
|
||||
flex: 1;
|
||||
min-width: 220px;
|
||||
max-width: 400px;
|
||||
}
|
||||
}
|
||||
|
||||
.op-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fill, minmax(340px, 1fr));
|
||||
gap: 16px;
|
||||
}
|
||||
|
||||
.op-card {
|
||||
background: white;
|
||||
border: 1px solid #e8e8e8;
|
||||
border-radius: 10px;
|
||||
padding: 16px;
|
||||
cursor: pointer;
|
||||
transition: box-shadow 0.2s, border-color 0.2s;
|
||||
|
||||
&:hover {
|
||||
box-shadow: 0 4px 12px rgb(0 0 0 / 10%);
|
||||
border-color: #6a1b9a40;
|
||||
}
|
||||
}
|
||||
|
||||
.op-card-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
|
||||
.op-card-id {
|
||||
font-size: 0.8rem;
|
||||
font-weight: 700;
|
||||
color: #6a1b9a;
|
||||
}
|
||||
|
||||
.op-card-type {
|
||||
font-size: 0.75rem;
|
||||
font-weight: 600;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.5px;
|
||||
}
|
||||
|
||||
.op-card-subject {
|
||||
margin: 0 0 10px;
|
||||
font-size: 0.95rem;
|
||||
font-weight: 500;
|
||||
color: #222;
|
||||
line-height: 1.4;
|
||||
}
|
||||
|
||||
.op-card-meta {
|
||||
display: flex;
|
||||
gap: 8px;
|
||||
margin-bottom: 10px;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.op-status-chip {
|
||||
display: inline-block;
|
||||
font-size: 0.7rem;
|
||||
font-weight: 600;
|
||||
color: white;
|
||||
padding: 2px 10px;
|
||||
border-radius: 12px;
|
||||
letter-spacing: 0.3px;
|
||||
}
|
||||
|
||||
.op-priority-chip {
|
||||
display: inline-block;
|
||||
font-size: 0.7rem;
|
||||
font-weight: 600;
|
||||
padding: 2px 10px;
|
||||
border-radius: 12px;
|
||||
border: 1.5px solid;
|
||||
background: transparent;
|
||||
letter-spacing: 0.3px;
|
||||
}
|
||||
|
||||
.op-card-info {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 12px;
|
||||
font-size: 0.8rem;
|
||||
color: #666;
|
||||
margin-bottom: 8px;
|
||||
|
||||
.op-info-icon {
|
||||
font-size: 0.7rem;
|
||||
color: #aaa;
|
||||
margin-right: 4px;
|
||||
}
|
||||
}
|
||||
|
||||
.op-progress {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
margin-bottom: 8px;
|
||||
|
||||
mat-progress-bar {
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.op-progress-label {
|
||||
font-size: 0.75rem;
|
||||
font-weight: 600;
|
||||
color: #555;
|
||||
min-width: 35px;
|
||||
}
|
||||
}
|
||||
|
||||
.op-card-actions {
|
||||
display: flex;
|
||||
gap: 4px;
|
||||
margin-top: 8px;
|
||||
border-top: 1px solid #f0f0f0;
|
||||
padding-top: 8px;
|
||||
|
||||
button {
|
||||
font-size: 0.75rem;
|
||||
}
|
||||
}
|
||||
|
||||
// OpenProject Detail Overlay
|
||||
.op-detail-overlay {
|
||||
position: fixed;
|
||||
inset: 0;
|
||||
background: rgb(0 0 0 / 40%);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
z-index: 1000;
|
||||
}
|
||||
|
||||
.op-detail-panel {
|
||||
background: white;
|
||||
border-radius: 12px;
|
||||
width: 90%;
|
||||
max-width: 600px;
|
||||
max-height: 85vh;
|
||||
overflow-y: auto;
|
||||
box-shadow: 0 8px 32px rgb(0 0 0 / 20%);
|
||||
}
|
||||
|
||||
.op-detail-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: flex-start;
|
||||
padding: 20px 24px 12px;
|
||||
border-bottom: 1px solid #f0f0f0;
|
||||
|
||||
h3 {
|
||||
margin: 0;
|
||||
font-size: 1.1rem;
|
||||
font-weight: 600;
|
||||
color: #222;
|
||||
line-height: 1.4;
|
||||
|
||||
.op-card-id {
|
||||
margin-right: 8px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.op-detail-body {
|
||||
padding: 16px 24px;
|
||||
}
|
||||
|
||||
.op-detail-row {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 12px;
|
||||
padding: 8px 0;
|
||||
border-bottom: 1px solid #f8f8f8;
|
||||
}
|
||||
|
||||
.op-detail-label {
|
||||
font-size: 0.8rem;
|
||||
font-weight: 600;
|
||||
color: #888;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.5px;
|
||||
min-width: 90px;
|
||||
}
|
||||
|
||||
.op-detail-value {
|
||||
font-size: 0.9rem;
|
||||
color: #333;
|
||||
}
|
||||
|
||||
.op-detail-progress {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
flex: 1;
|
||||
|
||||
mat-progress-bar {
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
span {
|
||||
font-size: 0.8rem;
|
||||
font-weight: 600;
|
||||
color: #555;
|
||||
}
|
||||
}
|
||||
|
||||
.op-detail-description {
|
||||
margin-top: 12px;
|
||||
|
||||
.op-description-content {
|
||||
margin-top: 8px;
|
||||
font-size: 0.85rem;
|
||||
color: #444;
|
||||
line-height: 1.6;
|
||||
padding: 12px;
|
||||
background: #f9f9f9;
|
||||
border-radius: 6px;
|
||||
}
|
||||
}
|
||||
|
||||
.op-detail-footer {
|
||||
display: flex;
|
||||
gap: 12px;
|
||||
justify-content: flex-end;
|
||||
padding: 16px 24px;
|
||||
border-top: 1px solid #f0f0f0;
|
||||
}
|
||||
|
||||
.op-detail-loading {
|
||||
background: white;
|
||||
border-radius: 12px;
|
||||
padding: 48px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
gap: 16px;
|
||||
|
||||
p {
|
||||
color: #666;
|
||||
}
|
||||
}
|
||||
|
||||
// Responsive
|
||||
@media screen and (width <= 768px) {
|
||||
.rbi-container {
|
||||
@@ -322,4 +681,22 @@
|
||||
.col-sno {
|
||||
width: 40px;
|
||||
}
|
||||
|
||||
.op-grid {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
|
||||
.op-toolbar {
|
||||
flex-direction: column;
|
||||
align-items: stretch;
|
||||
|
||||
.op-search-field {
|
||||
max-width: 100%;
|
||||
}
|
||||
}
|
||||
|
||||
.op-detail-panel {
|
||||
width: 95%;
|
||||
max-height: 90vh;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -6,16 +6,19 @@
|
||||
* file, You can obtain one at http://mozilla.org/MPL/2.0/.
|
||||
*/
|
||||
|
||||
import { Component, OnInit, inject } from '@angular/core';
|
||||
import { Component, OnInit, ViewChildren, QueryList, AfterViewInit, inject } from '@angular/core';
|
||||
import { RbiCircularsService, RbiFeedItem } from './rbi-circulars.service';
|
||||
import { OpenProjectService, OpenProjectWorkPackage } from './openproject.service';
|
||||
import { STANDALONE_SHARED_IMPORTS } from 'app/standalone-shared.module';
|
||||
import { FaIconComponent } from '@fortawesome/angular-fontawesome';
|
||||
import { MatProgressSpinner } from '@angular/material/progress-spinner';
|
||||
import { MatTooltip } from '@angular/material/tooltip';
|
||||
import { MatTabsModule } from '@angular/material/tabs';
|
||||
import { MatTableModule, MatTableDataSource } from '@angular/material/table';
|
||||
import { MatPaginatorModule } from '@angular/material/paginator';
|
||||
import { MatPaginatorModule, MatPaginator } from '@angular/material/paginator';
|
||||
import { MatSortModule } from '@angular/material/sort';
|
||||
import { MatChipsModule } from '@angular/material/chips';
|
||||
import { MatProgressBarModule } from '@angular/material/progress-bar';
|
||||
|
||||
interface CategoryTab {
|
||||
key: string;
|
||||
@@ -38,11 +41,15 @@ interface CategoryTab {
|
||||
MatTabsModule,
|
||||
MatTableModule,
|
||||
MatPaginatorModule,
|
||||
MatSortModule
|
||||
MatSortModule,
|
||||
MatChipsModule,
|
||||
MatProgressBarModule
|
||||
]
|
||||
})
|
||||
export class RbiCircularsComponent implements OnInit {
|
||||
export class RbiCircularsComponent implements OnInit, AfterViewInit {
|
||||
@ViewChildren(MatPaginator) paginators!: QueryList<MatPaginator>;
|
||||
private rbiService = inject(RbiCircularsService);
|
||||
private openProjectService = inject(OpenProjectService);
|
||||
|
||||
feedItems: RbiFeedItem[] = [];
|
||||
isLoading = true;
|
||||
@@ -57,8 +64,29 @@ export class RbiCircularsComponent implements OnInit {
|
||||
'actions'
|
||||
];
|
||||
|
||||
notificationColumns: string[] = [
|
||||
'sno',
|
||||
'title',
|
||||
'pubDate',
|
||||
'actions',
|
||||
'opDetails'
|
||||
];
|
||||
|
||||
tabs: CategoryTab[] = [];
|
||||
|
||||
/** OpenProject state */
|
||||
openProjectEnabled = this.openProjectService.isEnabled;
|
||||
opWorkPackages: OpenProjectWorkPackage[] = [];
|
||||
opFilteredWorkPackages: OpenProjectWorkPackage[] = [];
|
||||
opLoading = false;
|
||||
opError: string | null = null;
|
||||
opSelectedWorkPackage: OpenProjectWorkPackage | null = null;
|
||||
opDetailLoading = false;
|
||||
|
||||
/** Per-row OpenProject detail fetch state (keyed by RBI feed item id) */
|
||||
opRowLoading: Record<number, boolean> = {};
|
||||
opRowDetails: Record<number, OpenProjectWorkPackage | null> = {};
|
||||
|
||||
/** Fixed category config */
|
||||
private categoryConfig: Record<string, { label: string; icon: string; color: string }> = {
|
||||
ALL: { label: 'All Feeds', icon: 'list', color: '#1a237e' },
|
||||
@@ -74,6 +102,20 @@ export class RbiCircularsComponent implements OnInit {
|
||||
this.loadFeeds();
|
||||
}
|
||||
|
||||
ngAfterViewInit() {
|
||||
this.connectPaginators();
|
||||
this.paginators.changes.subscribe(() => this.connectPaginators());
|
||||
}
|
||||
|
||||
private connectPaginators() {
|
||||
const paginatorArr = this.paginators.toArray();
|
||||
this.tabs.forEach((tab, i) => {
|
||||
if (paginatorArr[i]) {
|
||||
tab.dataSource.paginator = paginatorArr[i];
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
loadFeeds() {
|
||||
this.isLoading = true;
|
||||
this.errorMessage = null;
|
||||
@@ -169,6 +211,101 @@ export class RbiCircularsComponent implements OnInit {
|
||||
return tab.dataSource.filteredData.indexOf(row) + 1;
|
||||
}
|
||||
|
||||
/** Fetch OpenProject details for a notification row */
|
||||
fetchOpDetailsForRow(row: RbiFeedItem) {
|
||||
if (!row.openProjectWpId) return;
|
||||
this.opRowLoading[row.id] = true;
|
||||
this.opRowDetails[row.id] = null;
|
||||
this.openProjectService.getWorkPackageById(row.openProjectWpId).subscribe({
|
||||
next: (wp) => {
|
||||
this.opRowDetails[row.id] = wp;
|
||||
this.opRowLoading[row.id] = false;
|
||||
},
|
||||
error: () => {
|
||||
this.opRowLoading[row.id] = false;
|
||||
this.opRowDetails[row.id] = null;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
getColumnsForTab(tab: CategoryTab): string[] {
|
||||
return tab.key === 'NOTIFICATION' ? this.notificationColumns : this.displayedColumns;
|
||||
}
|
||||
|
||||
/** OpenProject Methods */
|
||||
loadWorkPackages() {
|
||||
this.opLoading = true;
|
||||
this.opError = null;
|
||||
this.openProjectService.getWorkPackages().subscribe({
|
||||
next: (items) => {
|
||||
this.opWorkPackages = items;
|
||||
this.opFilteredWorkPackages = items;
|
||||
this.opLoading = false;
|
||||
},
|
||||
error: () => {
|
||||
this.opLoading = false;
|
||||
this.opError = 'Failed to load OpenProject work items. Please check your configuration.';
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
viewWorkPackageDetails(id: number) {
|
||||
this.opDetailLoading = true;
|
||||
this.opSelectedWorkPackage = null;
|
||||
this.openProjectService.getWorkPackageById(id).subscribe({
|
||||
next: (wp) => {
|
||||
this.opSelectedWorkPackage = wp;
|
||||
this.opDetailLoading = false;
|
||||
},
|
||||
error: () => {
|
||||
this.opDetailLoading = false;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
closeWorkPackageDetails() {
|
||||
this.opSelectedWorkPackage = null;
|
||||
}
|
||||
|
||||
openInOpenProject(wp: OpenProjectWorkPackage) {
|
||||
if (wp.openProjectUrl) {
|
||||
window.open(wp.openProjectUrl, '_blank');
|
||||
}
|
||||
}
|
||||
|
||||
filterWorkPackages(query: string) {
|
||||
if (!query.trim()) {
|
||||
this.opFilteredWorkPackages = this.opWorkPackages;
|
||||
return;
|
||||
}
|
||||
const q = query.toLowerCase();
|
||||
this.opFilteredWorkPackages = this.opWorkPackages.filter(
|
||||
(wp) => wp.subject?.toLowerCase().includes(q) || wp.status?.toLowerCase().includes(q) || wp.type?.toLowerCase().includes(q)
|
||||
);
|
||||
}
|
||||
|
||||
getStatusColor(status: string): string {
|
||||
const s = status?.toLowerCase() || '';
|
||||
if (s.includes('closed') || s.includes('done') || s.includes('resolved')) return '#2e7d32';
|
||||
if (s.includes('in progress') || s.includes('active')) return '#1976d2';
|
||||
if (s.includes('rejected') || s.includes('blocked')) return '#c62828';
|
||||
return '#f57c00';
|
||||
}
|
||||
|
||||
getPriorityColor(priority: string): string {
|
||||
const p = priority?.toLowerCase() || '';
|
||||
if (p.includes('immediate') || p.includes('urgent')) return '#c62828';
|
||||
if (p.includes('high')) return '#e65100';
|
||||
if (p.includes('low')) return '#2e7d32';
|
||||
return '#1976d2';
|
||||
}
|
||||
|
||||
formatOpDate(dateStr: string): string {
|
||||
if (!dateStr) return '—';
|
||||
const d = new Date(dateStr);
|
||||
return isNaN(d.getTime()) ? '—' : d.toLocaleDateString('en-IN', { day: '2-digit', month: 'short', year: 'numeric' });
|
||||
}
|
||||
|
||||
private buildTabs() {
|
||||
const categoryKeys = [
|
||||
'ALL',
|
||||
@@ -205,6 +342,8 @@ export class RbiCircularsComponent implements OnInit {
|
||||
tab.dataSource.filter = this.searchText.toLowerCase().trim();
|
||||
}
|
||||
});
|
||||
// Reconnect paginators after data update
|
||||
setTimeout(() => this.connectPaginators());
|
||||
}
|
||||
|
||||
private sortByDate(items: RbiFeedItem[]): RbiFeedItem[] {
|
||||
|
||||
@@ -18,6 +18,9 @@ export interface RbiFeedItem {
|
||||
pubDate: any; // Fineract returns [year,month,day,hour,min,sec] array
|
||||
category: string;
|
||||
fetchedOn?: any;
|
||||
openProjectWpId?: number; // OpenProject work package ID
|
||||
openProjectWpStatus?: string;
|
||||
openProjectWpProgress?: number;
|
||||
}
|
||||
|
||||
@Injectable({
|
||||
|
||||
Reference in New Issue
Block a user