diff --git a/.claude/scheduled_tasks.lock b/.claude/scheduled_tasks.lock deleted file mode 100644 index dc8228604..000000000 --- a/.claude/scheduled_tasks.lock +++ /dev/null @@ -1 +0,0 @@ -{"sessionId":"67d120b4-ff78-435d-be80-f79482a386b4","pid":19832,"acquiredAt":1778067581856} \ No newline at end of file diff --git a/src/app/core/authentication/authentication.service.ts b/src/app/core/authentication/authentication.service.ts index 57e5f30fc..6110afa22 100644 --- a/src/app/core/authentication/authentication.service.ts +++ b/src/app/core/authentication/authentication.service.ts @@ -420,6 +420,14 @@ export class AuthenticationService { return false; } + /** Returns true when the logged-in user has the "Cashier" role. */ + hasCashierRole(): boolean { + const creds = this.getCredentials(); + if (!creds || !creds.roles) return false; + const roleNames: string[] = (creds.roles as any[]).map((r) => (r.name || r.role || '').toLowerCase().trim()); + return roleNames.some((n) => n === 'cashier'); + } + /** * Sets the user credentials. * diff --git a/src/app/current-accounts/current-account-view/current-account-view.component.html b/src/app/current-accounts/current-account-view/current-account-view.component.html index 89a0f7303..85a03caaf 100644 --- a/src/app/current-accounts/current-account-view/current-account-view.component.html +++ b/src/app/current-accounts/current-account-view/current-account-view.component.html @@ -7,6 +7,9 @@ -->
+ + +
diff --git a/src/app/current-accounts/current-account-view/current-account-view.component.ts b/src/app/current-accounts/current-account-view/current-account-view.component.ts index 1412dd044..bbd93dca9 100644 --- a/src/app/current-accounts/current-account-view/current-account-view.component.ts +++ b/src/app/current-accounts/current-account-view/current-account-view.component.ts @@ -27,6 +27,7 @@ import { TranslateService } from '@ngx-translate/core'; import { AccountsService } from 'app/accounts/accounts.service'; import { SettingsService } from 'app/settings/settings.service'; import { Dates } from 'app/core/utils/dates'; +import { AuthenticationService } from '../../core/authentication/authentication.service'; /** Environment Configuration */ import { environment } from '../../../environments/environment'; @@ -48,6 +49,7 @@ import { FaIconComponent } from '@fortawesome/angular-fontawesome'; import { MatTabNav, MatTabLink, MatTabNavPanel } from '@angular/material/tabs'; import { StatusLookupPipe } from '../../pipes/status-lookup.pipe'; import { STANDALONE_SHARED_IMPORTS } from 'app/standalone-shared.module'; +import { DormancyAlertPanelComponent } from 'app/shared/dormancy-alert-panel/dormancy-alert-panel.component'; /** * Current Account View Component @@ -77,7 +79,8 @@ import { STANDALONE_SHARED_IMPORTS } from 'app/standalone-shared.module'; MatTabNavPanel, RouterOutlet, CurrencyPipe, - StatusLookupPipe + StatusLookupPipe, + DormancyAlertPanelComponent ] }) export class CurrentAccountViewComponent implements OnInit { @@ -89,6 +92,7 @@ export class CurrentAccountViewComponent implements OnInit { private accountsService = inject(AccountsService); private settingsService = inject(SettingsService); private dateUtils = inject(Dates); + private authenticationService = inject(AuthenticationService); dialog = inject(MatDialog); /** Current Account Data */ @@ -271,6 +275,14 @@ export class CurrentAccountViewComponent implements OnInit { }); } } + if (!this.authenticationService.hasCashierRole()) { + this.buttonConfig.buttonsArray = this.buttonConfig.buttonsArray.filter( + (btn) => btn.name !== 'Deposit' && btn.name !== 'Withdrawal' + ); + this.buttonConfig.optionArray = this.buttonConfig.optionArray.filter( + (opt) => opt.name !== 'Deposit' && opt.name !== 'Withdrawal' + ); + } } /** diff --git a/src/app/deposits/fixed-deposits/fixed-deposit-account-view/fixed-deposit-account-view.component.html b/src/app/deposits/fixed-deposits/fixed-deposit-account-view/fixed-deposit-account-view.component.html index ae0213ac6..55da78695 100644 --- a/src/app/deposits/fixed-deposits/fixed-deposit-account-view/fixed-deposit-account-view.component.html +++ b/src/app/deposits/fixed-deposits/fixed-deposit-account-view/fixed-deposit-account-view.component.html @@ -7,6 +7,9 @@ -->
+ + +
diff --git a/src/app/deposits/fixed-deposits/fixed-deposit-account-view/fixed-deposit-account-view.component.ts b/src/app/deposits/fixed-deposits/fixed-deposit-account-view/fixed-deposit-account-view.component.ts index 17faad6f1..7715b65c5 100644 --- a/src/app/deposits/fixed-deposits/fixed-deposit-account-view/fixed-deposit-account-view.component.ts +++ b/src/app/deposits/fixed-deposits/fixed-deposit-account-view/fixed-deposit-account-view.component.ts @@ -27,6 +27,7 @@ import { AccountsService } from 'app/accounts/accounts.service'; import { Currency } from 'app/shared/models/general.model'; import { SettingsService } from 'app/settings/settings.service'; import { Dates } from 'app/core/utils/dates'; +import { AuthenticationService } from '../../../core/authentication/authentication.service'; import { MatTooltip } from '@angular/material/tooltip'; import { NgClass, CurrencyPipe } from '@angular/common'; import { AccountNumberComponent } from '../../../shared/account-number/account-number.component'; @@ -37,6 +38,7 @@ import { FaIconComponent } from '@fortawesome/angular-fontawesome'; import { MatTabNav, MatTabLink, MatTabNavPanel } from '@angular/material/tabs'; import { StatusLookupPipe } from '../../../pipes/status-lookup.pipe'; import { STANDALONE_SHARED_IMPORTS } from 'app/standalone-shared.module'; +import { DormancyAlertPanelComponent } from 'app/shared/dormancy-alert-panel/dormancy-alert-panel.component'; /** * Fixed Deposits Account View Component @@ -62,7 +64,8 @@ import { STANDALONE_SHARED_IMPORTS } from 'app/standalone-shared.module'; MatTabNavPanel, RouterOutlet, CurrencyPipe, - StatusLookupPipe + StatusLookupPipe, + DormancyAlertPanelComponent ] }) export class FixedDepositAccountViewComponent implements OnInit { @@ -73,6 +76,7 @@ export class FixedDepositAccountViewComponent implements OnInit { private accountsService = inject(AccountsService); private settingsService = inject(SettingsService); private dateUtils = inject(Dates); + private authenticationService = inject(AuthenticationService); dialog = inject(MatDialog); /** Fixed Deposits Account Data */ @@ -194,6 +198,14 @@ export class FixedDepositAccountViewComponent implements OnInit { }); } } + if (!this.authenticationService.hasCashierRole()) { + this.buttonConfig.buttonsArray = this.buttonConfig.buttonsArray.filter( + (btn) => btn.name !== 'Deposit' && btn.name !== 'Withdrawal' + ); + this.buttonConfig.optionArray = this.buttonConfig.optionArray.filter( + (opt) => opt.name !== 'Deposit' && opt.name !== 'Withdrawal' + ); + } } /** diff --git a/src/app/deposits/recurring-deposits/recurring-deposits-account-view/recurring-deposits-account-view.component.html b/src/app/deposits/recurring-deposits/recurring-deposits-account-view/recurring-deposits-account-view.component.html index e70d3ad78..bb4c80bdf 100644 --- a/src/app/deposits/recurring-deposits/recurring-deposits-account-view/recurring-deposits-account-view.component.html +++ b/src/app/deposits/recurring-deposits/recurring-deposits-account-view/recurring-deposits-account-view.component.html @@ -7,6 +7,9 @@ -->
+ + +
diff --git a/src/app/deposits/recurring-deposits/recurring-deposits-account-view/recurring-deposits-account-view.component.ts b/src/app/deposits/recurring-deposits/recurring-deposits-account-view/recurring-deposits-account-view.component.ts index 55c4deb69..24ca434e9 100644 --- a/src/app/deposits/recurring-deposits/recurring-deposits-account-view/recurring-deposits-account-view.component.ts +++ b/src/app/deposits/recurring-deposits/recurring-deposits-account-view/recurring-deposits-account-view.component.ts @@ -27,6 +27,7 @@ import { TranslateService } from '@ngx-translate/core'; import { AccountsService } from 'app/accounts/accounts.service'; import { SettingsService } from 'app/settings/settings.service'; import { Dates } from 'app/core/utils/dates'; +import { AuthenticationService } from '../../../core/authentication/authentication.service'; import { MatTooltip } from '@angular/material/tooltip'; import { NgClass, DecimalPipe, CurrencyPipe } from '@angular/common'; import { AccountNumberComponent } from '../../../shared/account-number/account-number.component'; @@ -37,6 +38,7 @@ import { FaIconComponent } from '@fortawesome/angular-fontawesome'; import { MatTabNav, MatTabLink, MatTabNavPanel } from '@angular/material/tabs'; import { StatusLookupPipe } from '../../../pipes/status-lookup.pipe'; import { STANDALONE_SHARED_IMPORTS } from 'app/standalone-shared.module'; +import { DormancyAlertPanelComponent } from 'app/shared/dormancy-alert-panel/dormancy-alert-panel.component'; /** * RecurringDeposits Account View Component @@ -63,7 +65,8 @@ import { STANDALONE_SHARED_IMPORTS } from 'app/standalone-shared.module'; RouterOutlet, DecimalPipe, CurrencyPipe, - StatusLookupPipe + StatusLookupPipe, + DormancyAlertPanelComponent ] }) export class RecurringDepositsAccountViewComponent implements OnInit { @@ -74,6 +77,7 @@ export class RecurringDepositsAccountViewComponent implements OnInit { private accountsService = inject(AccountsService); private settingsService = inject(SettingsService); private dateUtils = inject(Dates); + private authenticationService = inject(AuthenticationService); dialog = inject(MatDialog); private translateService = inject(TranslateService); @@ -245,6 +249,14 @@ export class RecurringDepositsAccountViewComponent implements OnInit { } } } + if (!this.authenticationService.hasCashierRole()) { + this.buttonConfig.buttonsArray = this.buttonConfig.buttonsArray.filter( + (btn) => btn.name !== 'Deposit' && btn.name !== 'Withdrawal' + ); + this.buttonConfig.optionArray = this.buttonConfig.optionArray.filter( + (opt) => opt.name !== 'Deposit' && opt.name !== 'Withdrawal' + ); + } } /** diff --git a/src/app/letter-mgmt/certificates/certificate-pdf.ts b/src/app/letter-mgmt/certificates/certificate-pdf.ts index 939a556e5..5467eadfd 100644 --- a/src/app/letter-mgmt/certificates/certificate-pdf.ts +++ b/src/app/letter-mgmt/certificates/certificate-pdf.ts @@ -544,82 +544,315 @@ export function downloadLoanInterestPdf(record: LoanInterestRecord, fy: FyIntere // ───────────────────── Share Certificate ───────────────────── export function buildShareCertificatePdf(r: ShareCertificateRecord): jsPDF { - const c = newCtx(ACCENT_AMBER); + const doc = new jsPDF({ orientation: 'portrait', unit: 'mm', format: 'a4' }); + const pageW = doc.internal.pageSize.getWidth(); + const pageH = doc.internal.pageSize.getHeight(); + const margin = 20; + const contentW = pageW - 40; + const gold: [number, number, number] = [ + 197, + 160, + 89 + ]; // #c5a059 + const darkBlue: [number, number, number] = [ + 11, + 60, + 93 + ]; // #0b3c5d + const textDark: [number, number, number] = [ + 44, + 37, + 32 + ]; // #2c2520 + const textMuted: [number, number, number] = [ + 123, + 111, + 99 + ]; // #7b6f63 + const lightBg: [number, number, number] = [ + 253, + 252, + 247 + ]; // #fdfcf7 + const containerBg: [number, number, number] = [ + 250, + 248, + 245 + ]; // #faf8f5 + + // 1. Draw Cream Background (page body) + doc.setFillColor(containerBg[0], containerBg[1], containerBg[2]); + doc.rect(0, 0, pageW, pageH, 'F'); + + // 2. Draw Textured cream background inside the outer border + doc.setFillColor(lightBg[0], lightBg[1], lightBg[2]); + doc.rect(12, 12, pageW - 24, pageH - 24, 'F'); + + // 3. Draw Gold Double Borders (Outer and Inner) + doc.setDrawColor(gold[0], gold[1], gold[2]); + + // Outer double line + doc.setLineWidth(1.2); + doc.rect(12, 12, pageW - 24, pageH - 24, 'D'); + + doc.setLineWidth(0.3); + doc.rect(13.8, 13.8, pageW - 27.6, pageH - 27.6, 'D'); + + // Thin interior border line + doc.setDrawColor(220, 209, 189); // light brown/grey + doc.setLineWidth(0.2); + doc.rect(17, 17, pageW - 34, pageH - 34, 'D'); + + // 4. Certificate Header + let y = 28; + doc.setFont('times', 'bold'); + doc.setFontSize(22); + doc.setTextColor(darkBlue[0], darkBlue[1], darkBlue[2]); + doc.text('NEXORA CO-OPERATIVE BANK LTD.', pageW / 2, y, { align: 'center' }); + + y += 5; + doc.setFont('times', 'italic'); + doc.setFontSize(10.5); + doc.setTextColor(textMuted[0], textMuted[1], textMuted[2]); + doc.text('Mumbai, Maharashtra, India', pageW / 2, y, { align: 'center' }); + + y += 4; + doc.setDrawColor(242, 237, 213); // light gold separator + doc.setLineWidth(1.5); + doc.line(22, y, pageW - 22, y); + + y += 9; + doc.setFont('times', 'bold'); + doc.setFontSize(24); + doc.setTextColor(gold[0], gold[1], gold[2]); + doc.text('SHARE CERTIFICATE', pageW / 2, y, { align: 'center' }); + + y += 5.5; + doc.setFont('times', 'bold'); + doc.setFontSize(7.5); + doc.setTextColor(textMuted[0], textMuted[1], textMuted[2]); + doc.text('ISSUED UNDER THE COMMON SEAL OF THE BANK', pageW / 2, y, { align: 'center' }); + + y += 4; + doc.setDrawColor(242, 237, 213); // separator + doc.setLineWidth(0.3); + doc.line(22, y, pageW - 22, y); + + // 5. Meta Row (Certificate No, Folio No, Date of Issue) + y += 7; + + // Column 1: Certificate No + doc.setFont('helvetica', 'bold'); + doc.setFontSize(7.5); + doc.setTextColor(textMuted[0], textMuted[1], textMuted[2]); + doc.text('CERTIFICATE NO.', 25, y); + doc.setFont('times', 'bold'); + doc.setFontSize(10); + doc.setTextColor(textDark[0], textDark[1], textDark[2]); + doc.text(r.certificateNo, 25, y + 4.5); + + // Column 2: Folio No (Centered) + doc.setFont('helvetica', 'bold'); + doc.setFontSize(7.5); + doc.setTextColor(textMuted[0], textMuted[1], textMuted[2]); + doc.text('FOLIO NO.', pageW / 2, y, { align: 'center' }); + doc.setFont('times', 'bold'); + doc.setFontSize(10); + doc.setTextColor(textDark[0], textDark[1], textDark[2]); + doc.text(r.folioNo, pageW / 2, y + 4.5, { align: 'center' }); + + // Column 3: Date of Issue (Right Aligned) + doc.setFont('helvetica', 'bold'); + doc.setFontSize(7.5); + doc.setTextColor(textMuted[0], textMuted[1], textMuted[2]); + doc.text('DATE OF ISSUE', pageW - 25, y, { align: 'right' }); + doc.setFont('times', 'bold'); + doc.setFontSize(10); + doc.setTextColor(textDark[0], textDark[1], textDark[2]); + doc.text(formatLongDate(r.issueDate), pageW - 25, y + 4.5, { align: 'right' }); + + y += 8; + doc.setDrawColor(242, 237, 213); + doc.setLineWidth(0.3); + doc.line(22, y, pageW - 22, y); + + // 6. Centered Body Section + y += 9; const classLabel = r.shareClass === 'PREFERENCE' ? 'Preference' : 'Ordinary'; - let y = paintLetterhead(c, 'Share Certificate', 'CERTIFICATE REFERENCE', r.certificateNo, 'Folio No.', r.folioNo); + doc.setFont('times', 'normal'); + doc.setFontSize(11); + doc.setTextColor(textDark[0], textDark[1], textDark[2]); - y = paintAddressee(c, y, 'Issued To,', r.memberName, r.memberId, r.memberPan, r.memberAddress); - - // Centered, dramatic body paragraph (mirrors the highlighted "centered" style in the preview) - if (y > c.pageH - c.margin - 80) { - c.doc.addPage(); - setFill(c, c.accent); - c.doc.rect(0, 0, c.pageW, 4, 'F'); - y = c.margin; + const bodyText = `This is to certify that ${r.memberName} is the registered holder of ${r.shareCount.toLocaleString('en-IN')} (${classLabel}) shares of Rs. ${r.faceValue.toLocaleString('en-IN')} each, fully paid up, in Nexora Co-operative Bank Ltd., subject to the memorandum, articles of association, and bye-laws of the bank.`; + const bodyLines = doc.splitTextToSize(bodyText, pageW - 50); + for (const line of bodyLines) { + doc.text(line, pageW / 2, y, { align: 'center' }); + y += 5.5; } - const ctaText = `This is to certify that ${r.memberName} is the registered holder of ${r.shareCount.toLocaleString('en-IN')} (${classLabel}) shares of Rs. ${r.faceValue.toLocaleString('en-IN')} each, fully paid up, in ${BANK_NAME}, subject to the bye-laws of the bank.`; - const ctaLines = c.doc.splitTextToSize(ctaText, c.contentW - 10); - const ctaH = ctaLines.length * 5 + 6; - setFill(c, c.accentTint); - c.doc.rect(c.margin, y, c.contentW, ctaH, 'F'); - setFill(c, c.accent); - c.doc.rect(c.margin, y, 1.8, ctaH, 'F'); - c.doc.setFont('times', 'bold'); - c.doc.setFontSize(11); - setText(c, C_TEXT); - c.doc.text(ctaLines, c.margin + 6, y + 5, { align: 'justify', maxWidth: c.contentW - 10 }); - y += ctaH + 6; - y = paintSummary( - c, - y, - [ - [ - 'CERTIFICATE NO.', - r.certificateNo, - 'FOLIO NO.', - r.folioNo - ], - [ - 'BRANCH', - r.branch, - 'MEMBER SINCE', - formatLongDate(r.joiningDate) - ], - [ - 'SHARE CLASS', - classLabel, - 'DATE OF ISSUE', - formatLongDate(r.issueDate) - ], - [ - 'SHARE NUMBERS', - `${r.shareNoFrom.toLocaleString('en-IN')} – ${r.shareNoTo.toLocaleString('en-IN')}`, - 'DIVIDEND PAID', - `Rs. ${r.dividendYtd.toLocaleString('en-IN')}` - ] - ], - { - label: 'TOTAL FACE VALUE', - value: `Rs. ${(r.shareCount * r.faceValue).toLocaleString('en-IN')}` + // 7. ISSUED TO Box + y += 3; + const addrLines = doc.splitTextToSize(r.memberAddress || '', pageW - 60); + const boxH = 26 + addrLines.length * 4; + + doc.setFillColor(253, 252, 247); + doc.setDrawColor(230, 225, 210); + doc.rect(25, y, pageW - 50, boxH, 'FD'); + + let boxY = y + 6; + doc.setFont('helvetica', 'bold'); + doc.setFontSize(8); + doc.setTextColor(gold[0], gold[1], gold[2]); + doc.text('ISSUED TO:', 30, boxY); + + boxY += 5.5; + doc.setFont('times', 'bold'); + doc.setFontSize(13); + doc.setTextColor(darkBlue[0], darkBlue[1], darkBlue[2]); + doc.text(r.memberName, 30, boxY); + + boxY += 5.5; + doc.setFont('helvetica', 'normal'); + doc.setFontSize(9); + doc.setTextColor(textDark[0], textDark[1], textDark[2]); + let idLine = `Member ID: ${r.memberId}`; + if (r.memberPan && r.memberPan !== 'Not Available' && r.memberPan !== '—') { + idLine += ` · PAN: ${r.memberPan}`; + } + doc.text(idLine, 30, boxY); + + boxY += 5; + doc.setFontSize(8.5); + doc.setTextColor(textMuted[0], textMuted[1], textMuted[2]); + doc.text(addrLines, 30, boxY); + + y += boxH + 8; + + // 8. Details Grid + const tableW = pageW - 50; + const tableColW = tableW / 4; + const tableRowH = 8.5; + + const drawTableCell = ( + x: number, + tblY: number, + w: number, + h: number, + text: string, + isLabel: boolean, + align: 'left' | 'right' = 'left' + ) => { + if (isLabel) { + doc.setFillColor(253, 252, 247); + doc.rect(x, tblY, w, h, 'F'); + doc.setFont('helvetica', 'bold'); + doc.setFontSize(8); + doc.setTextColor(gold[0], gold[1], gold[2]); + } else { + doc.setFont('times', 'bold'); + doc.setFontSize(9.5); + doc.setTextColor(darkBlue[0], darkBlue[1], darkBlue[2]); } - ); + doc.setDrawColor(220, 209, 189); + doc.setLineWidth(0.25); + doc.rect(x, tblY, w, h, 'D'); + doc.text(text, align === 'right' ? x + w - 3 : x + 3, tblY + h / 2 + 1.2, { align }); + }; - y = paintPara( - c, + // Row 1 + drawTableCell(25, y, tableColW, tableRowH, 'Branch', true); + drawTableCell(25 + tableColW, y, tableColW, tableRowH, r.branch || 'Mumbai Main Branch', false); + drawTableCell(25 + tableColW * 2, y, tableColW, tableRowH, 'Member Since', true); + drawTableCell(25 + tableColW * 3, y, tableColW, tableRowH, formatLongDate(r.joiningDate), false); + y += tableRowH; + + // Row 2 + drawTableCell(25, y, tableColW, tableRowH, 'Share Class', true); + drawTableCell(25 + tableColW, y, tableColW, tableRowH, classLabel, false); + drawTableCell(25 + tableColW * 2, y, tableColW, tableRowH, 'Date of Issue', true); + drawTableCell(25 + tableColW * 3, y, tableColW, tableRowH, formatLongDate(r.issueDate), false); + y += tableRowH; + + // Row 3 + drawTableCell(25, y, tableColW, tableRowH, 'Share Numbers', true); + drawTableCell( + 25 + tableColW, y, - `Given under the common seal of the bank this ${new Date().toLocaleDateString('en-IN', { - day: 'numeric', - month: 'long', - year: 'numeric' - })}.` + tableColW, + tableRowH, + `${r.shareNoFrom.toLocaleString('en-IN')} – ${r.shareNoTo.toLocaleString('en-IN')}`, + false ); + drawTableCell(25 + tableColW * 2, y, tableColW, tableRowH, 'Dividend Paid', true); + drawTableCell(25 + tableColW * 3, y, tableColW, tableRowH, `Rs. ${r.dividendYtd.toLocaleString('en-IN')}`, false); + y += tableRowH; - y = paintSignoff(c, y + 6, r.branch); - paintFooter(c, y + 4); - return c.doc; + // Row 4 (Highlight) + doc.setFillColor(darkBlue[0], darkBlue[1], darkBlue[2]); + doc.rect(25, y, tableColW * 2, tableRowH, 'F'); + doc.rect(25 + tableColW * 2, y, tableColW * 2, tableRowH, 'F'); + + doc.setFont('helvetica', 'bold'); + doc.setFontSize(8.5); + doc.setTextColor(255, 255, 255); + doc.text('TOTAL FACE VALUE', 28, y + tableRowH / 2 + 1.2); + + doc.setFont('times', 'bold'); + doc.setFontSize(11); + doc.text(`Rs. ${(r.shareCount * r.faceValue).toLocaleString('en-IN')}`, pageW - 28, y + tableRowH / 2 + 1.2, { + align: 'right' + }); + y += tableRowH + 8; + + // 9. Footer Text (Given under common seal...) + doc.setFont('times', 'italic'); + doc.setFontSize(10.5); + doc.setTextColor(textDark[0], textDark[1], textDark[2]); + + const sealDay = new Date().getDate(); + const sealMonthYear = new Date().toLocaleDateString('en-IN', { month: 'long', year: 'numeric' }); + const sealPhrase = `Given under the common seal of the bank this ${sealDay} day of ${sealMonthYear}.`; + doc.text(sealPhrase, pageW / 2, y, { align: 'center' }); + + y += 8; + + // 10. Seal Circle (left) and Signatory block (right) + const sealX = 50; + const sealY = y + 14; + doc.setDrawColor(gold[0], gold[1], gold[2]); // Gold + doc.setLineWidth(0.6); + doc.circle(sealX, sealY, 14, 'D'); + + doc.setLineWidth(0.3); + doc.setLineDashPattern( + [ + 1, + 1 + ], + 0 + ); + doc.circle(sealX, sealY, 11, 'D'); + doc.setLineDashPattern([], 0); + + doc.setFont('helvetica', 'bold'); + doc.setFontSize(7.5); + doc.setTextColor(gold[0], gold[1], gold[2]); + doc.text('COMMON', sealX, sealY - 1, { align: 'center' }); + doc.text('SEAL', sealX, sealY + 3.5, { align: 'center' }); + + // Signatory Line + doc.setDrawColor(textMuted[0], textMuted[1], textMuted[2]); + doc.setLineWidth(0.3); + doc.line(pageW - 75, y + 20, pageW - 25, y + 20); + + doc.setFont('helvetica', 'bold'); + doc.setFontSize(9.5); + doc.setTextColor(textDark[0], textDark[1], textDark[2]); + doc.text('Authorised Signatory', pageW - 50, y + 25, { align: 'center' }); + + return doc; } export function downloadShareCertificatePdf(r: ShareCertificateRecord, dsc?: BankUserDsc): void { diff --git a/src/app/letter-mgmt/certificates/certificates.shared.scss b/src/app/letter-mgmt/certificates/certificates.shared.scss index c93a0bdc7..ed8bea4d2 100644 --- a/src/app/letter-mgmt/certificates/certificates.shared.scss +++ b/src/app/letter-mgmt/certificates/certificates.shared.scss @@ -89,6 +89,43 @@ line-height: 1.5; } +.cert-head-r { + margin-left: auto; + flex-shrink: 0; +} + +.cert-cta-btn { + height: 38px; + padding: 0 16px; + border: 1px solid var(--cert-blue-d); + border-radius: 8px; + background: var(--cert-blue-d); + color: #fff; + font-size: 12.5px; + font-weight: 700; + cursor: pointer; + display: inline-flex; + align-items: center; + gap: 8px; + white-space: nowrap; + + &:hover { + background: var(--cert-blue); + border-color: var(--cert-blue); + } + + &--muted { + background: #fff; + color: var(--cert-g700); + border-color: var(--cert-g200); + + &:hover { + background: var(--cert-g50); + border-color: var(--cert-g300); + } + } +} + /* ───────── KPI strip ───────── */ .cert-kpis { display: grid; diff --git a/src/app/letter-mgmt/certificates/share-certificate/real-share-accounts-view/real-share-accounts-view.component.html b/src/app/letter-mgmt/certificates/share-certificate/real-share-accounts-view/real-share-accounts-view.component.html new file mode 100644 index 000000000..c05fc904b --- /dev/null +++ b/src/app/letter-mgmt/certificates/share-certificate/real-share-accounts-view/real-share-accounts-view.component.html @@ -0,0 +1,174 @@ + + +
+ @if (loading) { +
+ + Loading real share accounts… +
+ } @else if (loadError) { +
+ + Could not load share accounts from the server. + +
+ } @else if (rows.length === 0) { +
+ + No share accounts found in m_share_account yet. +
+ } @else { + +
+
+ +
+
{{ filteredRows.length | number }}
+
Certificates
+
+
+
+ +
+
{{ totalShares() | number }}
+
Shares Held
+
+
+
+ +
+
₹{{ totalShareValue() | number: '1.0-0' }}
+
Total Face Value
+
+
+
+ + +
+
+ +
+ + +
+
+ +
+ + +
+ +
+ + +
+ + @if (search || classFilter || branchFilter) { + + } +
+ + +
+
+ + + + + + + + + + + + + + + + + @for (row of filteredRows; track row.record.certificateNo) { + + + + + + + + + + + + + } + @if (filteredRows.length === 0) { + + + + } + +
Member · FolioCertificate No.ClassBranchSharesFace ValueTotalShare Nos.IssuedActions
+
{{ row.record.memberName }}
+
+ {{ row.record.memberId }} · Folio {{ row.record.folioNo }} · PAN + {{ row.record.memberPan }} +
+
{{ row.record.certificateNo }} + + {{ classLabel(row.record.shareClass) }} + + {{ row.record.branch || '—' }} + {{ row.record.shareCount | number }} + ₹{{ row.record.faceValue | number: '1.0-0' }} + ₹{{ row.record.shareCount * row.record.faceValue | number: '1.0-0' }} + + {{ row.record.shareNoFrom | number }}–{{ row.record.shareNoTo | number }} + {{ row.record.issueDate | date: 'd MMM y' }} +
+ + +
+
+ + No share accounts match the current filters. +
+
+
+ } +
diff --git a/src/app/letter-mgmt/certificates/share-certificate/real-share-accounts-view/real-share-accounts-view.component.scss b/src/app/letter-mgmt/certificates/share-certificate/real-share-accounts-view/real-share-accounts-view.component.scss new file mode 100644 index 000000000..e76ed1e58 --- /dev/null +++ b/src/app/letter-mgmt/certificates/share-certificate/real-share-accounts-view/real-share-accounts-view.component.scss @@ -0,0 +1,25 @@ +/** + * 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/. + */ + +.rsad-state { + display: flex; + align-items: center; + justify-content: center; + gap: 10px; + padding: 48px 20px; + color: var(--cert-g500); + font-size: 13.5px; + + &--error { + color: var(--cert-red); + } +} + +.rsad-state-ic { + font-size: 16px; +} diff --git a/src/app/letter-mgmt/certificates/share-certificate/real-share-accounts-view/real-share-accounts-view.component.ts b/src/app/letter-mgmt/certificates/share-certificate/real-share-accounts-view/real-share-accounts-view.component.ts new file mode 100644 index 000000000..655d42238 --- /dev/null +++ b/src/app/letter-mgmt/certificates/share-certificate/real-share-accounts-view/real-share-accounts-view.component.ts @@ -0,0 +1,312 @@ +/** + * 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 } from '@angular/common'; +import { FormsModule } from '@angular/forms'; +import { FaIconComponent } from '@fortawesome/angular-fontawesome'; +import { MatDialog } from '@angular/material/dialog'; +import { MatTooltip } from '@angular/material/tooltip'; +import { forkJoin, of, Observable } from 'rxjs'; +import { catchError, map, shareReplay, switchMap } from 'rxjs/operators'; + +import { SharesService } from '../../../../shares/shares.service'; +import { ClientsService } from '../../../../clients/clients.service'; +import { ShareCertificateDialogComponent } from '../../../../shares/shares-account-view/share-certificate-dialog/share-certificate-dialog.component'; +import { downloadShareCertificatePdf } from '../../certificate-pdf'; +import { DscSignService } from '../../../dsc-sign-dialog/dsc-sign.service'; +import type { ShareClass, ShareCertificateRecord } from '../share-certificate.component'; + +/** A real share account, kept alongside the raw API payload needed to open the real certificate dialog. */ +export interface RealShareAccountRow { + record: ShareCertificateRecord; + sharesAccountData: any; + clientId: string; +} + +@Component({ + selector: 'mifosx-real-share-accounts-view', + templateUrl: './real-share-accounts-view.component.html', + styleUrls: [ + '../../certificates.shared.scss', + './real-share-accounts-view.component.scss' + ], + imports: [ + CommonModule, + FormsModule, + FaIconComponent, + MatTooltip + ] +}) +export class RealShareAccountsViewComponent implements OnInit { + private dialog = inject(MatDialog); + private sharesService = inject(SharesService); + private clientsService = inject(ClientsService); + private dscSign = inject(DscSignService); + + loading = true; + loadError = false; + rows: RealShareAccountRow[] = []; + + // ── Filters (mirrors the demo list above) ── + search = ''; + classFilter: '' | ShareClass = ''; + branchFilter = ''; + + readonly classOptions: { value: ShareClass; label: string }[] = [ + { value: 'ORDINARY', label: 'Ordinary' }, + { value: 'PREFERENCE', label: 'Preference' } + ]; + + // Per-client caches so accounts sharing the same client (a common case — one + // member can hold several share accounts) only fetch that client once. + private clientDataCache = new Map>(); + private clientIdentifiersCache = new Map>(); + + ngOnInit(): void { + this.loadRealAccounts(); + } + + private loadRealAccounts(): void { + this.loading = true; + this.loadError = false; + this.clientDataCache.clear(); + this.clientIdentifiersCache.clear(); + this.sharesService + .getAllShareAccounts(0, 200) + .pipe( + map((page: any) => page?.pageItems || page?.content || (Array.isArray(page) ? page : [])), + switchMap((items: any[]) => { + if (!items.length) { + return of([] as RealShareAccountRow[]); + } + return forkJoin(items.map((item) => this.buildRow(item))).pipe( + map((rows) => rows.filter((r): r is RealShareAccountRow => !!r)) + ); + }), + catchError(() => { + this.loadError = true; + return of([] as RealShareAccountRow[]); + }) + ) + .subscribe((rows) => { + this.rows = rows; + this.loading = false; + }); + } + + /** + * The share-account list already carries clientId/clientName (same mapper as the + * single-account read), so the account detail call and the client calls can fire + * together instead of waiting on each other — halving the round trips on the + * critical path. + */ + private buildRow(listItem: any): Observable { + const clientId = listItem?.clientId != null ? String(listItem.clientId) : null; + return forkJoin({ + sa: this.sharesService.getSharesAccountData(String(listItem.id), false), + client: clientId ? this.getClientDataCached(clientId) : of(null), + identifiers: clientId ? this.getClientIdentifiersCached(clientId) : of([]) + }).pipe( + switchMap(({ sa, client, identifiers }) => { + // Defensive fallback if the list payload didn't carry clientId for some reason. + if (clientId || !sa?.clientId) { + return of({ sa, client, identifiers }); + } + const fallbackClientId = String(sa.clientId); + return forkJoin({ + sa: of(sa), + client: this.getClientDataCached(fallbackClientId), + identifiers: this.getClientIdentifiersCached(fallbackClientId) + }); + }), + map(({ sa, client, identifiers }: any) => ({ + record: this.mapToRecord(sa, client, identifiers), + sharesAccountData: sa, + clientId: String(sa.clientId) + })), + catchError(() => of(null)) + ); + } + + private getClientDataCached(clientId: string): Observable { + if (!this.clientDataCache.has(clientId)) { + this.clientDataCache.set( + clientId, + this.clientsService.getClientData(clientId).pipe( + catchError(() => of(null)), + shareReplay(1) + ) + ); + } + return this.clientDataCache.get(clientId)!; + } + + private getClientIdentifiersCached(clientId: string): Observable { + if (!this.clientIdentifiersCache.has(clientId)) { + this.clientIdentifiersCache.set( + clientId, + this.clientsService.getClientIdentifiers(clientId).pipe( + catchError(() => of([])), + shareReplay(1) + ) + ); + } + return this.clientIdentifiersCache.get(clientId)!; + } + + private mapToRecord(sa: any, client: any, identifiers: any): ShareCertificateRecord { + const approvedShares = sa.summary?.totalApprovedShares || 0; + const faceValue = sa.currentMarketPrice || 0; + const issueDate = this.toIsoDate(sa.timeline?.activatedDate) || this.toIsoDate(sa.timeline?.approvedDate) || ''; + const joiningDate = + this.toIsoDate(client?.activationDate) || this.toIsoDate(client?.timeline?.submittedOnDate) || issueDate; + + const startNum = sa.id * 5000 + 201; + const endNum = startNum + approvedShares - 1; + + const idList = Array.isArray(identifiers) ? identifiers : identifiers?.pageItems || []; + const panId = idList.find((i: any) => /pan/i.test(i?.documentType?.name || '')); + const memberPan = panId ? panId.documentKey : '—'; + + const dividendYtd = (sa.dividends || []).reduce((sum: number, d: any) => sum + (d.amount || 0), 0); + + return { + certificateNo: `SC-2026-${sa.accountNo || String(sa.id).padStart(6, '0')}`, + folioNo: `F-${client?.accountNo || String(client?.id ?? sa.clientId).padStart(5, '0')}`, + memberId: client?.accountNo || '', + memberName: sa.clientName || client?.displayName || '', + memberPan, + // Address isn't shown in this table — fetched on demand when a PDF is downloaded. + memberAddress: '', + branch: client?.officeName || '', + joiningDate, + shareClass: /prefer/i.test(sa.productName || '') ? 'PREFERENCE' : 'ORDINARY', + shareCount: approvedShares, + faceValue, + issueDate, + shareNoFrom: startNum, + shareNoTo: approvedShares > 0 ? endNum : startNum, + dividendYtd + }; + } + + private toIsoDate(value: any): string { + if (!value) { + return ''; + } + if (Array.isArray(value)) { + const [ + y, + m, + d + ] = value; + if (!y) { + return ''; + } + return `${y}-${String(m || 1).padStart(2, '0')}-${String(d || 1).padStart(2, '0')}`; + } + return String(value); + } + + private formatAddress(addr: any): string { + if (!addr) { + return ''; + } + return [ + addr.addressLine1, + addr.addressLine2, + addr.townVillage, + addr.city, + addr.stateName, + addr.countryName, + addr.postalCode + ] + .filter((p) => !!p && String(p).trim()) + .join(', '); + } + + // ── Derived list (identical shape to the demo table above) ── + get filteredRows(): RealShareAccountRow[] { + const q = this.search.trim().toLowerCase(); + const branch = this.branchFilter.trim().toLowerCase(); + return this.rows.filter(({ record: r }) => { + if (this.classFilter && r.shareClass !== this.classFilter) return false; + if (branch && !r.branch.toLowerCase().includes(branch)) return false; + if (!q) return true; + return ( + r.certificateNo.toLowerCase().includes(q) || + r.folioNo.toLowerCase().includes(q) || + r.memberId.toLowerCase().includes(q) || + r.memberName.toLowerCase().includes(q) || + r.memberPan.toLowerCase().includes(q) + ); + }); + } + + get branchOptions(): string[] { + return Array.from(new Set(this.rows.map((row) => row.record.branch).filter(Boolean))).sort(); + } + + classLabel(c: ShareClass): string { + return this.classOptions.find((o) => o.value === c)?.label || c; + } + + totalShares(): number { + return this.filteredRows.reduce((s, { record: r }) => s + r.shareCount, 0); + } + + totalShareValue(): number { + return this.filteredRows.reduce((s, { record: r }) => s + r.shareCount * r.faceValue, 0); + } + + clearFilters(): void { + this.search = ''; + this.classFilter = ''; + this.branchFilter = ''; + } + + retry(): void { + this.loadRealAccounts(); + } + + /** Opens the same certificate dialog used from Client → Share Account → Share Certificate. */ + viewCertificate(row: RealShareAccountRow): void { + this.dialog.open(ShareCertificateDialogComponent, { + data: { + sharesAccountData: row.sharesAccountData, + clientId: row.clientId + }, + width: '850px', + maxWidth: '95vw' + }); + } + + /** Downloads the certificate as a PDF. Address is fetched here (only place it's needed). */ + downloadCertificate(row: RealShareAccountRow): void { + this.clientsService + .getClientAddressData(row.clientId) + .pipe(catchError(() => of([]))) + .subscribe((addresses: any) => { + const addrList = Array.isArray(addresses) ? addresses : []; + const permanent = addrList.find((a: any) => /permanent/i.test(a?.addressType || '')) || addrList[0]; + const r: ShareCertificateRecord = { + ...row.record, + memberAddress: permanent ? this.formatAddress(permanent) : row.record.memberAddress + }; + this.dscSign.promptThenDownload( + { + title: 'Share Certificate', + reference: `${r.certificateNo} · Folio ${r.folioNo} · ${r.memberName}`, + filename: `Share-Certificate-${r.certificateNo}.pdf` + }, + (dsc) => downloadShareCertificatePdf(r, dsc) + ); + }); + } +} diff --git a/src/app/letter-mgmt/certificates/share-certificate/share-certificate.component.html b/src/app/letter-mgmt/certificates/share-certificate/share-certificate.component.html index a5e1818ae..b2c7a8f71 100644 --- a/src/app/letter-mgmt/certificates/share-certificate/share-certificate.component.html +++ b/src/app/letter-mgmt/certificates/share-certificate/share-certificate.component.html @@ -14,151 +14,184 @@

Share Certificate

- Issue, preview and download member share certificates with folio + share-number range. + @if (viewMode === 'demo') { + Issue, preview and download member share certificates with folio + share-number range. + } @else { + Live holder details and certificates, sourced from m_share_account. + }
+
+ @if (viewMode === 'demo') { + + } @else { + + } +
+ @if (viewMode === 'real') { + + } + -
-
- -
-
{{ filteredRecords.length | number }}
-
Certificates
+ @if (viewMode === 'demo') { +
+
+ +
+
{{ filteredRecords.length | number }}
+
Certificates
+
-
-
- -
-
{{ totalShares() | number }}
-
Shares Held
+
+ +
+
{{ totalShares() | number }}
+
Shares Held
+
-
-
- -
-
₹{{ totalShareValue() | number: '1.0-0' }}
-
Total Face Value
-
-
-
- - -
-
- -
- - +
+ +
+
₹{{ totalShareValue() | number: '1.0-0' }}
+
Total Face Value
+
-
- - -
+ +
+
+ +
+ + +
+
-
- - -
- - @if (search || classFilter || branchFilter) { - - } -
- - -
-
- - - - - - - - - - - - - - - - - @for (r of filteredRecords; track r.certificateNo) { - - - - - - - - - - - - +
+ +
- - + + + +
+ +
-
Member · FolioCertificate No.ClassBranchSharesFace ValueTotalShare Nos.IssuedActions
-
{{ r.memberName }}
-
- {{ r.memberId }} · Folio {{ r.folioNo }} · PAN {{ r.memberPan }} -
-
{{ r.certificateNo }} - - {{ classLabel(r.shareClass) }} - - {{ r.branch }} - {{ r.shareCount | number }} - ₹{{ r.faceValue | number: '1.0-0' }} - ₹{{ r.shareCount * r.faceValue | number: '1.0-0' }} - {{ r.shareNoFrom }}–{{ r.shareNoTo }}{{ r.issueDate | date: 'd MMM y' }} -
- - -
-
- - No share certificates match the current filters. -
+ +
+ + @if (search || classFilter || branchFilter) { + + }
-
+ + +
+
+ + + + + + + + + + + + + + + + + @for (r of filteredRecords; track r.certificateNo) { + + + + + + + + + + + + + } + @if (filteredRecords.length === 0) { + + + + } + +
Member · FolioCertificate No.ClassBranchSharesFace ValueTotalShare Nos.IssuedActions
+
{{ r.memberName }}
+
+ {{ r.memberId }} · Folio {{ r.folioNo }} · PAN {{ r.memberPan }} +
+
{{ r.certificateNo }} + + {{ classLabel(r.shareClass) }} + + {{ r.branch }} + {{ r.shareCount | number }} + ₹{{ r.faceValue | number: '1.0-0' }} + ₹{{ r.shareCount * r.faceValue | number: '1.0-0' }} + {{ r.shareNoFrom }}–{{ r.shareNoTo }}{{ r.issueDate | date: 'd MMM y' }} +
+ + +
+
+ + No share certificates match the current filters. +
+
+
+ }
diff --git a/src/app/letter-mgmt/certificates/share-certificate/share-certificate.component.ts b/src/app/letter-mgmt/certificates/share-certificate/share-certificate.component.ts index 7dc0e3dd4..47e65b44a 100644 --- a/src/app/letter-mgmt/certificates/share-certificate/share-certificate.component.ts +++ b/src/app/letter-mgmt/certificates/share-certificate/share-certificate.component.ts @@ -21,6 +21,7 @@ import { } from '../certificate-preview-dialog/certificate-preview-dialog.component'; import { downloadShareCertificatePdf } from '../certificate-pdf'; import { DscSignService } from '../../dsc-sign-dialog/dsc-sign.service'; +import { RealShareAccountsViewComponent } from './real-share-accounts-view/real-share-accounts-view.component'; export type ShareClass = 'ORDINARY' | 'PREFERENCE'; @@ -53,7 +54,8 @@ export interface ShareCertificateRecord { CommonModule, FormsModule, FaIconComponent, - MatTooltip + MatTooltip, + RealShareAccountsViewComponent ] }) export class ShareCertificateComponent implements OnInit { @@ -61,6 +63,9 @@ export class ShareCertificateComponent implements OnInit { private snack = inject(MatSnackBar); private dscSign = inject(DscSignService); + /** Toggles between the static sample table and the live m_share_account view. */ + viewMode: 'demo' | 'real' = 'demo'; + // ── Filters ── search = ''; classFilter: '' | ShareClass = ''; @@ -143,6 +148,11 @@ export class ShareCertificateComponent implements OnInit { (dsc) => downloadShareCertificatePdf(r, dsc) ); } + + /** Switches between the static sample table and the live m_share_account view. */ + toggleViewMode(): void { + this.viewMode = this.viewMode === 'demo' ? 'real' : 'demo'; + } } // ───────────────────── Mock seed (top-level export) ───────────────────── diff --git a/src/app/savings/savings-account-view/savings-account-view.component.html b/src/app/savings/savings-account-view/savings-account-view.component.html index c29a4f77e..6134bb6c5 100644 --- a/src/app/savings/savings-account-view/savings-account-view.component.html +++ b/src/app/savings/savings-account-view/savings-account-view.component.html @@ -7,6 +7,9 @@ -->
+ + +
diff --git a/src/app/savings/savings-account-view/savings-account-view.component.ts b/src/app/savings/savings-account-view/savings-account-view.component.ts index 59501a2bd..2d0b9f17b 100644 --- a/src/app/savings/savings-account-view/savings-account-view.component.ts +++ b/src/app/savings/savings-account-view/savings-account-view.component.ts @@ -27,6 +27,7 @@ import { TranslateService } from '@ngx-translate/core'; import { AccountsService } from 'app/accounts/accounts.service'; import { SettingsService } from 'app/settings/settings.service'; import { Dates } from 'app/core/utils/dates'; +import { AuthenticationService } from '../../core/authentication/authentication.service'; /** Environment Configuration */ import { environment } from '../../../environments/environment'; @@ -41,6 +42,7 @@ import { FaIconComponent } from '@fortawesome/angular-fontawesome'; import { MatTabNav, MatTabLink, MatTabNavPanel } from '@angular/material/tabs'; import { StatusLookupPipe } from '../../pipes/status-lookup.pipe'; import { STANDALONE_SHARED_IMPORTS } from 'app/standalone-shared.module'; +import { DormancyAlertPanelComponent } from 'app/shared/dormancy-alert-panel/dormancy-alert-panel.component'; /** * Savings Account View Component @@ -70,7 +72,8 @@ import { STANDALONE_SHARED_IMPORTS } from 'app/standalone-shared.module'; MatTabNavPanel, RouterOutlet, CurrencyPipe, - StatusLookupPipe + StatusLookupPipe, + DormancyAlertPanelComponent ] }) export class SavingsAccountViewComponent implements OnInit { @@ -81,6 +84,7 @@ export class SavingsAccountViewComponent implements OnInit { private accountsService = inject(AccountsService); private settingsService = inject(SettingsService); private dateUtils = inject(Dates); + private authenticationService = inject(AuthenticationService); dialog = inject(MatDialog); /** Savings Account Data */ @@ -268,6 +272,14 @@ export class SavingsAccountViewComponent implements OnInit { }); } } + if (!this.authenticationService.hasCashierRole()) { + this.buttonConfig.buttonsArray = this.buttonConfig.buttonsArray.filter( + (btn) => btn.name !== 'Deposit' && btn.name !== 'Withdrawal' + ); + this.buttonConfig.optionArray = this.buttonConfig.optionArray.filter( + (opt) => opt.name !== 'Deposit' && opt.name !== 'Withdrawal' + ); + } } /** diff --git a/src/app/shared/dormancy-alert-panel/dormancy-alert-panel.component.html b/src/app/shared/dormancy-alert-panel/dormancy-alert-panel.component.html index 5aba5ae30..c85e9182c 100644 --- a/src/app/shared/dormancy-alert-panel/dormancy-alert-panel.component.html +++ b/src/app/shared/dormancy-alert-panel/dormancy-alert-panel.component.html @@ -195,6 +195,9 @@ @if (statusId >= 100) { Threshold Reached Account is inactive + } @else if (isOverdue(daysToInactive)) { + Overdue {{ -daysToInactive! }}d + {{ inactiveSubtext() }} } @else if (daysToInactive !== null) { {{ daysToInactive }}days @@ -217,6 +220,9 @@ @if (statusId >= 200) { Threshold Reached Account is dormant + } @else if (isOverdue(daysToDormancy)) { + Overdue {{ -daysToDormancy! }}d + {{ dormancySubtext() }} } @else if (daysToDormancy !== null) { {{ daysToDormancy }}days @@ -239,6 +245,9 @@ @if (statusId >= 300) { Threshold Reached Pending regulatory transfer + } @else if (isOverdue(daysToEscheat)) { + Overdue {{ -daysToEscheat! }}d + {{ escheatSubtext() }} } @else if (daysToEscheat !== null) { {{ daysToEscheat }}days @@ -295,7 +304,9 @@ {{ step.label }} - @if (!step.reached && step.days !== null) { + @if (!step.reached && isOverdue(step.days)) { + Overdue {{ -step.days! }}d + } @else if (!step.reached && step.days !== null) { {{ step.days }}d } @else if (step.reached) { Reached diff --git a/src/app/shared/dormancy-alert-panel/dormancy-alert-panel.component.ts b/src/app/shared/dormancy-alert-panel/dormancy-alert-panel.component.ts index 2797494ad..4a0e7f944 100644 --- a/src/app/shared/dormancy-alert-panel/dormancy-alert-panel.component.ts +++ b/src/app/shared/dormancy-alert-panel/dormancy-alert-panel.component.ts @@ -113,12 +113,10 @@ export class DormancyAlertPanelComponent implements OnInit { } this.computeRiskFromAccount(); - this.shouldShow = - this.isAlertEnabled || - this.isMonitoringActive || - this.statusId > 0 || - (this.daysToInactive !== null && this.daysToInactive <= 90) || - !!this.lastTxnDate; + // Only surface this panel once the account has actually left "Active" — + // i.e. subStatus is Inactive/Dormant/Escheat. A healthy active account + // (statusId === 0) should never show a danger-colored dormancy alert. + this.shouldShow = this.statusId > 0; } private computeRiskFromAccount(): void { @@ -179,17 +177,36 @@ export class DormancyAlertPanelComponent implements OnInit { ]; } - /** e.g. "Inactive after 30 days of no transaction" */ + /** true when the threshold date has already passed but the batch job hasn't reclassified the account yet */ + isOverdue(days: number | null): boolean { + return days !== null && days < 0; + } + + /** e.g. "11d" when still counting down, "Overdue 5d" when the threshold date already passed */ + daysLabel(days: number | null): string { + if (days === null) return ''; + return this.isOverdue(days) ? `Overdue ${Math.abs(days)}d` : `${days}d`; + } + + /** e.g. "Moves Active → Inactive after 30 days" */ inactiveSubtext(): string { - return this.daysToInactive !== null ? `No transaction for ${this.daysToInactive} days` : 'Threshold not configured'; + if (this.daysToInactive === null) return 'Threshold not configured'; + return this.isOverdue(this.daysToInactive) + ? `Active → Inactive was due ${Math.abs(this.daysToInactive)} days ago` + : `Moves Active → Inactive after ${this.daysToInactive} days`; } dormancySubtext(): string { - const total = (this.daysToInactive ?? 0) + (this.daysToDormancy ?? 0); - return this.daysToDormancy !== null ? `${this.daysToDormancy} days after inactive` : 'Threshold not configured'; + if (this.daysToDormancy === null) return 'Threshold not configured'; + return this.isOverdue(this.daysToDormancy) + ? `Inactive → Dormant was due ${Math.abs(this.daysToDormancy)} days ago` + : `Moves Inactive → Dormant after ${this.daysToDormancy} days`; } escheatSubtext(): string { - return this.daysToEscheat !== null ? `${this.daysToEscheat} days after dormant` : 'Threshold not configured'; + if (this.daysToEscheat === null) return 'Threshold not configured'; + return this.isOverdue(this.daysToEscheat) + ? `Dormant → Escheat was due ${Math.abs(this.daysToEscheat)} days ago` + : `Moves Dormant → Escheat after ${this.daysToEscheat} days`; } } diff --git a/src/app/shares/edit-share-account-details/edit-share-account-details.component.html b/src/app/shares/edit-share-account-details/edit-share-account-details.component.html new file mode 100644 index 000000000..336dac96e --- /dev/null +++ b/src/app/shares/edit-share-account-details/edit-share-account-details.component.html @@ -0,0 +1,93 @@ + + +
+ @if (hasPendingEdit) { +
+ + An edit submitted by {{ pendingEdit.makerUsername }} is already pending approval. You cannot submit another edit + until it is resolved. +
+ } + + + + + + + + + + + + + + + + + + + + + + + + {{ 'labels.inputs.DETAILS' | translate }} + + + + + + + {{ 'labels.inputs.TERMS' | translate }} + + + + + + + {{ 'labels.inputs.CHARGES' | translate }} + + + + + + @if (sharesAccountFormValidAndNotPristine) { + + {{ 'labels.inputs.PREVIEW' | translate }} + + + + } + +
diff --git a/src/app/shares/edit-share-account-details/edit-share-account-details.component.scss b/src/app/shares/edit-share-account-details/edit-share-account-details.component.scss new file mode 100644 index 000000000..e69de29bb diff --git a/src/app/shares/edit-share-account-details/edit-share-account-details.component.ts b/src/app/shares/edit-share-account-details/edit-share-account-details.component.ts new file mode 100644 index 000000000..0168971d7 --- /dev/null +++ b/src/app/shares/edit-share-account-details/edit-share-account-details.component.ts @@ -0,0 +1,170 @@ +/** + * 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/. + */ + +/** Angular Imports */ +import { Component, ViewChild, inject } from '@angular/core'; +import { Router, ActivatedRoute } from '@angular/router'; +import { MatStepper, MatStepperIcon, MatStep, MatStepLabel } from '@angular/material/stepper'; +import { FaIconComponent } from '@fortawesome/angular-fontawesome'; +import { STANDALONE_SHARED_IMPORTS } from 'app/standalone-shared.module'; + +/** Custom Components */ +import { SharesAccountDetailsStepComponent } from '../shares-account-stepper/shares-account-details-step/shares-account-details-step.component'; +import { SharesAccountTermsStepComponent } from '../shares-account-stepper/shares-account-terms-step/shares-account-terms-step.component'; +import { SharesAccountChargesStepComponent } from '../shares-account-stepper/shares-account-charges-step/shares-account-charges-step.component'; +import { SharesAccountPreviewStepComponent } from '../shares-account-stepper/shares-account-preview-step/shares-account-preview-step.component'; + +/** Custom Services */ +import { SharesService } from '../shares.service'; +import { SettingsService } from 'app/settings/settings.service'; + +/** + * Edit Share Account Details — maker-checker submission screen. + * + * Reuses the same Details/Terms/Charges/Preview stepper as Create/Modify Application, but with + * Product, External ID, Resolution Number, Total Number of Shares, Application Date and Charges + * locked (`approvalEditMode`) - only Default Savings Account, Allow Dividends for Inactive Clients, + * Minimum Active Period and Lockin Period are actually editable and submitted. Nothing is applied + * immediately: the request is staged and a different user (checker) must approve it from the + * "Pending Share Account Edits" screen before it takes effect. + */ +@Component({ + selector: 'mifosx-edit-share-account-details', + templateUrl: './edit-share-account-details.component.html', + styleUrls: ['./edit-share-account-details.component.scss'], + imports: [ + ...STANDALONE_SHARED_IMPORTS, + MatStepper, + MatStepperIcon, + FaIconComponent, + MatStep, + MatStepLabel, + SharesAccountDetailsStepComponent, + SharesAccountTermsStepComponent, + SharesAccountChargesStepComponent, + SharesAccountPreviewStepComponent + ] +}) +export class EditShareAccountDetailsComponent { + private route = inject(ActivatedRoute); + private router = inject(Router); + private sharesService = inject(SharesService); + private settingsService = inject(SettingsService); + + /** Shares Account and Template */ + sharesAccountAndTemplate: any; + /** Shares Account Product Template */ + sharesAccountProductTemplate: any; + + /** Pending edit already submitted for this account, if any. */ + pendingEdit: any = null; + pendingEditLoading = true; + submitting = false; + + @ViewChild(SharesAccountDetailsStepComponent, { static: true }) + sharesAccountDetailsStep: SharesAccountDetailsStepComponent; + @ViewChild(SharesAccountTermsStepComponent, { static: true }) sharesAccountTermsStep: SharesAccountTermsStepComponent; + @ViewChild(SharesAccountChargesStepComponent, { static: true }) + sharesAccountChargesStep: SharesAccountChargesStepComponent; + + constructor() { + this.route.data.subscribe((data: { sharesAccountAndTemplate: any }) => { + this.sharesAccountAndTemplate = data.sharesAccountAndTemplate; + this.loadPendingEdit(); + }); + } + + private loadPendingEdit(): void { + this.pendingEditLoading = true; + this.sharesService.getPendingShareAccountEdit(this.sharesAccountAndTemplate.id).subscribe({ + next: (edit: any) => { + this.pendingEdit = edit && edit.status === 'PENDING' ? edit : null; + this.pendingEditLoading = false; + }, + error: () => { + this.pendingEdit = null; + this.pendingEditLoading = false; + } + }); + } + + get hasPendingEdit(): boolean { + return !!this.pendingEdit; + } + + setTemplate($event: any) { + this.sharesAccountProductTemplate = $event; + } + + get sharesAccountDetailsForm() { + return this.sharesAccountDetailsStep.sharesAccountDetailsForm; + } + + get sharesAccountTermsForm() { + return this.sharesAccountTermsStep.sharesAccountTermsForm; + } + + get sharesAccountFormValidAndNotPristine() { + return ( + this.sharesAccountTermsForm.valid && + !this.sharesAccountTermsForm.pristine && + !this.hasPendingEdit && + !this.pendingEditLoading + ); + } + + get sharesAccount() { + return { + ...this.sharesAccountDetailsStep.sharesAccountDetails, + ...this.sharesAccountTermsStep.sharesAccountTerms, + ...this.sharesAccountChargesStep.sharesAccountCharges + }; + } + + /** + * Submits only the 6 fields this flow supports for approval - Product, External ID, Resolution + * Number, Total Number of Shares, Application Date and Charges are locked in the UI and are never + * part of this payload. + */ + submit() { + if (!this.sharesAccountFormValidAndNotPristine || this.submitting) { + return; + } + const locale = this.settingsService.language.code; + const terms = this.sharesAccountTermsForm.value; + const payload: any = { + locale, + dateFormat: 'yyyy-MM-dd', + savingsAccountId: terms.savingsAccountId, + allowDividendCalculationForInactiveClients: terms.allowDividendCalculationForInactiveClients + }; + if (terms.minimumActivePeriod) { + payload.minimumActivePeriod = terms.minimumActivePeriod; + } + if (terms.minimumActivePeriodFrequencyType) { + payload.minimumActivePeriodFrequencyType = terms.minimumActivePeriodFrequencyType; + } + if (terms.lockinPeriodFrequency) { + payload.lockinPeriodFrequency = terms.lockinPeriodFrequency; + } + if (terms.lockinPeriodFrequencyType) { + payload.lockinPeriodFrequencyType = terms.lockinPeriodFrequencyType; + } + + this.submitting = true; + this.sharesService.submitShareAccountEdit(this.sharesAccountAndTemplate.id, payload).subscribe({ + next: () => { + this.submitting = false; + this.router.navigate(['../'], { relativeTo: this.route }); + }, + error: () => { + this.submitting = false; + } + }); + } +} diff --git a/src/app/shares/shares-account-stepper/shares-account-charges-step/shares-account-charges-step.component.html b/src/app/shares/shares-account-stepper/shares-account-charges-step/shares-account-charges-step.component.html index 45947224a..19cc92659 100644 --- a/src/app/shares/shares-account-stepper/shares-account-charges-step/shares-account-charges-step.component.html +++ b/src/app/shares/shares-account-stepper/shares-account-charges-step/shares-account-charges-step.component.html @@ -7,23 +7,25 @@ -->
- - {{ 'labels.inputs.Charge' | translate }} - - @for (charge of chargeData | chargesFilter: chargesDataSource : currencyCode.value; track charge) { - - {{ charge.name }} - - } - - + @if (!approvalEditMode) { + + {{ 'labels.inputs.Charge' | translate }} + + @for (charge of chargeData | chargesFilter: chargesDataSource : currencyCode.value; track charge) { + + {{ charge.name }} + + } + + -
- -
+
+ +
+ } {{ 'labels.inputs.Amount' | translate }} @@ -65,9 +69,11 @@ diff --git a/src/app/shares/shares-account-stepper/shares-account-charges-step/shares-account-charges-step.component.ts b/src/app/shares/shares-account-stepper/shares-account-charges-step/shares-account-charges-step.component.ts index d6a3f7292..cd39dc79a 100644 --- a/src/app/shares/shares-account-stepper/shares-account-charges-step/shares-account-charges-step.component.ts +++ b/src/app/shares/shares-account-stepper/shares-account-charges-step/shares-account-charges-step.component.ts @@ -73,6 +73,9 @@ export class SharesAccountChargesStepComponent implements OnInit, OnChanges { @Input() sharesAccountTemplate: any; /** Currency Code */ @Input() currencyCode: UntypedFormControl; + /** When true (Share Account "Edit Details" maker-checker flow), charges are locked - they aren't + * part of that flow's editable field set. */ + @Input() approvalEditMode = false; /** Charge Data */ chargeData: any = []; diff --git a/src/app/shares/shares-account-stepper/shares-account-details-step/shares-account-details-step.component.html b/src/app/shares/shares-account-stepper/shares-account-details-step/shares-account-details-step.component.html index 79baf1efa..b0d1b0416 100644 --- a/src/app/shares/shares-account-stepper/shares-account-details-step/shares-account-details-step.component.html +++ b/src/app/shares/shares-account-stepper/shares-account-details-step/shares-account-details-step.component.html @@ -45,6 +45,15 @@ {{ 'labels.inputs.External ID' | translate }} + + + {{ 'labels.inputs.Resolution Number' | translate }} + + + {{ 'labels.inputs.Resolution Number' | translate }} {{ 'labels.commons.is' | translate }} + {{ 'labels.commons.required' | translate }} + +
@@ -56,7 +65,11 @@ mat-raised-button matStepperNext [disabled]=" - !(sharesAccountDetailsForm.get('productId')?.value && sharesAccountDetailsForm.get('submittedDate')?.value) + !( + sharesAccountDetailsForm.get('productId')?.value && + sharesAccountDetailsForm.get('submittedDate')?.value && + sharesAccountDetailsForm.get('resolutionNumber')?.value + ) " > {{ 'labels.buttons.Next' | translate }} diff --git a/src/app/shares/shares-account-stepper/shares-account-details-step/shares-account-details-step.component.ts b/src/app/shares/shares-account-stepper/shares-account-details-step/shares-account-details-step.component.ts index 8586cbb48..154f5cd7a 100644 --- a/src/app/shares/shares-account-stepper/shares-account-details-step/shares-account-details-step.component.ts +++ b/src/app/shares/shares-account-stepper/shares-account-details-step/shares-account-details-step.component.ts @@ -38,6 +38,9 @@ export class SharesAccountDetailsStepComponent implements OnInit { /** Shares Account Template */ @Input() sharesAccountTemplate: any; + /** When true (Share Account "Edit Details" maker-checker flow), Product/Submitted On/External ID/ + * Resolution Number are locked - none of them are part of that flow's editable field set. */ + @Input() approvalEditMode = false; /** Minimum date allowed. */ minDate = new Date(2000, 0, 1); @@ -72,10 +75,14 @@ export class SharesAccountDetailsStepComponent implements OnInit { submittedDate: this.sharesAccountTemplate.timeline.submittedOnDate && new Date(this.sharesAccountTemplate.timeline.submittedOnDate), - externalId: this.sharesAccountTemplate.externalId + externalId: this.sharesAccountTemplate.externalId, + resolutionNumber: this.sharesAccountTemplate.resolutionNumber }); } } + if (this.approvalEditMode) { + this.sharesAccountDetailsForm.disable(); + } } /** @@ -91,7 +98,11 @@ export class SharesAccountDetailsStepComponent implements OnInit { '', Validators.required ], - externalId: [''] + externalId: [''], + resolutionNumber: [ + '', + Validators.required + ] }); } diff --git a/src/app/shares/shares-account-stepper/shares-account-preview-step/shares-account-preview-step.component.html b/src/app/shares/shares-account-stepper/shares-account-preview-step/shares-account-preview-step.component.html index 3a9c88a84..e00b156cd 100644 --- a/src/app/shares/shares-account-stepper/shares-account-preview-step/shares-account-preview-step.component.html +++ b/src/app/shares/shares-account-stepper/shares-account-preview-step/shares-account-preview-step.component.html @@ -36,6 +36,11 @@ }
+
+ {{ 'labels.inputs.Resolution Number' | translate }} + {{ sharesAccount.resolutionNumber }} +
+

{{ 'labels.heading.Terms' | translate }}

diff --git a/src/app/shares/shares-account-stepper/shares-account-terms-step/shares-account-terms-step.component.ts b/src/app/shares/shares-account-stepper/shares-account-terms-step/shares-account-terms-step.component.ts index 2d2cc9c71..74853b52c 100644 --- a/src/app/shares/shares-account-stepper/shares-account-terms-step/shares-account-terms-step.component.ts +++ b/src/app/shares/shares-account-stepper/shares-account-terms-step/shares-account-terms-step.component.ts @@ -41,6 +41,10 @@ export class SharesAccountTermsStepComponent implements OnChanges, OnInit { @Input() sharesAccountProductTemplate: any; /** [Optional] Shares Account Template */ @Input() sharesAccountTemplate: any; + /** When true (Share Account "Edit Details" maker-checker flow), only Default Savings Account, Allow + * Dividends, Minimum Active Period and Lockin Period stay editable - Total Number of Shares and + * Application Date are transactional/historical and aren't part of that flow's editable field set. */ + @Input() approvalEditMode = false; /** Minimum date allowed. */ minDate = new Date(2000, 0, 1); @@ -105,6 +109,10 @@ export class SharesAccountTermsStepComponent implements OnChanges, OnInit { this.sharesAccountTemplate.allowDividendCalculationForInactiveClients }); } + if (this.approvalEditMode) { + this.sharesAccountTermsForm.get('requestedShares').disable(); + this.sharesAccountTermsForm.get('applicationDate').disable(); + } } /** diff --git a/src/app/shares/shares-account-view/general-tab/general-tab.component.html b/src/app/shares/shares-account-view/general-tab/general-tab.component.html index c1f83c0c4..beced4602 100644 --- a/src/app/shares/shares-account-view/general-tab/general-tab.component.html +++ b/src/app/shares/shares-account-view/general-tab/general-tab.component.html @@ -7,6 +7,13 @@ -->
+ @if (pendingEdit) { +
+ + A details edit submitted by {{ pendingEdit.makerUsername }} is awaiting checker approval. You cannot submit + another edit until it is resolved. +
+ }
{{ charge.amount || charge.amountOrPercentage }} - + @if (!approvalEditMode) { + + } {{ 'labels.inputs.Actions' | translate }} - + @if (!approvalEditMode) { + + }
+ + + + + + + + + + + + + + + + + + + + + + + + +
Branch{{ clientData?.officeName || 'Mumbai Main Branch' }}Member Since + {{ + (clientData?.activationDate + ? (clientData.activationDate | dateFormat) + : (clientData?.timeline?.submittedOnDate | dateFormat)) || 'N/A' + }} +
Share Class{{ sharesAccountData.productName || 'Ordinary' }}Date of Issue{{ issueDate | dateFormat }}
Share Numbers{{ shareNumbersRange }}Dividend Paid (Cumulative) + {{ cumulativeDividend | currency: sharesAccountData.currency.code : 'symbol-narrow' }} +
TOTAL FACE VALUE + {{ totalFaceValue | currency: sharesAccountData.currency.code : 'symbol-narrow' }} +
+ + +
+
+
+
+ + + + + diff --git a/src/app/shares/shares-account-view/share-certificate-dialog/share-certificate-dialog.component.scss b/src/app/shares/shares-account-view/share-certificate-dialog/share-certificate-dialog.component.scss new file mode 100644 index 000000000..ddfd4101c --- /dev/null +++ b/src/app/shares/shares-account-view/share-certificate-dialog/share-certificate-dialog.component.scss @@ -0,0 +1,351 @@ +/** + * 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/. + */ + +.dialog-header { + display: flex; + align-items: center; + border-bottom: 1px solid #e0e0e0; + padding-bottom: 12px; + margin-bottom: 16px; + font-weight: 500; + color: #1a252c; +} + +.dialog-content-container { + max-height: 80vh; + overflow-y: auto; + padding: 10px; +} + +/* Certificate Outer Container */ +.certificate-container { + background-color: #faf8f5; + padding: 20px; + font-family: Georgia, 'Times New Roman', Times, serif; + color: #2c2520; + box-shadow: 0 4px 20px rgb(0 0 0 / 8%); + border-radius: 8px; + max-width: 800px; + margin: 0 auto; +} + +/* Classic Ornate Border System */ +.certificate-border-outer { + border: 5px double #c5a059; /* Gold double border */ + padding: 8px; + background-color: #fdfcf7; /* Textured light cream */ + border-radius: 4px; + position: relative; +} + +.certificate-border-inner { + border: 1px solid #dcd1bd; + padding: 30px; + border-radius: 2px; + position: relative; + overflow: hidden; +} + +/* Watermark */ +.certificate-watermark { + position: absolute; + top: 50%; + left: 50%; + transform: translate(-50%, -50%); + opacity: 0.03; + pointer-events: none; + z-index: 1; + + .watermark-icon { + font-size: 280px; + width: 280px; + height: 280px; + color: #c5a059; + } +} + +.certificate-header, +.cert-meta-row, +.cert-body-section, +.certificate-details-table, +.cert-footer { + position: relative; + z-index: 2; /* Ensure content is above watermark */ +} + +/* Header styling */ +.certificate-header { + text-align: center; + margin-bottom: 30px; + border-bottom: 2px solid #f2edd5; + padding-bottom: 20px; + + .bank-name { + font-size: 26px; + font-weight: bold; + color: #0b3c5d; /* Dark Premium Blue */ + text-transform: uppercase; + letter-spacing: 1.5px; + margin-bottom: 4px; + } + + .bank-sub { + font-size: 13px; + color: #7b6f63; + font-style: italic; + letter-spacing: 1px; + margin-bottom: 20px; + } + + .cert-title { + font-size: 28px; + font-weight: 400; + color: #c5a059; /* Classic Gold */ + letter-spacing: 4px; + margin-bottom: 5px; + font-family: Georgia, serif; + } + + .cert-subtitle { + font-size: 11px; + color: #7b6f63; + text-transform: uppercase; + letter-spacing: 2px; + } +} + +/* Meta Data Grid (Top of cert) */ +.cert-meta-row { + display: flex; + justify-content: space-between; + margin-bottom: 30px; + border-bottom: 1px solid #f2edd5; + padding-bottom: 15px; + + .meta-item { + display: flex; + flex-direction: column; + + .meta-label { + font-size: 11px; + text-transform: uppercase; + color: #928374; + letter-spacing: 1px; + margin-bottom: 3px; + } + + .meta-value { + font-size: 15px; + font-weight: bold; + color: #322f2b; + } + } +} + +/* Main Body text */ +.cert-body-section { + margin-bottom: 35px; + + .cert-main-text { + font-size: 15px; + line-height: 1.8; + text-align: center; + color: #4a453f; + margin-bottom: 25px; + + .highlight-text { + color: #1a202c; + font-weight: 600; + border-bottom: 1px dotted #c5a059; + padding-bottom: 1px; + } + } +} + +/* Issued To Details Block */ +.issued-to-details { + border: 1px solid #f0e6d2; + background-color: #fbfaf5; + padding: 15px 20px; + border-radius: 4px; + margin: 20px auto; + max-width: 90%; + text-align: left; + + .issued-to-title { + font-size: 10px; + font-weight: bold; + color: #c5a059; + letter-spacing: 1.5px; + margin-bottom: 6px; + } + + .client-name-details { + font-size: 18px; + font-weight: bold; + color: #0b3c5d; + margin-bottom: 4px; + } + + .client-ids { + font-size: 12px; + color: #666; + margin-bottom: 6px; + } + + .client-address { + font-size: 12px; + color: #7b6f63; + line-height: 1.4; + } +} + +/* Certificate Data Table */ +.certificate-details-table { + width: 100%; + border-collapse: collapse; + margin-bottom: 40px; + + td { + padding: 10px 12px; + border: 1px solid #ede8db; + font-size: 13px; + } + + .lbl-col { + background-color: #f7f5ee; + font-weight: bold; + color: #7b6f63; + width: 25%; + } + + .val-col { + color: #322f2b; + width: 25%; + } + + .highlight-row { + background-color: #f5f2e9; + border-top: 2px solid #c5a059; + } + + .lbl-col-highlight { + font-weight: bold; + color: #0b3c5d; + font-size: 14px; + letter-spacing: 1px; + text-align: right; + padding-right: 20px; + } + + .val-col-highlight { + font-size: 16px; + font-weight: bold; + color: #b8860b; /* Dark Goldenrod */ + } +} + +/* Certificate Footer Seal / Signatures */ +.cert-footer { + display: flex; + justify-content: space-between; + align-items: center; + margin-top: 20px; + + .seal-container { + width: 25%; + + .seal-ring { + width: 90px; + height: 90px; + border: 2px dashed #b8860b; + border-radius: 50%; + display: flex; + align-items: center; + justify-content: center; + margin: 0 auto; + transform: rotate(-15deg); + + .seal-text { + font-size: 10px; + font-weight: bold; + color: #b8860b; + text-align: center; + line-height: 1.2; + letter-spacing: 1px; + } + } + } + + .closing-text { + width: 45%; + font-size: 12px; + color: #7b6f63; + font-style: italic; + line-height: 1.5; + text-align: center; + } + + .signatures-container { + width: 30%; + text-align: center; + + .signature-block { + display: inline-block; + width: 80%; + + .signature-line { + border-bottom: 1px solid #7b6f63; + margin-bottom: 8px; + height: 40px; /* Space for signature */ + } + + .signatory-title { + font-size: 12px; + color: #7b6f63; + font-weight: bold; + text-transform: uppercase; + letter-spacing: 0.5px; + } + } + } +} + +/* Print CSS */ +@media print { + /* Hide non-overlay contents during print */ + body > :not(.cdk-overlay-container) { + display: none !important; + } + + /* Make certificate fill page */ + #share-certificate-print { + position: absolute; + left: 0; + top: 0; + width: 100% !important; + height: 100% !important; + padding: 0 !important; + margin: 0 !important; + background-color: #fff !important; + box-shadow: none !important; + } + + .certificate-border-outer { + border-width: 8px !important; + background-color: #fff !important; + } + + .no-print { + display: none !important; + } + + .mat-mdc-dialog-container { + padding: 0 !important; + } +} diff --git a/src/app/shares/shares-account-view/share-certificate-dialog/share-certificate-dialog.component.ts b/src/app/shares/shares-account-view/share-certificate-dialog/share-certificate-dialog.component.ts new file mode 100644 index 000000000..1f5ea5703 --- /dev/null +++ b/src/app/shares/shares-account-view/share-certificate-dialog/share-certificate-dialog.component.ts @@ -0,0 +1,145 @@ +/** + * 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 { + MAT_DIALOG_DATA, + MatDialogRef, + MatDialogTitle, + MatDialogContent, + MatDialogActions, + MatDialogClose +} from '@angular/material/dialog'; +import { ClientsService } from 'app/clients/clients.service'; +import { STANDALONE_SHARED_IMPORTS } from 'app/standalone-shared.module'; +import { DateFormatPipe } from '../../../pipes/date-format.pipe'; +import { FormatNumberPipe } from '../../../pipes/format-number.pipe'; +import { CurrencyPipe } from '@angular/common'; +import { MatButton } from '@angular/material/button'; +import { MatIcon } from '@angular/material/icon'; + +@Component({ + selector: 'mifosx-share-certificate-dialog', + templateUrl: './share-certificate-dialog.component.html', + styleUrls: ['./share-certificate-dialog.component.scss'], + imports: [ + ...STANDALONE_SHARED_IMPORTS, + MatDialogTitle, + MatDialogContent, + MatDialogActions, + MatDialogClose, + DateFormatPipe, + FormatNumberPipe, + CurrencyPipe, + MatButton, + MatIcon + ] +}) +export class ShareCertificateDialogComponent implements OnInit { + dialogRef = inject>(MatDialogRef); + data = inject(MAT_DIALOG_DATA); + clientsService = inject(ClientsService); + + sharesAccountData: any; + clientId: string; + + clientData: any; + clientAddress: string = ''; + panNumber: string = 'Not Available'; + memberId: string = ''; + + certificateNo: string = ''; + folioNo: string = ''; + shareNumbersRange: string = ''; + cumulativeDividend: number = 0; + totalFaceValue: number = 0; + issueDate: Date = new Date(); + todayDate: Date = new Date(); + + ngOnInit() { + this.sharesAccountData = this.data.sharesAccountData; + this.clientId = String(this.data.clientId); + + // Initialize shares certificate values + this.issueDate = this.sharesAccountData.timeline?.activatedDate + ? new Date(this.sharesAccountData.timeline.activatedDate) + : new Date(); + + this.certificateNo = `SC-2026-${this.sharesAccountData.accountNo || String(this.sharesAccountData.id).padStart(6, '0')}`; + + // Calculate total face value + const approvedShares = this.sharesAccountData.summary?.totalApprovedShares || 0; + const price = this.sharesAccountData.currentMarketPrice || 0; + this.totalFaceValue = approvedShares * price; + + // Calculate share numbers range (seeded dynamically from account ID) + const startNum = this.sharesAccountData.id * 5000 + 201; + const endNum = startNum + approvedShares - 1; + this.shareNumbersRange = approvedShares > 0 ? `${startNum.toLocaleString()} – ${endNum.toLocaleString()}` : 'None'; + + // Calculate cumulative dividends + if (this.sharesAccountData.dividends) { + this.cumulativeDividend = this.sharesAccountData.dividends.reduce( + (sum: number, d: any) => sum + (d.amount || 0), + 0 + ); + } + + // Fetch client details + this.clientsService.getClientData(this.clientId).subscribe({ + next: (client: any) => { + this.clientData = client; + this.memberId = client.accountNo || ''; + this.folioNo = `F-${client.accountNo || String(client.id).padStart(5, '0')}`; + }, + error: () => {} + }); + + // Fetch address details + this.clientsService.getClientAddressData(this.clientId).subscribe({ + next: (addresses: any[]) => { + if (addresses && addresses.length > 0) { + const permanent = addresses.find((a) => /permanent/i.test(a?.addressType || '')) || addresses[0]; + this.clientAddress = this.formatAddress(permanent); + } + }, + error: () => {} + }); + + // Fetch identifiers (for PAN number) + this.clientsService.getClientIdentifiers(this.clientId).subscribe({ + next: (identifiers: any[]) => { + const idList = Array.isArray(identifiers) ? identifiers : (identifiers as any)?.pageItems || []; + const panId = idList.find((id: any) => /pan/i.test(id?.documentType?.name || '')); + if (panId) { + this.panNumber = panId.documentKey; + } + }, + error: () => {} + }); + } + + formatAddress(addr: any): string { + if (!addr) return ''; + return [ + addr.addressLine1, + addr.addressLine2, + addr.townVillage, + addr.city, + addr.stateName, + addr.countryName, + addr.postalCode + ] + .filter((p) => !!p && String(p).trim()) + .join(', '); + } + + printCertificate() { + window.print(); + } +} diff --git a/src/app/shares/shares-account-view/shares-account-view.component.html b/src/app/shares/shares-account-view/shares-account-view.component.html index f33b345c9..9761aeeef 100644 --- a/src/app/shares/shares-account-view/shares-account-view.component.html +++ b/src/app/shares/shares-account-view/shares-account-view.component.html @@ -71,7 +71,7 @@
-
+
@if (buttonConfig.singleButtons.length > 0) { diff --git a/src/app/shares/shares-account-view/shares-account-view.component.ts b/src/app/shares/shares-account-view/shares-account-view.component.ts index 28cfc1f28..b28ad3c42 100644 --- a/src/app/shares/shares-account-view/shares-account-view.component.ts +++ b/src/app/shares/shares-account-view/shares-account-view.component.ts @@ -13,6 +13,7 @@ import { MatDialog } from '@angular/material/dialog'; /** Custom Dialogs */ import { DeleteDialogComponent } from 'app/shared/delete-dialog/delete-dialog.component'; +import { ShareCertificateDialogComponent } from './share-certificate-dialog/share-certificate-dialog.component'; /** Custom Services */ import { SharesService } from '../shares.service'; @@ -98,6 +99,9 @@ export class SharesAccountViewComponent implements OnInit { }); } + /** Whether this account has a details edit awaiting checker approval. */ + hasPendingEdit = false; + ngOnInit() { this.setConditionalButtons(); if (this.router.url.includes('clients')) { @@ -107,6 +111,17 @@ export class SharesAccountViewComponent implements OnInit { } else if (this.router.url.includes('centers')) { this.entityType = 'Center'; } + this.sharesService.getPendingShareAccountEdit(this.sharesAccountData.id).subscribe({ + next: (edit: any) => { + this.hasPendingEdit = !!edit && edit.status === 'PENDING'; + if (this.hasPendingEdit) { + this.buttonConfig.removeOption('Edit Details'); + } + }, + error: () => { + this.hasPendingEdit = false; + } + }); } /** @@ -164,6 +179,12 @@ export class SharesAccountViewComponent implements OnInit { case 'Modify Application': this.router.navigate(['edit'], { relativeTo: this.route }); break; + case 'Edit Details': + this.router.navigate(['edit-details'], { relativeTo: this.route }); + break; + case 'Share Certificate': + this.viewShareCertificate(); + break; case 'Delete': this.deleteSharesAccount(); break; @@ -185,4 +206,15 @@ export class SharesAccountViewComponent implements OnInit { } }); } + + private viewShareCertificate() { + this.dialog.open(ShareCertificateDialogComponent, { + data: { + sharesAccountData: this.sharesAccountData, + clientId: this.sharesAccountData.clientId + }, + width: '850px', + maxWidth: '95vw' + }); + } } diff --git a/src/app/shares/shares-account-view/shares-buttons.config.ts b/src/app/shares/shares-account-view/shares-buttons.config.ts index d4b6f33df..9dec6666a 100644 --- a/src/app/shares/shares-account-view/shares-buttons.config.ts +++ b/src/app/shares/shares-account-view/shares-buttons.config.ts @@ -95,6 +95,14 @@ export class SharesButtonsConfiguration { switch (status) { case 'Active': this.optionArray = [ + { + name: 'Edit Details', + taskPermissionName: 'SUBMITEDIT_SHAREACCOUNT' + }, + { + name: 'Share Certificate', + taskPermissionName: 'READ_SHAREACCOUNTPURCHASE' + }, { name: 'Close', taskPermissionName: 'CLOSE_SHAREACCOUNT' @@ -128,4 +136,12 @@ export class SharesButtonsConfiguration { const index = buttonNames.indexOf(name); this.buttonsArray.splice(index, 1); } + + removeOption(name: string) { + const optionNames = this.optionArray.map((entry) => entry.name); + const index = optionNames.indexOf(name); + if (index > -1) { + this.optionArray.splice(index, 1); + } + } } diff --git a/src/app/shares/shares-account-view/transactions-tab/transactions-tab.component.ts b/src/app/shares/shares-account-view/transactions-tab/transactions-tab.component.ts index 2780ed344..c28640324 100644 --- a/src/app/shares/shares-account-view/transactions-tab/transactions-tab.component.ts +++ b/src/app/shares/shares-account-view/transactions-tab/transactions-tab.component.ts @@ -73,7 +73,9 @@ export class TransactionsTabComponent implements OnInit { constructor() { this.route.parent.data.subscribe((data: { sharesAccountData: any }) => { this.shareAccountData = data.sharesAccountData; - this.transactionsData = this.shareAccountData.purchasedShares; + this.transactionsData = this.shareAccountData.purchasedShares.filter( + (transaction: any) => transaction.type.value !== 'Charge Payment' + ); }); } diff --git a/src/app/shares/shares-routing.module.ts b/src/app/shares/shares-routing.module.ts index 4a9fc4072..b062886da 100644 --- a/src/app/shares/shares-routing.module.ts +++ b/src/app/shares/shares-routing.module.ts @@ -18,6 +18,7 @@ import { DividendsTabComponent } from './shares-account-view/dividends-tab/divid import { CreateSharesAccountComponent } from './create-shares-account/create-shares-account.component'; import { EditSharesAccountComponent } from './edit-shares-account/edit-shares-account.component'; import { SharesAccountActionsComponent } from './shares-account-actions/shares-account-actions.component'; +import { EditShareAccountDetailsComponent } from './edit-share-account-details/edit-share-account-details.component'; /** Custom Resolvers */ import { SharesAccountViewResolver } from './common-resolvers/share-account-view.resolver'; @@ -89,6 +90,14 @@ const routes: Routes = [ sharesAccountAndTemplate: SharesAccountAndTemplateResolver } }, + { + path: 'edit-details', + data: { title: 'Edit Share Account Details', breadcrumb: 'Edit Details', routeParamBreadcrumb: false }, + component: EditShareAccountDetailsComponent, + resolve: { + sharesAccountAndTemplate: SharesAccountAndTemplateResolver + } + }, { path: 'actions/:name', data: { title: 'Shares Account Actions', breadcrumb: 'Actions', routeParamBreadcrumb: 'name' }, diff --git a/src/app/shares/shares.service.ts b/src/app/shares/shares.service.ts index 6d1945428..1dc19b090 100644 --- a/src/app/shares/shares.service.ts +++ b/src/app/shares/shares.service.ts @@ -32,6 +32,16 @@ export class SharesService { return this.http.get(`/accounts/share/${accountId}`, { params: httpParams }); } + /** + * @param {number} offset Paging offset. + * @param {number} limit Paging size limit. + * @returns {Observable} A page of all (active) share accounts, backed by m_share_account. + */ + getAllShareAccounts(offset: number = 0, limit: number = 200): Observable { + const httpParams = new HttpParams().set('offset', offset.toString()).set('limit', limit.toString()); + return this.http.get('/accounts/share', { params: httpParams }); + } + /** * @param {string} clientId Client Id assosciated with shares account. * @param {string} productId Product Id @@ -78,4 +88,45 @@ export class SharesService { const httpParams = new HttpParams().set('command', command); return this.http.post(`/accounts/share/${accountId}`, data, { params: httpParams }); } + + /** + * @param {string} accountId Shares Account Id. + * @param {any} data Details edit payload. + * @returns {Observable} + */ + submitShareAccountEdit(accountId: string, data: any): Observable { + return this.http.post(`/shareaccounts/${accountId}/edit`, data); + } + + /** + * @param {string} accountId Shares Account Id. + * @returns {Observable} The pending details edit for this account, if any. + */ + getPendingShareAccountEdit(accountId: string): Observable { + return this.http.get(`/shareaccounts/${accountId}/edit/pending`); + } + + /** + * @returns {Observable} All Share Account details edits pending approval. + */ + getAllPendingShareAccountEdits(): Observable { + return this.http.get(`/shareaccounts/edit/pending`); + } + + /** + * @param {string} accountId Shares Account Id. + * @returns {Observable} + */ + approveShareAccountEdit(accountId: string): Observable { + return this.http.post(`/shareaccounts/${accountId}/edit/approve`, {}); + } + + /** + * @param {string} accountId Shares Account Id. + * @param {string} rejectReason Optional reason for rejection. + * @returns {Observable} + */ + rejectShareAccountEdit(accountId: string, rejectReason?: string): Observable { + return this.http.post(`/shareaccounts/${accountId}/edit/reject`, { rejectReason: rejectReason || '' }); + } } diff --git a/src/app/tasks/checker-inbox-and-tasks-tabs/pending-share-account-edits/pending-share-account-edits.component.html b/src/app/tasks/checker-inbox-and-tasks-tabs/pending-share-account-edits/pending-share-account-edits.component.html new file mode 100644 index 000000000..226cb7e37 --- /dev/null +++ b/src/app/tasks/checker-inbox-and-tasks-tabs/pending-share-account-edits/pending-share-account-edits.component.html @@ -0,0 +1,108 @@ + + +
+ @if (showData) { +
+ + search + + +
+ +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
Account No.{{ edit.accountNo }}Client{{ edit.clientName }}Submitted By{{ edit.makerUsername }}Submitted On{{ formatDate(edit.createdOn) }}Status + {{ edit.status }} + Actions + + + +
+
+ + @if (selectedEdit) { +
+
+

Proposed Changes — Account "{{ selectedEdit.accountNo }}"

+ +
+ +
+

Details

+ + + + + + + + + @for (row of entries(selectedEdit); track row.key) { + + + + + } + +
FieldProposed Value
{{ row.key }}{{ row.value }}
+
+ + @if (selectedEdit.rejectReason) { +

Reject Reason: {{ selectedEdit.rejectReason }}

+ } +
+ } + } + + @if (!showData) { +
+ +

No pending account edits

+

There are no Share Account details edits awaiting approval at this time.

+
+ } +
diff --git a/src/app/tasks/checker-inbox-and-tasks-tabs/pending-share-account-edits/pending-share-account-edits.component.scss b/src/app/tasks/checker-inbox-and-tasks-tabs/pending-share-account-edits/pending-share-account-edits.component.scss new file mode 100644 index 000000000..2b605f603 --- /dev/null +++ b/src/app/tasks/checker-inbox-and-tasks-tabs/pending-share-account-edits/pending-share-account-edits.component.scss @@ -0,0 +1,190 @@ +/** + * 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/. + */ + +$border: #e2e8f0; +$text-muted: #64748b; + +.psae-tab-content { + padding: 16px 0; +} + +.psae-action-bar { + display: flex; + align-items: center; + gap: 8px; + margin-bottom: 16px; + flex-wrap: wrap; +} + +.psae-search-field { + flex: 1; + min-width: 200px; + max-width: 320px; +} + +.psae-btn { + font-size: 11.5px; + font-weight: 600; + border-radius: 8px !important; + height: 32px; + padding: 0 10px; + margin-right: 6px; + + fa-icon { + margin-right: 4px; + font-size: 11px; + } +} + +.psae-btn-view { + border-color: #2563eb; + color: #2563eb; + + &:hover { + background: #eff6ff; + } +} + +.psae-btn-approve { + border-color: #16a34a; + color: #16a34a; + + &:hover { + background: #f0fdf4; + } +} + +.psae-btn-reject { + border-color: #dc2626; + color: #dc2626; + + &:hover { + background: #fef2f2; + } +} + +.status-badge { + display: inline-flex; + align-items: center; + padding: 2px 10px; + border-radius: 20px; + font-size: 11px; + font-weight: 600; + + &--pending { + background: #fef9c3; + color: #854d0e; + } +} + +.psae-diff-panel { + margin-top: 20px; + border: 1px solid $border; + border-radius: 12px; + background: #fff; + padding: 16px; +} + +.psae-diff-header { + display: flex; + align-items: center; + justify-content: space-between; + + h3 { + margin: 0; + font-size: 14px; + font-weight: 700; + } +} + +.psae-diff-block { + margin-top: 16px; + + h4 { + margin: 0 0 6px; + font-size: 12.5px; + font-weight: 700; + color: #0d1b2a; + text-transform: uppercase; + letter-spacing: 0.03em; + } +} + +.psae-diff-loading { + color: $text-muted; + font-size: 13px; +} + +.psae-diff-table { + width: 100%; + border-collapse: collapse; + margin-top: 4px; + font-size: 12.5px; + + th, + td { + text-align: left; + padding: 8px 10px; + border-bottom: 1px solid $border; + } + + th { + color: $text-muted; + font-weight: 600; + } +} + +.psae-diff-label { + font-weight: 600; + color: #0d1b2a; +} + +.psae-diff-proposed { + color: #15803d; + font-weight: 600; +} + +.psae-reject-reason { + margin-top: 14px; + font-size: 12.5px; + color: #b91c1c; +} + +.empty-state { + display: flex; + flex-direction: column; + align-items: center; + justify-content: center; + padding: 60px 24px; + background: #fff; + border: 1px dashed $border; + border-radius: 14px; + text-align: center; + margin: 16px 0; + + .empty-icon { + font-size: 42px; + color: #cbd5e1; + margin-bottom: 14px; + } + + .empty-title { + margin: 0 0 4px; + font-size: 15px; + font-weight: 700; + color: #0d1b2a; + } + + .empty-text { + margin: 0; + font-size: 12.5px; + color: $text-muted; + max-width: 380px; + line-height: 1.5; + } +} diff --git a/src/app/tasks/checker-inbox-and-tasks-tabs/pending-share-account-edits/pending-share-account-edits.component.ts b/src/app/tasks/checker-inbox-and-tasks-tabs/pending-share-account-edits/pending-share-account-edits.component.ts new file mode 100644 index 000000000..730c62163 --- /dev/null +++ b/src/app/tasks/checker-inbox-and-tasks-tabs/pending-share-account-edits/pending-share-account-edits.component.ts @@ -0,0 +1,192 @@ +/** + * 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, inject } from '@angular/core'; +import { ActivatedRoute, Router } from '@angular/router'; +import { + MatTableDataSource, + MatTable, + MatColumnDef, + MatHeaderCellDef, + MatHeaderCell, + MatCellDef, + MatCell, + MatHeaderRowDef, + MatHeaderRow, + MatRowDef, + MatRow +} from '@angular/material/table'; +import { MatDialog } from '@angular/material/dialog'; +import { MatIcon } from '@angular/material/icon'; +import { MatIconButton } from '@angular/material/button'; +import { FaIconComponent } from '@fortawesome/angular-fontawesome'; + +import { SharesService } from 'app/shares/shares.service'; +import { ConfirmationDialogComponent } from 'app/shared/confirmation-dialog/confirmation-dialog.component'; +import { FormDialogComponent } from 'app/shared/form-dialog/form-dialog.component'; +import { FormfieldBase } from 'app/shared/form-dialog/formfield/model/formfield-base'; +import { InputBase } from 'app/shared/form-dialog/formfield/model/input-base'; +import { STANDALONE_SHARED_IMPORTS } from 'app/standalone-shared.module'; + +/** + * Checker-facing list of Share Account details edits awaiting approval. + * Mirrors PendingSavingsAccountEditsComponent's list + compare + approve/reject pattern. + */ +@Component({ + selector: 'mifosx-pending-share-account-edits', + templateUrl: './pending-share-account-edits.component.html', + styleUrls: ['./pending-share-account-edits.component.scss'], + imports: [ + ...STANDALONE_SHARED_IMPORTS, + FaIconComponent, + MatTable, + MatColumnDef, + MatHeaderCellDef, + MatHeaderCell, + MatCellDef, + MatCell, + MatHeaderRowDef, + MatHeaderRow, + MatRowDef, + MatRow, + MatIcon, + MatIconButton + ] +}) +export class PendingShareAccountEditsComponent { + private route = inject(ActivatedRoute); + private router = inject(Router); + private dialog = inject(MatDialog); + private sharesService = inject(SharesService); + + dataSource: MatTableDataSource; + showData = false; + + displayedColumns: string[] = [ + 'accountNo', + 'clientName', + 'makerUsername', + 'createdOn', + 'status', + 'actions' + ]; + + selectedEdit: any = null; + + constructor() { + this.route.data.subscribe((data: { pendingShareAccountEditsData: any }) => { + const items = data.pendingShareAccountEditsData || []; + this.showData = items.length > 0; + this.dataSource = new MatTableDataSource(items); + }); + } + + applyFilter(filterValue: string = '') { + this.dataSource.filter = filterValue.trim().toLowerCase(); + } + + formatDate(date: any): string { + if (!date) return ''; + const d = Array.isArray(date) + ? new Date(date[0], date[1] - 1, date[2], date[3] || 0, date[4] || 0) + : new Date(date); + return isNaN(d.getTime()) ? String(date) : d.toLocaleString('en-IN'); + } + + /** Readable list of "key: value" pairs for the proposed details changes. */ + entries(edit: any): { key: string; value: any }[] { + if (!edit) return []; + const fields: [string, any][] = [ + [ + 'Linked Savings Account', + edit.savingsAccountNo + ], + [ + 'Allow Dividends for Inactive Clients', + edit.allowDividendCalculationForInactiveClients + ], + [ + 'Lockin Period Frequency', + edit.lockinPeriodFrequency + ], + [ + 'Minimum Active Period Frequency', + edit.minimumActivePeriodFrequency + ] + ]; + return fields + .filter( + ([ + , + value + ]) => value !== null && value !== undefined + ) + .map( + ([ + key, + value + ]) => ({ key, value }) + ); + } + + viewCompare(edit: any): void { + this.selectedEdit = this.selectedEdit === edit ? null : edit; + } + + closeCompare(): void { + this.selectedEdit = null; + } + + approve(edit: any): void { + const dialogRef = this.dialog.open(ConfirmationDialogComponent, { + data: { + heading: 'Approve Share Account Edit', + dialogContext: `Apply the pending details edit for account "${edit.accountNo}"? This cannot be undone.`, + type: 'basic' + } + }); + dialogRef.afterClosed().subscribe((response: any) => { + if (response?.confirm) { + this.sharesService.approveShareAccountEdit(edit.shareAccountId).subscribe(() => this.reload()); + } + }); + } + + reject(edit: any): void { + const formfields: FormfieldBase[] = [ + new InputBase({ + controlName: 'rejectReason', + label: 'Reason for rejection', + value: '', + type: 'text', + required: false + }) + ]; + const dialogRef = this.dialog.open(FormDialogComponent, { + data: { + title: `Reject edit for account "${edit.accountNo}"`, + layout: { addButtonText: 'Reject' }, + formfields + } + }); + dialogRef.afterClosed().subscribe((response: any) => { + if (response?.data) { + const rejectReason = response.data.value.rejectReason || ''; + this.sharesService.rejectShareAccountEdit(edit.shareAccountId, rejectReason).subscribe(() => this.reload()); + } + }); + } + + reload(): void { + this.closeCompare(); + const url = this.router.url; + this.router + .navigateByUrl('/checker-inbox-and-tasks', { skipLocationChange: true }) + .then(() => this.router.navigate([url])); + } +} diff --git a/src/app/tasks/checker-inbox-and-tasks/checker-inbox-and-tasks.component.html b/src/app/tasks/checker-inbox-and-tasks/checker-inbox-and-tasks.component.html index f5c176f5b..9bc4863d7 100644 --- a/src/app/tasks/checker-inbox-and-tasks/checker-inbox-and-tasks.component.html +++ b/src/app/tasks/checker-inbox-and-tasks/checker-inbox-and-tasks.component.html @@ -118,6 +118,18 @@ Pending Account Edits + + + Pending Share Account Edits + { + private sharesService = inject(SharesService); + + resolve(): Observable { + return this.sharesService.getAllPendingShareAccountEdits(); + } +} diff --git a/src/app/tasks/tasks-routing.module.ts b/src/app/tasks/tasks-routing.module.ts index 2ea67ff26..0fb7bf752 100644 --- a/src/app/tasks/tasks-routing.module.ts +++ b/src/app/tasks/tasks-routing.module.ts @@ -29,6 +29,7 @@ import { TerminatedAccountComponent } from './checker-inbox-and-tasks-tabs/termi import { PendingSavingsProductsComponent } from './checker-inbox-and-tasks-tabs/pending-savings-products/pending-savings-products.component'; import { ChequebookRequestsComponent } from './checker-inbox-and-tasks-tabs/chequebook-requests/chequebook-requests.component'; import { PendingSavingsAccountEditsComponent } from './checker-inbox-and-tasks-tabs/pending-savings-account-edits/pending-savings-account-edits.component'; +import { PendingShareAccountEditsComponent } from './checker-inbox-and-tasks-tabs/pending-share-account-edits/pending-share-account-edits.component'; import { ViewCheckerInboxComponent } from './view-checker-inbox/view-checker-inbox.component'; /** Custom Resolvers */ @@ -46,6 +47,7 @@ import { GetTerminatedAccounts } from './common-resolvers/getTerminatedAccounts. import { GetPendingSavingsProducts } from './common-resolvers/getPendingSavingsProducts.resolver'; import { GetPendingSavingsAccountEdits } from './common-resolvers/getPendingSavingsAccountEdits.resolver'; import { GetPendingCurrentAccountEdits } from './common-resolvers/getPendingCurrentAccountEdits.resolver'; +import { GetPendingShareAccountEdits } from './common-resolvers/getPendingShareAccountEdits.resolver'; /** Tasks Routes */ const routes: Routes = [ @@ -142,6 +144,14 @@ const routes: Routes = [ path: 'pending-current-account-edits', redirectTo: 'pending-savings-account-edits' }, + { + path: 'pending-share-account-edits', + component: PendingShareAccountEditsComponent, + data: { title: 'Pending Share Account Edits' }, + resolve: { + pendingShareAccountEditsData: GetPendingShareAccountEdits + } + }, { path: 'withdrawal-requests', component: WithdrawalRequestsComponent, @@ -221,7 +231,8 @@ const routes: Routes = [ GetTerminatedAccounts, GetPendingSavingsProducts, GetPendingSavingsAccountEdits, - GetPendingCurrentAccountEdits + GetPendingCurrentAccountEdits, + GetPendingShareAccountEdits ] }) export class TasksRoutingModule {} diff --git a/src/assets/translations/en-US.json b/src/assets/translations/en-US.json index e1a61c5fa..e813856e8 100644 --- a/src/assets/translations/en-US.json +++ b/src/assets/translations/en-US.json @@ -2037,6 +2037,7 @@ "External ID lookup failed": "External ID lookup failed", "External ID lookup timed out": "External ID lookup timed out", "External ID not found": "External ID not found", + "Resolution Number": "Resolution Number", "External ID verified successfully": "External ID verified successfully", "External Id": "External Id", "External Reference ID": "External Reference ID", @@ -3102,6 +3103,7 @@ "Working with Code": "Working with Code" }, "menus": { + "Share Certificate": "Share Certificate", "Unassign Staff": "Unassign Staff", "Accounting": "Accounting", "Account Overview": "Account Overview", @@ -4493,6 +4495,7 @@ "A unique identifier for the share product": "A unique identifier for the share product", "Add future share value with date range": "Add future share value with date range", "Activate": "Activate", + "Editing details is disabled": "Editing details is disabled", "Add": "Add", "Add Filters": "Add Filters", "Analytics": "Analytics Dashboard (Apache Superset)", diff --git a/src/environments/.env.ts b/src/environments/.env.ts index 80abab109..11e3f647f 100644 --- a/src/environments/.env.ts +++ b/src/environments/.env.ts @@ -15,3 +15,4 @@ export default { 'allow_switching_backend_instance': true }; /* tslint:enable */ +