feat: add RBI circulars, CKYC, KYC, Superset analytics, passbook modules and UI updates
Add new feature modules (RBI feeds, CKYC, KYC tab, Form60, Superset analytics, passbook), update client family members, dashboard, login page, sidenav, images, translations, environment config, and fix RBI feeds API auth by using relative URLs. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 4.6
parent
07f701f654
commit
73aa69ee02
@@ -0,0 +1,29 @@
|
||||
/**
|
||||
* 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 { NgModule } from '@angular/core';
|
||||
import { Routes, RouterModule } from '@angular/router';
|
||||
|
||||
import { Route } from '../core/route/route.service';
|
||||
import { RbiCircularsComponent } from './rbi-circulars.component';
|
||||
|
||||
const routes: Routes = [
|
||||
Route.withShell([
|
||||
{
|
||||
path: 'rbi-circulars',
|
||||
component: RbiCircularsComponent,
|
||||
data: { title: 'RBI Circulars', breadcrumb: 'RBI Circulars' }
|
||||
}
|
||||
])
|
||||
];
|
||||
|
||||
@NgModule({
|
||||
imports: [RouterModule.forChild(routes)],
|
||||
exports: [RouterModule]
|
||||
})
|
||||
export class RbiCircularsRoutingModule {}
|
||||
@@ -0,0 +1,144 @@
|
||||
<!--
|
||||
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="rbi-container">
|
||||
<!-- Header -->
|
||||
<div class="rbi-header">
|
||||
<div class="rbi-header-left">
|
||||
<div class="rbi-title">
|
||||
<fa-icon icon="newspaper" class="title-icon"></fa-icon>
|
||||
<h2>RBI Circulars & Notifications</h2>
|
||||
</div>
|
||||
<span class="rbi-subtitle">Stay updated with the latest Reserve Bank of India communications</span>
|
||||
</div>
|
||||
<div class="rbi-header-actions">
|
||||
<button
|
||||
mat-raised-button
|
||||
color="primary"
|
||||
(click)="refreshFeeds()"
|
||||
[disabled]="isRefreshing"
|
||||
matTooltip="Fetch latest feeds from RBI"
|
||||
>
|
||||
<fa-icon icon="sync" class="mr-05"></fa-icon>
|
||||
{{ isRefreshing ? 'Fetching...' : 'Refresh Feeds' }}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Search Bar -->
|
||||
<div class="search-bar">
|
||||
<mat-form-field class="search-field" appearance="outline">
|
||||
<mat-label>Search circulars...</mat-label>
|
||||
<input
|
||||
matInput
|
||||
[value]="searchText"
|
||||
(input)="onSearchChange($any($event.target).value)"
|
||||
placeholder="Search by title or description"
|
||||
/>
|
||||
<fa-icon matSuffix icon="search" class="search-icon"></fa-icon>
|
||||
</mat-form-field>
|
||||
</div>
|
||||
|
||||
<!-- Tabs always visible -->
|
||||
<mat-tab-group animationDuration="200ms" class="rbi-tabs">
|
||||
<mat-tab *ngFor="let tab of tabs">
|
||||
<!-- Tab Label -->
|
||||
<ng-template mat-tab-label>
|
||||
<div class="tab-label">
|
||||
<fa-icon [icon]="tab.icon" class="tab-icon" [style.color]="tab.color"></fa-icon>
|
||||
<span>{{ tab.label }}</span>
|
||||
<span class="tab-badge" [style.background]="tab.color">{{ tab.dataSource.filteredData.length }}</span>
|
||||
</div>
|
||||
</ng-template>
|
||||
|
||||
<!-- Tab Content -->
|
||||
<div class="tab-content">
|
||||
<!-- Loading State -->
|
||||
<div *ngIf="isLoading" class="loading-state">
|
||||
<mat-spinner diameter="40"></mat-spinner>
|
||||
<p>Loading RBI circulars...</p>
|
||||
</div>
|
||||
|
||||
<!-- Error State -->
|
||||
<div *ngIf="errorMessage && !isLoading" class="error-state">
|
||||
<fa-icon icon="exclamation-triangle" class="error-icon"></fa-icon>
|
||||
<p>{{ errorMessage }}</p>
|
||||
<button mat-raised-button color="primary" (click)="loadFeeds()">
|
||||
<fa-icon icon="sync" class="mr-05"></fa-icon> Retry
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<!-- Empty state for this tab -->
|
||||
<div *ngIf="!isLoading && !errorMessage && tab.dataSource.filteredData.length === 0" class="empty-state">
|
||||
<fa-icon icon="archive" class="empty-icon"></fa-icon>
|
||||
<h3>No {{ tab.label.toLowerCase() }} found</h3>
|
||||
<p *ngIf="searchText">No results matching "{{ searchText }}"</p>
|
||||
<p *ngIf="!searchText">Click "Refresh Feeds" to fetch the latest RBI communications</p>
|
||||
</div>
|
||||
|
||||
<!-- Data Table -->
|
||||
<div *ngIf="!isLoading && !errorMessage && tab.dataSource.filteredData.length > 0" class="table-wrapper">
|
||||
<table mat-table [dataSource]="tab.dataSource" class="feed-table">
|
||||
<!-- S.No Column -->
|
||||
<ng-container matColumnDef="sno">
|
||||
<th mat-header-cell *matHeaderCellDef class="col-sno">S.No</th>
|
||||
<td mat-cell *matCellDef="let row" class="col-sno">
|
||||
{{ getRowIndex(tab, row) }}
|
||||
</td>
|
||||
</ng-container>
|
||||
|
||||
<!-- Title Column -->
|
||||
<ng-container matColumnDef="title">
|
||||
<th mat-header-cell *matHeaderCellDef>Title</th>
|
||||
<td mat-cell *matCellDef="let row" class="col-title">
|
||||
<div class="title-cell">
|
||||
<span class="new-badge" *ngIf="isNew(row.pubDate)">NEW</span>
|
||||
<span class="title-text">{{ row.title }}</span>
|
||||
</div>
|
||||
<div *ngIf="row.description" class="description-text">{{ stripHtml(row.description) }}</div>
|
||||
</td>
|
||||
</ng-container>
|
||||
|
||||
<!-- Date Column -->
|
||||
<ng-container matColumnDef="pubDate">
|
||||
<th mat-header-cell *matHeaderCellDef class="col-date">Published Date</th>
|
||||
<td mat-cell *matCellDef="let row" class="col-date">
|
||||
<div class="date-cell">
|
||||
<fa-icon icon="calendar-alt" class="date-icon"></fa-icon>
|
||||
{{ formatDate(row.pubDate) }}
|
||||
</div>
|
||||
</td>
|
||||
</ng-container>
|
||||
|
||||
<!-- Actions Column -->
|
||||
<ng-container matColumnDef="actions">
|
||||
<th mat-header-cell *matHeaderCellDef class="col-actions">Link</th>
|
||||
<td mat-cell *matCellDef="let row" class="col-actions">
|
||||
<button
|
||||
mat-raised-button
|
||||
color="primary"
|
||||
class="view-btn"
|
||||
(click)="openLink(row.link)"
|
||||
matTooltip="Open on RBI website"
|
||||
>
|
||||
<fa-icon icon="external-link-alt" class="mr-05"></fa-icon>
|
||||
View
|
||||
</button>
|
||||
</td>
|
||||
</ng-container>
|
||||
|
||||
<tr mat-header-row *matHeaderRowDef="displayedColumns"></tr>
|
||||
<tr mat-row *matRowDef="let row; columns: displayedColumns" class="feed-row"></tr>
|
||||
</table>
|
||||
|
||||
<div class="results-info">Showing {{ tab.dataSource.filteredData.length }} items</div>
|
||||
</div>
|
||||
</div>
|
||||
</mat-tab>
|
||||
</mat-tab-group>
|
||||
</div>
|
||||
@@ -0,0 +1,325 @@
|
||||
/**
|
||||
* 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/.
|
||||
*/
|
||||
|
||||
.rbi-container {
|
||||
padding: 24px;
|
||||
max-width: 1400px;
|
||||
margin: 0 auto;
|
||||
}
|
||||
|
||||
// Header
|
||||
.rbi-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: flex-start;
|
||||
margin-bottom: 20px;
|
||||
flex-wrap: wrap;
|
||||
gap: 16px;
|
||||
}
|
||||
|
||||
.rbi-header-left {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 4px;
|
||||
}
|
||||
|
||||
.rbi-title {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 12px;
|
||||
|
||||
h2 {
|
||||
margin: 0;
|
||||
font-size: 1.5rem;
|
||||
font-weight: 600;
|
||||
color: #1a237e;
|
||||
}
|
||||
|
||||
.title-icon {
|
||||
font-size: 1.4rem;
|
||||
color: #1a237e;
|
||||
}
|
||||
}
|
||||
|
||||
.rbi-subtitle {
|
||||
font-size: 0.85rem;
|
||||
color: #666;
|
||||
margin-left: 36px;
|
||||
}
|
||||
|
||||
.mr-05 {
|
||||
margin-right: 5px;
|
||||
}
|
||||
|
||||
// Search
|
||||
.search-bar {
|
||||
margin-bottom: 8px;
|
||||
|
||||
.search-field {
|
||||
width: 100%;
|
||||
max-width: 500px;
|
||||
}
|
||||
|
||||
.search-icon {
|
||||
color: #999;
|
||||
margin-right: 8px;
|
||||
}
|
||||
}
|
||||
|
||||
// Tabs
|
||||
.rbi-tabs {
|
||||
::ng-deep .mat-mdc-tab-header {
|
||||
border-bottom: 2px solid #e8e8e8;
|
||||
}
|
||||
|
||||
::ng-deep .mat-mdc-tab {
|
||||
min-width: 120px;
|
||||
}
|
||||
}
|
||||
|
||||
.tab-label {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
font-size: 0.85rem;
|
||||
|
||||
.tab-icon {
|
||||
font-size: 0.85rem;
|
||||
}
|
||||
}
|
||||
|
||||
.tab-badge {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
min-width: 22px;
|
||||
height: 20px;
|
||||
padding: 0 6px;
|
||||
border-radius: 10px;
|
||||
color: white;
|
||||
font-size: 0.7rem;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.tab-content {
|
||||
padding: 16px 0;
|
||||
}
|
||||
|
||||
// Table
|
||||
.table-wrapper {
|
||||
overflow-x: auto;
|
||||
background: white;
|
||||
border-radius: 8px;
|
||||
box-shadow: 0 1px 4px rgb(0 0 0 / 8%);
|
||||
}
|
||||
|
||||
.feed-table {
|
||||
width: 100%;
|
||||
|
||||
.col-sno {
|
||||
width: 60px;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.col-date {
|
||||
width: 160px;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.col-actions {
|
||||
width: 100px;
|
||||
text-align: center;
|
||||
}
|
||||
}
|
||||
|
||||
.title-cell {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.title-text {
|
||||
font-weight: 500;
|
||||
color: #222;
|
||||
line-height: 1.4;
|
||||
}
|
||||
|
||||
.description-text {
|
||||
font-size: 0.8rem;
|
||||
color: #888;
|
||||
margin-top: 4px;
|
||||
line-height: 1.4;
|
||||
display: -webkit-box;
|
||||
-webkit-line-clamp: 1;
|
||||
-webkit-box-orient: vertical;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.date-cell {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
color: #555;
|
||||
font-size: 0.85rem;
|
||||
|
||||
.date-icon {
|
||||
font-size: 0.75rem;
|
||||
color: #aaa;
|
||||
}
|
||||
}
|
||||
|
||||
.view-btn {
|
||||
font-size: 0.8rem;
|
||||
padding: 0 12px;
|
||||
line-height: 30px;
|
||||
height: 30px;
|
||||
}
|
||||
|
||||
.new-badge {
|
||||
display: inline-block;
|
||||
background: linear-gradient(135deg, #ff5252, #d32f2f);
|
||||
color: white;
|
||||
font-size: 0.6rem;
|
||||
font-weight: 700;
|
||||
padding: 2px 8px;
|
||||
border-radius: 10px;
|
||||
letter-spacing: 0.5px;
|
||||
animation: pulse 2s infinite;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
@keyframes pulse {
|
||||
0%,
|
||||
100% {
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
50% {
|
||||
opacity: 0.7;
|
||||
}
|
||||
}
|
||||
|
||||
.feed-row {
|
||||
cursor: default;
|
||||
|
||||
&:hover {
|
||||
background: #f5f7ff;
|
||||
}
|
||||
}
|
||||
|
||||
// mat-header styling
|
||||
::ng-deep .mat-mdc-header-cell {
|
||||
font-weight: 600;
|
||||
color: #444;
|
||||
font-size: 0.85rem;
|
||||
background: #fafafa;
|
||||
}
|
||||
|
||||
::ng-deep .mat-mdc-cell {
|
||||
font-size: 0.85rem;
|
||||
padding: 12px 16px !important;
|
||||
}
|
||||
|
||||
// Results info
|
||||
.results-info {
|
||||
font-size: 0.8rem;
|
||||
color: #888;
|
||||
padding: 12px 16px;
|
||||
border-top: 1px solid #f0f0f0;
|
||||
}
|
||||
|
||||
// Loading
|
||||
.loading-state {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
padding: 60px 0;
|
||||
gap: 16px;
|
||||
|
||||
p {
|
||||
color: #888;
|
||||
font-size: 0.9rem;
|
||||
}
|
||||
}
|
||||
|
||||
// Error
|
||||
.error-state {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
padding: 48px 0;
|
||||
gap: 12px;
|
||||
text-align: center;
|
||||
|
||||
.error-icon {
|
||||
font-size: 2rem;
|
||||
color: #f44336;
|
||||
}
|
||||
|
||||
p {
|
||||
color: #666;
|
||||
font-size: 0.9rem;
|
||||
}
|
||||
}
|
||||
|
||||
// Empty State
|
||||
.empty-state {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
padding: 48px 0;
|
||||
text-align: center;
|
||||
|
||||
.empty-icon {
|
||||
font-size: 2.5rem;
|
||||
color: #ccc;
|
||||
margin-bottom: 12px;
|
||||
}
|
||||
|
||||
h3 {
|
||||
margin: 0 0 8px;
|
||||
color: #555;
|
||||
font-size: 1rem;
|
||||
}
|
||||
|
||||
p {
|
||||
color: #999;
|
||||
font-size: 0.85rem;
|
||||
}
|
||||
}
|
||||
|
||||
// Responsive
|
||||
@media screen and (width <= 768px) {
|
||||
.rbi-container {
|
||||
padding: 16px;
|
||||
}
|
||||
|
||||
.rbi-header {
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.rbi-title h2 {
|
||||
font-size: 1.2rem;
|
||||
}
|
||||
|
||||
.rbi-subtitle {
|
||||
margin-left: 0;
|
||||
}
|
||||
|
||||
.search-bar .search-field {
|
||||
max-width: 100%;
|
||||
}
|
||||
|
||||
.description-text {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.col-sno {
|
||||
width: 40px;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,217 @@
|
||||
/**
|
||||
* 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 { RbiCircularsService, RbiFeedItem } from './rbi-circulars.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 { MatSortModule } from '@angular/material/sort';
|
||||
|
||||
interface CategoryTab {
|
||||
key: string;
|
||||
label: string;
|
||||
icon: string;
|
||||
color: string;
|
||||
dataSource: MatTableDataSource<RbiFeedItem>;
|
||||
}
|
||||
|
||||
@Component({
|
||||
selector: 'mifosx-rbi-circulars',
|
||||
standalone: true,
|
||||
templateUrl: './rbi-circulars.component.html',
|
||||
styleUrls: ['./rbi-circulars.component.scss'],
|
||||
imports: [
|
||||
...STANDALONE_SHARED_IMPORTS,
|
||||
FaIconComponent,
|
||||
MatProgressSpinner,
|
||||
MatTooltip,
|
||||
MatTabsModule,
|
||||
MatTableModule,
|
||||
MatPaginatorModule,
|
||||
MatSortModule
|
||||
]
|
||||
})
|
||||
export class RbiCircularsComponent implements OnInit {
|
||||
private rbiService = inject(RbiCircularsService);
|
||||
|
||||
feedItems: RbiFeedItem[] = [];
|
||||
isLoading = true;
|
||||
isRefreshing = false;
|
||||
errorMessage: string | null = null;
|
||||
searchText = '';
|
||||
|
||||
displayedColumns: string[] = [
|
||||
'sno',
|
||||
'title',
|
||||
'pubDate',
|
||||
'actions'
|
||||
];
|
||||
|
||||
tabs: CategoryTab[] = [];
|
||||
|
||||
/** Fixed category config */
|
||||
private categoryConfig: Record<string, { label: string; icon: string; color: string }> = {
|
||||
ALL: { label: 'All Feeds', icon: 'list', color: '#1a237e' },
|
||||
PRESS_RELEASE: { label: 'Press Releases', icon: 'newspaper', color: '#1976d2' },
|
||||
NOTIFICATION: { label: 'Notifications', icon: 'bell', color: '#f57c00' },
|
||||
SPEECH: { label: 'Speeches', icon: 'comment-alt', color: '#4527a0' },
|
||||
TENDER: { label: 'Tenders', icon: 'file', color: '#546e7a' },
|
||||
PUBLICATION: { label: 'Publications', icon: 'book-open', color: '#2e7d32' }
|
||||
};
|
||||
|
||||
ngOnInit() {
|
||||
this.buildTabs(); // Build empty tabs immediately so they always show
|
||||
this.loadFeeds();
|
||||
}
|
||||
|
||||
loadFeeds() {
|
||||
this.isLoading = true;
|
||||
this.errorMessage = null;
|
||||
this.rbiService.getAll().subscribe({
|
||||
next: (items) => {
|
||||
this.feedItems = this.sortByDate(items);
|
||||
this.updateTabData();
|
||||
this.isLoading = false;
|
||||
this.isRefreshing = false;
|
||||
},
|
||||
error: () => {
|
||||
this.isLoading = false;
|
||||
this.isRefreshing = false;
|
||||
this.errorMessage = 'Failed to load RBI circulars. Click "Refresh Feeds" to try again.';
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
refreshFeeds() {
|
||||
this.isRefreshing = true;
|
||||
this.rbiService.triggerFetch().subscribe({
|
||||
next: () => this.loadFeeds(),
|
||||
error: () => this.loadFeeds()
|
||||
});
|
||||
}
|
||||
|
||||
onSearchChange(value: string) {
|
||||
this.searchText = value;
|
||||
const query = value.toLowerCase().trim();
|
||||
this.tabs.forEach((tab) => {
|
||||
tab.dataSource.filter = query;
|
||||
});
|
||||
}
|
||||
|
||||
openLink(url: string) {
|
||||
if (url) {
|
||||
window.open(url, '_blank');
|
||||
}
|
||||
}
|
||||
|
||||
/** Parse Fineract date (array [y,m,d,h,min,s] or ISO string) into a Date */
|
||||
parseDate(value: any): Date | null {
|
||||
if (!value) return null;
|
||||
if (Array.isArray(value)) {
|
||||
const [
|
||||
y,
|
||||
m,
|
||||
d,
|
||||
h = 0,
|
||||
min = 0,
|
||||
s = 0
|
||||
] = value;
|
||||
return new Date(y, m - 1, d, h, min, s);
|
||||
}
|
||||
const parsed = new Date(value);
|
||||
return isNaN(parsed.getTime()) ? null : parsed;
|
||||
}
|
||||
|
||||
formatDate(pubDate: any): string {
|
||||
const date = this.parseDate(pubDate);
|
||||
if (!date) return '-';
|
||||
return date.toLocaleDateString('en-IN', {
|
||||
day: '2-digit',
|
||||
month: 'short',
|
||||
year: 'numeric'
|
||||
});
|
||||
}
|
||||
|
||||
isNew(pubDate: any): boolean {
|
||||
const date = this.parseDate(pubDate);
|
||||
if (!date) return false;
|
||||
const threeDaysAgo = new Date();
|
||||
threeDaysAgo.setDate(threeDaysAgo.getDate() - 3);
|
||||
return date > threeDaysAgo;
|
||||
}
|
||||
|
||||
getCategoryLabel(category: string): string {
|
||||
return this.categoryConfig[category]?.label || category;
|
||||
}
|
||||
|
||||
getCategoryColor(category: string): string {
|
||||
return this.categoryConfig[category]?.color || '#607d8b';
|
||||
}
|
||||
|
||||
stripHtml(html: string): string {
|
||||
if (!html) return '';
|
||||
const div = document.createElement('div');
|
||||
div.innerHTML = html;
|
||||
return div.textContent || div.innerText || '';
|
||||
}
|
||||
|
||||
getRowIndex(tab: CategoryTab, row: RbiFeedItem): number {
|
||||
return tab.dataSource.filteredData.indexOf(row) + 1;
|
||||
}
|
||||
|
||||
private buildTabs() {
|
||||
const categoryKeys = [
|
||||
'ALL',
|
||||
'PRESS_RELEASE',
|
||||
'NOTIFICATION',
|
||||
'SPEECH',
|
||||
'TENDER',
|
||||
'PUBLICATION'
|
||||
];
|
||||
|
||||
this.tabs = categoryKeys.map((key) => {
|
||||
const config = this.categoryConfig[key];
|
||||
const ds = new MatTableDataSource<RbiFeedItem>([]);
|
||||
ds.filterPredicate = (data: RbiFeedItem, filter: string) => {
|
||||
return (
|
||||
(data.title?.toLowerCase().includes(filter) || data.description?.toLowerCase().includes(filter)) ?? false
|
||||
);
|
||||
};
|
||||
return {
|
||||
key,
|
||||
label: config.label,
|
||||
icon: config.icon,
|
||||
color: config.color,
|
||||
dataSource: ds
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
private updateTabData() {
|
||||
this.tabs.forEach((tab) => {
|
||||
const items = tab.key === 'ALL' ? this.feedItems : this.feedItems.filter((i) => i.category === tab.key);
|
||||
tab.dataSource.data = items;
|
||||
if (this.searchText.trim()) {
|
||||
tab.dataSource.filter = this.searchText.toLowerCase().trim();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private sortByDate(items: RbiFeedItem[]): RbiFeedItem[] {
|
||||
return [...items].sort((a, b) => {
|
||||
const dateA = this.parseDate(a.pubDate)?.getTime() || 0;
|
||||
const dateB = this.parseDate(b.pubDate)?.getTime() || 0;
|
||||
return dateB - dateA;
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,22 @@
|
||||
/**
|
||||
* 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 { NgModule } from '@angular/core';
|
||||
|
||||
import { SharedModule } from '../shared/shared.module';
|
||||
import { RbiCircularsRoutingModule } from './rbi-circulars-routing.module';
|
||||
import { RbiCircularsComponent } from './rbi-circulars.component';
|
||||
|
||||
@NgModule({
|
||||
imports: [
|
||||
SharedModule,
|
||||
RbiCircularsRoutingModule,
|
||||
RbiCircularsComponent
|
||||
]
|
||||
})
|
||||
export class RbiCircularsModule {}
|
||||
@@ -0,0 +1,44 @@
|
||||
/**
|
||||
* 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';
|
||||
|
||||
export interface RbiFeedItem {
|
||||
id: number;
|
||||
title: string;
|
||||
link: string;
|
||||
description: string;
|
||||
pubDate: any; // Fineract returns [year,month,day,hour,min,sec] array
|
||||
category: string;
|
||||
fetchedOn?: any;
|
||||
}
|
||||
|
||||
@Injectable({
|
||||
providedIn: 'root'
|
||||
})
|
||||
export class RbiCircularsService {
|
||||
private http = inject(HttpClient);
|
||||
|
||||
getAll(category?: string): Observable<RbiFeedItem[]> {
|
||||
let params = new HttpParams();
|
||||
if (category) {
|
||||
params = params.set('category', category);
|
||||
}
|
||||
return this.http.get<RbiFeedItem[]>('/rbi-feeds', { params });
|
||||
}
|
||||
|
||||
getById(id: number): Observable<RbiFeedItem> {
|
||||
return this.http.get<RbiFeedItem>(`/rbi-feeds/${id}`);
|
||||
}
|
||||
|
||||
triggerFetch(): Observable<any> {
|
||||
return this.http.post('/rbi-feeds/fetch', {});
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user