diff --git a/src/app/clients/client-stepper/client-general-step/client-general-step.component.html b/src/app/clients/client-stepper/client-general-step/client-general-step.component.html index 7f60c80af..59666449d 100644 --- a/src/app/clients/client-stepper/client-general-step/client-general-step.component.html +++ b/src/app/clients/client-stepper/client-general-step/client-general-step.component.html @@ -220,6 +220,39 @@ } +
+ @if (!sanctionsVerified && !sanctionsBlocked) { + + } + @if (sanctionsVerified) { +
+ + Client verified — No match found in sanctions list. +
+ } + @if (sanctionsBlocked) { +
+ + + SANCTIONS MATCH FOUND — Client cannot be onboarded. Account opening is blocked. + + +
+ } +
+
@@ -356,7 +389,7 @@ {{ 'labels.buttons.Previous' | translate }} - diff --git a/src/app/clients/client-stepper/client-general-step/client-general-step.component.scss b/src/app/clients/client-stepper/client-general-step/client-general-step.component.scss index 42a67d85c..1c5c9c623 100644 --- a/src/app/clients/client-stepper/client-general-step/client-general-step.component.scss +++ b/src/app/clients/client-stepper/client-general-step/client-general-step.component.scss @@ -172,6 +172,64 @@ mat-checkbox { } } +.sanctions-verify-section { + margin: 12px 0; + + .verify-client-btn { + height: 40px; + font-size: 13px; + white-space: nowrap; + } + + .sanctions-progress { + mat-progress-bar { + margin-bottom: 8px; + } + + .sanctions-msg { + font-size: 13px; + color: #555; + line-height: 1.5; + margin: 0; + } + } + + .sanctions-result { + display: flex; + align-items: center; + gap: 8px; + + .text-success { + color: #388e3c; + font-size: 14px; + font-weight: 600; + } + } +} + +.sanctions-blocked { + display: flex; + align-items: center; + gap: 8px; + background: #ffebee; + border: 1px solid #ef9a9a; + border-left: 4px solid #d32f2f; + border-radius: 4px; + padding: 10px 16px; + margin: 12px 0; + + .text-danger { + color: #d32f2f; + font-size: 13px; + } + + .recheck-btn { + margin-left: auto; + font-size: 12px; + white-space: nowrap; + } +} + .external-id-status { font-size: 12px; font-weight: 500; diff --git a/src/app/clients/client-stepper/client-general-step/client-general-step.component.ts b/src/app/clients/client-stepper/client-general-step/client-general-step.component.ts index 4295b9203..0128db64b 100644 --- a/src/app/clients/client-stepper/client-general-step/client-general-step.component.ts +++ b/src/app/clients/client-stepper/client-general-step/client-general-step.component.ts @@ -31,7 +31,10 @@ import { MatStepperPrevious, MatStepperNext } from '@angular/material/stepper'; import { FaIconComponent } from '@fortawesome/angular-fontawesome'; import { MatIcon } from '@angular/material/icon'; import { MatTooltip } from '@angular/material/tooltip'; +import { MatDialog } from '@angular/material/dialog'; import { STANDALONE_SHARED_IMPORTS } from 'app/standalone-shared.module'; +import { SanctionsVerifyDialogComponent } from './sanctions-verify-dialog.component'; +import { DipVerifyDialogComponent } from './dip-verify-dialog.component'; /** * Create Client Component @@ -59,6 +62,7 @@ export class ClientGeneralStepComponent implements OnInit, OnDestroy { private settingsService = inject(SettingsService); private clientService = inject(ClientsService); externalNationalIdService = inject(ExternalNationalIdService); + private dialog = inject(MatDialog); /** Subject to trigger unsubscription on destroy */ private destroy$ = new Subject(); @@ -98,6 +102,10 @@ export class ClientGeneralStepComponent implements OnInit, OnDestroy { savingProductOptions: any; /** Mobile number DIP verification status */ mobileVerified = false; + /** Sanctions verification state */ + sanctionsVerified = false; + /** Sanctions match was found — blocks account opening */ + sanctionsBlocked = false; /** * @param {FormBuilder} formBuilder Form Builder @@ -258,10 +266,57 @@ export class ClientGeneralStepComponent implements OnInit, OnDestroy { } /** - * Verify mobile number with DIP. + * Verify mobile number with DIP via dialog. */ verifyWithDIP() { - this.mobileVerified = true; + const mobileNo = this.createClientForm.get('mobileNo')?.value || ''; + const dialogRef = this.dialog.open(DipVerifyDialogComponent, { + width: '500px', + data: { mobileNo } + }); + dialogRef.afterClosed().subscribe((result: boolean) => { + if (result) { + this.mobileVerified = true; + } + }); + } + + /** + * Verify client name against sanctions lists via dialog. + * Passes the client name to the dialog for real API screening. + * If a match is found, sanctionsBlocked is set to true blocking further steps. + */ + verifySanctions() { + const firstname = this.createClientForm.get('firstname')?.value || ''; + const middlename = this.createClientForm.get('middlename')?.value || ''; + const lastname = this.createClientForm.get('lastname')?.value || ''; + const fullname = this.createClientForm.get('fullname')?.value || ''; + + const name = + fullname || + [ + firstname, + middlename, + lastname + ] + .filter(Boolean) + .join(' '); + + const dialogRef = this.dialog.open(SanctionsVerifyDialogComponent, { + width: '550px', + data: { name } + }); + dialogRef.afterClosed().subscribe((verified: boolean) => { + if (verified === true) { + // CLEAR — no match found + this.sanctionsVerified = true; + this.sanctionsBlocked = false; + } else if (verified === false) { + // FLAGGED — match found, block account opening + this.sanctionsVerified = false; + this.sanctionsBlocked = true; + } + }); } getDateLabel(legalFormId: number, values: string[]): string { diff --git a/src/app/clients/client-stepper/client-general-step/dip-verify-dialog.component.ts b/src/app/clients/client-stepper/client-general-step/dip-verify-dialog.component.ts new file mode 100644 index 000000000..ea4537d37 --- /dev/null +++ b/src/app/clients/client-stepper/client-general-step/dip-verify-dialog.component.ts @@ -0,0 +1,343 @@ +import { Component, OnInit, OnDestroy, inject } from '@angular/core'; +import { CommonModule } from '@angular/common'; +import { MAT_DIALOG_DATA, MatDialogRef, MatDialogModule } from '@angular/material/dialog'; +import { MatProgressBarModule } from '@angular/material/progress-bar'; +import { MatButtonModule } from '@angular/material/button'; +import { FaIconComponent } from '@fortawesome/angular-fontawesome'; +import { ClientsService } from 'app/clients/clients.service'; + +@Component({ + selector: 'mifosx-dip-verify-dialog', + template: ` +

+ + DIP Verification +

+ + @if (verifying) { + +

{{ progress }}%

+
+ +

+ Please wait while the system verifies the entered mobile number against the + Mobile Number Revocation List (MNRL) available on the + Digital Intelligence Platform (DIP) of the Department of Telecommunications (DoT), + Government of India. +

+
+
+ + This may take a few seconds. Do not close or refresh this window. +
+ } + @if (!verifying && serviceUnavailable) { +
+ +

{{ resultMessage }}

+
+ + Number treated as CLEAN by default. Proceed with caution. +
+
+ } + @if (!verifying && !suspected && !serviceUnavailable && !error) { +
+ +

Mobile number verified — Not found in MNRL revocation list.

+
+ } + @if (!verifying && suspected) { +
+ +

{{ resultMessage }}

+ @if (suspectedReason) { +
+

Status: {{ suspectedStatus }}

+

Reason: {{ suspectedReason }}

+

Reported by: {{ suspectedReportedBy }}

+
+ } +
+ } + @if (!verifying && error) { +
+ +

{{ error }}

+
+ } +
+ + @if (!verifying) { + + } + + `, + styles: [ + ` + :host { + display: block; + min-width: 450px; + } + + h2[mat-dialog-title] { + display: flex; + align-items: center; + gap: 8px; + margin: 0; + padding: 16px 24px; + font-size: 18px; + font-weight: 600; + color: #1074b9; + } + + .dialog-title-icon { + font-size: 20px; + } + + mat-dialog-content { + padding: 16px 24px; + } + + mat-progress-bar { + margin-bottom: 4px; + } + + .progress-percent { + text-align: center; + font-size: 22px; + font-weight: 700; + color: #1074b9; + margin: 4px 0 12px; + } + + .verify-info { + display: flex; + align-items: flex-start; + gap: 12px; + padding: 12px; + background: #e3f2fd; + border-radius: 8px; + margin-bottom: 12px; + margin-top: 12px; + } + + .info-icon { + font-size: 24px; + color: #1074b9; + margin-top: 2px; + flex-shrink: 0; + } + + .verify-msg { + font-size: 14px; + color: #333; + line-height: 1.6; + margin: 0; + } + + .warning-row { + display: flex; + align-items: center; + gap: 8px; + padding: 8px 12px; + background: #fff3e0; + border-radius: 6px; + } + + .warning-icon { + font-size: 16px; + color: #e65100; + flex-shrink: 0; + } + + .warning-text { + font-size: 13px; + color: #e65100; + font-weight: 500; + } + + .verify-result { + display: flex; + flex-direction: column; + align-items: center; + gap: 12px; + padding: 16px 0; + } + + .result-icon { + font-size: 48px; + color: #388e3c; + } + + .result-msg { + font-size: 16px; + font-weight: 600; + color: #388e3c; + margin: 0; + text-align: center; + } + + .suspected-icon { + color: #e65100; + } + + .suspected-msg { + color: #e65100; + } + + .suspected-detail { + background: #fff3e0; + border-radius: 8px; + padding: 12px 16px; + width: 100%; + text-align: left; + } + + .suspected-detail p { + margin: 4px 0; + font-size: 14px; + font-weight: 400; + color: #333; + } + + .unavailable-icon { + color: #1565c0; + } + + .unavailable-msg { + color: #1565c0; + } + + .unavailable-detail { + display: flex; + align-items: center; + gap: 8px; + background: #e3f2fd; + border-radius: 8px; + padding: 10px 16px; + width: 100%; + font-size: 13px; + color: #333; + } + + .unavailable-detail-icon { + font-size: 16px; + color: #1565c0; + flex-shrink: 0; + } + + .error-icon { + color: #d32f2f; + } + + .error-msg { + color: #d32f2f; + } + + mat-dialog-actions { + padding: 8px 24px 16px; + } + ` + ], + imports: [ + CommonModule, + MatDialogModule, + MatProgressBarModule, + MatButtonModule, + FaIconComponent + ] +}) +export class DipVerifyDialogComponent implements OnInit, OnDestroy { + private dialogRef = inject(MatDialogRef); + private data: { mobileNo: string } = inject(MAT_DIALOG_DATA); + private clientsService = inject(ClientsService); + + verifying = true; + progress = 0; + suspected = false; + serviceUnavailable = false; + error: string | null = null; + resultMessage = ''; + suspectedStatus = ''; + suspectedReason = ''; + suspectedReportedBy = ''; + private subscription: any; + private intervalId: any; + private apiResponse: any = null; + private apiError: any = null; + private apiDone = false; + + ngOnInit() { + this.dialogRef.disableClose = true; + + // Animate progress bar over ~3 seconds (0 → 100) + this.intervalId = setInterval(() => { + this.progress += 2; + if (this.progress >= 100) { + clearInterval(this.intervalId); + this.intervalId = null; + this.tryShowResult(); + } + }, 60); + + // Fire the API call in parallel + this.subscription = this.clientsService.checkDipLookup(this.data.mobileNo).subscribe({ + next: (res: any) => { + this.apiResponse = res; + this.apiDone = true; + this.tryShowResult(); + }, + error: (err: any) => { + this.apiError = err; + this.apiDone = true; + this.tryShowResult(); + } + }); + } + + /** Show result only after both progress bar completes and API responds */ + private tryShowResult() { + if (!this.apiDone || this.progress < 100) { + return; + } + + this.verifying = false; + this.dialogRef.disableClose = false; + + if (this.apiError) { + this.error = + this.apiError?.error?.message || this.apiError?.message || 'DIP verification failed. Please try again.'; + return; + } + + const res = this.apiResponse; + if (res?.data?.status === 'SERVICE_UNAVAILABLE') { + this.serviceUnavailable = true; + this.resultMessage = res.message || 'DIP/MNRL lookup service is unavailable. Number treated as CLEAN by default.'; + } else if (res?.data?.suspected) { + this.suspected = true; + this.resultMessage = res.message || `Mobile number is ${res.data.status} — found in DIP records`; + const detail = res.data.suspectedDetail; + if (detail) { + this.suspectedStatus = detail.dipStatus || res.data.status; + this.suspectedReason = detail.reason || ''; + this.suspectedReportedBy = detail.reportedBy || ''; + } + } + } + + ngOnDestroy() { + if (this.subscription) { + this.subscription.unsubscribe(); + } + if (this.intervalId) { + clearInterval(this.intervalId); + } + } + + close() { + // Return true if clear or service unavailable (treated as clean), false if suspected or error + this.dialogRef.close(!this.suspected && !this.error); + } +} diff --git a/src/app/clients/client-stepper/client-general-step/sanctions-verify-dialog.component.ts b/src/app/clients/client-stepper/client-general-step/sanctions-verify-dialog.component.ts new file mode 100644 index 000000000..42ad876a8 --- /dev/null +++ b/src/app/clients/client-stepper/client-general-step/sanctions-verify-dialog.component.ts @@ -0,0 +1,271 @@ +import { Component, OnInit, OnDestroy, inject } from '@angular/core'; +import { CommonModule } from '@angular/common'; +import { MatDialogRef, MatDialogModule, MAT_DIALOG_DATA } from '@angular/material/dialog'; +import { MatProgressBarModule } from '@angular/material/progress-bar'; +import { MatButtonModule } from '@angular/material/button'; +import { FaIconComponent } from '@fortawesome/angular-fontawesome'; +import { ClientsService } from 'app/clients/clients.service'; + +@Component({ + selector: 'mifosx-sanctions-verify-dialog', + template: ` +

+ + Sanctions Verification +

+ + @if (verifying) { + +

+ Checking {{ clientName }} against sanctions list
+ UN Security Council & + Financial Intelligence Unit - India (FIU-IND under UNSC Resolution 2231) +

+ } + @if (!verifying && !matchFound) { +
+ +

Client verified — No match found in sanctions list.

+
+ } + @if (!verifying && matchFound) { +
+ +

SANCTIONS MATCH FOUND

+

+ {{ matchCount }} potential match(es) found in UN Security Council consolidated sanctions + list for "{{ clientName }}". +

+
+ @for (match of matches; track match.id) { +
+
{{ match.fullName }}
+
+ {{ match.entryType }} + {{ match.referenceNumber }} + @if (match.unListType) { + {{ match.unListType }} + } +
+ @if (match.aliases && match.aliases.length > 0) { +
Also known as: {{ match.aliases.join(', ') }}
+ } +
+ } +
+

This client cannot be onboarded. Account opening is blocked due to sanctions match.

+
+ } + @if (!verifying && errorOccurred) { +
+ +

Verification failed. Please try again.

+

{{ errorMessage }}

+
+ } +
+ + @if (!verifying && !matchFound && !errorOccurred) { + + } + @if (!verifying && matchFound) { + + } + @if (!verifying && errorOccurred) { + + } + + `, + styles: [ + ` + :host { + display: block; + min-width: 500px; + } + + h2[mat-dialog-title] { + display: flex; + align-items: center; + gap: 8px; + margin: 0; + padding: 16px 24px; + font-size: 18px; + font-weight: 600; + color: #1074b9; + } + + .dialog-title-icon { + font-size: 20px; + } + + mat-dialog-content { + padding: 16px 24px; + max-height: 60vh; + } + + .sanctions-msg { + font-size: 14px; + color: #555; + line-height: 1.6; + margin: 16px 0 0; + text-align: center; + } + + .sanctions-result { + display: flex; + flex-direction: column; + align-items: center; + gap: 8px; + padding: 16px 0; + } + + .result-icon { + font-size: 48px; + } + + .result-icon.success { + color: #388e3c; + } + .result-icon.danger { + color: #d32f2f; + } + .result-icon.warning { + color: #f57c00; + } + + .result-msg { + font-size: 16px; + font-weight: 600; + margin: 0; + text-align: center; + } + + .result-msg.success { + color: #388e3c; + } + .result-msg.danger { + color: #d32f2f; + } + .result-msg.warning { + color: #f57c00; + } + + .result-detail { + font-size: 14px; + color: #555; + text-align: center; + margin: 4px 0; + } + + .match-list { + width: 100%; + max-height: 200px; + overflow-y: auto; + margin: 8px 0; + } + + .match-item { + background: #fce4ec; + border-left: 4px solid #d32f2f; + padding: 8px 12px; + margin-bottom: 6px; + border-radius: 4px; + } + + .match-name { + font-weight: 600; + font-size: 14px; + color: #333; + } + + .match-meta { + display: flex; + gap: 8px; + margin-top: 4px; + flex-wrap: wrap; + } + + .match-type, + .match-ref, + .match-list-type { + font-size: 11px; + padding: 2px 6px; + border-radius: 3px; + background: #ffcdd2; + color: #b71c1c; + } + + .match-aliases { + font-size: 12px; + color: #666; + margin-top: 4px; + } + + .block-msg { + font-size: 14px; + font-weight: 600; + color: #d32f2f; + background: #ffebee; + padding: 10px 16px; + border-radius: 4px; + text-align: center; + margin-top: 8px; + width: 100%; + } + + mat-dialog-actions { + padding: 8px 24px 16px; + } + ` + ], + imports: [ + CommonModule, + MatDialogModule, + MatProgressBarModule, + MatButtonModule, + FaIconComponent + ] +}) +export class SanctionsVerifyDialogComponent implements OnInit, OnDestroy { + private dialogRef = inject(MatDialogRef); + private data: { name: string } = inject(MAT_DIALOG_DATA); + private clientsService = inject(ClientsService); + + clientName = ''; + verifying = true; + matchFound = false; + matchCount = 0; + matches: any[] = []; + errorOccurred = false; + errorMessage = ''; + + ngOnInit() { + this.clientName = this.data?.name || ''; + this.dialogRef.disableClose = true; + + this.clientsService.screenSanctions(this.clientName).subscribe({ + next: (result: any) => { + this.verifying = false; + this.dialogRef.disableClose = false; + if (result.matchFound) { + this.matchFound = true; + this.matchCount = result.matchCount || 0; + this.matches = result.matches || []; + } + }, + error: (err: any) => { + this.verifying = false; + this.dialogRef.disableClose = false; + this.errorOccurred = true; + this.errorMessage = err?.error?.message || err?.message || 'Unable to connect to sanctions screening service.'; + } + }); + } + + ngOnDestroy() {} + + close(verified: boolean) { + this.dialogRef.close(verified); + } +} diff --git a/src/app/clients/clients-routing.module.ts b/src/app/clients/clients-routing.module.ts index d41b97718..154f1b622 100644 --- a/src/app/clients/clients-routing.module.ts +++ b/src/app/clients/clients-routing.module.ts @@ -288,6 +288,11 @@ const routes: Routes = [ path: 'savings-accounts', loadChildren: () => import('../savings/savings.module').then((m) => m.SavingsModule) }, + { + path: 'current-accounts', + loadChildren: () => + import('../current-accounts/current-accounts.module').then((m) => m.CurrentAccountsModule) + }, { path: 'recurring-deposits-accounts', loadChildren: () => diff --git a/src/app/clients/clients-view/clients-view.component.html b/src/app/clients/clients-view/clients-view.component.html index fb86db405..e4702a913 100644 --- a/src/app/clients/clients-view/clients-view.component.html +++ b/src/app/clients/clients-view/clients-view.component.html @@ -274,6 +274,13 @@ > {{ 'labels.buttons.New Fixed Deposits Account' | translate }} + diff --git a/src/app/clients/clients.service.ts b/src/app/clients/clients.service.ts index e672575cf..68e3fd3ac 100644 --- a/src/app/clients/clients.service.ts +++ b/src/app/clients/clients.service.ts @@ -490,6 +490,15 @@ export class ClientsService { return this.http.post(`/clients/${clientId}/kyc`, kycData); } + /** + * Screen a client name against the UN Security Council consolidated sanctions list. + * @param name The name to screen + * @returns Observable with screening result (status: FLAGGED or CLEAR) + */ + screenSanctions(name: string): Observable { + return this.http.post('/sanctions/screen', { name }); + } + /** * Updates existing KYC details for a client. * @param clientId Client ID @@ -550,6 +559,16 @@ export class ClientsService { return this.http.post(`/clients/${clientId}/ckyc/documents`, formData); } + /** + * Check mobile number against the DIP (Digital Intelligence Platform) MNRL. + * Calls an external service directly, bypassing Fineract interceptors. + * @param mobileNo Mobile number to verify + */ + checkDipLookup(mobileNo: string): Observable { + const httpParams = new HttpParams().set('mobileNumber', mobileNo); + return this.http.get('/lookup/check', { params: httpParams }); + } + getClientForm60(clientId: string): Observable { return this.http.get(`/clients/${clientId}/form60`); } diff --git a/src/app/clients/create-client/create-client.component.ts b/src/app/clients/create-client/create-client.component.ts index 417a4f0f0..430b3a854 100644 --- a/src/app/clients/create-client/create-client.component.ts +++ b/src/app/clients/create-client/create-client.component.ts @@ -116,6 +116,10 @@ export class CreateClientComponent { areFormvalids(): boolean { let areValids = this.clientGeneralForm.valid; + // Block form if sanctions match found + if (this.clientGeneralStep.sanctionsBlocked) { + return false; + } if (this.clientTemplate.isAddressEnabled) { areValids = areValids && this.clientAddressStep.address.address.length > 0; } diff --git a/src/app/core/shell/shell.component.html b/src/app/core/shell/shell.component.html index 46bead8ac..596e393e6 100644 --- a/src/app/core/shell/shell.component.html +++ b/src/app/core/shell/shell.component.html @@ -27,6 +27,57 @@ + + + + + + + + + + + + + + + + + + + + @if (progressBarMode !== 'none') {
diff --git a/src/app/core/shell/shell.component.scss b/src/app/core/shell/shell.component.scss index 4f27b4fa8..3483c5663 100644 --- a/src/app/core/shell/shell.component.scss +++ b/src/app/core/shell/shell.component.scss @@ -36,6 +36,51 @@ $color: #e2e4ec; overflow-x: hidden; } +.sub-menu-bar { + display: flex; + flex-wrap: nowrap; + align-items: center; + background-color: #1074b9; + padding: 2px 0.5rem 2px calc(48px + 2rem); + width: 100%; + z-index: 1; + position: relative; + font-family: Roboto, 'Helvetica Neue', sans-serif; + font-size: 14px; + font-weight: 500; + letter-spacing: normal; + line-height: 1; + -webkit-font-smoothing: antialiased; + -moz-osx-font-smoothing: grayscale; +} + +.sub-menu-link { + display: inline-flex; + align-items: center; + padding: 6px 0.5rem; + text-decoration: none; + font-weight: 500; + font-size: 14px; + color: rgb(255 255 255 / 70%); + white-space: nowrap; + cursor: pointer; + flex-shrink: 0; + line-height: 1; + + &:hover { + color: #fff; + } +} + +.sub-menu-icon { + margin-right: 0.35rem; +} + +.sub-menu-caret { + margin-left: 0.35rem; + font-size: 12px; +} + @keyframes move { 0% { left: 0; diff --git a/src/app/core/shell/shell.component.ts b/src/app/core/shell/shell.component.ts index ee10accce..195017f02 100644 --- a/src/app/core/shell/shell.component.ts +++ b/src/app/core/shell/shell.component.ts @@ -23,6 +23,8 @@ import { ToolbarComponent } from './toolbar/toolbar.component'; import { BreadcrumbComponent } from './breadcrumb/breadcrumb.component'; import { ContentComponent } from './content/content.component'; import { FooterComponent } from '../../shared/footer/footer.component'; +import { FaIconComponent } from '@fortawesome/angular-fontawesome'; +import { MatMenuModule } from '@angular/material/menu'; import { STANDALONE_SHARED_IMPORTS } from 'app/standalone-shared.module'; /** @@ -43,7 +45,9 @@ import { STANDALONE_SHARED_IMPORTS } from 'app/standalone-shared.module'; BreadcrumbComponent, ContentComponent, FooterComponent, - AsyncPipe + AsyncPipe, + FaIconComponent, + MatMenuModule ] }) export class ShellComponent implements OnInit, OnDestroy { diff --git a/src/app/core/shell/toolbar/toolbar.component.html b/src/app/core/shell/toolbar/toolbar.component.html index bc82637aa..544b10aef 100644 --- a/src/app/core/shell/toolbar/toolbar.component.html +++ b/src/app/core/shell/toolbar/toolbar.component.html @@ -46,6 +46,7 @@ > {{ 'labels.menus.Institution' | translate }} + @@ -79,6 +80,7 @@ > {{ 'labels.menus.Reports' | translate }} + {{ 'labels.menus.Admin' | translate }} + {{ 'labels.menus.Configuration Wizard' | translate }} + + + Ledger + +
- - -
- -
- - -
@@ -134,11 +123,6 @@
- - - - - + + + - - - - - -

{{ 'labels.menus.Global Search' | translate }}

-

{{ 'labels.text.Navigate system selecting entity' | translate }}

-
- - - -
-
- - -

{{ 'labels.menus.Language Selector' | translate }}

-

{{ 'labels.text.Organization available languages' | translate }}

-
- - diff --git a/src/app/core/shell/toolbar/toolbar.component.scss b/src/app/core/shell/toolbar/toolbar.component.scss index 0ddc9526d..3ef52cab1 100644 --- a/src/app/core/shell/toolbar/toolbar.component.scss +++ b/src/app/core/shell/toolbar/toolbar.component.scss @@ -8,10 +8,7 @@ #mifosx-toolbar { position: relative; - box-shadow: - 0 3px 1px -2px rgb(0 0 0 / 20%), - 0 2px 2px 0 rgb(0 0 0 / 14%), - 0 1px 5px 0 rgb(0 0 0 / 12%); + box-shadow: none; .tab-nav-bar { display: flex; @@ -21,7 +18,7 @@ .tab-link { min-width: auto; - padding: 15px 1rem; + padding: 15px 0.5rem; text-decoration: none; font-weight: 500; font-size: 14px; @@ -34,21 +31,6 @@ cursor: pointer; } - .search-bar { - .search { - max-width: 400px; - width: 100%; - font-size: 1rem; - } - } - - .language { - min-width: 80px; - width: 80px; - font-size: 0.875rem; - margin-top: 0.375rem; - } - .img-button { img { width: 32px; @@ -57,6 +39,11 @@ } } + .ml-05 { + margin-left: 0.35rem; + font-size: 12px; + } + .ml-1 { margin-left: 1rem; } @@ -66,40 +53,6 @@ } } -// Show language icon button only on small screens (when full selector is hidden) -.show-lt-md { - display: none; -} - -@media (width <= 768px) { - .show-lt-md { - display: inline-flex; - } -} - .toolbar-spacer { flex: 1 1 auto; } - -.white-text-language ::ng-deep mifosx-language-selector { - color: white !important; - /* stylelint-disable-next-line selector-max-universal */ - * { - background-color: transparent !important; - color: white !important; - } -} - -.white-text-search ::ng-deep mifosx-search-tool { - color: white !important; - /* stylelint-disable-next-line selector-max-universal */ - * { - background-color: transparent !important; - color: white !important; - margin-bottom: 0; - } -} - -.white-text-language { - margin-top: 27px; -} diff --git a/src/app/core/utils/charges.ts b/src/app/core/utils/charges.ts index 64687873a..77e6d880a 100644 --- a/src/app/core/utils/charges.ts +++ b/src/app/core/utils/charges.ts @@ -18,7 +18,8 @@ export class Charges { { id: 1, code: 'chargeAppliesTo.loan', value: 'Loan' }, { id: 2, code: 'chargeAppliesTo.savings', value: 'Savings' }, { id: 3, code: 'chargeAppliesTo.client', value: 'Client' }, - { id: 4, code: 'chargeAppliesTo.shares', value: 'Shares' } + { id: 4, code: 'chargeAppliesTo.shares', value: 'Shares' }, + { id: 5, code: 'chargeAppliesTo.currentAccount', value: 'Current Account' } ]; } } diff --git a/src/app/current-accounts/common-resolvers/current-account-actions.resolver.ts b/src/app/current-accounts/common-resolvers/current-account-actions.resolver.ts new file mode 100644 index 000000000..9197404d6 --- /dev/null +++ b/src/app/current-accounts/common-resolvers/current-account-actions.resolver.ts @@ -0,0 +1,78 @@ +/** + * 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 { Injectable, inject } from '@angular/core'; +import { ActivatedRouteSnapshot } from '@angular/router'; + +/** rxjs Imports */ +import { Observable, forkJoin, of } from 'rxjs'; +import { map, switchMap } from 'rxjs/operators'; + +/** Custom Services */ +import { CurrentAccountsService } from '../current-accounts.service'; +import { ClientsService } from 'app/clients/clients.service'; +import { GroupsService } from 'app/groups/groups.service'; +import { OrganizationService } from 'app/organization/organization.service'; + +/** + * Current Account Actions data resolver. + */ +@Injectable() +export class CurrentAccountActionsResolver { + private currentAccountsService = inject(CurrentAccountsService); + private clientsService = inject(ClientsService); + private groupsService = inject(GroupsService); + private organizationService = inject(OrganizationService); + + /** + * Returns the Current account actions data. + * @param {ActivatedRouteSnapshot} route Route Snapshot + * @returns {Observable} + */ + resolve(route: ActivatedRouteSnapshot): Observable { + const actionName = route.paramMap.get('name'); + const currentAccountId = + route.paramMap.get('currentAccountId') || route.parent.parent.paramMap.get('currentAccountId'); + switch (actionName) { + case 'Assign Staff': + return this.currentAccountsService.getCurrentAccountData(currentAccountId).pipe( + switchMap((account: any) => { + if (!account.clientId && !account.groupId) { + return of({ ...account, fieldOfficerOptions: [] }); + } + const entityObs = account.clientId + ? this.clientsService.getClientData(account.clientId) + : this.groupsService.getGroupData(account.groupId); + return entityObs.pipe( + switchMap((entity: any) => this.organizationService.getStaff(entity.officeId, true)), + map((staff: any) => ({ + ...account, + fieldOfficerOptions: staff + })) + ); + }) + ); + case 'Add Charge': + return this.currentAccountsService.getCurrentAccountChargeTemplateResource(currentAccountId); + case 'Withdrawal': + case 'Deposit': + case 'Hold Amount': + return this.currentAccountsService.getCurrentAccountTransactionTemplateResource(currentAccountId); + case 'Close': + return forkJoin([ + this.currentAccountsService.getCurrentAccountTransactionTemplateResource(currentAccountId), + this.currentAccountsService.getCurrentAccountData(currentAccountId) + ]); + case 'Apply Annual Fees': + return this.currentAccountsService.getCurrentAccountData(currentAccountId); + default: + return undefined; + } + } +} diff --git a/src/app/current-accounts/common-resolvers/current-account-and-template.resolver.ts b/src/app/current-accounts/common-resolvers/current-account-and-template.resolver.ts new file mode 100644 index 000000000..369728e36 --- /dev/null +++ b/src/app/current-accounts/common-resolvers/current-account-and-template.resolver.ts @@ -0,0 +1,35 @@ +/** + * 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 { Injectable, inject } from '@angular/core'; +import { ActivatedRouteSnapshot } from '@angular/router'; + +/** rxjs Imports */ +import { Observable } from 'rxjs'; + +/** Custom Services */ +import { CurrentAccountsService } from '../current-accounts.service'; + +/** + * Current Account data and template resolver. + */ +@Injectable() +export class CurrentAccountAndTemplateResolver { + private currentAccountsService = inject(CurrentAccountsService); + + /** + * Returns the Current Account data and template. + * @param {ActivatedRouteSnapshot} route Route Snapshot + * @returns {Observable} + */ + resolve(route: ActivatedRouteSnapshot): Observable { + const currentAccountId = route.paramMap.get('currentAccountId'); + return this.currentAccountsService.getCurrentAccountAndTemplate(currentAccountId, true); + } +} diff --git a/src/app/current-accounts/common-resolvers/current-account-charge.resolver.ts b/src/app/current-accounts/common-resolvers/current-account-charge.resolver.ts new file mode 100644 index 000000000..d2cd66d65 --- /dev/null +++ b/src/app/current-accounts/common-resolvers/current-account-charge.resolver.ts @@ -0,0 +1,36 @@ +/** + * 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 { Injectable, inject } from '@angular/core'; +import { ActivatedRouteSnapshot } from '@angular/router'; + +/** rxjs Imports */ +import { Observable } from 'rxjs'; + +/** Custom Services */ +import { CurrentAccountsService } from '../current-accounts.service'; + +/** + * Current Account Charge data resolver. + */ +@Injectable() +export class CurrentAccountChargeResolver { + private currentAccountsService = inject(CurrentAccountsService); + + /** + * Returns the Current Account Charge data. + * @param {ActivatedRouteSnapshot} route Route Snapshot + * @returns {Observable} + */ + resolve(route: ActivatedRouteSnapshot): Observable { + const currentAccountId = route.parent.paramMap.get('currentAccountId'); + const chargeId = route.paramMap.get('id'); + return this.currentAccountsService.getCurrentAccountCharge(currentAccountId, chargeId); + } +} diff --git a/src/app/current-accounts/common-resolvers/current-account-template.resolver.ts b/src/app/current-accounts/common-resolvers/current-account-template.resolver.ts new file mode 100644 index 000000000..32eb410af --- /dev/null +++ b/src/app/current-accounts/common-resolvers/current-account-template.resolver.ts @@ -0,0 +1,36 @@ +/** + * 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 { Injectable, inject } from '@angular/core'; +import { ActivatedRouteSnapshot } from '@angular/router'; + +/** rxjs Imports */ +import { Observable } from 'rxjs'; + +/** Custom Services */ +import { CurrentAccountsService } from '../current-accounts.service'; + +/** + * Current Account Template resolver. + */ +@Injectable() +export class CurrentAccountTemplateResolver { + private currentAccountsService = inject(CurrentAccountsService); + + /** + * Returns the Shares Account Template. + * @param {ActivatedRouteSnapshot} route Route Snapshot + * @returns {Observable} + */ + resolve(route: ActivatedRouteSnapshot): Observable { + const entityId = route.paramMap.get('clientId') || route.paramMap.get('groupId') || route.paramMap.get('centerId'); + const isGroup = route.paramMap.get('groupId') || route.paramMap.get('centerId') ? true : false; + return this.currentAccountsService.getCurrentAccountTemplate(entityId, undefined, isGroup); + } +} diff --git a/src/app/current-accounts/common-resolvers/current-account-transaction-template.resolver.ts b/src/app/current-accounts/common-resolvers/current-account-transaction-template.resolver.ts new file mode 100644 index 000000000..9b467e3af --- /dev/null +++ b/src/app/current-accounts/common-resolvers/current-account-transaction-template.resolver.ts @@ -0,0 +1,36 @@ +/** + * 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 { Injectable, inject } from '@angular/core'; +import { ActivatedRouteSnapshot } from '@angular/router'; + +/** rxjs Imports */ +import { Observable } from 'rxjs'; + +/** Custom Services */ +import { CurrentAccountsService } from '../current-accounts.service'; + +/** + * Current Account Transaction Template data resolver. + */ +@Injectable() +export class CurrentAccountTransactionTemplateResolver { + private currentAccountsService = inject(CurrentAccountsService); + + /** + * Returns the Current Account Transaction Template data. + * @param {ActivatedRouteSnapshot} route Route Snapshot + * @returns {Observable} + */ + resolve(route: ActivatedRouteSnapshot): Observable { + const currentAccountId = route.parent.paramMap.get('currentAccountId'); + const transactionId = route.paramMap.get('id'); + return this.currentAccountsService.getCurrentAccountTransactionTemplate(currentAccountId, transactionId); + } +} diff --git a/src/app/current-accounts/common-resolvers/current-account-transaction.resolver.ts b/src/app/current-accounts/common-resolvers/current-account-transaction.resolver.ts new file mode 100644 index 000000000..eda0a0315 --- /dev/null +++ b/src/app/current-accounts/common-resolvers/current-account-transaction.resolver.ts @@ -0,0 +1,36 @@ +/** + * 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 { Injectable, inject } from '@angular/core'; +import { ActivatedRouteSnapshot } from '@angular/router'; + +/** rxjs Imports */ +import { Observable } from 'rxjs'; + +/** Custom Services */ +import { CurrentAccountsService } from '../current-accounts.service'; + +/** + * Current Account Transaction data resolver. + */ +@Injectable() +export class CurrentAccountTransactionResolver { + private currentAccountsService = inject(CurrentAccountsService); + + /** + * Returns the Current Account Transaction data. + * @param {ActivatedRouteSnapshot} route Route Snapshot + * @returns {Observable} + */ + resolve(route: ActivatedRouteSnapshot): Observable { + const currentAccountId = route.parent.paramMap.get('currentAccountId'); + const transactionId = route.parent.paramMap.get('id'); + return this.currentAccountsService.getCurrentAccountTransaction(currentAccountId, transactionId); + } +} diff --git a/src/app/current-accounts/common-resolvers/current-account-view.resolver.ts b/src/app/current-accounts/common-resolvers/current-account-view.resolver.ts new file mode 100644 index 000000000..eb92b8510 --- /dev/null +++ b/src/app/current-accounts/common-resolvers/current-account-view.resolver.ts @@ -0,0 +1,35 @@ +/** + * 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 { Injectable, inject } from '@angular/core'; +import { ActivatedRouteSnapshot } from '@angular/router'; + +/** rxjs Imports */ +import { Observable } from 'rxjs'; + +/** Custom Services */ +import { CurrentAccountsService } from '../current-accounts.service'; + +/** + * Current Account data resolver. + */ +@Injectable() +export class CurrentAccountViewResolver { + private currentAccountsService = inject(CurrentAccountsService); + + /** + * Returns the Current Account data. + * @param {ActivatedRouteSnapshot} route Route Snapshot + * @returns {Observable} + */ + resolve(route: ActivatedRouteSnapshot): Observable { + const currentAccountId = route.paramMap.get('currentAccountId'); + return this.currentAccountsService.getCurrentAccountData(currentAccountId); + } +} diff --git a/src/app/current-accounts/common-resolvers/current-accounts-datatable.resolver.ts b/src/app/current-accounts/common-resolvers/current-accounts-datatable.resolver.ts new file mode 100644 index 000000000..642ecf7b2 --- /dev/null +++ b/src/app/current-accounts/common-resolvers/current-accounts-datatable.resolver.ts @@ -0,0 +1,38 @@ +/** + * 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 { Injectable, inject } from '@angular/core'; +import { ActivatedRouteSnapshot } from '@angular/router'; + +/** rxjs Imports */ +import { Observable } from 'rxjs'; + +/** Custom Services */ +import { CurrentAccountsService } from '../current-accounts.service'; + +/** + * Current Account Datatable data resolver. + */ +@Injectable() +export class CurrentAccountDatatableResolver { + private currentAccountsService = inject(CurrentAccountsService); + + /** + * Returns the Current Account's Datatable data. + * @returns {Observable} + */ + resolve(route: ActivatedRouteSnapshot): Observable { + const accountId = + route.parent.parent.paramMap.get('currentAccountId') || + route.parent.parent.paramMap.get('fixedDepositAccountId') || + route.parent.parent.paramMap.get('recurringDepositAccountId'); + const datatableName = route.paramMap.get('datatableName'); + return this.currentAccountsService.getCurrentAccountDatatable(accountId, datatableName); + } +} diff --git a/src/app/current-accounts/common-resolvers/current-accounts-datatables.resolver.ts b/src/app/current-accounts/common-resolvers/current-accounts-datatables.resolver.ts new file mode 100644 index 000000000..24fa09ed2 --- /dev/null +++ b/src/app/current-accounts/common-resolvers/current-accounts-datatables.resolver.ts @@ -0,0 +1,33 @@ +/** + * 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 { Injectable, inject } from '@angular/core'; +import { ActivatedRouteSnapshot } from '@angular/router'; + +/** rxjs Imports */ +import { Observable } from 'rxjs'; + +/** Custom Services */ +import { CurrentAccountsService } from '../current-accounts.service'; + +/** + * Current Accounts Datatables data resolver. + */ +@Injectable() +export class CurrentAccountDatatablesResolver { + private currentAccountsService = inject(CurrentAccountsService); + + /** + * Returns the Current Account's Datatables data. + * @returns {Observable} + */ + resolve(): Observable { + return this.currentAccountsService.getCurrentAccountDatatables(); + } +} diff --git a/src/app/current-accounts/common-resolvers/current-accounts-documents.resolver.ts b/src/app/current-accounts/common-resolvers/current-accounts-documents.resolver.ts new file mode 100644 index 000000000..94a21f486 --- /dev/null +++ b/src/app/current-accounts/common-resolvers/current-accounts-documents.resolver.ts @@ -0,0 +1,28 @@ +/** + * Copyright since 2025 Mifos Initiative + * + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. + */ + +import { Injectable, inject } from '@angular/core'; +import { ActivatedRouteSnapshot } from '@angular/router'; +import { Observable } from 'rxjs'; +import { CurrentAccountsService } from '../current-accounts.service'; + +@Injectable({ + providedIn: 'root' +}) +export class CurrentAccountDocumentsResolver { + private currentAccountsService = inject(CurrentAccountsService); + + /** + * Returns the Current Account data. + * @returns {Observable} + */ + resolve(route: ActivatedRouteSnapshot): Observable { + const currentAccountId = route.parent.paramMap.get('currentAccountId'); + return this.currentAccountsService.getCurrentAccountDocuments(currentAccountId); + } +} diff --git a/src/app/current-accounts/common-resolvers/current-accounts-notes.resolver.ts b/src/app/current-accounts/common-resolvers/current-accounts-notes.resolver.ts new file mode 100644 index 000000000..1ca1288b9 --- /dev/null +++ b/src/app/current-accounts/common-resolvers/current-accounts-notes.resolver.ts @@ -0,0 +1,28 @@ +/** + * Copyright since 2025 Mifos Initiative + * + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. + */ + +import { Injectable, inject } from '@angular/core'; +import { ActivatedRouteSnapshot } from '@angular/router'; +import { Observable } from 'rxjs'; +import { CurrentAccountsService } from '../current-accounts.service'; + +@Injectable({ + providedIn: 'root' +}) +export class CurrentAccountNotesResolver { + private currentAccountsService = inject(CurrentAccountsService); + + /** + * Returns the Current Account data. + * @returns {Observable} + */ + resolve(route: ActivatedRouteSnapshot): Observable { + const currentAccountId = route.parent.paramMap.get('currentAccountId'); + return this.currentAccountsService.getCurrentAccountNotes(currentAccountId); + } +} diff --git a/src/app/current-accounts/common-resolvers/current-accounts-transaction-reciept.resolver.ts b/src/app/current-accounts/common-resolvers/current-accounts-transaction-reciept.resolver.ts new file mode 100644 index 000000000..4a033953f --- /dev/null +++ b/src/app/current-accounts/common-resolvers/current-accounts-transaction-reciept.resolver.ts @@ -0,0 +1,47 @@ +/** + * 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 { Injectable, inject } from '@angular/core'; +import { ActivatedRouteSnapshot } from '@angular/router'; + +/** rxjs Imports */ +import { Observable } from 'rxjs'; + +/** Custom Services */ +import { ReportsService } from 'app/reports/reports.service'; +import { SettingsService } from 'app/settings/settings.service'; + +/** + * Current Account Transaction Reciept resolver. + */ +@Injectable() +export class CurrentAccountTransactionRecieptResolver { + private reportsService = inject(ReportsService); + private settingsService = inject(SettingsService); + + /** + * Returns the Current Account Transaction Reciept + * @param {ActivatedRouteSnapshot} route Route Snapshot + * @returns {Observable} + */ + resolve(route: ActivatedRouteSnapshot): Observable { + const transactionId = route.paramMap.get('id'); + const data = { + 'output-type': 'PDF', + R_transactionId: transactionId + }; + return this.reportsService.getPentahoRunReportData( + 'Current Account Transaction Receipt', + data, + 'default', + this.settingsService.language.code, + this.settingsService.dateFormat + ); + } +} diff --git a/src/app/current-accounts/common-resolvers/transaction-datatable.resolver.ts b/src/app/current-accounts/common-resolvers/transaction-datatable.resolver.ts new file mode 100644 index 000000000..585ad29ef --- /dev/null +++ b/src/app/current-accounts/common-resolvers/transaction-datatable.resolver.ts @@ -0,0 +1,33 @@ +/** + * 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 { Injectable, inject } from '@angular/core'; +import { ActivatedRouteSnapshot } from '@angular/router'; + +// rxjs Imports +import { Observable } from 'rxjs'; + +// Custom Service +import { CurrentAccountsService } from '../current-accounts.service'; + +@Injectable() +export class TransactionDatatableResolver { + private currentAccountsService = inject(CurrentAccountsService); + + /** + * Returns the Transactions Account's Datatable data. + * @returns {Observable} + */ + + resolve(route: ActivatedRouteSnapshot): Observable { + const accountId = route.parent.parent.paramMap.get('id'); + const datatableName = route.paramMap.get('datatableName'); + return this.currentAccountsService.getCurrentAccountTransactionDatatable(accountId, datatableName); + } +} diff --git a/src/app/current-accounts/common-resolvers/transaction-datatables.resolver.ts b/src/app/current-accounts/common-resolvers/transaction-datatables.resolver.ts new file mode 100644 index 000000000..bcd7b19f5 --- /dev/null +++ b/src/app/current-accounts/common-resolvers/transaction-datatables.resolver.ts @@ -0,0 +1,31 @@ +/** + * 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 { Injectable, inject } from '@angular/core'; +import { ActivatedRouteSnapshot } from '@angular/router'; + +// rxjs Imports +import { Observable } from 'rxjs'; + +// Custom Service +import { CurrentAccountsService } from '../current-accounts.service'; + +@Injectable() +export class TransactionDatatablesResolver { + private currentAccountsService = inject(CurrentAccountsService); + + /** + * + * @param route + * @returns {Observable} + */ + resolve(route: ActivatedRouteSnapshot): Observable { + return this.currentAccountsService.getCurrentAccountTransactionDatatables(); + } +} diff --git a/src/app/current-accounts/create-current-account/create-current-account.component.html b/src/app/current-accounts/create-current-account/create-current-account.component.html new file mode 100644 index 000000000..e6ffe3794 --- /dev/null +++ b/src/app/current-accounts/create-current-account/create-current-account.component.html @@ -0,0 +1,80 @@ + + +
+ + + + + + + + + + + + + + + + + + + + + + + {{ 'labels.inputs.DETAILS' | translate }} + + + + + + + {{ 'labels.inputs.TERMS' | translate }} + + + + + + {{ 'labels.inputs.CHARGES' | translate }} + + + + + + @if (currentAccountFormValid) { + + {{ 'labels.inputs.PREVIEW' | translate }} + + + + } + +
diff --git a/src/app/current-accounts/create-current-account/create-current-account.component.scss b/src/app/current-accounts/create-current-account/create-current-account.component.scss new file mode 100644 index 000000000..4d4789083 --- /dev/null +++ b/src/app/current-accounts/create-current-account/create-current-account.component.scss @@ -0,0 +1,77 @@ +/** + * 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/. + */ + +// Container styling with proper spacing and margins +.container { + margin: 1.5rem auto; + max-width: 75rem; + width: 100%; + padding: 0 1.5rem; + box-sizing: border-box; +} + +// Material Stepper styling with proper spacing +:host ::ng-deep mat-stepper { + background: transparent; + + .mat-step-header { + padding: 1rem 1.5rem; + margin-bottom: 0.5rem; + } + + .mat-step-content { + padding: 1.5rem; + margin: 1rem 0; + } +} + +// Form field spacing within steps +:host ::ng-deep mat-step-content { + mat-form-field { + margin-bottom: 1rem; + width: 100%; + } + + .layout-row-wrap { + gap: 1rem; + margin-bottom: 1rem; + } +} + +// Responsive design +@media (width <= 768px) { + .container { + padding: 0 1rem; + margin: 1rem auto; + } + + :host ::ng-deep mat-stepper { + .mat-step-content { + padding: 1rem; + margin: 0.75rem 0; + } + + .mat-step-header { + padding: 0.75rem 1rem; + } + } +} + +@media (width <= 480px) { + .container { + padding: 0 0.75rem; + margin: 0.75rem auto; + } + + :host ::ng-deep mat-stepper { + .mat-step-content { + padding: 0.75rem; + margin: 0.5rem 0; + } + } +} diff --git a/src/app/current-accounts/create-current-account/create-current-account.component.ts b/src/app/current-accounts/create-current-account/create-current-account.component.ts new file mode 100644 index 000000000..89e7978e6 --- /dev/null +++ b/src/app/current-accounts/create-current-account/create-current-account.component.ts @@ -0,0 +1,162 @@ +/** + * 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'; + +/** Custom Components */ +import { CurrentAccountDetailsStepComponent } from '../current-account-stepper/current-account-details-step/current-account-details-step.component'; +import { CurrentAccountTermsStepComponent } from '../current-account-stepper/current-account-terms-step/current-account-terms-step.component'; +import { CurrentAccountChargesStepComponent } from '../current-account-stepper/current-account-charges-step/current-account-charges-step.component'; + +/** Custom Services */ +import { CurrentAccountsService } from '../current-accounts.service'; +import { SettingsService } from 'app/settings/settings.service'; +import { Dates } from 'app/core/utils/dates'; +import { MatStepper, MatStepperIcon, MatStep, MatStepLabel } from '@angular/material/stepper'; +import { FaIconComponent } from '@fortawesome/angular-fontawesome'; +import { CurrentAccountPreviewStepComponent } from '../current-account-stepper/current-account-preview-step/current-account-preview-step.component'; +import { STANDALONE_SHARED_IMPORTS } from 'app/standalone-shared.module'; + +/** + * Create Current Account Component + */ +@Component({ + selector: 'mifosx-create-current-account-account', + templateUrl: './create-current-account.component.html', + styleUrls: ['./create-current-account.component.scss'], + imports: [ + ...STANDALONE_SHARED_IMPORTS, + MatStepper, + MatStepperIcon, + FaIconComponent, + MatStep, + MatStepLabel, + CurrentAccountDetailsStepComponent, + CurrentAccountTermsStepComponent, + CurrentAccountChargesStepComponent, + CurrentAccountPreviewStepComponent + ] +}) +export class CreateCurrentAccountComponent { + private route = inject(ActivatedRoute); + private router = inject(Router); + private dateUtils = inject(Dates); + private currentAccountsService = inject(CurrentAccountsService); + private settingsService = inject(SettingsService); + + /** Current Account Template */ + currentAccountTemplate: any; + /** Current Account Product Template */ + currentAccountProductTemplate: any; + + /** Current Account Details Step */ + @ViewChild(CurrentAccountDetailsStepComponent, { static: true }) + currentAccountDetailsStep: CurrentAccountDetailsStepComponent; + /** Current Account Terms Step */ + @ViewChild(CurrentAccountTermsStepComponent, { static: true }) + currentAccountTermsStep: CurrentAccountTermsStepComponent; + /** Current Account Charges Step */ + @ViewChild(CurrentAccountChargesStepComponent, { static: true }) + currentAccountChargesStep: CurrentAccountChargesStepComponent; + + /** + * Fetches current account template from `resolve` + * @param {ActivatedRoute} route Activated Route + * @param {Router} router Router + * @param {Dates} dateUtils Date Utils + * @param {CurrentAccountsService} currentAccountsService Current Accounts Service + * @param {SettingsService} settingsService Settings Service + */ + constructor() { + this.route.data.subscribe((data: { currentAccountTemplate: any }) => { + this.currentAccountTemplate = data.currentAccountTemplate; + }); + } + + /** + * Sets current account product template. + * @param {any} $event API response + */ + setTemplate($event: any) { + this.currentAccountProductTemplate = $event; + } + + /** + * Retrieves current account details form. + */ + get currentAccountDetailsForm() { + return this.currentAccountDetailsStep.currentAccountDetailsForm; + } + + /** + * Retrieves current account terms form. + */ + get currentAccountTermsForm() { + return this.currentAccountTermsStep.currentAccountTermsForm; + } + + /** + * Checks validity of overall current account form. + */ + get currentAccountFormValid() { + return this.currentAccountDetailsForm.valid && this.currentAccountTermsForm.valid; + } + + /** + * Retrieves current account object. + */ + get currentAccount() { + return { + ...this.currentAccountDetailsStep.currentAccountDetails, + ...this.currentAccountTermsStep.currentAccountTerms, + ...this.currentAccountChargesStep.currentAccountCharges + }; + } + + /** + * Creates a new share account. + */ + submit() { + // TODO: Update once language and date settings are setup + const locale = this.settingsService.language.code; + const dateFormat = this.settingsService.dateFormat; + const monthDayFormat = 'dd MMMM'; + const currentAccount = { + ...this.currentAccount, + charges: this.currentAccount.charges.map((charge: any) => ({ + chargeId: charge.id, + amount: charge.amount, + dueDate: charge.dueDate ? this.dateUtils.formatDate(charge.dueDate, dateFormat) : charge.dueDate, + feeOnMonthDay: charge.feeOnMonthDay + ? this.dateUtils.formatDate(charge.feeOnMonthDay, monthDayFormat) + : charge.feeOnMonthDay, + feeInterval: charge.feeInterval + })), + submittedOnDate: this.dateUtils.formatDate(this.currentAccount.submittedOnDate, dateFormat), + dateFormat, + monthDayFormat, + locale + }; + if (this.currentAccountTemplate.clientId) { + currentAccount.clientId = this.currentAccountTemplate.clientId; + } else { + currentAccount.groupId = this.currentAccountTemplate.groupId; + } + this.currentAccountsService.createCurrentAccount(currentAccount).subscribe((response: any) => { + this.router.navigate( + [ + '../', + response.resourceId + ], + { relativeTo: this.route } + ); + }); + } +} diff --git a/src/app/current-accounts/current-account-actions/activate-current-account/activate-current-account.component.html b/src/app/current-accounts/current-account-actions/activate-current-account/activate-current-account.component.html new file mode 100644 index 000000000..19abc5fdc --- /dev/null +++ b/src/app/current-accounts/current-account-actions/activate-current-account/activate-current-account.component.html @@ -0,0 +1,46 @@ + + +
+ +
+ +
+ + {{ 'labels.inputs.Activated On Date' | translate }} + + + + @if (activateCurrentAccountForm.controls.activatedOnDate.hasError('required')) { + + {{ 'labels.inputs.Activated On Date' | translate }} {{ 'labels.commons.is' | translate }} + {{ 'labels.commons.required' | translate }} + + } + +
+
+ + + + + +
+
+
diff --git a/src/app/current-accounts/current-account-actions/activate-current-account/activate-current-account.component.scss b/src/app/current-accounts/current-account-actions/activate-current-account/activate-current-account.component.scss new file mode 100644 index 000000000..7c3f1555b --- /dev/null +++ b/src/app/current-accounts/current-account-actions/activate-current-account/activate-current-account.component.scss @@ -0,0 +1,11 @@ +/** + * 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/. + */ + +.container { + max-width: 37rem; +} diff --git a/src/app/current-accounts/current-account-actions/activate-current-account/activate-current-account.component.ts b/src/app/current-accounts/current-account-actions/activate-current-account/activate-current-account.component.ts new file mode 100644 index 000000000..ae85c6d92 --- /dev/null +++ b/src/app/current-accounts/current-account-actions/activate-current-account/activate-current-account.component.ts @@ -0,0 +1,101 @@ +/** + * 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, OnInit, inject } from '@angular/core'; +import { UntypedFormGroup, UntypedFormBuilder, Validators, ReactiveFormsModule } from '@angular/forms'; +import { ActivatedRoute, Router, RouterLink } from '@angular/router'; +import { Dates } from 'app/core/utils/dates'; + +/** Custom Services */ +import { CurrentAccountsService } from 'app/current-accounts/current-accounts.service'; +import { SettingsService } from 'app/settings/settings.service'; +import { STANDALONE_SHARED_IMPORTS } from 'app/standalone-shared.module'; + +/** + * Activate Current Account Component + */ +@Component({ + selector: 'mifosx-activate-current-account', + templateUrl: './activate-current-account.component.html', + styleUrls: ['./activate-current-account.component.scss'], + imports: [ + ...STANDALONE_SHARED_IMPORTS + ] +}) +export class ActivateCurrentAccountComponent implements OnInit { + private formBuilder = inject(UntypedFormBuilder); + private currentAccountsService = inject(CurrentAccountsService); + private dateUtils = inject(Dates); + private route = inject(ActivatedRoute); + private router = inject(Router); + private settingsService = inject(SettingsService); + + /** Minimum date allowed. */ + minDate = new Date(2000, 0, 1); + /** Maximum date allowed. */ + maxDate = new Date(); + /** Activate Current Account form. */ + activateCurrentAccountForm: UntypedFormGroup; + /** Current Account Id */ + accountId: any; + + /** + * @param {FormBuilder} formBuilder Form Builder + * @param {CurrentAccountsService} currentAccountsService Current Accounts Service + * @param {Dates} dateUtils Date Utils + * @param {ActivatedRoute} route Activated Route + * @param {Router} router Router + * @param {SettingsService} settingsService Setting service + */ + constructor() { + this.accountId = this.route.snapshot.params['currentAccountId']; + } + + /** + * Creates the activate current account form. + */ + ngOnInit() { + this.maxDate = this.settingsService.businessDate; + this.createActivateCurrentAccountForm(); + } + + /** + * Creates the activate current account form. + */ + createActivateCurrentAccountForm() { + this.activateCurrentAccountForm = this.formBuilder.group({ + activatedOnDate: [ + '', + Validators.required + ] + }); + } + + /** + * Submits the form and activates the current account, + * if successful redirects to the current account. + */ + submit() { + const activateCurrentAccountFormData = this.activateCurrentAccountForm.value; + const locale = this.settingsService.language.code; + const dateFormat = this.settingsService.dateFormat; + const prevActivatedOnDate: Date = this.activateCurrentAccountForm.value.activatedOnDate; + if (activateCurrentAccountFormData.activatedOnDate instanceof Date) { + activateCurrentAccountFormData.activatedOnDate = this.dateUtils.formatDate(prevActivatedOnDate, dateFormat); + } + const data = { + ...activateCurrentAccountFormData, + dateFormat, + locale + }; + this.currentAccountsService.executeCurrentAccountCommand(this.accountId, 'activate', data).subscribe(() => { + this.router.navigate(['../../transactions'], { relativeTo: this.route }); + }); + } +} diff --git a/src/app/current-accounts/current-account-actions/add-charge-current-account/add-charge-current-account.component.html b/src/app/current-accounts/current-account-actions/add-charge-current-account/add-charge-current-account.component.html new file mode 100644 index 000000000..61ff03cfa --- /dev/null +++ b/src/app/current-accounts/current-account-actions/add-charge-current-account/add-charge-current-account.component.html @@ -0,0 +1,126 @@ + + +
+ +
+ +
+ + {{ 'labels.inputs.Charge' | translate }} + + @for (currentAccountCharge of currentAccountChargeOptions; track currentAccountCharge) { + + {{ currentAccountCharge.name + ' (' + currentAccountCharge.currency.name + ')' }} + + } + + @if (currentAccountChargeForm.controls.chargeId.hasError('required')) { + + {{ 'labels.inputs.Charge' | translate }} {{ 'labels.commons.is' | translate }} + {{ 'labels.commons.required' | translate }} + + } + + + @if (chargeDetails) { +
+ + {{ 'labels.inputs.Amount' | translate }} + + @if (currentAccountChargeForm.controls.amount.hasError('required')) { + + {{ 'labels.inputs.Amount' | translate }} {{ 'labels.commons.is' | translate }} + {{ 'labels.commons.required' | translate }} + + } + + + {{ 'labels.inputs.Charge Calculation' | translate }} + + @for (chargeCalculation of chargeDetails.chargeCalculationTypeOptions; track chargeCalculation) { + + {{ chargeCalculation.value }} + + } + + + + {{ 'labels.inputs.Charge time type' | translate }} + + @for (chargeTime of chargeDetails.chargeTimeTypeOptions; track chargeTime) { + + {{ chargeTime.value }} + + } + + + @if (currentAccountChargeForm.contains('dueDate')) { + + {{ 'labels.inputs.Due for collection on' | translate }} + + + + @if (currentAccountChargeForm.controls.dueDate.hasError('required')) { + + {{ 'labels.inputs.Due for collection on' | translate }} {{ 'labels.commons.is' | translate }} + {{ 'labels.commons.required' | translate }} + + } + + } + @if (currentAccountChargeForm.contains('feeOnMonthDay')) { + + {{ 'labels.inputs.Due On' | translate }} + + + + @if (currentAccountChargeForm.controls.feeOnMonthDay.hasError('required')) { + + {{ 'labels.inputs.Due Date' | translate }} {{ 'labels.commons.is' | translate }} + {{ 'labels.commons.required' | translate }} + + } + + } + @if (currentAccountChargeForm.contains('feeInterval')) { + + {{ 'labels.inputs.Repeats Every' | translate }} + + + } +
+ } +
+ + + + + +
+
+
+
diff --git a/src/app/current-accounts/current-account-actions/add-charge-current-account/add-charge-current-account.component.scss b/src/app/current-accounts/current-account-actions/add-charge-current-account/add-charge-current-account.component.scss new file mode 100644 index 000000000..e1ea46f9f --- /dev/null +++ b/src/app/current-accounts/current-account-actions/add-charge-current-account/add-charge-current-account.component.scss @@ -0,0 +1,11 @@ +/** + * 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/. + */ + +.container { + width: 37rem; +} diff --git a/src/app/current-accounts/current-account-actions/add-charge-current-account/add-charge-current-account.component.ts b/src/app/current-accounts/current-account-actions/add-charge-current-account/add-charge-current-account.component.ts new file mode 100644 index 000000000..671a5b8fb --- /dev/null +++ b/src/app/current-accounts/current-account-actions/add-charge-current-account/add-charge-current-account.component.ts @@ -0,0 +1,174 @@ +/** + * 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, OnInit, inject } from '@angular/core'; +import { + UntypedFormGroup, + UntypedFormBuilder, + Validators, + UntypedFormControl, + ReactiveFormsModule +} from '@angular/forms'; +import { Router, ActivatedRoute, RouterLink } from '@angular/router'; + +/** Custom Services */ +import { CurrentAccountsService } from '../../current-accounts.service'; +import { SettingsService } from 'app/settings/settings.service'; +import { Dates } from 'app/core/utils/dates'; +import { STANDALONE_SHARED_IMPORTS } from 'app/standalone-shared.module'; + +/** + * Add Current Account Charge component. + */ +@Component({ + selector: 'mifosx-add-charge-current-account', + templateUrl: './add-charge-current-account.component.html', + styleUrls: ['./add-charge-current-account.component.scss'], + imports: [ + ...STANDALONE_SHARED_IMPORTS + ] +}) +export class AddChargeCurrentAccountComponent implements OnInit { + private formBuilder = inject(UntypedFormBuilder); + private route = inject(ActivatedRoute); + private router = inject(Router); + private dateUtils = inject(Dates); + private currentAccountsService = inject(CurrentAccountsService); + private settingsService = inject(SettingsService); + + /** Minimum Due Date allowed. */ + minDate = new Date(2000, 0, 1); + /** Maximum Due Date allowed. */ + maxDate = new Date(); + /** Add Current Account Charge form. */ + currentAccountChargeForm: UntypedFormGroup; + /** current account charge options. */ + currentAccountChargeOptions: any; + /** current account Id of the current account. */ + currentAccountId: string; + /** charge details */ + chargeDetails: any; + + /** + * Retrieves charge template data from `resolve` + * @param {FormBuilder} formBuilder Form Builder + * @param {ActivatedRoute} route Activated Route + * @param {Router} router Router + * @param {Dates} dateUtils Date Utils + * @param {CurrentAccountsService} currentAccountsService Current Accounts Service + * @param {SettingsService} settingsService Settings Service + */ + constructor() { + this.route.data.subscribe((data: { currentAccountActionData: any }) => { + this.currentAccountChargeOptions = data.currentAccountActionData.chargeOptions; + }); + this.currentAccountId = this.route.snapshot.params['currentAccountId']; + } + + /** + * Creates the Current Account Charge form. + */ + ngOnInit() { + this.maxDate = this.settingsService.businessDate; + this.createCurrentAccountChargeForm(); + this.buildDependencies(); + } + + buildDependencies() { + this.currentAccountChargeForm.controls.chargeId.valueChanges.subscribe((chargeId) => { + this.currentAccountsService.getChargeTemplate(chargeId).subscribe((data: any) => { + this.chargeDetails = data; + const chargeTimeType = data.chargeTimeType.id; + if ( + data.chargeTimeType.value === 'Withdrawal Fee' || + data.chargeTimeType.value === 'Current Account No Activity Fee' + ) { + this.chargeDetails.dueDateNotRequired = true; + } + if (data.chargeTimeType.value === 'Annual Fee' || data.chargeTimeType.value === 'Monthly Fee') { + this.chargeDetails.chargeTimeTypeAnnualOrMonth = true; + } + if (!this.chargeDetails.dueDateNotRequired && !this.chargeDetails.chargeTimeTypeAnnualOrMonth) { + this.currentAccountChargeForm.addControl('dueDate', new UntypedFormControl('', Validators.required)); + } else { + this.currentAccountChargeForm.removeControl('dueDate'); + } + if (!this.chargeDetails.dueDateNotRequired && this.chargeDetails.chargeTimeTypeAnnualOrMonth) { + this.currentAccountChargeForm.addControl('feeOnMonthDay', new UntypedFormControl('', Validators.required)); + } else { + this.currentAccountChargeForm.removeControl('feeOnMonthDay'); + } + if (chargeTimeType.value === 'Monthly Fee') { + this.currentAccountChargeForm.addControl( + 'feeInterval', + new UntypedFormControl(data.feeInterval, Validators.required) + ); + } else { + this.currentAccountChargeForm.removeControl('feeInterval'); + } + this.currentAccountChargeForm.patchValue({ + amount: data.amount, + chargeCalculationType: data.chargeCalculationType.id, + chargeTimeType: data.chargeTimeType.id + }); + }); + }); + } + + /** + * Creates the Current Account Charge form. + */ + createCurrentAccountChargeForm() { + this.currentAccountChargeForm = this.formBuilder.group({ + chargeId: [ + '', + Validators.required + ], + amount: [ + '', + Validators.required + ], + chargeCalculationType: [{ value: '', disabled: true }], + chargeTimeType: [{ value: '', disabled: true }] + }); + } + + /** + * Submits current account charge. + */ + submit() { + const currentAccountCharge = this.currentAccountChargeForm.value; + currentAccountCharge.locale = this.settingsService.language.code; + if (!currentAccountCharge.feeInterval) { + currentAccountCharge.feeInterval = this.chargeDetails.feeInterval; + } + if (this.chargeDetails.dueDateNotRequired !== true) { + if (this.chargeDetails.chargeTimeTypeAnnualOrMonth) { + const monthDayFormat = 'MMMM-dd'; // TODO: Update once language and date settings are setup + currentAccountCharge.monthDayFormat = monthDayFormat; + if (currentAccountCharge.feeOnMonthDay) { + const prevDate = this.currentAccountChargeForm.value.feeOnMonthDay; + currentAccountCharge.feeOnMonthDay = this.dateUtils.formatDate(prevDate, monthDayFormat); + } + } else { + const dateFormat = this.settingsService.dateFormat; + currentAccountCharge.dateFormat = dateFormat; + if (currentAccountCharge.dueDate) { + const prevDate = this.currentAccountChargeForm.value.dueDate; + currentAccountCharge.dueDate = this.dateUtils.formatDate(prevDate, dateFormat); + } + } + } + this.currentAccountsService + .createCurrentAccountCharge(this.currentAccountId, 'charges', currentAccountCharge) + .subscribe(() => { + this.router.navigate(['../../transactions'], { relativeTo: this.route }); + }); + } +} diff --git a/src/app/current-accounts/current-account-actions/apply-annual-fees-current-account/apply-annual-fees-current-account.component.html b/src/app/current-accounts/current-account-actions/apply-annual-fees-current-account/apply-annual-fees-current-account.component.html new file mode 100644 index 000000000..c8a028eb7 --- /dev/null +++ b/src/app/current-accounts/current-account-actions/apply-annual-fees-current-account/apply-annual-fees-current-account.component.html @@ -0,0 +1,57 @@ + + +
+ +
+ +
+ + {{ 'labels.inputs.Annual Fees Transaction Date' | translate }} + + + + @if (applyAnnualFeesForm.controls.dueDate.hasError('required')) { + + {{ 'labels.inputs.Due Date' | translate }} {{ 'labels.commons.is' | translate }} + {{ 'labels.commons.required' | translate }} + + } + + + + {{ 'labels.inputs.Amount' | translate }} + + @if (applyAnnualFeesForm.controls.amount.hasError('required')) { + + {{ 'labels.inputs.Amount' | translate }} {{ 'labels.commons.is' | translate }} + {{ 'labels.commons.required' | translate }} + + } + +
+
+ + + + + +
+
+
diff --git a/src/app/current-accounts/current-account-actions/apply-annual-fees-current-account/apply-annual-fees-current-account.component.scss b/src/app/current-accounts/current-account-actions/apply-annual-fees-current-account/apply-annual-fees-current-account.component.scss new file mode 100644 index 000000000..7c3f1555b --- /dev/null +++ b/src/app/current-accounts/current-account-actions/apply-annual-fees-current-account/apply-annual-fees-current-account.component.scss @@ -0,0 +1,11 @@ +/** + * 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/. + */ + +.container { + max-width: 37rem; +} diff --git a/src/app/current-accounts/current-account-actions/apply-annual-fees-current-account/apply-annual-fees-current-account.component.ts b/src/app/current-accounts/current-account-actions/apply-annual-fees-current-account/apply-annual-fees-current-account.component.ts new file mode 100644 index 000000000..1519da8fd --- /dev/null +++ b/src/app/current-accounts/current-account-actions/apply-annual-fees-current-account/apply-annual-fees-current-account.component.ts @@ -0,0 +1,125 @@ +/** + * 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, OnInit, inject } from '@angular/core'; +import { UntypedFormGroup, UntypedFormBuilder, Validators, ReactiveFormsModule } from '@angular/forms'; +import { ActivatedRoute, Router, RouterLink } from '@angular/router'; +import { Dates } from 'app/core/utils/dates'; + +/** Custom Services */ +import { CurrentAccountsService } from 'app/current-accounts/current-accounts.service'; +import { SettingsService } from 'app/settings/settings.service'; +import { STANDALONE_SHARED_IMPORTS } from 'app/standalone-shared.module'; + +/** + * Apply Annual Fees Component + */ +@Component({ + selector: 'mifosx-apply-annual-fees-current-account', + templateUrl: './apply-annual-fees-current-account.component.html', + styleUrls: ['./apply-annual-fees-current-account.component.scss'], + imports: [ + ...STANDALONE_SHARED_IMPORTS + ] +}) +export class ApplyAnnualFeesCurrentAccountComponent implements OnInit { + private formBuilder = inject(UntypedFormBuilder); + private currentAccountsService = inject(CurrentAccountsService); + private dateUtils = inject(Dates); + private route = inject(ActivatedRoute); + private router = inject(Router); + private settingsService = inject(SettingsService); + + /** Minimum date allowed. */ + minDate = new Date(2000, 0, 1); + /** Maximum date allowed. */ + maxDate = new Date(); + /** Apply annual fees form. */ + applyAnnualFeesForm: UntypedFormGroup; + /** Current Account Id */ + accountId: any; + /** Annual Fees charge Id */ + chargeId: any; + /** Current Account Data */ + currentAccountData: any; + + /** + * @param {FormBuilder} formBuilder Form Builder + * @param {CurrentAccountsService} currentAccountsService Current Accounts Service + * @param {Dates} dateUtils Date Utils + * @param {ActivatedRoute} route Activated Route + * @param {Router} router Router + * @param {SettingsService} settingsService Setting service + */ + constructor() { + this.accountId = this.route.snapshot.params['currentAccountId']; + this.route.data.subscribe((data: { currentAccountActionData: any }) => { + this.currentAccountData = data.currentAccountActionData; + }); + } + + /** + * Creates the apply annual fees form. + */ + ngOnInit() { + this.maxDate = this.settingsService.businessDate; + this.createApplyAnnualFeesForm(); + this.applyCharge(); + } + + /** + * Creates the apply annual fees form. + */ + createApplyAnnualFeesForm() { + this.applyAnnualFeesForm = this.formBuilder.group({ + dueDate: [ + '', + Validators.required + ], + amount: [''] + }); + } + + /** + * Retireves apply annual fees charge for ID and amount. + */ + applyCharge() { + const charges: any[] = this.currentAccountData.charges; + charges.forEach((charge: any) => { + if (charge.name === 'Annual fee - INR') { + this.chargeId = charge.id; + this.applyAnnualFeesForm.get('amount').patchValue(charge.amount); + } + }); + } + + /** + * Submits the form and applies the annual fees, + * if successful redirects to the current account. + */ + submit() { + const applyAnnualFeesFormData = this.applyAnnualFeesForm.value; + const locale = this.settingsService.language.code; + const dateFormat = this.settingsService.dateFormat; + const prevApprovedOnDate: Date = this.applyAnnualFeesForm.value.dueDate; + if (applyAnnualFeesFormData.dueDate instanceof Date) { + applyAnnualFeesFormData.dueDate = this.dateUtils.formatDate(prevApprovedOnDate, dateFormat); + } + const data = { + ...applyAnnualFeesFormData, + dateFormat, + locale + }; + this.currentAccountsService + .executeCurrentAccountChargesCommand(this.accountId, 'paycharge', data, this.chargeId) + .subscribe(() => { + this.router.navigate(['../../'], { relativeTo: this.route }); + }); + } +} diff --git a/src/app/current-accounts/current-account-actions/approve-current-account/approve-current-account.component.html b/src/app/current-accounts/current-account-actions/approve-current-account/approve-current-account.component.html new file mode 100644 index 000000000..78322c36e --- /dev/null +++ b/src/app/current-accounts/current-account-actions/approve-current-account/approve-current-account.component.html @@ -0,0 +1,51 @@ + + +
+ +
+ +
+ + {{ 'labels.inputs.Approved On Date' | translate }} + + + + @if (approveCurrentAccountForm.controls.approvedOnDate.hasError('required')) { + + {{ 'labels.inputs.Approved On Date' | translate }} {{ 'labels.commons.is' | translate }} + {{ 'labels.commons.required' | translate }} + + } + + + + {{ 'labels.inputs.Note' | translate }} + + +
+
+ + + + + +
+
+
diff --git a/src/app/current-accounts/current-account-actions/approve-current-account/approve-current-account.component.scss b/src/app/current-accounts/current-account-actions/approve-current-account/approve-current-account.component.scss new file mode 100644 index 000000000..7c3f1555b --- /dev/null +++ b/src/app/current-accounts/current-account-actions/approve-current-account/approve-current-account.component.scss @@ -0,0 +1,11 @@ +/** + * 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/. + */ + +.container { + max-width: 37rem; +} diff --git a/src/app/current-accounts/current-account-actions/approve-current-account/approve-current-account.component.ts b/src/app/current-accounts/current-account-actions/approve-current-account/approve-current-account.component.ts new file mode 100644 index 000000000..ca8791c73 --- /dev/null +++ b/src/app/current-accounts/current-account-actions/approve-current-account/approve-current-account.component.ts @@ -0,0 +1,104 @@ +/** + * 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, OnInit, inject } from '@angular/core'; +import { UntypedFormGroup, UntypedFormBuilder, Validators, ReactiveFormsModule } from '@angular/forms'; +import { ActivatedRoute, Router, RouterLink } from '@angular/router'; +import { Dates } from 'app/core/utils/dates'; + +/** Custom Services */ +import { CurrentAccountsService } from 'app/current-accounts/current-accounts.service'; +import { SettingsService } from 'app/settings/settings.service'; +import { CdkTextareaAutosize } from '@angular/cdk/text-field'; +import { STANDALONE_SHARED_IMPORTS } from 'app/standalone-shared.module'; + +/** + * Approve Current Account Component + */ +@Component({ + selector: 'mifosx-approve-current-account', + templateUrl: './approve-current-account.component.html', + styleUrls: ['./approve-current-account.component.scss'], + imports: [ + ...STANDALONE_SHARED_IMPORTS, + CdkTextareaAutosize + ] +}) +export class ApproveCurrentAccountComponent implements OnInit { + private formBuilder = inject(UntypedFormBuilder); + private currentAccountsService = inject(CurrentAccountsService); + private dateUtils = inject(Dates); + private route = inject(ActivatedRoute); + private router = inject(Router); + private settingsService = inject(SettingsService); + + /** Minimum date allowed. */ + minDate = new Date(2000, 0, 1); + /** Maximum date allowed. */ + maxDate = new Date(); + /** Approve Current Account form. */ + approveCurrentAccountForm: UntypedFormGroup; + /** Current Account Id */ + accountId: any; + + /** + * @param {FormBuilder} formBuilder Form Builder + * @param {CurrentAccountsService} currentAccountsService Current Accounts Service + * @param {Dates} dateUtils Date Utils + * @param {ActivatedRoute} route Activated Route + * @param {Router} router Router + * @param {SettingsService} settingsService Setting service + */ + constructor() { + this.accountId = this.route.snapshot.params['currentAccountId']; + } + + /** + * Creates the approve current account form. + */ + ngOnInit() { + this.maxDate = this.settingsService.businessDate; + this.createApproveCurrentAccountForm(); + } + + /** + * Creates the approve current account form. + */ + createApproveCurrentAccountForm() { + this.approveCurrentAccountForm = this.formBuilder.group({ + approvedOnDate: [ + '', + Validators.required + ], + note: [''] + }); + } + + /** + * Submits the form and approves the current account, + * if successful redirects to the current account. + */ + submit() { + const approveCurrentAccountFormData = this.approveCurrentAccountForm.value; + const locale = this.settingsService.language.code; + const dateFormat = this.settingsService.dateFormat; + const prevApprovedOnDate: Date = this.approveCurrentAccountForm.value.approvedOnDate; + if (approveCurrentAccountFormData.approvedOnDate instanceof Date) { + approveCurrentAccountFormData.approvedOnDate = this.dateUtils.formatDate(prevApprovedOnDate, dateFormat); + } + const data = { + ...approveCurrentAccountFormData, + dateFormat, + locale + }; + this.currentAccountsService.executeCurrentAccountCommand(this.accountId, 'approve', data).subscribe(() => { + this.router.navigate(['../../transactions'], { relativeTo: this.route }); + }); + } +} diff --git a/src/app/current-accounts/current-account-actions/close-current-account/close-current-account.component.html b/src/app/current-accounts/current-account-actions/close-current-account/close-current-account.component.html new file mode 100644 index 000000000..c872e5e18 --- /dev/null +++ b/src/app/current-accounts/current-account-actions/close-current-account/close-current-account.component.html @@ -0,0 +1,121 @@ + + +
+ +
+ +
+ + {{ 'labels.inputs.Closed On Date' | translate }} + + + + @if (closeCurrentAccountForm.controls.closedOnDate.hasError('required')) { + + {{ 'labels.inputs.Closed On Date' | translate }} {{ 'labels.commons.is' | translate }} + {{ 'labels.commons.required' | translate }} + + } + + + + {{ 'labels.inputs.Withdraw Balance' | translate }} + + + + {{ 'labels.inputs.Is Interest Posting required on closure date' | translate }}? + + + @if (closeCurrentAccountForm.controls['withdrawBalance'].value) { +
+ @if (closeCurrentAccountForm.controls['amount']) { + + {{ 'labels.inputs.Transaction Amount' | translate }} + + + } + @if (closeCurrentAccountForm.controls['paymentTypeId']) { + + {{ 'labels.inputs.Payment Type' | translate }} + + @for (paymentType of paymentTypeOptions; track paymentType) { + + {{ paymentType.name }} + + } + + + } +
+ + +
+ {{ 'labels.inputs.Show Payment Details' | translate }} +
+
+
+
+ @if (closeCurrentAccountForm.controls['accountNumber']) { + + {{ 'labels.inputs.Account Number' | translate }} + + + } + @if (closeCurrentAccountForm.controls['checkNumber']) { + + {{ 'labels.inputs.Cheque' | translate }} + + + } + @if (closeCurrentAccountForm.controls['routingCode']) { + + {{ 'labels.inputs.Routing Code' | translate }} + + + } + @if (closeCurrentAccountForm.controls['receiptNumber']) { + + {{ 'labels.inputs.Receipt Number' | translate }} + + + } + @if (closeCurrentAccountForm.controls['bankNumber']) { + + {{ 'labels.inputs.Bank' | translate }} + + + } +
+ } + + + {{ 'labels.inputs.Note' | translate }} + + +
+
+ + + + + +
+
+
diff --git a/src/app/current-accounts/current-account-actions/close-current-account/close-current-account.component.scss b/src/app/current-accounts/current-account-actions/close-current-account/close-current-account.component.scss new file mode 100644 index 000000000..7c3f1555b --- /dev/null +++ b/src/app/current-accounts/current-account-actions/close-current-account/close-current-account.component.scss @@ -0,0 +1,11 @@ +/** + * 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/. + */ + +.container { + max-width: 37rem; +} diff --git a/src/app/current-accounts/current-account-actions/close-current-account/close-current-account.component.ts b/src/app/current-accounts/current-account-actions/close-current-account/close-current-account.component.ts new file mode 100644 index 000000000..b4561526d --- /dev/null +++ b/src/app/current-accounts/current-account-actions/close-current-account/close-current-account.component.ts @@ -0,0 +1,165 @@ +/** + * 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, OnInit, inject } from '@angular/core'; +import { + UntypedFormGroup, + UntypedFormBuilder, + Validators, + UntypedFormControl, + ReactiveFormsModule +} from '@angular/forms'; +import { ActivatedRoute, Router, RouterLink } from '@angular/router'; +import { Dates } from 'app/core/utils/dates'; + +/** Custom Services */ +import { CurrentAccountsService } from 'app/current-accounts/current-accounts.service'; +import { SettingsService } from 'app/settings/settings.service'; +import { MatCheckbox } from '@angular/material/checkbox'; +import { MatSlideToggle } from '@angular/material/slide-toggle'; +import { CdkTextareaAutosize } from '@angular/cdk/text-field'; +import { STANDALONE_SHARED_IMPORTS } from 'app/standalone-shared.module'; + +/** + * Close Current Account Component + */ +@Component({ + selector: 'mifosx-close-current-account', + templateUrl: './close-current-account.component.html', + styleUrls: ['./close-current-account.component.scss'], + imports: [ + ...STANDALONE_SHARED_IMPORTS, + MatCheckbox, + MatSlideToggle, + CdkTextareaAutosize + ] +}) +export class CloseCurrentAccountComponent implements OnInit { + private formBuilder = inject(UntypedFormBuilder); + private currentAccountsService = inject(CurrentAccountsService); + private dateUtils = inject(Dates); + private route = inject(ActivatedRoute); + private router = inject(Router); + private settingsService = inject(SettingsService); + + /** Minimum date allowed. */ + minDate = new Date(2000, 0, 1); + /** Maximum date allowed. */ + maxDate = new Date(); + /** Close Current Account form. */ + closeCurrentAccountForm: UntypedFormGroup; + /** Current Account Id */ + accountId: any; + /** Flag to enable payment details fields. */ + addPaymentDetailsFlag = false; + /** Payment Type Options */ + paymentTypeOptions: any; + /** Transaction Amount */ + transactionAmount: any; + + /** + * @param {FormBuilder} formBuilder Form Builder + * @param {CurrentAccountsService} currentAccountsService Current Accounts Service + * @param {Dates} dateUtils Date Utils + * @param {ActivatedRoute} route Activated Route + * @param {Router} router Router + * @param {SettingsService} settingsService Setting service + */ + constructor() { + this.route.data.subscribe((data: { currentAccountActionData: any }) => { + this.paymentTypeOptions = data.currentAccountActionData[0].paymentTypeOptions; + this.transactionAmount = data.currentAccountActionData[1].summary.accountBalance; + }); + this.accountId = this.route.snapshot.params['currentAccountId']; + } + + /** + * Creates the close current account form. + */ + ngOnInit() { + this.maxDate = this.settingsService.businessDate; + this.createCloseCurrentAccountForm(); + this.buildDependencies(); + } + + /** + * Creates the close current account form. + */ + createCloseCurrentAccountForm() { + this.closeCurrentAccountForm = this.formBuilder.group({ + closedOnDate: [ + '', + Validators.required + ], + withdrawBalance: [false], + postInterestValidationOnClosure: [false], + note: [''] + }); + } + + /** + * Subscribe to value changes of withdraw balance checkbox. + */ + buildDependencies() { + this.closeCurrentAccountForm.get('withdrawBalance').valueChanges.subscribe((value: boolean) => { + if (value) { + this.closeCurrentAccountForm.addControl( + 'amount', + new UntypedFormControl({ value: this.transactionAmount, disabled: true }) + ); + this.closeCurrentAccountForm.addControl('paymentTypeId', new UntypedFormControl('')); + } else { + this.closeCurrentAccountForm.removeControl('amount'); + this.closeCurrentAccountForm.removeControl('paymentTypeId'); + } + }); + } + + /** + * Method to add payment detail fields to the UI. + */ + addPaymentDetails() { + this.addPaymentDetailsFlag = !this.addPaymentDetailsFlag; + if (this.addPaymentDetailsFlag) { + this.closeCurrentAccountForm.addControl('accountNumber', new UntypedFormControl('')); + this.closeCurrentAccountForm.addControl('checkNumber', new UntypedFormControl('')); + this.closeCurrentAccountForm.addControl('routingCode', new UntypedFormControl('')); + this.closeCurrentAccountForm.addControl('receiptNumber', new UntypedFormControl('')); + this.closeCurrentAccountForm.addControl('bankNumber', new UntypedFormControl('')); + } else { + this.closeCurrentAccountForm.removeControl('accountNumber'); + this.closeCurrentAccountForm.removeControl('checkNumber'); + this.closeCurrentAccountForm.removeControl('routingCode'); + this.closeCurrentAccountForm.removeControl('receiptNumber'); + this.closeCurrentAccountForm.removeControl('bankNumber'); + } + } + + /** + * Submits the form and closes the current account, + * if successful redirects to the current account. + */ + submit() { + const closeCurrentAccountFormData = this.closeCurrentAccountForm.value; + const locale = this.settingsService.language.code; + const dateFormat = this.settingsService.dateFormat; + const prevClosedOnDate: Date = this.closeCurrentAccountForm.value.closedOnDate; + if (closeCurrentAccountFormData.closedOnDate instanceof Date) { + closeCurrentAccountFormData.closedOnDate = this.dateUtils.formatDate(prevClosedOnDate, dateFormat); + } + const data = { + ...closeCurrentAccountFormData, + dateFormat, + locale + }; + this.currentAccountsService.executeCurrentAccountCommand(this.accountId, 'close', data).subscribe(() => { + this.router.navigate(['../../transactions'], { relativeTo: this.route }); + }); + } +} diff --git a/src/app/current-accounts/current-account-actions/current-account-actions.component.html b/src/app/current-accounts/current-account-actions/current-account-actions.component.html new file mode 100644 index 000000000..b1442a445 --- /dev/null +++ b/src/app/current-accounts/current-account-actions/current-account-actions.component.html @@ -0,0 +1,47 @@ + + +@if (actions['Approve']) { + +} +@if (actions['Reject']) { + +} +@if (actions['Activate']) { + +} +@if (actions['Undo Approval']) { + +} +@if (actions['Post Interest As On']) { + +} +@if (actions['Assign Staff']) { + +} +@if (actions['Unassign Staff']) { + +} +@if (actions['Withdrawn by Client']) { + +} +@if (actions['Add Charge']) { + +} +@if (actions['Withdrawal'] || actions['Deposit']) { + +} +@if (actions['Close']) { + +} +@if (actions['Apply Annual Fees']) { + +} +@if (actions['Hold Amount'] || actions['Block Account'] || actions['Block Deposit'] || actions['Block Withdrawal']) { + +} diff --git a/src/app/current-accounts/current-account-actions/current-account-actions.component.scss b/src/app/current-accounts/current-account-actions/current-account-actions.component.scss new file mode 100644 index 000000000..af9f4c075 --- /dev/null +++ b/src/app/current-accounts/current-account-actions/current-account-actions.component.scss @@ -0,0 +1,7 @@ +/** + * 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/. + */ diff --git a/src/app/current-accounts/current-account-actions/current-account-actions.component.ts b/src/app/current-accounts/current-account-actions/current-account-actions.component.ts new file mode 100644 index 000000000..ac9d3bbc3 --- /dev/null +++ b/src/app/current-accounts/current-account-actions/current-account-actions.component.ts @@ -0,0 +1,116 @@ +/** + * 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, inject } from '@angular/core'; +import { ActivatedRoute } from '@angular/router'; +import { Currency } from 'app/shared/models/general.model'; +import { ApproveCurrentAccountComponent } from './approve-current-account/approve-current-account.component'; +import { RejectCurrentAccountComponent } from './reject-current-account/reject-current-account.component'; +import { ActivateCurrentAccountComponent } from './activate-current-account/activate-current-account.component'; +import { UndoApprovalCurrentAccountComponent } from './undo-approval-current-account/undo-approval-current-account.component'; +import { PostInterestAsOnCurrentAccountComponent } from './post-interest-as-on-current-account/post-interest-as-on-current-account.component'; +import { CurrentAccountAssignStaffComponent } from './current-account-assign-staff/current-account-assign-staff.component'; +import { CurrentAccountUnassignStaffComponent } from './current-account-unassign-staff/current-account-unassign-staff.component'; +import { WithdrawByClientCurrentAccountComponent } from './withdraw-by-client-current-account/withdraw-by-client-current-account.component'; +import { AddChargeCurrentAccountComponent } from './add-charge-current-account/add-charge-current-account.component'; +import { CurrentAccountTransactionsComponent } from './current-account-transactions/current-account-transactions.component'; +import { CloseCurrentAccountComponent } from './close-current-account/close-current-account.component'; +import { ApplyAnnualFeesCurrentAccountComponent } from './apply-annual-fees-current-account/apply-annual-fees-current-account.component'; +import { ManageCurrentAccountComponent } from './manage-current-account/manage-current-account.component'; +import { STANDALONE_SHARED_IMPORTS } from 'app/standalone-shared.module'; + +/** + * Current account actions component. + */ +@Component({ + selector: 'mifosx-current-account-account-actions', + templateUrl: './current-account-actions.component.html', + styleUrls: ['./current-account-actions.component.scss'], + imports: [ + ...STANDALONE_SHARED_IMPORTS, + ApproveCurrentAccountComponent, + RejectCurrentAccountComponent, + ActivateCurrentAccountComponent, + UndoApprovalCurrentAccountComponent, + PostInterestAsOnCurrentAccountComponent, + CurrentAccountAssignStaffComponent, + CurrentAccountUnassignStaffComponent, + WithdrawByClientCurrentAccountComponent, + AddChargeCurrentAccountComponent, + CurrentAccountTransactionsComponent, + CloseCurrentAccountComponent, + ApplyAnnualFeesCurrentAccountComponent, + ManageCurrentAccountComponent + ] +}) +export class CurrentAccountActionsComponent { + private route = inject(ActivatedRoute); + + /** Flag object to store possible actions and render appropriate UI to the user */ + actions: { + Approve: boolean; + Reject: boolean; + Withdrawal: boolean; + Deposit: boolean; + Activate: boolean; + Close: boolean; + 'Undo Approval': boolean; + 'Post Interest As On': boolean; + 'Assign Staff': boolean; + 'Add Charge': boolean; + 'Unassign Staff': boolean; + 'Withdrawn by Client': boolean; + 'Apply Annual Fees': boolean; + 'Hold Amount': boolean; + 'Block Account': boolean; + 'Unblock Account': boolean; + 'Block Deposit': boolean; + 'Unblock Deposit': boolean; + 'Block Withdrawal': boolean; + 'Unblock Withdrawal': boolean; + } = { + Approve: false, + Reject: false, + Withdrawal: false, + Deposit: false, + Activate: false, + Close: false, + 'Undo Approval': false, + 'Post Interest As On': false, + 'Assign Staff': false, + 'Add Charge': false, + 'Unassign Staff': false, + 'Withdrawn by Client': false, + 'Apply Annual Fees': false, + 'Hold Amount': false, + 'Block Account': false, + 'Unblock Account': false, + 'Block Deposit': false, + 'Unblock Deposit': false, + 'Block Withdrawal': false, + 'Unblock Withdrawal': false + }; + + currency: Currency; + + /** + * @param {ActivatedRoute} route Activated Route + */ + constructor() { + this.route.data.subscribe((data: { currentAccountActionData: any }) => { + if (data.currentAccountActionData) { + this.currency = data.currentAccountActionData.currency; + } + }); + const name = this.route.snapshot.params['name'] as string; + if (name in this.actions) { + this.actions[name as keyof typeof this.actions] = true; + } + } +} diff --git a/src/app/current-accounts/current-account-actions/current-account-assign-staff/current-account-assign-staff.component.html b/src/app/current-accounts/current-account-actions/current-account-assign-staff/current-account-assign-staff.component.html new file mode 100644 index 000000000..2534aa8e8 --- /dev/null +++ b/src/app/current-accounts/current-account-actions/current-account-assign-staff/current-account-assign-staff.component.html @@ -0,0 +1,63 @@ + + +
+ +
+ +
+ + {{ 'labels.inputs.To Current Account Officer' | translate }} + + @for (officer of fieldOfficerData; track officer) { + + {{ officer.displayName }} + + } + + @if (currentAccountAssignStaffForm.controls.toCurrentAccountOfficerId.hasError('required')) { + + {{ 'labels.inputs.To Current Account Officer' | translate }} {{ 'labels.commons.is' | translate }} + {{ 'labels.commons.required' | translate }} + + } + + + + {{ 'labels.inputs.Assignment Date' | translate }} + + + + @if (currentAccountAssignStaffForm.controls.assignmentDate.hasError('required')) { + + {{ 'labels.inputs.Assignment Date' | translate }} {{ 'labels.commons.is' | translate }} + {{ 'labels.commons.required' | translate }} + + } + +
+
+ + + + + +
+
+
diff --git a/src/app/current-accounts/current-account-actions/current-account-assign-staff/current-account-assign-staff.component.scss b/src/app/current-accounts/current-account-actions/current-account-assign-staff/current-account-assign-staff.component.scss new file mode 100644 index 000000000..7c3f1555b --- /dev/null +++ b/src/app/current-accounts/current-account-actions/current-account-assign-staff/current-account-assign-staff.component.scss @@ -0,0 +1,11 @@ +/** + * 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/. + */ + +.container { + max-width: 37rem; +} diff --git a/src/app/current-accounts/current-account-actions/current-account-assign-staff/current-account-assign-staff.component.ts b/src/app/current-accounts/current-account-actions/current-account-assign-staff/current-account-assign-staff.component.ts new file mode 100644 index 000000000..dcae5e55d --- /dev/null +++ b/src/app/current-accounts/current-account-actions/current-account-assign-staff/current-account-assign-staff.component.ts @@ -0,0 +1,116 @@ +/** + * 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, OnInit, inject } from '@angular/core'; +import { UntypedFormGroup, UntypedFormBuilder, Validators } from '@angular/forms'; +import { ActivatedRoute, Router } from '@angular/router'; +import { Dates } from 'app/core/utils/dates'; + +/** Custom Services */ +import { CurrentAccountsService } from 'app/current-accounts/current-accounts.service'; +import { SettingsService } from 'app/settings/settings.service'; +import { STANDALONE_SHARED_IMPORTS } from 'app/standalone-shared.module'; + +/** + * Current Account Assign Staff Component + */ +@Component({ + selector: 'mifosx-current-account-account-assign-staff', + templateUrl: './current-account-assign-staff.component.html', + styleUrls: ['./current-account-assign-staff.component.scss'], + imports: [ + ...STANDALONE_SHARED_IMPORTS + ] +}) +export class CurrentAccountAssignStaffComponent implements OnInit { + private formBuilder = inject(UntypedFormBuilder); + private currentAccountsService = inject(CurrentAccountsService); + private dateUtils = inject(Dates); + private route = inject(ActivatedRoute); + private router = inject(Router); + private settingsService = inject(SettingsService); + + /** Minimum date allowed. */ + minDate = new Date(2000, 0, 1); + /** Maximum date allowed. */ + maxDate = new Date(); + /** Current Account Assign Staff form. */ + currentAccountAssignStaffForm: UntypedFormGroup; + /** Current Account Id */ + accountId: any; + /** Field Officer Data */ + fieldOfficerData: any; + /** Current Account Data */ + currentAccountData: any; + + /** + * @param {FormBuilder} formBuilder Form Builder + * @param {CurrentAccountsService} currentAccountsService Current Accounts Service + * @param {Dates} dateUtils Date Utils + * @param {ActivatedRoute} route Activated Route + * @param {Router} router Router + * @param {SettingsService} settingsService Setting service + */ + constructor() { + this.accountId = this.route.snapshot.params['currentAccountId']; + this.route.data.subscribe((data: { currentAccountActionData: any }) => { + this.currentAccountData = data.currentAccountActionData; + }); + } + + /** + * Creates the current account assign staff form. + */ + ngOnInit() { + this.maxDate = this.settingsService.businessDate; + this.fieldOfficerData = this.currentAccountData.fieldOfficerOptions; + this.createCurrentAccountAssignStaffForm(); + } + + /** + * Creates the current account assign staff form. + */ + createCurrentAccountAssignStaffForm() { + this.currentAccountAssignStaffForm = this.formBuilder.group({ + toCurrentAccountOfficerId: [ + '', + Validators.required + ], + assignmentDate: [ + '', + Validators.required + ] + }); + } + + /** + * Submits the form and assigns staff the current account, + * if successful redirects to the current account. + */ + submit() { + const locale = this.settingsService.language.code; + const dateFormat = this.settingsService.dateFormat; + const formValue = this.currentAccountAssignStaffForm.value; + const assignmentDate = + formValue.assignmentDate instanceof Date + ? this.dateUtils.formatDate(formValue.assignmentDate, dateFormat) + : formValue.assignmentDate; + const data = { + toCurrentAccountOfficerId: formValue.toCurrentAccountOfficerId, + assignmentDate, + dateFormat, + locale + }; + this.currentAccountsService + .executeCurrentAccountCommand(this.accountId, 'assignCurrentAccountOfficer', data) + .subscribe(() => { + this.router.navigate(['../../transactions'], { relativeTo: this.route }); + }); + } +} diff --git a/src/app/current-accounts/current-account-actions/current-account-transactions/current-account-transactions.component.html b/src/app/current-accounts/current-account-actions/current-account-transactions/current-account-transactions.component.html new file mode 100644 index 000000000..7db86b12c --- /dev/null +++ b/src/app/current-accounts/current-account-actions/current-account-transactions/current-account-transactions.component.html @@ -0,0 +1,132 @@ + + +
+ + @if (transactionType.withdrawal) { + + {{ 'labels.heading.Withdraw Money From Current Account' | translate }} + + } + @if (transactionType.deposit) { + + {{ 'labels.heading.Deposit Money To Current Account' | translate }} + + } + +
+ +
+ + {{ 'labels.inputs.Transaction Date' | translate }} + + + + @if (currentAccountTransactionForm.controls.transactionDate.hasError('required')) { + + {{ 'labels.inputs.Transaction Date' | translate }} {{ 'labels.commons.is' | translate }} + {{ 'labels.commons.required' | translate }} + + } + + + + + + + {{ 'labels.inputs.Payment Type' | translate }} + + @for (paymentType of paymentTypeOptions; track paymentType) { + + {{ paymentType.name }} + + } + + @if (currentAccountTransactionForm.controls.paymentTypeId.hasError('required')) { + + {{ 'labels.inputs.Payment Type' | translate }} {{ 'labels.commons.is' | translate }} + {{ 'labels.commons.required' | translate }} + + } + + +
+ + +
+ {{ 'labels.inputs.Show Payment Details' | translate }} +
+
+
+
+ + @if (addPaymentDetailsFlag) { + + {{ 'labels.inputs.Account Number' | translate }} + + + } + + @if (addPaymentDetailsFlag) { + + {{ 'labels.inputs.Cheque' | translate }} + + + } + + @if (addPaymentDetailsFlag) { + + {{ 'labels.inputs.Routing Code' | translate }} + + + } + + @if (addPaymentDetailsFlag) { + + {{ 'labels.inputs.Receipt Number' | translate }} + + + } + + @if (addPaymentDetailsFlag) { + + {{ 'labels.inputs.Bank' | translate }} + + + } + + + {{ 'labels.inputs.Notes' | translate }} + + + + + + + +
+
+
+
+
diff --git a/src/app/current-accounts/current-account-actions/current-account-transactions/current-account-transactions.component.scss b/src/app/current-accounts/current-account-actions/current-account-transactions/current-account-transactions.component.scss new file mode 100644 index 000000000..b21a3cf9c --- /dev/null +++ b/src/app/current-accounts/current-account-actions/current-account-transactions/current-account-transactions.component.scss @@ -0,0 +1,29 @@ +/** + * Copyright since 2025 Mifos Initiative + * + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. + */ + +.expandcollapsebutton { + margin-top: -7px; +} + +.container { + max-width: 37rem; +} + +.right-input { + text-align: right; +} + +.right-label { + padding-right: 25px !important; +} + +.form-heading { + font-size: 1.25rem; + font-weight: 500; + margin-bottom: 0.5rem; +} diff --git a/src/app/current-accounts/current-account-actions/current-account-transactions/current-account-transactions.component.ts b/src/app/current-accounts/current-account-actions/current-account-transactions/current-account-transactions.component.ts new file mode 100644 index 000000000..c1ea4cee7 --- /dev/null +++ b/src/app/current-accounts/current-account-actions/current-account-transactions/current-account-transactions.component.ts @@ -0,0 +1,169 @@ +/** + * 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, OnInit, inject } from '@angular/core'; +import { + UntypedFormGroup, + UntypedFormBuilder, + Validators, + UntypedFormControl, + ReactiveFormsModule +} from '@angular/forms'; +import { Router, ActivatedRoute, RouterLink } from '@angular/router'; + +/** Custom Services */ +import { CurrentAccountsService } from '../../current-accounts.service'; +import { SettingsService } from 'app/settings/settings.service'; +import { Dates } from 'app/core/utils/dates'; +import { Currency } from 'app/shared/models/general.model'; +import { InputAmountComponent } from '../../../shared/input-amount/input-amount.component'; +import { MatSlideToggle } from '@angular/material/slide-toggle'; +import { CdkTextareaAutosize } from '@angular/cdk/text-field'; +import { STANDALONE_SHARED_IMPORTS } from 'app/standalone-shared.module'; + +/** + * Create current account transactions component. + */ +@Component({ + selector: 'mifosx-current-account-transactions', + templateUrl: './current-account-transactions.component.html', + styleUrls: ['./current-account-transactions.component.scss'], + imports: [ + ...STANDALONE_SHARED_IMPORTS, + InputAmountComponent, + MatSlideToggle, + CdkTextareaAutosize + ] +}) +export class CurrentAccountTransactionsComponent implements OnInit { + private formBuilder = inject(UntypedFormBuilder); + private route = inject(ActivatedRoute); + private router = inject(Router); + private dateUtils = inject(Dates); + private currentAccountsService = inject(CurrentAccountsService); + private settingsService = inject(SettingsService); + + /** Minimum Due Date allowed. */ + minDate = new Date(2000, 0, 1); + /** Maximum Due Date allowed. */ + maxDate = new Date(); + /** Current account transaction form. */ + currentAccountTransactionForm: UntypedFormGroup; + /** current account transaction payment options. */ + paymentTypeOptions: { + id: number; + name: string; + description: string; + isCashPayment: boolean; + position: number; + }[]; + /** Flag to enable payment details fields. */ + addPaymentDetailsFlag: Boolean = false; + /** transaction type flag to render required UI */ + transactionType: { deposit: boolean; withdrawal: boolean } = { deposit: false, withdrawal: false }; + /** transaction command for submit request */ + transactionCommand: string; + /** current account's Id */ + currentAccountId: string; + currency: Currency | null = null; + + /** + * Retrieves the Current Account transaction template data from `resolve`. + * @param {FormBuilder} formBuilder Form Builder. + * @param {CurrentAccountsService} currentAccountsService Current Accounts Service. + * @param {ActivatedRoute} route Activated Route. + * @param {Dates} dateUtils Date Utils. + * @param {Router} router Router for navigation. + * @param {SettingsService} settingsService Settings Service + */ + constructor() { + this.route.data.subscribe((data: { currentAccountActionData: any }) => { + this.paymentTypeOptions = data.currentAccountActionData.paymentTypeOptions; + if (data.currentAccountActionData.currency) { + this.currency = data.currentAccountActionData.currency; + } + }); + this.transactionCommand = this.route.snapshot.params['name'].toLowerCase(); + this.transactionType[this.transactionCommand as 'deposit' | 'withdrawal'] = true; + this.currentAccountId = this.route.snapshot.params['currentAccountId']; + } + + /** + * Creates the Current Account account transaction form when component loads. + */ + ngOnInit() { + this.maxDate = this.settingsService.businessDate; + this.createCurrentAccountTransactionForm(); + } + + /** + * Method to create the Current Account Transaction Form. + */ + createCurrentAccountTransactionForm() { + this.currentAccountTransactionForm = this.formBuilder.group({ + transactionDate: [ + this.settingsService.businessDate, + Validators.required + ], + transactionAmount: [ + 0, + Validators.required + ], + paymentTypeId: [ + '', + Validators.required + ], + note: [''] + }); + } + + /** + * Method to add payment detail fields to the UI. + */ + addPaymentDetails() { + this.addPaymentDetailsFlag = !this.addPaymentDetailsFlag; + if (this.addPaymentDetailsFlag) { + this.currentAccountTransactionForm.addControl('accountNumber', new UntypedFormControl('')); + this.currentAccountTransactionForm.addControl('checkNumber', new UntypedFormControl('')); + this.currentAccountTransactionForm.addControl('routingCode', new UntypedFormControl('')); + this.currentAccountTransactionForm.addControl('receiptNumber', new UntypedFormControl('')); + this.currentAccountTransactionForm.addControl('bankNumber', new UntypedFormControl('')); + } else { + this.currentAccountTransactionForm.removeControl('accountNumber'); + this.currentAccountTransactionForm.removeControl('checkNumber'); + this.currentAccountTransactionForm.removeControl('routingCode'); + this.currentAccountTransactionForm.removeControl('receiptNumber'); + this.currentAccountTransactionForm.removeControl('bankNumber'); + } + } + + /** + * Method to submit the transaction details. + */ + submit() { + const currentAccountTransactionFormData = this.currentAccountTransactionForm.value; + const locale = this.settingsService.language.code; + const dateFormat = this.settingsService.dateFormat; + const prevTransactionDate: Date = this.currentAccountTransactionForm.value.transactionDate; + if (currentAccountTransactionFormData.transactionDate instanceof Date) { + currentAccountTransactionFormData.transactionDate = this.dateUtils.formatDate(prevTransactionDate, dateFormat); + } + const data = { + ...currentAccountTransactionFormData, + dateFormat, + locale + }; + data['transactionAmount'] = data['transactionAmount'] * 1; + this.currentAccountsService + .executeCurrentAccountTransactionsCommand(this.currentAccountId, this.transactionCommand, data) + .subscribe((res) => { + this.router.navigate(['../../transactions'], { relativeTo: this.route }); + }); + } +} diff --git a/src/app/current-accounts/current-account-actions/current-account-unassign-staff/current-account-unassign-staff.component.html b/src/app/current-accounts/current-account-actions/current-account-unassign-staff/current-account-unassign-staff.component.html new file mode 100644 index 000000000..caa02aa82 --- /dev/null +++ b/src/app/current-accounts/current-account-actions/current-account-unassign-staff/current-account-unassign-staff.component.html @@ -0,0 +1,44 @@ + + +
+ +
+ + + {{ 'labels.inputs.Unassignment Date' | translate }} + + + + @if (currentAccountUnassignStaffForm.controls.unassignedDate.hasError('required')) { + + {{ 'labels.inputs.Unassignment Date' | translate }} {{ 'labels.commons.is' | translate }} + {{ 'labels.commons.required' | translate }} + + } + + + + + + + +
+
+
diff --git a/src/app/current-accounts/current-account-actions/current-account-unassign-staff/current-account-unassign-staff.component.scss b/src/app/current-accounts/current-account-actions/current-account-unassign-staff/current-account-unassign-staff.component.scss new file mode 100644 index 000000000..7c3f1555b --- /dev/null +++ b/src/app/current-accounts/current-account-actions/current-account-unassign-staff/current-account-unassign-staff.component.scss @@ -0,0 +1,11 @@ +/** + * 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/. + */ + +.container { + max-width: 37rem; +} diff --git a/src/app/current-accounts/current-account-actions/current-account-unassign-staff/current-account-unassign-staff.component.ts b/src/app/current-accounts/current-account-actions/current-account-unassign-staff/current-account-unassign-staff.component.ts new file mode 100644 index 000000000..f622ebea5 --- /dev/null +++ b/src/app/current-accounts/current-account-actions/current-account-unassign-staff/current-account-unassign-staff.component.ts @@ -0,0 +1,103 @@ +/** + * 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, OnInit, Input, inject } from '@angular/core'; +import { UntypedFormGroup, UntypedFormBuilder, Validators, ReactiveFormsModule } from '@angular/forms'; +import { ActivatedRoute, Router, RouterLink } from '@angular/router'; +import { Dates } from 'app/core/utils/dates'; + +/** Custom Services */ +import { CurrentAccountsService } from 'app/current-accounts/current-accounts.service'; +import { SettingsService } from 'app/settings/settings.service'; +import { STANDALONE_SHARED_IMPORTS } from 'app/standalone-shared.module'; + +/** + * Current Account Unassign Staff Component + */ +@Component({ + selector: 'mifosx-current-account-account-unassign-staff', + templateUrl: './current-account-unassign-staff.component.html', + styleUrls: ['./current-account-unassign-staff.component.scss'], + imports: [ + ...STANDALONE_SHARED_IMPORTS + ] +}) +export class CurrentAccountUnassignStaffComponent implements OnInit { + private formBuilder = inject(UntypedFormBuilder); + private currentAccountsService = inject(CurrentAccountsService); + private dateUtils = inject(Dates); + private route = inject(ActivatedRoute); + private router = inject(Router); + private settingsService = inject(SettingsService); + + /** Minimum date allowed. */ + minDate = new Date(2000, 0, 1); + /** Maximum date allowed. */ + maxDate = new Date(); + /** Current Account Unassign Staff form. */ + currentAccountUnassignStaffForm: UntypedFormGroup; + /** Current Account Id */ + accountId: any; + + /** + * @param {FormBuilder} formBuilder Form Builder + * @param {CurrentAccountsService} currentAccountsService Current Accounts Service + * @param {Dates} dateUtils Date Utils + * @param {ActivatedRoute} route Activated Route + * @param {Router} router Router + * @param {SettingsService} settingsService Setting service + */ + constructor() { + this.accountId = this.route.snapshot.params['currentAccountId']; + } + + /** + * Creates the current account unassign staff form. + */ + ngOnInit() { + this.maxDate = this.settingsService.businessDate; + this.createCurrentAccountUnassignStaffForm(); + } + + /** + * Creates the current account unassign staff form. + */ + createCurrentAccountUnassignStaffForm() { + this.currentAccountUnassignStaffForm = this.formBuilder.group({ + unassignedDate: [ + '', + Validators.required + ] + }); + } + + /** + * Submits the form and unassigns staff of the current account, + * if successful redirects to the current account. + */ + submit() { + const currentAccountUnassignStaffFormData = this.currentAccountUnassignStaffForm.value; + const locale = this.settingsService.language.code; + const dateFormat = this.settingsService.dateFormat; + const prevUnassignmentDate: Date = this.currentAccountUnassignStaffForm.value.unassignedDate; + if (currentAccountUnassignStaffFormData.unassignedDate instanceof Date) { + currentAccountUnassignStaffFormData.unassignedDate = this.dateUtils.formatDate(prevUnassignmentDate, dateFormat); + } + const data = { + ...currentAccountUnassignStaffFormData, + dateFormat, + locale + }; + this.currentAccountsService + .executeCurrentAccountCommand(this.accountId, 'unassignCurrentAccountOfficer', data) + .subscribe(() => { + this.router.navigate(['../../transactions'], { relativeTo: this.route }); + }); + } +} diff --git a/src/app/current-accounts/current-account-actions/manage-current-account/manage-current-account.component.html b/src/app/current-accounts/current-account-actions/manage-current-account/manage-current-account.component.html new file mode 100644 index 000000000..8cb5e9224 --- /dev/null +++ b/src/app/current-accounts/current-account-actions/manage-current-account/manage-current-account.component.html @@ -0,0 +1,81 @@ + + +
+ + @if (transactionType.holdamount) { + {{ 'labels.heading.Hold Amount' | translate }} + } + @if (transactionType.blockaccount) { + {{ 'labels.heading.Block Current Account' | translate }} + } + +
+ + + {{ 'labels.inputs.Reason' | translate }} + + @for (codeValue of reasonOptions; track codeValue) { + + {{ codeValue.name }} + + } + + @if (manageCurrentAccountForm.controls.reasonForBlock.hasError('required')) { + + {{ 'labels.inputs.Reason' | translate }} {{ 'labels.commons.is' | translate }} + {{ 'labels.commons.required' | translate }} + + } + + + @if (transactionType.holdamount) { + + {{ 'labels.inputs.Transaction Date' | translate }} + + + + @if (manageCurrentAccountForm.controls.transactionDate.hasError('required')) { + + {{ 'labels.inputs.Transaction Date' | translate }} {{ 'labels.commons.is' | translate }} + {{ 'labels.commons.required' | translate }} + + } + + } + + @if (transactionType.holdamount) { + + + } + + + + + + +
+
+
diff --git a/src/app/current-accounts/current-account-actions/manage-current-account/manage-current-account.component.scss b/src/app/current-accounts/current-account-actions/manage-current-account/manage-current-account.component.scss new file mode 100644 index 000000000..e1ea46f9f --- /dev/null +++ b/src/app/current-accounts/current-account-actions/manage-current-account/manage-current-account.component.scss @@ -0,0 +1,11 @@ +/** + * 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/. + */ + +.container { + width: 37rem; +} diff --git a/src/app/current-accounts/current-account-actions/manage-current-account/manage-current-account.component.ts b/src/app/current-accounts/current-account-actions/manage-current-account/manage-current-account.component.ts new file mode 100644 index 000000000..f02b920a7 --- /dev/null +++ b/src/app/current-accounts/current-account-actions/manage-current-account/manage-current-account.component.ts @@ -0,0 +1,194 @@ +/** + * 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, Input, OnInit, inject } from '@angular/core'; +import { UntypedFormBuilder, UntypedFormGroup, Validators, ReactiveFormsModule } from '@angular/forms'; +import { ActivatedRoute, Router, RouterLink } from '@angular/router'; +import { Dates } from 'app/core/utils/dates'; +import { CurrentAccountsService } from 'app/current-accounts/current-accounts.service'; +import { SettingsService } from 'app/settings/settings.service'; +import { Currency } from 'app/shared/models/general.model'; +import { SystemService } from 'app/system/system.service'; +import { MatCard, MatCardTitle, MatCardContent, MatCardActions } from '@angular/material/card'; +import { InputAmountComponent } from '../../../shared/input-amount/input-amount.component'; +import { STANDALONE_SHARED_IMPORTS } from 'app/standalone-shared.module'; + +type TransactionCommandType = 'holdamount' | 'blockaccount' | 'blockdeposit' | 'blockwithdrawal'; + +interface TransactionType { + holdamount: boolean; + blockaccount: boolean; + blockdeposit: boolean; + blockwithdrawal: boolean; +} + +@Component({ + selector: 'mifosx-manage-current-account', + templateUrl: './manage-current-account.component.html', + styleUrls: ['./manage-current-account.component.scss'], + imports: [ + ...STANDALONE_SHARED_IMPORTS, + MatCardTitle, + InputAmountComponent + ] +}) +export class ManageCurrentAccountComponent implements OnInit { + private formBuilder = inject(UntypedFormBuilder); + private currentAccountsService = inject(CurrentAccountsService); + private dateUtils = inject(Dates); + private route = inject(ActivatedRoute); + private router = inject(Router); + private systemService = inject(SystemService); + private settingsService = inject(SettingsService); + + @Input() currency: Currency; + /** Minimum date allowed. */ + minDate = new Date(2000, 0, 1); + /** Maximum date allowed. */ + maxDate = new Date(); + /** Manage Current Account form. */ + manageCurrentAccountForm: UntypedFormGroup; + /** Current Account Id */ + currentAccountId: string; + transactionCommand: TransactionCommandType; + + reasonOptions: any = []; + + transactionType: TransactionType = { + holdamount: false, + blockaccount: false, + blockdeposit: false, + blockwithdrawal: false + }; + + /** + * @param {FormBuilder} formBuilder Form Builder + * @param {CurrentAccountsService} currentAccountsService Current Accounts Service + * @param {Dates} dateUtils Date Utils + * @param {ActivatedRoute} route Activated Route + * @param {Router} router Router + * @param {SettingsService} settingsService Setting service + */ + constructor() { + this.transactionCommand = this.route.snapshot.params['name'].toLowerCase().replaceAll(' ', ''); + this.transactionType[this.transactionCommand] = true; + this.currentAccountId = this.route.snapshot.params['currentAccountId']; + } + + /** + * Creates the post interest current account form. + */ + ngOnInit() { + this.maxDate = this.settingsService.businessDate; + this.createManageCurrentAccountForm(); + if ( + this.transactionType.holdamount || + this.transactionType.blockaccount || + this.transactionType.blockdeposit || + this.transactionType.blockwithdrawal + ) { + this.getCodeValues(); + } + } + + getCodeValues() { + let codeName = 'CurrentAccountTransactionFreezeReasons'; // Default Hold Amount + if (this.transactionType.blockaccount) { + codeName = 'CurrentAccountBlockReasons'; + } else if (this.transactionType.blockdeposit) { + codeName = 'CreditTransactionFreezeReasons'; + } else if (this.transactionType.blockwithdrawal) { + codeName = 'DebitTransactionFreezeReasons'; + } + + this.systemService.getCodes().subscribe((codes: any) => { + codes.some((code: any) => { + if (code.name === codeName) { + this.systemService.getCodeValues(code.id).subscribe((codeValues: any) => { + this.reasonOptions = codeValues; + return true; + }); + } + return false; + }); + }); + } + + /** + * Creates the manage current account form. + */ + createManageCurrentAccountForm() { + if (this.transactionType.holdamount) { + this.manageCurrentAccountForm = this.formBuilder.group({ + reasonForBlock: [ + '', + Validators.required + ], + transactionDate: [ + '', + Validators.required + ], + transactionAmount: [ + 0.0, + Validators.required + ] + }); + } else { + this.manageCurrentAccountForm = this.formBuilder.group({ + reasonForBlock: [ + '', + Validators.required + ] + }); + } + } + + submit() { + let command = ''; + let payload: { transactionAmount?: number; [key: string]: any } = {}; + + if (this.transactionType.holdamount) { + const manageCurrentAccountFormData = this.manageCurrentAccountForm.value; + const locale = this.settingsService.language.code; + const dateFormat = this.settingsService.dateFormat; + const prevTransactionDate: Date = this.manageCurrentAccountForm.value.transactionDate; + if (manageCurrentAccountFormData.transactionDate instanceof Date) { + manageCurrentAccountFormData.transactionDate = this.dateUtils.formatDate(prevTransactionDate, dateFormat); + } + payload = { + ...manageCurrentAccountFormData, + dateFormat, + locale + }; + command = 'holdAmount'; + payload['transactionAmount'] = payload['transactionAmount'] * 1; + + this.currentAccountsService + .executeCurrentAccountTransactionsCommand(this.currentAccountId, command, payload) + .subscribe((response: any) => { + this.router.navigate(['../../transactions'], { relativeTo: this.route }); + }); + } else { + payload = { + ...this.manageCurrentAccountForm.value + }; + command = 'block'; + if (this.transactionType.blockdeposit) { + command = 'blockCredit'; + } else if (this.transactionType.blockwithdrawal) { + command = 'blockDebit'; + } + + this.currentAccountsService + .executeCurrentAccountCommand(this.currentAccountId, command, payload) + .subscribe((response: any) => { + this.router.navigate(['../../transactions'], { relativeTo: this.route }); + }); + } + } +} diff --git a/src/app/current-accounts/current-account-actions/post-interest-as-on-current-account/post-interest-as-on-current-account.component.html b/src/app/current-accounts/current-account-actions/post-interest-as-on-current-account/post-interest-as-on-current-account.component.html new file mode 100644 index 000000000..97786760c --- /dev/null +++ b/src/app/current-accounts/current-account-actions/post-interest-as-on-current-account/post-interest-as-on-current-account.component.html @@ -0,0 +1,44 @@ + + +
+ +
+ + + {{ 'labels.inputs.Transaction Date' | translate }} + + + + @if (postInterestCurrentAccountForm.controls.transactionDate.hasError('required')) { + + {{ 'labels.inputs.Transaction Date' | translate }} {{ 'labels.commons.is' | translate }} + {{ 'labels.commons.required' | translate }} + + } + + + + + + + +
+
+
diff --git a/src/app/current-accounts/current-account-actions/post-interest-as-on-current-account/post-interest-as-on-current-account.component.scss b/src/app/current-accounts/current-account-actions/post-interest-as-on-current-account/post-interest-as-on-current-account.component.scss new file mode 100644 index 000000000..7c3f1555b --- /dev/null +++ b/src/app/current-accounts/current-account-actions/post-interest-as-on-current-account/post-interest-as-on-current-account.component.scss @@ -0,0 +1,11 @@ +/** + * 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/. + */ + +.container { + max-width: 37rem; +} diff --git a/src/app/current-accounts/current-account-actions/post-interest-as-on-current-account/post-interest-as-on-current-account.component.ts b/src/app/current-accounts/current-account-actions/post-interest-as-on-current-account/post-interest-as-on-current-account.component.ts new file mode 100644 index 000000000..a356df1fb --- /dev/null +++ b/src/app/current-accounts/current-account-actions/post-interest-as-on-current-account/post-interest-as-on-current-account.component.ts @@ -0,0 +1,104 @@ +/** + * 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, OnInit, inject } from '@angular/core'; +import { UntypedFormGroup, UntypedFormBuilder, Validators, ReactiveFormsModule } from '@angular/forms'; +import { ActivatedRoute, Router, RouterLink } from '@angular/router'; +import { Dates } from 'app/core/utils/dates'; + +/** Custom Services */ +import { CurrentAccountsService } from 'app/current-accounts/current-accounts.service'; +import { SettingsService } from 'app/settings/settings.service'; +import { STANDALONE_SHARED_IMPORTS } from 'app/standalone-shared.module'; + +/** + * Post Interest Current Account Component + */ +@Component({ + selector: 'mifosx-post-interest-as-on-current-account', + templateUrl: './post-interest-as-on-current-account.component.html', + styleUrls: ['./post-interest-as-on-current-account.component.scss'], + imports: [ + ...STANDALONE_SHARED_IMPORTS + ] +}) +export class PostInterestAsOnCurrentAccountComponent implements OnInit { + private formBuilder = inject(UntypedFormBuilder); + private currentAccountsService = inject(CurrentAccountsService); + private dateUtils = inject(Dates); + private route = inject(ActivatedRoute); + private router = inject(Router); + private settingsService = inject(SettingsService); + + /** Minimum date allowed. */ + minDate = new Date(2000, 0, 1); + /** Maximum date allowed. */ + maxDate = new Date(); + /** Post Interest Current Account form. */ + postInterestCurrentAccountForm: UntypedFormGroup; + /** Current Account Id */ + accountId: any; + + /** + * @param {FormBuilder} formBuilder Form Builder + * @param {CurrentAccountsService} currentAccountsService Current Accounts Service + * @param {Dates} dateUtils Date Utils + * @param {ActivatedRoute} route Activated Route + * @param {Router} router Router + * @param {SettingsService} settingsService Setting service + */ + constructor() { + this.accountId = this.route.snapshot.params['currentAccountId']; + } + + /** + * Creates the post interest current account form. + */ + ngOnInit() { + this.maxDate = this.settingsService.businessDate; + this.createPostInterestCurrentAccountForm(); + } + + /** + * Creates the post interest current account form. + */ + createPostInterestCurrentAccountForm() { + this.postInterestCurrentAccountForm = this.formBuilder.group({ + transactionDate: [ + '', + Validators.required + ] + }); + } + + /** + * Submits the form and post interests of the current account, + * if successful redirects to the current account. + */ + submit() { + const postInterestCurrentAccountFormData = this.postInterestCurrentAccountForm.value; + const locale = this.settingsService.language.code; + const dateFormat = this.settingsService.dateFormat; + const prevTransactionDate: Date = this.postInterestCurrentAccountForm.value.transactionDate; + if (postInterestCurrentAccountFormData.transactionDate instanceof Date) { + postInterestCurrentAccountFormData.transactionDate = this.dateUtils.formatDate(prevTransactionDate, dateFormat); + } + const data = { + ...postInterestCurrentAccountFormData, + IsPostInterestAsOn: true, + dateFormat, + locale + }; + this.currentAccountsService + .executeCurrentAccountTransactionsCommand(this.accountId, 'postInterestAsOn', data) + .subscribe(() => { + this.router.navigate(['../../transactions'], { relativeTo: this.route }); + }); + } +} diff --git a/src/app/current-accounts/current-account-actions/reject-current-account/reject-current-account.component.html b/src/app/current-accounts/current-account-actions/reject-current-account/reject-current-account.component.html new file mode 100644 index 000000000..79a2b26ff --- /dev/null +++ b/src/app/current-accounts/current-account-actions/reject-current-account/reject-current-account.component.html @@ -0,0 +1,51 @@ + + +
+ +
+ +
+ + {{ 'labels.inputs.Rejected On Date' | translate }} + + + + @if (rejectCurrentAccountForm.controls.rejectedOnDate.hasError('required')) { + + {{ 'labels.inputs.Rejected On Date' | translate }} {{ 'labels.commons.is' | translate }} + {{ 'labels.commons.required' | translate }} + + } + + + + {{ 'labels.inputs.Note' | translate }} + + +
+
+ + + + + +
+
+
diff --git a/src/app/current-accounts/current-account-actions/reject-current-account/reject-current-account.component.scss b/src/app/current-accounts/current-account-actions/reject-current-account/reject-current-account.component.scss new file mode 100644 index 000000000..7c3f1555b --- /dev/null +++ b/src/app/current-accounts/current-account-actions/reject-current-account/reject-current-account.component.scss @@ -0,0 +1,11 @@ +/** + * 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/. + */ + +.container { + max-width: 37rem; +} diff --git a/src/app/current-accounts/current-account-actions/reject-current-account/reject-current-account.component.ts b/src/app/current-accounts/current-account-actions/reject-current-account/reject-current-account.component.ts new file mode 100644 index 000000000..cb5071059 --- /dev/null +++ b/src/app/current-accounts/current-account-actions/reject-current-account/reject-current-account.component.ts @@ -0,0 +1,104 @@ +/** + * 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, OnInit, inject } from '@angular/core'; +import { UntypedFormGroup, UntypedFormBuilder, Validators, ReactiveFormsModule } from '@angular/forms'; +import { ActivatedRoute, Router, RouterLink } from '@angular/router'; +import { Dates } from 'app/core/utils/dates'; + +/** Custom Services */ +import { CurrentAccountsService } from 'app/current-accounts/current-accounts.service'; +import { SettingsService } from 'app/settings/settings.service'; +import { CdkTextareaAutosize } from '@angular/cdk/text-field'; +import { STANDALONE_SHARED_IMPORTS } from 'app/standalone-shared.module'; + +/** + * Reject Current Account Component + */ +@Component({ + selector: 'mifosx-reject-current-account', + templateUrl: './reject-current-account.component.html', + styleUrls: ['./reject-current-account.component.scss'], + imports: [ + ...STANDALONE_SHARED_IMPORTS, + CdkTextareaAutosize + ] +}) +export class RejectCurrentAccountComponent implements OnInit { + private formBuilder = inject(UntypedFormBuilder); + private currentAccountsService = inject(CurrentAccountsService); + private dateUtils = inject(Dates); + private route = inject(ActivatedRoute); + private router = inject(Router); + private settingsService = inject(SettingsService); + + /** Minimum date allowed. */ + minDate = new Date(2000, 0, 1); + /** Maximum date allowed. */ + maxDate = new Date(); + /** Reject Current Account form. */ + rejectCurrentAccountForm: UntypedFormGroup; + /** Current Account Id */ + accountId: any; + + /** + * @param {FormBuilder} formBuilder Form Builder + * @param {CurrentAccountsService} currentAccountsService Current Accounts Service + * @param {Dates} dateUtils Date Utils + * @param {ActivatedRoute} route Activated Route + * @param {Router} router Router + * @param {SettingsService} settingsService Setting service + */ + constructor() { + this.accountId = this.route.snapshot.params['currentAccountId']; + } + + /** + * Creates the reject current account form. + */ + ngOnInit() { + this.maxDate = this.settingsService.businessDate; + this.createRejectCurrentAccountForm(); + } + + /** + * Creates the reject current account form. + */ + createRejectCurrentAccountForm() { + this.rejectCurrentAccountForm = this.formBuilder.group({ + rejectedOnDate: [ + '', + Validators.required + ], + note: [''] + }); + } + + /** + * Submits the form and rejects the current account, + * if successful redirects to the current account. + */ + submit() { + const rejectCurrentAccountFormData = this.rejectCurrentAccountForm.value; + const locale = this.settingsService.language.code; + const dateFormat = this.settingsService.dateFormat; + const prevRejectedOnDate: Date = this.rejectCurrentAccountForm.value.rejectedOnDate; + if (rejectCurrentAccountFormData.rejectedOnDate instanceof Date) { + rejectCurrentAccountFormData.rejectedOnDate = this.dateUtils.formatDate(prevRejectedOnDate, dateFormat); + } + const data = { + ...rejectCurrentAccountFormData, + dateFormat, + locale + }; + this.currentAccountsService.executeCurrentAccountCommand(this.accountId, 'reject', data).subscribe(() => { + this.router.navigate(['../../'], { relativeTo: this.route }); + }); + } +} diff --git a/src/app/current-accounts/current-account-actions/undo-approval-current-account/undo-approval-current-account.component.html b/src/app/current-accounts/current-account-actions/undo-approval-current-account/undo-approval-current-account.component.html new file mode 100644 index 000000000..0358dc07b --- /dev/null +++ b/src/app/current-accounts/current-account-actions/undo-approval-current-account/undo-approval-current-account.component.html @@ -0,0 +1,29 @@ + + +
+ +
+ + + {{ 'labels.inputs.Note' | translate }} + + + + + + + + +
+
+
diff --git a/src/app/current-accounts/current-account-actions/undo-approval-current-account/undo-approval-current-account.component.scss b/src/app/current-accounts/current-account-actions/undo-approval-current-account/undo-approval-current-account.component.scss new file mode 100644 index 000000000..7c3f1555b --- /dev/null +++ b/src/app/current-accounts/current-account-actions/undo-approval-current-account/undo-approval-current-account.component.scss @@ -0,0 +1,11 @@ +/** + * 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/. + */ + +.container { + max-width: 37rem; +} diff --git a/src/app/current-accounts/current-account-actions/undo-approval-current-account/undo-approval-current-account.component.ts b/src/app/current-accounts/current-account-actions/undo-approval-current-account/undo-approval-current-account.component.ts new file mode 100644 index 000000000..528d4bebc --- /dev/null +++ b/src/app/current-accounts/current-account-actions/undo-approval-current-account/undo-approval-current-account.component.ts @@ -0,0 +1,80 @@ +/** + * 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, OnInit, inject } from '@angular/core'; +import { UntypedFormGroup, UntypedFormBuilder, ReactiveFormsModule } from '@angular/forms'; +import { ActivatedRoute, Router, RouterLink } from '@angular/router'; + +/** Custom Services */ +import { CurrentAccountsService } from 'app/current-accounts/current-accounts.service'; +import { CdkTextareaAutosize } from '@angular/cdk/text-field'; +import { STANDALONE_SHARED_IMPORTS } from 'app/standalone-shared.module'; + +/** + * Undo Approval Current Account Component + */ +@Component({ + selector: 'mifosx-undo-approval-current-account', + templateUrl: './undo-approval-current-account.component.html', + styleUrls: ['./undo-approval-current-account.component.scss'], + imports: [ + ...STANDALONE_SHARED_IMPORTS, + CdkTextareaAutosize + ] +}) +export class UndoApprovalCurrentAccountComponent implements OnInit { + private formBuilder = inject(UntypedFormBuilder); + private currentAccountsService = inject(CurrentAccountsService); + private route = inject(ActivatedRoute); + private router = inject(Router); + + /** Undo Approval Current Account form. */ + undoApprovalCurrentAccountForm: UntypedFormGroup; + /** Current Account Id */ + accountId: any; + + /** + * @param {FormBuilder} formBuilder Form Builder + * @param {CurrentAccountsService} currentAccountsService Current Accounts Service + * @param {ActivatedRoute} route Activated Route + * @param {Router} router Router + */ + constructor() { + this.accountId = this.route.snapshot.params['currentAccountId']; + } + + /** + * Creates the undo-approval current account form. + */ + ngOnInit() { + this.createUndoApprovalCurrentAccountForm(); + } + + /** + * Creates the undo-approval current account form. + */ + createUndoApprovalCurrentAccountForm() { + this.undoApprovalCurrentAccountForm = this.formBuilder.group({ + note: [''] + }); + } + + /** + * Submits the form and undo the approval of share account, + * if successful redirects to the share account. + */ + submit() { + const data = { + ...this.undoApprovalCurrentAccountForm.value + }; + this.currentAccountsService.executeCurrentAccountCommand(this.accountId, 'undoapproval', data).subscribe(() => { + this.router.navigate(['../../transactions'], { relativeTo: this.route }); + }); + } +} diff --git a/src/app/current-accounts/current-account-actions/withdraw-by-client-current-account/withdraw-by-client-current-account.component.html b/src/app/current-accounts/current-account-actions/withdraw-by-client-current-account/withdraw-by-client-current-account.component.html new file mode 100644 index 000000000..9dc3244dd --- /dev/null +++ b/src/app/current-accounts/current-account-actions/withdraw-by-client-current-account/withdraw-by-client-current-account.component.html @@ -0,0 +1,51 @@ + + +
+ +
+ +
+ + {{ 'labels.inputs.Withdrawn On Date' | translate }} + + + + @if (withdrawCurrentAccountForm.controls.withdrawnOnDate.hasError('required')) { + + {{ 'labels.inputs.Withdrawn On Date' | translate }} {{ 'labels.commons.is' | translate }} + {{ 'labels.commons.required' | translate }} + + } + + + + {{ 'labels.inputs.Note' | translate }} + + +
+
+ + + + + +
+
+
diff --git a/src/app/current-accounts/current-account-actions/withdraw-by-client-current-account/withdraw-by-client-current-account.component.scss b/src/app/current-accounts/current-account-actions/withdraw-by-client-current-account/withdraw-by-client-current-account.component.scss new file mode 100644 index 000000000..7c3f1555b --- /dev/null +++ b/src/app/current-accounts/current-account-actions/withdraw-by-client-current-account/withdraw-by-client-current-account.component.scss @@ -0,0 +1,11 @@ +/** + * 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/. + */ + +.container { + max-width: 37rem; +} diff --git a/src/app/current-accounts/current-account-actions/withdraw-by-client-current-account/withdraw-by-client-current-account.component.ts b/src/app/current-accounts/current-account-actions/withdraw-by-client-current-account/withdraw-by-client-current-account.component.ts new file mode 100644 index 000000000..b4cc91a92 --- /dev/null +++ b/src/app/current-accounts/current-account-actions/withdraw-by-client-current-account/withdraw-by-client-current-account.component.ts @@ -0,0 +1,106 @@ +/** + * 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, OnInit, inject } from '@angular/core'; +import { UntypedFormGroup, UntypedFormBuilder, Validators, ReactiveFormsModule } from '@angular/forms'; +import { ActivatedRoute, Router, RouterLink } from '@angular/router'; +import { Dates } from 'app/core/utils/dates'; + +/** Custom Services */ +import { CurrentAccountsService } from 'app/current-accounts/current-accounts.service'; +import { SettingsService } from 'app/settings/settings.service'; +import { CdkTextareaAutosize } from '@angular/cdk/text-field'; +import { STANDALONE_SHARED_IMPORTS } from 'app/standalone-shared.module'; + +/** + * Withdrawn by Client Current Account Component + */ +@Component({ + selector: 'mifosx-withdraw-by-client-current-account', + templateUrl: './withdraw-by-client-current-account.component.html', + styleUrls: ['./withdraw-by-client-current-account.component.scss'], + imports: [ + ...STANDALONE_SHARED_IMPORTS, + CdkTextareaAutosize + ] +}) +export class WithdrawByClientCurrentAccountComponent implements OnInit { + private formBuilder = inject(UntypedFormBuilder); + private currentAccountsService = inject(CurrentAccountsService); + private dateUtils = inject(Dates); + private route = inject(ActivatedRoute); + private router = inject(Router); + private settingsService = inject(SettingsService); + + /** Minimum date allowed. */ + minDate = new Date(2000, 0, 1); + /** Maximum date allowed. */ + maxDate = new Date(); + /** Withdraw Current Account form. */ + withdrawCurrentAccountForm: UntypedFormGroup; + /** Current Account Id */ + accountId: any; + + /** + * @param {FormBuilder} formBuilder Form Builder + * @param {CurrentAccountsService} currentAccountsService Current Accounts Service + * @param {Dates} dateUtils Date Utils + * @param {ActivatedRoute} route Activated Route + * @param {Router} router Router + * @param {SettingsService} settingsService Setting service + */ + constructor() { + this.accountId = this.route.snapshot.params['currentAccountId']; + } + + /** + * Creates the withdraw current account form. + */ + ngOnInit() { + this.maxDate = this.settingsService.businessDate; + this.createWithdrawCurrentAccountForm(); + } + + /** + * Creates the withdraw current account form. + */ + createWithdrawCurrentAccountForm() { + this.withdrawCurrentAccountForm = this.formBuilder.group({ + withdrawnOnDate: [ + '', + Validators.required + ], + note: [''] + }); + } + + /** + * Submits the form and withdraws the current account by client, + * if successful redirects to the current account. + */ + submit() { + const withdrawCurrentAccountFormData = this.withdrawCurrentAccountForm.value; + const locale = this.settingsService.language.code; + const dateFormat = this.settingsService.dateFormat; + const prevWithdrawnOnDate: Date = this.withdrawCurrentAccountForm.value.withdrawnOnDate; + if (withdrawCurrentAccountFormData.withdrawnOnDate instanceof Date) { + withdrawCurrentAccountFormData.withdrawnOnDate = this.dateUtils.formatDate(prevWithdrawnOnDate, dateFormat); + } + const data = { + ...withdrawCurrentAccountFormData, + dateFormat, + locale + }; + this.currentAccountsService + .executeCurrentAccountCommand(this.accountId, 'withdrawnByApplicant', data) + .subscribe(() => { + this.router.navigate(['../../'], { relativeTo: this.route }); + }); + } +} diff --git a/src/app/current-accounts/current-account-stepper/current-account-charges-step/current-account-charges-step.component.html b/src/app/current-accounts/current-account-stepper/current-account-charges-step/current-account-charges-step.component.html new file mode 100644 index 000000000..cf428e53f --- /dev/null +++ b/src/app/current-accounts/current-account-stepper/current-account-charges-step/current-account-charges-step.component.html @@ -0,0 +1,131 @@ + + +
+ + {{ 'labels.inputs.Charge' | translate }} + + @for (charge of chargeData | chargesFilter: chargesDataSource : currencyCode.value; track charge) { + + {{ charge.name }} + + } + + + +
+ +
+ + @if (chargesDataSource.length > 0) { + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
{{ 'labels.inputs.Name' | translate }} + {{ charge.name + ', ' + charge.currency.displaySymbol }} + {{ 'labels.inputs.Type' | translate }} + {{ charge.chargeCalculationType.value }} + {{ 'labels.inputs.Amount' | translate }} + {{ charge.amount | formatNumber }} + + {{ 'labels.inputs.Collected On' | translate }} + {{ charge.chargeTimeType.value | translateKey: 'catalogs' }} + {{ 'labels.inputs.Date' | translate }} + @if (charge.chargeTimeType.value === 'Specified due date' || charge.chargeTimeType.value === 'Weekly Fee') { + + {{ (charge.dueDate | dateFormat) || 'Unassigned' }} + + } + @if (charge.chargeTimeType.value === 'Monthly Fee' || charge.chargeTimeType.value === 'Annual Fee') { + + {{ (charge.feeOnMonthDay | dateFormat) || 'Unassigned' }} + + } + @if ( + !( + charge.chargeTimeType.value === 'Monthly Fee' || + charge.chargeTimeType.value === 'Annual Fee' || + charge.chargeTimeType.value === 'Specified due date' || + charge.chargeTimeType.value === 'Weekly Fee' + ) + ) { + N/A + } + @if ( + charge.chargeTimeType.value === 'Weekly Fee' || + charge.chargeTimeType.value === 'Monthly Fee' || + charge.chargeTimeType.value === 'Annual Fee' || + charge.chargeTimeType.value === 'Specified due date' + ) { + + } + {{ 'labels.inputs.Repayments Every' | translate }} + @if (charge.feeInterval) { + {{ charge.feeInterval }} + } + @if (!charge.feeInterval) { + {{ 'labels.inputs.Not Provided' | translate }} + } + @if (charge.chargeTimeType.value === 'Weekly Fee' || charge.chargeTimeType.value === 'Monthly Fee') { + + } + {{ 'labels.inputs.Actions' | translate }} + +
+ } + +
+ + +
+
diff --git a/src/app/current-accounts/current-account-stepper/current-account-charges-step/current-account-charges-step.component.scss b/src/app/current-accounts/current-account-stepper/current-account-charges-step/current-account-charges-step.component.scss new file mode 100644 index 000000000..9116c0c7c --- /dev/null +++ b/src/app/current-accounts/current-account-stepper/current-account-charges-step/current-account-charges-step.component.scss @@ -0,0 +1,19 @@ +/** + * 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/. + */ + +table { + width: 100%; +} + +.mat-elevation-z1 { + margin: 1em 0 1.5em; +} + +.margin-t { + margin-top: 1em; +} diff --git a/src/app/current-accounts/current-account-stepper/current-account-charges-step/current-account-charges-step.component.ts b/src/app/current-accounts/current-account-stepper/current-account-charges-step/current-account-charges-step.component.ts new file mode 100644 index 000000000..8ac930f21 --- /dev/null +++ b/src/app/current-accounts/current-account-stepper/current-account-charges-step/current-account-charges-step.component.ts @@ -0,0 +1,265 @@ +/** + * 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, OnInit, OnChanges, Input, inject } from '@angular/core'; +import { MatDialog } from '@angular/material/dialog'; +import { UntypedFormControl } from '@angular/forms'; + +/** Custom Dialogs */ +import { FormDialogComponent } from 'app/shared/form-dialog/form-dialog.component'; + +/** Custom Models */ +import { FormfieldBase } from 'app/shared/form-dialog/formfield/model/formfield-base'; +import { InputBase } from 'app/shared/form-dialog/formfield/model/input-base'; +import { DatepickerBase } from 'app/shared/form-dialog/formfield/model/datepicker-base'; +import { Dates } from 'app/core/utils/dates'; +import { + MatTableDataSource, + MatTable, + MatColumnDef, + MatHeaderCellDef, + MatHeaderCell, + MatCellDef, + MatCell, + MatHeaderRowDef, + MatHeaderRow, + MatRowDef, + MatRow +} from '@angular/material/table'; +import { TranslateService } from '@ngx-translate/core'; +import { MatButton, MatIconButton } from '@angular/material/button'; +import { FaIconComponent } from '@fortawesome/angular-fontawesome'; +import { MatStepperPrevious, MatStepperNext } from '@angular/material/stepper'; +import { ChargesFilterPipe } from '../../../pipes/charges-filter.pipe'; +import { DateFormatPipe } from '../../../pipes/date-format.pipe'; +import { FormatNumberPipe } from '../../../pipes/format-number.pipe'; +import { STANDALONE_SHARED_IMPORTS } from 'app/standalone-shared.module'; + +/** + * Current Account Charges Step + */ +@Component({ + selector: 'mifosx-current-account-charges-step', + templateUrl: './current-account-charges-step.component.html', + styleUrls: ['./current-account-charges-step.component.scss'], + imports: [ + ...STANDALONE_SHARED_IMPORTS, + FaIconComponent, + MatTable, + MatColumnDef, + MatHeaderCellDef, + MatHeaderCell, + MatCellDef, + MatCell, + MatIconButton, + MatHeaderRowDef, + MatHeaderRow, + MatRowDef, + MatRow, + MatStepperPrevious, + MatStepperNext, + ChargesFilterPipe, + DateFormatPipe, + FormatNumberPipe + ] +}) +export class CurrentAccountChargesStepComponent implements OnInit, OnChanges { + private dialog = inject(MatDialog); + private dateUtils = inject(Dates); + private translateService = inject(TranslateService); + + /** Current Account Product Template */ + @Input() currentAccountProductTemplate: any; + /** Current Account Template */ + @Input() currentAccountTemplate: any; + /** Currency Code */ + @Input() currencyCode: UntypedFormControl; + @Input() activeClientMembers?: any; + + /** Charge Data */ + chargeData: any = []; + /** Charges Data Source */ + chargesDataSource: {}[] = []; + /** Component is pristine if there has been no changes by user interaction */ + pristine = true; + /** For Edit Current Account Form */ + isChargesPatched = false; + /** Display columns for charges table */ + displayedColumns: string[] = [ + 'name', + 'chargeCalculationType', + 'amount', + 'chargeTimeType', + 'date', + 'repaymentsEvery', + 'action' + ]; + /** Table Data Source */ + dataSource: any; + /** Check for select all the Clients List */ + selectAllItems = false; + /** Loan Purpose Options */ + loanPurposeOptions: string[] = []; + /** Table Displayed Columns */ + displayedColumn: string[] = [ + 'check', + 'id', + 'name' + ]; + + ngOnInit() { + if (this.currentAccountTemplate) { + if (!this.isChargesPatched && this.currentAccountTemplate.charges) { + this.chargesDataSource = + this.currentAccountProductTemplate.charges.map((charge: any) => ({ ...charge, id: charge.chargeId })) || []; + this.isChargesPatched = true; + } else { + this.chargesDataSource = []; + } + } + } + + ngOnChanges() { + if (this.currentAccountProductTemplate) { + this.chargeData = this.currentAccountProductTemplate.chargeOptions; + this.chargesDataSource = + this.currentAccountProductTemplate.charges.map((charge: any) => ({ ...charge, id: charge.chargeId })) || []; + } + } + + /** + * Adds the charge to charges table + * @param {any} charge Charge + */ + addCharge(charge: any) { + this.chargesDataSource = this.chargesDataSource.concat([charge.value]); + charge.value = ''; + this.pristine = false; + } + + /** + * Edits the Charge Amount + * @param {any} charge Charge + */ + editChargeAmount(charge: any) { + const formfields: FormfieldBase[] = [ + new InputBase({ + controlName: 'amount', + label: this.translateService.instant('labels.inputs.Amount'), + value: charge.amount, + type: 'number', + required: false + }) + ]; + const data = { + title: this.translateService.instant('labels.heading.Edit Charge Amount'), + layout: { addButtonText: 'Confirm' }, + formfields: formfields + }; + const editNoteDialogRef = this.dialog.open(FormDialogComponent, { data }); + editNoteDialogRef.afterClosed().subscribe((response: any) => { + if (response.data) { + const newCharge = { ...charge, amount: response.data.value.amount }; + this.chargesDataSource.splice(this.chargesDataSource.indexOf(charge), 1, newCharge); + this.chargesDataSource = this.chargesDataSource.concat([]); + } + }); + this.pristine = false; + } + + /** + * Edits the Charge Date + * @param {any} charge Charge + */ + editChargeDate(charge: any) { + const formfields: FormfieldBase[] = [ + new DatepickerBase({ + controlName: 'date', + label: this.translateService.instant('labels.inputs.Date'), + value: charge.dueDate || charge.feeOnMonthDay || '', + type: 'datetime-local', + required: false + }) + ]; + const data = { + title: this.translateService.instant('labels.heading.Edit Charge Date'), + layout: { addButtonText: 'Confirm' }, + formfields: formfields + }; + const editNoteDialogRef = this.dialog.open(FormDialogComponent, { data }); + editNoteDialogRef.afterClosed().subscribe((response: any) => { + if (response.data) { + let newCharge: any; + const dateFormat = 'dd MMMM yyyy'; + const date = this.dateUtils.formatDate(response.data.value.date, dateFormat); + switch (charge.chargeTimeType.value) { + case 'Specified due date': + case 'Weekly Fee': + newCharge = { ...charge, dueDate: date }; + break; + case 'Annual Fee': + case 'Monthly Fee': + const dateMonthDay = this.dateUtils.formatDate(response.data.value.date, 'dd MMMM'); + newCharge = { ...charge, feeOnMonthDay: dateMonthDay }; + break; + } + this.chargesDataSource.splice(this.chargesDataSource.indexOf(charge), 1, newCharge); + this.chargesDataSource = this.chargesDataSource.concat([]); + } + }); + this.pristine = false; + } + + /** + * Edits the Charge Fee Interval + * @param {any} charge Charge + */ + editChargeFeeInterval(charge: any) { + const formfields: FormfieldBase[] = [ + new InputBase({ + controlName: 'feeInterval', + label: 'Fee Interval', + value: charge.feeInterval, + type: 'text', + required: false + }) + ]; + const data = { + title: 'Edit Charge Fee Interval', + layout: { addButtonText: 'Confirm' }, + formfields: formfields + }; + const editNoteDialogRef = this.dialog.open(FormDialogComponent, { data }); + editNoteDialogRef.afterClosed().subscribe((response: any) => { + if (response.data) { + const newCharge = { ...charge, feeInterval: response.data.value.feeInterval }; + this.chargesDataSource.splice(this.chargesDataSource.indexOf(charge), 1, newCharge); + this.chargesDataSource = this.chargesDataSource.concat([]); + } + }); + this.pristine = false; + } + + /** + * Removes the charge from charges table + * @param {any} charge Charge + */ + deleteCharge(charge: any) { + this.chargesDataSource.splice(this.chargesDataSource.indexOf(charge), 1); + this.chargesDataSource = this.chargesDataSource.concat([]); + this.pristine = false; + } + + /** + * Returns current account charges + */ + get currentAccountCharges() { + return { charges: this.chargesDataSource }; + } +} diff --git a/src/app/current-accounts/current-account-stepper/current-account-details-step/current-account-details-step.component.html b/src/app/current-accounts/current-account-stepper/current-account-details-step/current-account-details-step.component.html new file mode 100644 index 000000000..52c79fcb1 --- /dev/null +++ b/src/app/current-accounts/current-account-stepper/current-account-details-step/current-account-details-step.component.html @@ -0,0 +1,77 @@ + + +
+
+ + {{ 'labels.inputs.Product Name' | translate }} + + @for (product of productData; track product) { + + {{ product.name }} + + } + + + {{ 'labels.inputs.Product Name' | translate }} {{ 'labels.commons.is' | translate }} + {{ 'labels.commons.required' | translate }} + + + + @if (currentAccountProductSelected) { + + {{ 'labels.inputs.Submitted On' | translate }} + + + + + {{ 'labels.inputs.Submitted On' | translate }} {{ 'labels.commons.is' | translate }} + {{ 'labels.commons.required' | translate }} + + + } + + @if (currentAccountProductSelected) { + + {{ 'labels.inputs.Field Officer' | translate }} + + @for (officer of fieldOfficerData; track officer) { + + {{ officer.displayName }} + + } + + + } + + @if (currentAccountProductSelected) { + + {{ 'labels.inputs.External ID' | translate }} + + + } +
+ +
+ + +
+
diff --git a/src/app/current-accounts/current-account-stepper/current-account-details-step/current-account-details-step.component.scss b/src/app/current-accounts/current-account-stepper/current-account-details-step/current-account-details-step.component.scss new file mode 100644 index 000000000..af9f4c075 --- /dev/null +++ b/src/app/current-accounts/current-account-stepper/current-account-details-step/current-account-details-step.component.scss @@ -0,0 +1,7 @@ +/** + * 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/. + */ diff --git a/src/app/current-accounts/current-account-stepper/current-account-details-step/current-account-details-step.component.ts b/src/app/current-accounts/current-account-stepper/current-account-details-step/current-account-details-step.component.ts new file mode 100644 index 000000000..51a1d21e0 --- /dev/null +++ b/src/app/current-accounts/current-account-stepper/current-account-details-step/current-account-details-step.component.ts @@ -0,0 +1,137 @@ +/** + * 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, OnInit, Input, Output, EventEmitter, inject } from '@angular/core'; +import { UntypedFormGroup, UntypedFormBuilder, Validators, ReactiveFormsModule } from '@angular/forms'; + +/** Custom Services */ +import { CurrentAccountsService } from 'app/current-accounts/current-accounts.service'; +import { SettingsService } from 'app/settings/settings.service'; +import { MatStepperPrevious, MatStepperNext } from '@angular/material/stepper'; +import { FaIconComponent } from '@fortawesome/angular-fontawesome'; +import { STANDALONE_SHARED_IMPORTS } from 'app/standalone-shared.module'; + +/** + * Current Account Details Step + */ +@Component({ + selector: 'mifosx-current-account-details-step', + templateUrl: './current-account-details-step.component.html', + styleUrls: ['./current-account-details-step.component.scss'], + imports: [ + ...STANDALONE_SHARED_IMPORTS, + MatStepperPrevious, + FaIconComponent, + MatStepperNext + ] +}) +export class CurrentAccountDetailsStepComponent implements OnInit { + private formBuilder = inject(UntypedFormBuilder); + private currentAccountsService = inject(CurrentAccountsService); + private settingsService = inject(SettingsService); + + /** Current Account Template */ + @Input() currentAccountTemplate: any; + + /** Minimum date allowed. */ + minDate = new Date(2000, 0, 1); + /** Maximum date allowed. */ + maxDate = new Date(); + /** Product Data */ + productData: any; + /** Field Officer Data */ + fieldOfficerData: any; + /** For edit current account form */ + isFieldOfficerPatched = false; + /** Current Account Details Form */ + currentAccountDetailsForm: UntypedFormGroup; + + currentAccountProductSelected = false; + + /** Current Account Template with product data */ + @Output() currentAccountProductTemplate = new EventEmitter(); + + /** + * Sets share account details form. + * @param {FormBuilder} formBuilder Form Builder. + * @param {CurrentAccountsService} currentAccountsService Current Accounts Service. + * @param {SettingsService} settingsService Setting service + */ + constructor() { + this.createCurrentAccountDetailsForm(); + } + + ngOnInit() { + this.maxDate = this.settingsService.businessDate; + this.buildDependencies(); + if (this.currentAccountTemplate) { + this.productData = this.currentAccountTemplate.productOptions; + if (this.currentAccountTemplate.currentAccountProductId) { + this.currentAccountDetailsForm.patchValue({ + productId: this.currentAccountTemplate.currentAccountProductId, + submittedOnDate: + this.currentAccountTemplate.timeline.submittedOnDate && + new Date(this.currentAccountTemplate.timeline.submittedOnDate), + externalId: this.currentAccountTemplate.externalId + }); + } else { + this.currentAccountDetailsForm.patchValue({ + submittedOnDate: new Date() + }); + } + } + } + + /** + * Creates current account details form. + */ + createCurrentAccountDetailsForm() { + this.currentAccountDetailsForm = this.formBuilder.group({ + productId: [ + '', + Validators.required + ], + submittedOnDate: [ + '', + Validators.required + ], + fieldOfficerId: [''], + externalId: [''] + }); + } + + /** + * Fetches current account product template on productId value changes + */ + buildDependencies() { + const entityId = this.currentAccountTemplate.groupId || this.currentAccountTemplate.clientId; + this.currentAccountDetailsForm.get('productId').valueChanges.subscribe((productId: string) => { + this.currentAccountsService + .getCurrentAccountTemplate(entityId, productId, this.currentAccountTemplate.groupId ? true : false) + .subscribe((response: any) => { + this.currentAccountProductTemplate.emit(response); + this.fieldOfficerData = response.fieldOfficerOptions; + this.currentAccountProductSelected = true; + if (!this.isFieldOfficerPatched && this.currentAccountTemplate.fieldOfficerId) { + this.currentAccountDetailsForm.get('fieldOfficerId').patchValue(this.currentAccountTemplate.fieldOfficerId); + this.isFieldOfficerPatched = true; + } else { + this.currentAccountDetailsForm.get('fieldOfficerId').patchValue(''); + } + }); + }); + } + + /** + * Returns current account form value. + */ + get currentAccountDetails() { + return this.currentAccountDetailsForm.getRawValue(); + } +} diff --git a/src/app/current-accounts/current-account-stepper/current-account-preview-step/current-account-preview-step.component.html b/src/app/current-accounts/current-account-stepper/current-account-preview-step/current-account-preview-step.component.html new file mode 100644 index 000000000..e503d952c --- /dev/null +++ b/src/app/current-accounts/current-account-stepper/current-account-preview-step/current-account-preview-step.component.html @@ -0,0 +1,269 @@ + + +
+

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

+ + +
+ {{ 'labels.inputs.Product' | translate }} + {{ + currentAccount.productId | find: currentAccountTemplate.productOptions : 'id' : 'name' + }} +
+ +
+ {{ 'labels.inputs.Submitted On' | translate }} + {{ currentAccount.submittedOnDate | dateFormat }} +
+ +
+ {{ 'labels.inputs.External Id' | translate }} + @if (currentAccount.externalId) { + + + + } + @if (!currentAccount.externalId) { + + {{ 'labels.inputs.Unassigned' | translate }} + + } +
+ +
+ {{ 'labels.inputs.Field Officer' | translate }} + {{ + currentAccount.fieldOfficerId | find: currentAccountProductTemplate.fieldOfficerOptions : 'id' : 'displayName' + }} +
+ + @if (activeClientMembers) { +

{{ 'labels.heading.Client Members' | translate }}

+ +
+ + + + + + + + + + + + + +
{{ 'labels.inputs.Client ID' | translate }}{{ element.id }}{{ 'labels.inputs.Client Name' | translate }}{{ element.displayName }}
+
+ } + +

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

+ + +
+ {{ 'labels.inputs.Currency' | translate }} + {{ currentAccountTermsForm.get('currencyCode').value }} +
+ +
+ {{ 'labels.inputs.Decimal Places' | translate }} + {{ currentAccountTermsForm.get('decimal').value }} +
+ +
+ {{ 'labels.inputs.Nominal Annual Interest' | translate }}: + {{ currentAccount.nominalAnnualInterestRate | formatNumber }} % +
+ +
+ {{ 'labels.inputs.Interest Compounding Period' | translate }}: + {{ + getCatalogTranslation( + currentAccount.interestCompoundingPeriodType + | find: currentAccountProductTemplate.interestCompoundingPeriodTypeOptions : 'id' : 'value' + ) + }} +
+ +
+ {{ 'labels.inputs.Interest Posting Period' | translate }}: + {{ + getCatalogTranslation( + currentAccount.interestPostingPeriodType + | find: currentAccountProductTemplate.interestPostingPeriodTypeOptions : 'id' : 'value' + ) + }} +
+ +
+ {{ 'labels.inputs.Interest Calculated using' | translate }}: + {{ + getCatalogTranslation( + currentAccount.interestCalculationType + | find: currentAccountProductTemplate.interestCalculationTypeOptions : 'id' : 'value' + ) + }} +
+ +
+ {{ 'labels.inputs.Days in Year' | translate }}: + {{ + getCatalogTranslation( + currentAccount.interestCalculationDaysInYearType + | find: currentAccountProductTemplate.interestCalculationDaysInYearTypeOptions : 'id' : 'value' + ) + }} +
+ + @if (currentAccount.minRequiredOpeningBalance) { +
+ {{ 'labels.inputs.Minimum Opening Balance' | translate }}: + {{ currentAccount.minRequiredOpeningBalance | formatNumber }} +
+ } + +
+ {{ 'labels.inputs.Apply Withdrawal Fee for Transfers' | translate }}: + {{ currentAccount.withdrawalFeeForTransfers | yesNo }} +
+ + @if (currentAccount.lockinPeriodFrequency) { +
+ {{ 'labels.inputs.Lock-in Period' | translate }}: + {{ + getCatalogTranslation( + currentAccount.lockinPeriodFrequency + + ' ' + + (currentAccount.lockinPeriodFrequencyType + | find: currentAccountProductTemplate.lockinPeriodFrequencyTypeOptions : 'id' : 'value') + ) + }} +
+ } + +
+ {{ 'labels.inputs.Is Overdraft Allowed' | translate }}: + {{ currentAccount.allowOverdraft | yesNo }} +
+ + @if (currentAccount.minOverdraftForInterestCalculation) { +
+ {{ 'labels.inputs.Minimum Overdraft Required for Interest Calculation' | translate }}: + {{ currentAccount.minOverdraftForInterestCalculation | formatNumber }} +
+ } + + @if (currentAccount.nominalAnnualInterestRateOverdraft) { +
+ {{ 'labels.inputs.Nominal Annual Interest for Overdraft' | translate }}: + {{ currentAccount.nominalAnnualInterestRateOverdraft | formatNumber }} % +
+ } + + @if (currentAccount.overdraftLimit) { +
+ {{ 'labels.inputs.Maximum Overdraft Amount Limit' | translate }}: + {{ currentAccount.overdraftLimit | formatNumber }} +
+ } + +
+ {{ 'labels.inputs.Enforce Minimum Balance' | translate }}: + {{ currentAccount.enforceMinRequiredBalance | yesNo }} +
+ + @if (currentAccount.minRequiredBalance) { +
+ {{ 'labels.inputs.Minimum Balance' | translate }}: + {{ currentAccount.minRequiredBalance }} +
+ } + + @if (currentAccount.charges.length) { +
+

{{ 'labels.inputs.Charges' | translate }}

+ + + + + + + + + + + + + + + + + + + + + + + + + + + + +
{{ 'labels.inputs.Name' | translate }} + {{ charge.name + ', ' + charge.currency.displaySymbol }} + {{ 'labels.inputs.Type' | translate }} + {{ charge.chargeCalculationType.value | translateKey: 'catalogs' }} + {{ 'labels.inputs.Amount' | translate }} + {{ charge.amount | formatNumber }} + {{ 'labels.inputs.Date' | translate }} + @if (charge.chargeTimeType.value === 'Specified due date' || charge.chargeTimeType.value === 'Weekly Fee') { + + {{ (charge.dueDate | dateFormat) || 'Unassigned' }} + + } + @if (charge.chargeTimeType.value === 'Monthly Fee' || charge.chargeTimeType.value === 'Annual Fee') { + + {{ (charge.feeOnMonthDay | dateFormat) || 'Unassigned' }} + + } + @if ( + !( + charge.chargeTimeType.value === 'Monthly Fee' || + charge.chargeTimeType.value === 'Annual Fee' || + charge.chargeTimeType.value === 'Specified due date' || + charge.chargeTimeType.value === 'Weekly Fee' + ) + ) { + N/A + } + {{ 'labels.inputs.Repayments Every' | translate }} + {{ charge.feeInterval || 'Not Provided' }} + {{ 'labels.inputs.Collected On' | translate }} + {{ charge.chargeTimeType.value }} +
+
+ } +
+ +
+ + + +
diff --git a/src/app/current-accounts/current-account-stepper/current-account-preview-step/current-account-preview-step.component.scss b/src/app/current-accounts/current-account-stepper/current-account-preview-step/current-account-preview-step.component.scss new file mode 100644 index 000000000..6a4a4b201 --- /dev/null +++ b/src/app/current-accounts/current-account-stepper/current-account-preview-step/current-account-preview-step.component.scss @@ -0,0 +1,34 @@ +/** + * 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/. + */ + +table { + width: 100%; +} + +.mat-elevation-z1 { + margin: 1em 0 1.5em; +} + +h2, +h3, +h4 { + margin: 0; + font-weight: 500; +} + +span { + margin: 0.5em 0; +} + +mat-divider { + margin: 0 0 0.5em; +} + +.margin-t { + margin-top: 1em; +} diff --git a/src/app/current-accounts/current-account-stepper/current-account-preview-step/current-account-preview-step.component.ts b/src/app/current-accounts/current-account-stepper/current-account-preview-step/current-account-preview-step.component.ts new file mode 100644 index 000000000..50cd396a7 --- /dev/null +++ b/src/app/current-accounts/current-account-stepper/current-account-preview-step/current-account-preview-step.component.ts @@ -0,0 +1,107 @@ +/** + * 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, Input, Output, EventEmitter, OnChanges, inject } from '@angular/core'; +import { TranslateService } from '@ngx-translate/core'; +import { + MatTableDataSource, + MatTable, + MatColumnDef, + MatHeaderCellDef, + MatHeaderCell, + MatCellDef, + MatCell, + MatHeaderRowDef, + MatHeaderRow, + MatRowDef, + MatRow +} from '@angular/material/table'; +import { MatDivider } from '@angular/material/divider'; +import { ExternalIdentifierComponent } from '../../../shared/external-identifier/external-identifier.component'; +import { MatStepperPrevious } from '@angular/material/stepper'; +import { FaIconComponent } from '@fortawesome/angular-fontawesome'; +import { FindPipe } from '../../../pipes/find.pipe'; +import { DateFormatPipe } from '../../../pipes/date-format.pipe'; +import { FormatNumberPipe } from '../../../pipes/format-number.pipe'; +import { YesnoPipe } from '../../../pipes/yesno.pipe'; +import { STANDALONE_SHARED_IMPORTS } from 'app/standalone-shared.module'; + +/** + * Current account preview step + */ +@Component({ + selector: 'mifosx-current-account-account-preview-step', + templateUrl: './current-account-preview-step.component.html', + styleUrls: ['./current-account-preview-step.component.scss'], + imports: [ + ...STANDALONE_SHARED_IMPORTS, + MatDivider, + ExternalIdentifierComponent, + MatTable, + MatColumnDef, + MatHeaderCellDef, + MatHeaderCell, + MatCellDef, + MatCell, + MatHeaderRowDef, + MatHeaderRow, + MatRowDef, + MatRow, + MatStepperPrevious, + FaIconComponent, + FindPipe, + DateFormatPipe, + FormatNumberPipe, + YesnoPipe + ] +}) +export class CurrentAccountPreviewStepComponent implements OnChanges { + private translateService = inject(TranslateService); + + /** Current Account Product Template */ + @Input() currentAccountProductTemplate: any; + /** Current Account Template */ + @Input() currentAccountTemplate: any; + /** Current Account Terms Form */ + @Input() currentAccountTermsForm: any; + /** Current Account */ + @Input() currentAccount: any; + @Input() activeClientMembers?: any; + + /** Table Data Source */ + dataSource: any; + + /** Display columns for charges table */ + chargesDisplayedColumns: string[] = [ + 'name', + 'chargeCalculationType', + 'amount', + 'chargeTimeType', + 'date', + 'repaymentsEvery' + ]; + /** Columns to be displayed in active members table. */ + membersDisplayedColumns: string[] = [ + 'id', + 'name' + ]; + + /** Form submission event */ + @Output() submitEvent = new EventEmitter(); + + ngOnChanges(): void { + if (this.activeClientMembers?.length > 0) { + this.dataSource = new MatTableDataSource(this.activeClientMembers.filter((member: any) => member.selected)); + } + } + + getCatalogTranslation(text: string): string { + return this.translateService.instant('labels.catalogs.' + text); + } +} diff --git a/src/app/current-accounts/current-account-stepper/current-account-terms-step/current-account-terms-step.component.html b/src/app/current-accounts/current-account-stepper/current-account-terms-step/current-account-terms-step.component.html new file mode 100644 index 000000000..e3327969c --- /dev/null +++ b/src/app/current-accounts/current-account-stepper/current-account-terms-step/current-account-terms-step.component.html @@ -0,0 +1,174 @@ + + +
+
+ + {{ 'labels.inputs.Currency' | translate }} + + + + + {{ 'labels.inputs.Decimal Places' | translate }} + + + + + {{ 'labels.inputs.Nominal Annual Interest' | translate }} + + + {{ 'labels.inputs.Nominal Annual Interest' | translate }} {{ 'labels.commons.is' | translate }} + {{ 'labels.commons.required' | translate }} + + + + + {{ 'labels.inputs.Interest Compounding Period' | translate }} + + @for (interestCompoundingPeriodType of interestCompoundingPeriodTypeData; track interestCompoundingPeriodType) { + + {{ interestCompoundingPeriodType.value | translateKey: 'catalogs' }} + + } + + + {{ 'labels.inputs.Interest Compounding Period' | translate }} {{ 'labels.commons.is' | translate }} + {{ 'labels.commons.required' | translate }} + + + + + {{ 'labels.inputs.Interest Posting Period' | translate }} + + @for (interestPostingPeriodType of interestPostingPeriodTypeData; track interestPostingPeriodType) { + + {{ interestPostingPeriodType.value | translateKey: 'catalogs' }} + + } + + + {{ 'labels.inputs.Interest Posting Period' | translate }} {{ 'labels.commons.is' | translate }} + {{ 'labels.commons.required' | translate }} + + + + + {{ 'labels.inputs.Interest Calculated using' | translate }} + + @for (interestCalculationType of interestCalculationTypeData; track interestCalculationType) { + + {{ interestCalculationType.value | translateKey: 'catalogs' }} + + } + + + {{ 'labels.inputs.Interest Calculated using' | translate }} {{ 'labels.commons.is' | translate }} + {{ 'labels.commons.required' | translate }} + + + + + {{ 'labels.inputs.Days in Year' | translate }} + + @for ( + interestCalculationDaysInYearType of interestCalculationDaysInYearTypeData; + track interestCalculationDaysInYearType + ) { + + {{ interestCalculationDaysInYearType.value | translateKey: 'catalogs' }} + + } + + + {{ 'labels.inputs.Days in Year' | translate }} {{ 'labels.commons.is' | translate }} + {{ 'labels.commons.required' | translate }} + + + + + {{ 'labels.inputs.Minimum Opening Balance' | translate }} + + + + + {{ 'labels.inputs.Apply Withdrawal Fee for Transfers' | translate }} + + +

{{ 'labels.inputs.Lock-in Period' | translate }}

+ + + {{ 'labels.inputs.Frequency' | translate }} + + + + + {{ 'labels.inputs.Type' | translate }} + + @for (lockinPeriodFrequencyType of lockinPeriodFrequencyTypeData; track lockinPeriodFrequencyType) { + + {{ lockinPeriodFrequencyType.value | translateKey: 'catalogs' }} + + } + + + + + +

{{ 'labels.inputs.Overdraft' | translate }}

+ + + {{ 'labels.inputs.Is Overdraft Allowed' | translate }} + + + @if (currentAccountTermsForm.value.allowOverdraft) { +
+ + {{ 'labels.inputs.Minimum Overdraft Required for Interest Calculation' | translate }} + + + + {{ 'labels.inputs.Nominal Annual Interest for Overdraft' | translate }} + + + + {{ 'labels.inputs.Maximum Overdraft Amount Limit' | translate }} + + +
+ } + + + + + {{ 'labels.inputs.Enforce Minimum Balance' | translate }} + + + + {{ 'labels.inputs.Minimum Balance' | translate }} + + + + @if (currentAccountTermsForm.controls.minBalanceForInterestCalculation.value) { + + {{ 'labels.inputs.Balance Required for Interest Calculation' | translate }} + + + } +
+ +
+ + +
+
diff --git a/src/app/current-accounts/current-account-stepper/current-account-terms-step/current-account-terms-step.component.scss b/src/app/current-accounts/current-account-stepper/current-account-terms-step/current-account-terms-step.component.scss new file mode 100644 index 000000000..c95283bdc --- /dev/null +++ b/src/app/current-accounts/current-account-stepper/current-account-terms-step/current-account-terms-step.component.scss @@ -0,0 +1,32 @@ +/** + * 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/. + */ + +h4 { + font-weight: 500; + margin: 1em 0; +} + +h3 { + font-weight: 500; +} + +mat-divider { + margin: 1em 0 2em; +} + +.margin-v { + margin: 1em 0; +} + +.margin-b { + margin: 0 0 1em; +} + +.margin-t { + margin-top: 1em; +} diff --git a/src/app/current-accounts/current-account-stepper/current-account-terms-step/current-account-terms-step.component.ts b/src/app/current-accounts/current-account-stepper/current-account-terms-step/current-account-terms-step.component.ts new file mode 100644 index 000000000..2111b43d7 --- /dev/null +++ b/src/app/current-accounts/current-account-stepper/current-account-terms-step/current-account-terms-step.component.ts @@ -0,0 +1,199 @@ +/** + * 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, OnChanges, OnInit, Input, inject } from '@angular/core'; +import { + UntypedFormGroup, + UntypedFormBuilder, + Validators, + UntypedFormControl, + ReactiveFormsModule +} from '@angular/forms'; +import { SettingsService } from 'app/settings/settings.service'; +import { MatCheckbox } from '@angular/material/checkbox'; +import { MatDivider } from '@angular/material/divider'; +import { MatStepperPrevious, MatStepperNext } from '@angular/material/stepper'; +import { FaIconComponent } from '@fortawesome/angular-fontawesome'; +import { STANDALONE_SHARED_IMPORTS } from 'app/standalone-shared.module'; + +/** + * Current Account Terms Step + */ +@Component({ + selector: 'mifosx-current-account-terms-step', + templateUrl: './current-account-terms-step.component.html', + styleUrls: ['./current-account-terms-step.component.scss'], + imports: [ + ...STANDALONE_SHARED_IMPORTS, + MatCheckbox, + MatDivider, + MatStepperPrevious, + FaIconComponent, + MatStepperNext + ] +}) +export class CurrentAccountTermsStepComponent implements OnChanges, OnInit { + private formBuilder = inject(UntypedFormBuilder); + private settingsService = inject(SettingsService); + + /** Current Account and Product Template */ + @Input() currentAccountProductTemplate: any; + /** Current Account Template */ + @Input() currentAccountTemplate: any; + + /** Minimum date allowed. */ + minDate = new Date(2000, 0, 1); + /** Maximum date allowed. */ + maxDate = new Date(); + /** Current Account Terms Form */ + currentAccountTermsForm: UntypedFormGroup; + /** Lockin Period Frequency Type Data */ + lockinPeriodFrequencyTypeData: any; + /** Interest Compounding Period Type Data */ + interestCompoundingPeriodTypeData: any; + /** Interest Posting Period Type Data */ + interestPostingPeriodTypeData: any; + /** Interest Calculation Type Data */ + interestCalculationTypeData: any; + /** Interest Calculation Days in Year Data */ + interestCalculationDaysInYearTypeData: any; + + /** + * @param {FormBuilder} formBuilder Form Builder + * @param {SettingsService} settingsService Setting service + */ + constructor() { + this.createCurrentAccountTermsForm(); + this.buildDependencies(); + } + + ngOnChanges() { + if (this.currentAccountProductTemplate) { + this.currentAccountTermsForm.patchValue({ + currencyCode: this.currentAccountProductTemplate.currency.code, + decimal: this.currentAccountProductTemplate.currency.decimalPlaces, + minBalanceForInterestCalculation: this.currentAccountProductTemplate.minBalanceForInterestCalculation, + nominalAnnualInterestRate: this.currentAccountProductTemplate.nominalAnnualInterestRate, + interestCompoundingPeriodType: this.currentAccountProductTemplate.interestCompoundingPeriodType.id, + interestPostingPeriodType: this.currentAccountProductTemplate.interestPostingPeriodType.id, + interestCalculationType: this.currentAccountProductTemplate.interestCalculationType.id, + interestCalculationDaysInYearType: this.currentAccountProductTemplate.interestCalculationDaysInYearType.id, + minRequiredOpeningBalance: this.currentAccountProductTemplate.minRequiredOpeningBalance, + allowOverdraft: this.currentAccountProductTemplate.allowOverdraft, + overdraftLimit: this.currentAccountProductTemplate.overdraftLimit, + enforceMinRequiredBalance: this.currentAccountProductTemplate.enforceMinRequiredBalance, + minOverdraftForInterestCalculation: this.currentAccountProductTemplate.minOverdraftForInterestCalculation, + nominalAnnualInterestRateOverdraft: this.currentAccountProductTemplate.nominalAnnualInterestRateOverdraft, + minRequiredBalance: this.currentAccountProductTemplate.minRequiredBalance, + withdrawalFeeForTransfers: this.currentAccountProductTemplate.withdrawalFeeForTransfers + }); + this.setOptions(); + } + } + + ngOnInit() { + this.maxDate = this.settingsService.businessDate; + if (this.currentAccountTemplate) { + this.currentAccountTermsForm.patchValue({ + nominalAnnualInterestRate: this.currentAccountTemplate.nominalAnnualInterestRate, + interestCompoundingPeriodType: this.currentAccountTemplate.interestCompoundingPeriodType.id, + interestPostingPeriodType: this.currentAccountTemplate.interestPostingPeriodType.id, + interestCalculationType: this.currentAccountTemplate.interestCalculationType.id, + interestCalculationDaysInYearType: this.currentAccountTemplate.interestCalculationDaysInYearType.id, + minRequiredOpeningBalance: this.currentAccountTemplate.minRequiredOpeningBalance, + withdrawalFeeForTransfers: this.currentAccountTemplate.withdrawalFeeForTransfers, + lockinPeriodFrequency: this.currentAccountTemplate.lockinPeriodFrequency, + lockinPeriodFrequencyType: + this.currentAccountTemplate.lockinPeriodFrequencyType && + this.currentAccountTemplate.lockinPeriodFrequencyType.id, + allowOverdraft: this.currentAccountTemplate.allowOverdraft, + enforceMinRequiredBalance: this.currentAccountTemplate.enforceMinRequiredBalance, + minRequiredBalance: this.currentAccountTemplate.minRequiredBalance + }); + } + } + + /** + * Creates current account terms form. + */ + createCurrentAccountTermsForm() { + this.currentAccountTermsForm = this.formBuilder.group({ + currencyCode: [{ value: '', disabled: true }], + decimal: [{ value: '', disabled: true }], + nominalAnnualInterestRate: [ + '', + Validators.required + ], + interestCompoundingPeriodType: [ + '', + Validators.required + ], + interestPostingPeriodType: [ + '', + Validators.required + ], + interestCalculationType: [ + '', + Validators.required + ], + interestCalculationDaysInYearType: [ + '', + Validators.required + ], + minRequiredOpeningBalance: [''], + withdrawalFeeForTransfers: [false], + lockinPeriodFrequency: [''], + lockinPeriodFrequencyType: [''], + allowOverdraft: [false], + enforceMinRequiredBalance: [false], + minRequiredBalance: [''], + minBalanceForInterestCalculation: [{ value: '', disabled: true }] + }); + } + + /** + * Sets all select dropdown options. + */ + setOptions() { + this.lockinPeriodFrequencyTypeData = this.currentAccountProductTemplate.lockinPeriodFrequencyTypeOptions; + this.interestCompoundingPeriodTypeData = this.currentAccountProductTemplate.interestCompoundingPeriodTypeOptions; + this.interestPostingPeriodTypeData = this.currentAccountProductTemplate.interestPostingPeriodTypeOptions; + this.interestCalculationTypeData = this.currentAccountProductTemplate.interestCalculationTypeOptions; + this.interestCalculationDaysInYearTypeData = + this.currentAccountProductTemplate.interestCalculationDaysInYearTypeOptions; + } + + /** + * Subscribes to value changes and sets new form controls accordingly. + */ + buildDependencies() { + this.currentAccountTermsForm.get('allowOverdraft').valueChanges.subscribe((allowOverdraft: any) => { + if (allowOverdraft) { + this.currentAccountTermsForm.addControl('minOverdraftForInterestCalculation', new UntypedFormControl('')); + this.currentAccountTermsForm.addControl('nominalAnnualInterestRateOverdraft', new UntypedFormControl('')); + this.currentAccountTermsForm.addControl('overdraftLimit', new UntypedFormControl('')); + } else { + this.currentAccountTermsForm.removeControl('minOverdraftForInterestCalculation'); + this.currentAccountTermsForm.removeControl('nominalAnnualInterestRateOverdraft'); + this.currentAccountTermsForm.removeControl('overdraftLimit'); + } + }); + } + + /** + * Returns current account terms form value. + */ + get currentAccountTerms() { + const payload = this.currentAccountTermsForm.getRawValue(); + delete payload.currencyCode; + delete payload.decimal; + delete payload.minBalanceForInterestCalculation; // Backend is not accepting minBalanceForInterestCalculation value + return payload; + } +} diff --git a/src/app/current-accounts/current-account-stepper/current-accounts-active-client-members/current-accounts-active-client-members.component.html b/src/app/current-accounts/current-account-stepper/current-accounts-active-client-members/current-accounts-active-client-members.component.html new file mode 100644 index 000000000..ba1d4b5dd --- /dev/null +++ b/src/app/current-accounts/current-account-stepper/current-accounts-active-client-members/current-accounts-active-client-members.component.html @@ -0,0 +1,46 @@ + + +@if (activeClientMembers) { +
+ + + + + + + + + + + + + + + + + + +
+ + + + {{ 'labels.inputs.Client ID' | translate }}{{ element.id }}{{ 'labels.inputs.Client Name' | translate }}{{ element.displayName }}
+
+} + +
+ + +
diff --git a/src/app/current-accounts/current-account-stepper/current-accounts-active-client-members/current-accounts-active-client-members.component.scss b/src/app/current-accounts/current-account-stepper/current-accounts-active-client-members/current-accounts-active-client-members.component.scss new file mode 100644 index 000000000..9116c0c7c --- /dev/null +++ b/src/app/current-accounts/current-account-stepper/current-accounts-active-client-members/current-accounts-active-client-members.component.scss @@ -0,0 +1,19 @@ +/** + * 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/. + */ + +table { + width: 100%; +} + +.mat-elevation-z1 { + margin: 1em 0 1.5em; +} + +.margin-t { + margin-top: 1em; +} diff --git a/src/app/current-accounts/current-account-stepper/current-accounts-active-client-members/current-accounts-active-client-members.component.ts b/src/app/current-accounts/current-account-stepper/current-accounts-active-client-members/current-accounts-active-client-members.component.ts new file mode 100644 index 000000000..95e0095ba --- /dev/null +++ b/src/app/current-accounts/current-account-stepper/current-accounts-active-client-members/current-accounts-active-client-members.component.ts @@ -0,0 +1,84 @@ +/** + * 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, OnChanges, Input } from '@angular/core'; +import { + MatTableDataSource, + MatTable, + MatColumnDef, + MatHeaderCellDef, + MatHeaderCell, + MatCellDef, + MatCell, + MatHeaderRowDef, + MatHeaderRow, + MatRowDef, + MatRow +} from '@angular/material/table'; +import { MatCheckbox } from '@angular/material/checkbox'; +import { ReactiveFormsModule, FormsModule } from '@angular/forms'; +import { MatStepperPrevious, MatStepperNext } from '@angular/material/stepper'; +import { FaIconComponent } from '@fortawesome/angular-fontawesome'; +import { STANDALONE_SHARED_IMPORTS } from 'app/standalone-shared.module'; + +@Component({ + selector: 'mifosx-current-account-active-client-members', + templateUrl: './current-accounts-active-client-members.component.html', + styleUrls: ['./current-accounts-active-client-members.component.scss'], + imports: [ + ...STANDALONE_SHARED_IMPORTS, + MatTable, + MatColumnDef, + MatHeaderCellDef, + MatHeaderCell, + MatCheckbox, + FormsModule, + MatCellDef, + MatCell, + MatHeaderRowDef, + MatHeaderRow, + MatRowDef, + MatRow, + MatStepperPrevious, + FaIconComponent, + MatStepperNext + ] +}) +export class CurrentAccountActiveClientMembersComponent implements OnInit { + @Input() activeClientMembers?: any; + selectAllItems = false; + displayedColumn: string[] = [ + 'check', + 'id', + 'name' + ]; + + constructor() {} + + dataSource: any; + ngOnInit(): void { + this.dataSource = new MatTableDataSource(this.activeClientMembers); + } + get selectedClientMembers() { + return { selectedMembers: this.activeClientMembers.filter((item: any) => item.selected) }; + } + + /** Toggle all checks */ + toggleSelects() { + for (const member of this.activeClientMembers) { + member.selected = this.selectAllItems; + } + } + + /** Check if all the checks are selected */ + toggleSelect() { + const len = this.activeClientMembers.length; + this.selectAllItems = + len === 0 ? false : this.activeClientMembers.filter((item: any) => item.selected).length === len; + } +} diff --git a/src/app/current-accounts/current-account-view/charges-tab/charges-tab.component.html b/src/app/current-accounts/current-account-view/charges-tab/charges-tab.component.html new file mode 100644 index 000000000..b9d8150c5 --- /dev/null +++ b/src/app/current-accounts/current-account-view/charges-tab/charges-tab.component.html @@ -0,0 +1,157 @@ + + +
+
+
+

{{ 'labels.inputs.Charges' | translate }}

+
+ @if (this.chargesData) { +
+ +
+ } +
+ +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
{{ 'labels.inputs.Name' | translate }}{{ charge.name }}{{ 'labels.inputs.Charge Type' | translate }}{{ charge.penalty ? 'Penalty' : ('Fee' | translateKey: 'catalogs') }}{{ 'labels.inputs.Payment Due At' | translate }}{{ charge.chargeTimeType.value | translateKey: 'catalogs' }}{{ 'labels.inputs.Due As Of' | translate }}{{ charge.dueDate | dateFormat }}{{ 'labels.inputs.Repeats On (M/d)' | translate }}{{ charge.feeOnMonthDay ? charge.feeOnMonthDay.join('/') : '' }}{{ 'labels.inputs.Calculation Type' | translate }}{{ charge.chargeCalculationType.value | translateKey: 'catalogs' }}{{ 'labels.inputs.Due' | translate }} + {{ charge.amount | currency: charge.currency.code : 'symbol-narrow' : '1.2-2' }} + {{ 'labels.inputs.Paid' | translate }} + {{ charge.amountPaid | currency: charge.currency.code : 'symbol-narrow' : '1.2-2' }} + {{ 'labels.inputs.Waived' | translate }} + {{ charge.amountWaived | currency: charge.currency.code : 'symbol-narrow' : '1.2-2' }} + {{ 'labels.inputs.Outstanding' | translate }} + {{ charge.amountOutstanding | currency: charge.currency.code : 'symbol-narrow' : '1.2-2' }} + {{ 'labels.inputs.Actions' | translate }} + @if (currentAccountData.status.value === 'Submitted and pending approval') { + + + + + } + @if (currentAccountData.status.value === 'Active' && charge.amountOutstanding !== 0) { + + + + @if (isRecurringCharge(charge)) { + + } + + } +
+
+
diff --git a/src/app/current-accounts/current-account-view/charges-tab/charges-tab.component.scss b/src/app/current-accounts/current-account-view/charges-tab/charges-tab.component.scss new file mode 100644 index 000000000..aff23bdef --- /dev/null +++ b/src/app/current-accounts/current-account-view/charges-tab/charges-tab.component.scss @@ -0,0 +1,35 @@ +/** + * 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/. + */ + +.tab-container { + padding: 1%; + margin: 1%; + + h3 { + margin: 1% auto; + } + + .action-button { + margin-left: auto; + } + + table { + width: 100%; + + .account-action-button { + min-width: 26px; + padding: 0 6px; + margin: 4px; + line-height: 25px; + } + + .select-row:hover { + cursor: pointer; + } + } +} diff --git a/src/app/current-accounts/current-account-view/charges-tab/charges-tab.component.ts b/src/app/current-accounts/current-account-view/charges-tab/charges-tab.component.ts new file mode 100644 index 000000000..f8ce4763f --- /dev/null +++ b/src/app/current-accounts/current-account-view/charges-tab/charges-tab.component.ts @@ -0,0 +1,314 @@ +/** + * 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, OnInit, ViewChild, inject } from '@angular/core'; +import { ActivatedRoute, Router, RouterLink } from '@angular/router'; +import { MatDialog } from '@angular/material/dialog'; +import { + MatTableDataSource, + MatTable, + MatColumnDef, + MatHeaderCellDef, + MatHeaderCell, + MatCellDef, + MatCell, + MatHeaderRowDef, + MatHeaderRow, + MatRowDef, + MatRow +} from '@angular/material/table'; + +/** Custom Services */ +import { CurrentAccountsService } from 'app/current-accounts/current-accounts.service'; +import { SettingsService } from 'app/settings/settings.service'; + +/** Custom Dialogs */ +import { FormDialogComponent } from 'app/shared/form-dialog/form-dialog.component'; +import { WaiveChargeDialogComponent } from '../custom-dialogs/waive-charge-dialog/waive-charge-dialog.component'; +import { InactivateChargeDialogComponent } from '../custom-dialogs/inactivate-charge-dialog/inactivate-charge-dialog.component'; +import { DeleteDialogComponent } from 'app/shared/delete-dialog/delete-dialog.component'; + +/** Custom Models */ +import { FormfieldBase } from 'app/shared/form-dialog/formfield/model/formfield-base'; +import { InputBase } from 'app/shared/form-dialog/formfield/model/input-base'; +import { DatepickerBase } from 'app/shared/form-dialog/formfield/model/datepicker-base'; +import { Dates } from 'app/core/utils/dates'; +import { TranslateService } from '@ngx-translate/core'; +import { CurrencyPipe } from '@angular/common'; +import { MatTooltip } from '@angular/material/tooltip'; +import { DateFormatPipe } from '../../../pipes/date-format.pipe'; +import { STANDALONE_SHARED_IMPORTS } from 'app/standalone-shared.module'; + +/** + * Charges Tab Component + */ +@Component({ + selector: 'mifosx-charges-tab', + templateUrl: './charges-tab.component.html', + styleUrls: ['./charges-tab.component.scss'], + imports: [ + ...STANDALONE_SHARED_IMPORTS, + MatTable, + MatColumnDef, + MatHeaderCellDef, + MatHeaderCell, + MatCellDef, + MatCell, + MatTooltip, + MatHeaderRowDef, + MatHeaderRow, + MatRowDef, + MatRow, + CurrencyPipe, + DateFormatPipe + ] +}) +export class ChargesTabComponent implements OnInit { + private currentAccountsService = inject(CurrentAccountsService); + private route = inject(ActivatedRoute); + private dateUtils = inject(Dates); + private router = inject(Router); + private dialog = inject(MatDialog); + private settingsService = inject(SettingsService); + private translateService = inject(TranslateService); + + /** Current Account Data */ + currentAccountData: any; + /** Charges Data */ + chargesData: any[]; + /** Data source for charges table. */ + dataSource: MatTableDataSource; + /** Toggles Charges Table */ + showInactiveCharges = false; + /** Columns to be displayed in charges table. */ + displayedColumns: string[] = [ + 'name', + 'feeOrPenalty', + 'paymentDueAt', + 'dueAsOf', + 'repeatsOn', + 'calculationType', + 'due', + 'paid', + 'waived', + 'outstanding', + 'actions' + ]; + + /** Charges Table Reference */ + @ViewChild('chargesTable', { static: true }) chargesTableRef: MatTable; + + /** + * Retrieves the Current account data from `resolve`. + * @param {CurrentAccountsService} currentAccountsService Current Accounts Service + * @param {ActivatedRoute} route Activated Route. + * @param {Router} router Router for navigation. + * @param {MatDialog} dialog Dialog reference. + * @param {Dates} dateUtils Date Utils. + * @param {SettingsService} settingsService Setting service + */ + constructor() { + this.route.parent.data.subscribe((data: { currentAccountData: any }) => { + this.currentAccountData = data.currentAccountData; + this.chargesData = this.currentAccountData.charges; + }); + } + + ngOnInit() { + const activeCharges = this.chargesData ? this.chargesData.filter((charge) => charge.isActive) : []; + this.dataSource = new MatTableDataSource(activeCharges); + } + + /** + * Toggles datasource for active/inactive charges. + */ + toggleCharges() { + this.showInactiveCharges = !this.showInactiveCharges; + if (!this.showInactiveCharges) { + const activeCharges = this.chargesData.filter((charge) => charge.isActive); + this.dataSource.data = activeCharges; + } else { + const inActiveCharges = this.chargesData.filter((charge) => !charge.isActive); + this.dataSource.data = inActiveCharges; + } + this.chargesTableRef.renderRows(); + } + + /** + * Pays the charge. + * @param {any} chargeId Charge Id + */ + payCharge(chargeId: any) { + const formfields: FormfieldBase[] = [ + new InputBase({ + controlName: 'amount', + label: 'Amount', + value: '', + type: 'number', + required: true + }), + new DatepickerBase({ + controlName: 'dueDate', + label: 'Payment Date', + value: '', + type: 'date', + required: true + }) + ]; + const data = { + title: `Pay Charge ${chargeId}`, + layout: { addButtonText: 'Confirm' }, + formfields: formfields + }; + const payChargeDialogRef = this.dialog.open(FormDialogComponent, { data }); + payChargeDialogRef.afterClosed().subscribe((response: any) => { + if (response.data) { + const locale = this.settingsService.language.code; + const dateFormat = this.settingsService.dateFormat; + const dataObject = { + ...response.data.value, + dueDate: this.dateUtils.formatDate(response.data.value.dueDate, dateFormat), + dateFormat, + locale + }; + this.currentAccountsService + .executeCurrentAccountChargesCommand(this.currentAccountData.id, 'paycharge', dataObject, chargeId) + .subscribe(() => { + this.reload(); + }); + } + }); + } + + /** + * Waive's the charge + * @param {any} chargeId Charge Id + */ + waiveCharge(chargeId: any) { + const waiveChargeDialogRef = this.dialog.open(WaiveChargeDialogComponent, { data: { id: chargeId } }); + waiveChargeDialogRef.afterClosed().subscribe((response: any) => { + if (response.confirm) { + this.currentAccountsService + .executeCurrentAccountChargesCommand(this.currentAccountData.id, 'waive', {}, chargeId) + .subscribe(() => { + this.reload(); + }); + } + }); + } + + /** + * Inactivate's the charge + * @param {any} chargeId Charge Id + */ + inactivateCharge(chargeId: any) { + const inactivateChargeDialogRef = this.dialog.open(InactivateChargeDialogComponent, { data: { id: chargeId } }); + inactivateChargeDialogRef.afterClosed().subscribe((response: any) => { + if (response.confirm) { + this.currentAccountsService + .executeCurrentAccountChargesCommand(this.currentAccountData.id, 'inactivate', {}, chargeId) + .subscribe(() => { + this.reload(); + }); + } + }); + } + + /** + * Edits the charge + * @param {any} charge Charge + */ + editCharge(charge: any) { + const formfields: FormfieldBase[] = [ + new InputBase({ + controlName: 'amount', + label: 'Amount', + value: charge.amount || charge.amountOrPercentage, + type: 'number', + required: true + }) + ]; + const data = { + title: `Edit Charge ${charge.id}`, + layout: { addButtonText: 'Confirm' }, + formfields: formfields + }; + const editChargeDialogRef = this.dialog.open(FormDialogComponent, { data }); + editChargeDialogRef.afterClosed().subscribe((response: any) => { + if (response.data) { + const locale = this.settingsService.language.code; + const dateFormat = this.settingsService.dateFormat; + const dataObject = { + ...response.data.value, + dateFormat, + locale + }; + this.currentAccountsService + .editCurrentAccountCharge(this.currentAccountData.id, dataObject, charge.id) + .subscribe(() => { + this.reload(); + }); + } + }); + } + + /** + * Deletes the charge + * @param {any} chargeId Charge Id + */ + deleteCharge(chargeId: any) { + const deleteChargeDialogRef = this.dialog.open(DeleteDialogComponent, { + data: { deleteContext: `charge id:${chargeId}` } + }); + deleteChargeDialogRef.afterClosed().subscribe((response: any) => { + if (response.delete) { + this.currentAccountsService.deleteCurrentAccountCharge(this.currentAccountData.id, chargeId).subscribe(() => { + this.reload(); + }); + } + }); + } + + /** + * Checks if charge is recurring. + * @param {any} charge Charge + */ + isRecurringCharge(charge: any) { + return ( + charge.chargeTimeType.value === 'Monthly Fee' || + charge.chargeTimeType.value === 'Annual Fee' || + charge.chargeTimeType.value === 'Weekly Fee' + ); + } + + /** + * Stops the propagation to view charge page. + * @param $event Mouse Event + */ + routeEdit($event: MouseEvent) { + $event.stopPropagation(); + } + + /** + * Refetches data fot the component + * TODO: Replace by a custom reload component instead of hard-coded back-routing. + */ + private reload() { + const url: string = this.router.url; + const refreshUrl: string = this.router.url.slice( + 0, + this.router.url.indexOf('current-accounts') + 'current-accounts'.length + ); + this.router.navigateByUrl(refreshUrl, { skipLocationChange: true }).then(() => this.router.navigate([url])); + } + + viewAllChargeButtons(text: string): string { + return this.translateService.instant('labels.buttons.' + text); + } +} 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 new file mode 100644 index 000000000..ebbf6adb4 --- /dev/null +++ b/src/app/current-accounts/current-account-view/current-account-view.component.html @@ -0,0 +1,235 @@ + + + diff --git a/src/app/current-accounts/current-account-view/current-account-view.component.scss b/src/app/current-accounts/current-account-view/current-account-view.component.scss new file mode 100644 index 000000000..6a53700a4 --- /dev/null +++ b/src/app/current-accounts/current-account-view/current-account-view.component.scss @@ -0,0 +1,17 @@ +/** + * 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/. + */ + +mat-card-title { + display: flex; +} + +.product-row, +.account-number-row, +.external-id-row { + display: block; +} 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 new file mode 100644 index 000000000..67746577e --- /dev/null +++ b/src/app/current-accounts/current-account-view/current-account-view.component.ts @@ -0,0 +1,387 @@ +/** + * 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, OnInit, inject } from '@angular/core'; +import { ActivatedRoute, Router, RouterLinkActive, RouterLink, RouterOutlet } from '@angular/router'; +import { MatDialog } from '@angular/material/dialog'; + +/** Custom Dialogs */ +import { DeleteDialogComponent } from 'app/shared/delete-dialog/delete-dialog.component'; +import { CalculateInterestDialogComponent } from './custom-dialogs/calculate-interest-dialog/calculate-interest-dialog.component'; +import { PostInterestDialogComponent } from './custom-dialogs/post-interest-dialog/post-interest-dialog.component'; +import { ToggleWithholdTaxDialogComponent } from './custom-dialogs/toggle-withhold-tax-dialog/toggle-withhold-tax-dialog.component'; + +/** Custom Buttons Configuration */ +import { CurrentAccountButtonsConfiguration } from './current-accounts-buttons.config'; +import { CurrentAccountsService } from '../current-accounts.service'; +import { ConfirmationDialogComponent } from 'app/shared/confirmation-dialog/confirmation-dialog.component'; +import { Currency } from 'app/shared/models/general.model'; +import { TranslateService } from '@ngx-translate/core'; + +/** Environment Configuration */ +import { environment } from '../../../environments/environment'; +import { + MatCard, + MatCardHeader, + MatCardTitleGroup, + MatCardMdImage, + MatCardTitle, + MatCardContent +} from '@angular/material/card'; +import { MatTooltip } from '@angular/material/tooltip'; +import { NgClass, CurrencyPipe } from '@angular/common'; +import { LongTextComponent } from '../../shared/long-text/long-text.component'; +import { AccountNumberComponent } from '../../shared/account-number/account-number.component'; +import { MatIconButton } from '@angular/material/button'; +import { MatMenuTrigger, MatMenu, MatMenuItem } from '@angular/material/menu'; +import { MatIcon } from '@angular/material/icon'; +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'; + +/** + * Current Account View Component + */ +@Component({ + selector: 'mifosx-current-account-account-view', + templateUrl: './current-account-view.component.html', + styleUrls: ['./current-account-view.component.scss'], + imports: [ + ...STANDALONE_SHARED_IMPORTS, + MatCardHeader, + MatCardTitleGroup, + MatCardMdImage, + MatTooltip, + MatCardTitle, + NgClass, + LongTextComponent, + AccountNumberComponent, + MatIconButton, + MatMenuTrigger, + MatIcon, + FaIconComponent, + MatMenu, + MatMenuItem, + MatTabNav, + MatTabLink, + RouterLinkActive, + MatTabNavPanel, + RouterOutlet, + CurrencyPipe, + StatusLookupPipe + ] +}) +export class CurrentAccountViewComponent implements OnInit { + private route = inject(ActivatedRoute); + private router = inject(Router); + private currentAccountsService = inject(CurrentAccountsService); + private translateService = inject(TranslateService); + dialog = inject(MatDialog); + + /** Current Account Data */ + currentAccountData: any; + /** Current Account Data Tables */ + currentAccountDatatables: any; + /** Button Configurations */ + buttonConfig: CurrentAccountButtonsConfiguration; + /** Entity Type */ + entityType: string; + + isActive = false; + currency: Currency; + + /** + * Fetches current account data from `resolve` + * @param {ActivatedRoute} route Activated Route + * @param {Router} router Router + * @param {CurrentAccountsService} currentAccountsService Current Accounts Service + */ + constructor() { + this.route.data.subscribe((data: { currentAccountData: any; currentAccountDatatables: any }) => { + this.currentAccountData = data.currentAccountData; + this.currency = this.currentAccountData.currency; + this.currentAccountDatatables = data.currentAccountDatatables; + }); + if (this.router.url.includes('clients')) { + this.entityType = 'Client'; + } else if (this.router.url.includes('groups')) { + this.entityType = 'Group'; + } else if (this.router.url.includes('centers')) { + this.entityType = 'Center'; + } + } + + ngOnInit() { + this.setConditionalButtons(); + } + + /** + * Adds options to button config. conditionaly. + */ + setConditionalButtons() { + const status = this.currentAccountData.status.value; + this.isActive = status === 'Active'; + const subStatus = this.currentAccountData.subStatus; + this.buttonConfig = new CurrentAccountButtonsConfiguration(status, subStatus); + if (this.currentAccountData.clientId) { + this.buttonConfig.addOption({ + name: 'Transfer Funds', + taskPermissionName: 'CREATE_ACCOUNTTRANSFER' + }); + } + if (this.currentAccountData.externalId && environment.mifosInterbankTransfersEnabled) { + this.buttonConfig.addOption({ + name: 'Interbank Transfer', + taskPermissionName: 'CREATE_ACCOUNTTRANSFER' + }); + } + if (!this.currentAccountData.fieldOfficerId) { + this.buttonConfig.addOption({ + name: 'Assign Staff', + taskPermissionName: 'UPDATESAVINGSOFFICER_CURRENTACCOUNT' + }); + } else { + this.buttonConfig.addOption({ + name: 'Unassign Staff', + taskPermissionName: 'REMOVESAVINGSOFFICER_CURRENTACCOUNT' + }); + } + if (this.currentAccountData.charges) { + const charges: any[] = this.currentAccountData.charges; + charges.forEach((charge: any) => { + if (charge.name === 'Annual fee - INR') { + this.buttonConfig.addOption({ + name: 'Apply Annual Fees', + taskPermissionName: 'APPLYANNUALFEE_CURRENTACCOUNT' + }); + } + }); + } + if (this.currentAccountData.taxGroup) { + if (this.currentAccountData.withHoldTax) { + this.buttonConfig.addOption({ + name: 'Disable Withhold Tax', + taskPermissionName: 'UPDATEWITHHOLDTAX_CURRENTACCOUNT' + }); + } else { + this.buttonConfig.addOption({ + name: 'Enable Withhold Tax', + taskPermissionName: 'UPDATEWITHHOLDTAX_CURRENTACCOUNT' + }); + } + } + } + + /** + * Refetches data foe the component + * TODO: Replace by a custom reload component instead of hard-coded back-routing. + */ + private reload() { + const url: string = this.router.url; + const refreshUrl: string = this.router.url.slice( + 0, + this.router.url.indexOf('current-accounts') + 'current-accounts'.length + ); + this.router.navigateByUrl(refreshUrl, { skipLocationChange: true }).then(() => this.router.navigate([url])); + } + + /** + * Performs action button/option action. + * @param {string} name action name. + */ + doAction(name: string) { + switch (name) { + case 'Approve': + case 'Reject': + case 'Deposit': + case 'Activate': + case 'Close': + case 'Undo Approval': + case 'Post Interest As On': + case 'Assign Staff': + case 'Add Charge': + case 'Hold Amount': + case 'Block Account': + case 'Block Deposit': + case 'Block Withdrawal': + case 'Unassign Staff': + case 'Withdrawn by Client': + case 'Apply Annual Fees': + this.router.navigate([`actions/${name}`], { relativeTo: this.route }); + break; + case 'Withdrawal': + this.router.navigate([`actions/Withdrawal`], { relativeTo: this.route }); + break; + case 'Modify Application': + this.router.navigate(['edit'], { relativeTo: this.route }); + break; + case 'Delete': + this.deleteCurrentAccount(); + break; + case 'Calculate Interest': + this.calculateInterest(); + break; + case 'Post Interest': + this.postInterest(); + break; + case 'Enable Withhold Tax': + this.enableWithHoldTax(); + break; + case 'Disable Withhold Tax': + this.disableWithHoldTax(); + break; + case 'Transfer Funds': + const queryParams: any = { + interbank: false, + currentAccountId: this.currentAccountData.id, + accountType: 'fromcurrentaccount' + }; + this.router.navigate(['transfer-funds/make-account-transfer'], { + relativeTo: this.route, + queryParams: queryParams, + state: { balance: this.currentAccountData.summary.availableBalance } + }); + break; + case 'Interbank Transfer': { + const queryParams: any = { + interbank: true, + currentAccountId: this.currentAccountData.id, + accountType: 'interbank' + }; + this.router.navigate(['transfer-funds/make-account-transfer'], { + relativeTo: this.route, + queryParams: queryParams, + state: { balance: this.currentAccountData.summary.availableBalance } + }); + break; + } + case 'Unblock Account': + case 'Unblock Deposit': + case 'Unblock Withdrawal': + this.unblockCurrentAccount(name); + break; + } + } + + /** + * Deletes Current Account. + */ + private deleteCurrentAccount() { + const deleteCurrentAccountDialogRef = this.dialog.open(DeleteDialogComponent, { + data: { deleteContext: `current account with id: ${this.currentAccountData.id}` } + }); + deleteCurrentAccountDialogRef.afterClosed().subscribe((response: any) => { + if (response.delete) { + this.currentAccountsService.deleteCurrentAccount(this.currentAccountData.id).subscribe(() => { + this.router.navigate(['../../'], { relativeTo: this.route }); + }); + } + }); + } + + /** + * Calculates current account's interest + */ + private calculateInterest() { + const calculateInterestAccountDialogRef = this.dialog.open(CalculateInterestDialogComponent); + calculateInterestAccountDialogRef.afterClosed().subscribe((response: any) => { + if (response.confirm) { + this.currentAccountsService + .executeCurrentAccountCommand(this.currentAccountData.id, 'calculateInterest', {}) + .subscribe(() => { + this.reload(); + }); + } + }); + } + + /** + * Posts current account's interest + */ + private postInterest() { + const postInterestAccountDialogRef = this.dialog.open(PostInterestDialogComponent); + postInterestAccountDialogRef.afterClosed().subscribe((response: any) => { + if (response.confirm) { + this.currentAccountsService + .executeCurrentAccountCommand(this.currentAccountData.id, 'postInterest', {}) + .subscribe(() => { + this.reload(); + }); + } + }); + } + + /** + * Enables withhold tax for current account. + */ + private enableWithHoldTax() { + const deleteCurrentAccountDialogRef = this.dialog.open(ToggleWithholdTaxDialogComponent, { + data: { isEnable: true } + }); + deleteCurrentAccountDialogRef.afterClosed().subscribe((response: any) => { + if (response.confirm) { + this.currentAccountsService + .executeCurrentAccountUpdateCommand(this.currentAccountData.id, 'updateWithHoldTax', { withHoldTax: true }) + .subscribe(() => { + this.reload(); + }); + } + }); + } + + /** + * Disables withhold tac for current account + */ + private disableWithHoldTax() { + const disableWithHoldTaxDialogRef = this.dialog.open(ToggleWithholdTaxDialogComponent, { + data: { isEnable: false } + }); + disableWithHoldTaxDialogRef.afterClosed().subscribe((response: any) => { + if (response.confirm) { + this.currentAccountsService + .executeCurrentAccountUpdateCommand(this.currentAccountData.id, 'updateWithHoldTax', { withHoldTax: false }) + .subscribe(() => { + this.reload(); + }); + } + }); + } + + /** + * Unblock Current Account. + */ + private unblockCurrentAccount(action: string) { + const unblockCurrentAccountDialogRef = this.dialog.open(ConfirmationDialogComponent, { + data: { + heading: this.translateService.instant('labels.heading.Current Account'), + dialogContext: + this.translateService.instant('labels.dialogContext.Are you sure you want') + + action + + this.translateService.instant('this Current Account') + } + }); + let command = 'unblock'; + if (action === 'Unblock Deposit') { + command = 'unblockCredit'; + } + if (action === 'Unblock Withdrawal') { + command = 'unblockDebit'; + } + unblockCurrentAccountDialogRef.afterClosed().subscribe((response: { confirm: any }) => { + if (response.confirm) { + this.currentAccountsService + .executeCurrentAccountCommand(this.currentAccountData.id, command, {}) + .subscribe(() => { + this.reload(); + }); + } + }); + } +} diff --git a/src/app/current-accounts/current-account-view/current-accounts-buttons.config.ts b/src/app/current-accounts/current-account-view/current-accounts-buttons.config.ts new file mode 100644 index 000000000..8c56acba3 --- /dev/null +++ b/src/app/current-accounts/current-account-view/current-accounts-buttons.config.ts @@ -0,0 +1,197 @@ +/** + * 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/. + */ + +/** Current Account Buttons Configuration */ +export class CurrentAccountButtonsConfiguration { + optionArray: { + name: string; + taskPermissionName: string; + }[]; + + buttonsArray: { + name: string; + icon: string; + taskPermissionName: string; + }[]; + + constructor(status: string, substatus: any) { + this.setOptions(status); + this.setButtons(status, substatus); + } + + get singleButtons() { + return this.buttonsArray; + } + + get options() { + return this.optionArray; + } + + setButtons(status: string, substatus: any) { + switch (status) { + case 'Active': + this.buttonsArray = []; + if (!substatus.block) { + if (!substatus.blockCredit) { + this.buttonsArray.push( + { + name: 'Deposit', + icon: 'arrow-up', + taskPermissionName: 'DEPOSIT_CURRENTACCOUNT' + }, + { + name: 'Block Deposit', + icon: 'lock', + taskPermissionName: 'BLOCKDEPOSIT_CURRENTACCOUNT' + } + ); + } else { + this.buttonsArray.push({ + name: 'Unblock Deposit', + icon: 'lock-open', + taskPermissionName: 'UNBLOCKDEPOSIT_CURRENTACCOUNT' + }); + } + if (!substatus.blockDebit) { + this.buttonsArray.push( + { + name: 'Withdrawal', + icon: 'arrow-down', + taskPermissionName: 'WITHDRAWAL_CURRENTACCOUNT' + }, + { + name: 'Block Withdrawal', + icon: 'lock', + taskPermissionName: 'BLOCKWITHDRAWAL_CURRENTACCOUNT' + } + ); + } else { + this.buttonsArray.push({ + name: 'Unblock Withdrawal', + icon: 'lock-open', + taskPermissionName: 'UNBLOCKWITHDRAWAL_CURRENTACCOUNT' + }); + } + this.buttonsArray = this.buttonsArray.concat([ + { + name: 'Block Account', + icon: 'lock', + taskPermissionName: 'BLOCK_CURRENTACCOUNT' + }, + { + name: 'Hold Amount', + icon: 'lock', + taskPermissionName: 'HOLDAMOUNT_CURRENTACCOUNT' + }, + { + name: 'Calculate Interest', + icon: 'table', + taskPermissionName: 'CALCULATEINTEREST_CURRENTACCOUNT' + }, + { + name: 'Post Interest As On', + icon: 'arrow-right', + taskPermissionName: 'POSTINTEREST_CURRENTACCOUNT' + } + ]); + } else { + this.buttonsArray = [ + { + name: 'Unblock Account', + icon: 'lock-open', + taskPermissionName: 'UNBLOCK_CURRENTACCOUNT' + } + ]; + } + break; + case 'Submitted and pending approval': + this.buttonsArray = [ + { + name: 'Modify Application', + icon: 'pen', + taskPermissionName: 'UPDATE_CURRENTACCOUNT' + }, + { + name: 'Approve', + icon: 'check', + taskPermissionName: 'APPROVE_CURRENTACCOUNT' + } + ]; + break; + case 'Approved': + this.buttonsArray = [ + { + name: 'Undo Approval', + icon: 'undo', + taskPermissionName: 'APPROVALUNDO_CURRENTACCOUNT' + }, + { + name: 'Activate', + icon: 'check', + taskPermissionName: 'ACTIVATE_CURRENTACCOUNT' + }, + { + name: 'Add Charge', + icon: 'plus', + taskPermissionName: 'CREATE_CURRENTACCOUNTCHARGE' + } + ]; + break; + default: + this.buttonsArray = []; + } + } + + setOptions(status: string) { + switch (status) { + case 'Active': + this.optionArray = [ + { + name: 'Post Interest', + taskPermissionName: 'POSTINTEREST_CURRENTACCOUNT' + }, + { + name: 'Add Charge', + taskPermissionName: 'CREATE_CURRENTACCOUNTCHARGE' + }, + { + name: 'Close', + taskPermissionName: 'CLOSE_CURRENTACCOUNT' + } + ]; + break; + case 'Submitted and pending approval': + this.optionArray = [ + { + name: 'Reject', + taskPermissionName: 'REJECT_CURRENTACCOUNT' + }, + { + name: 'Withdrawn by Client', + taskPermissionName: 'WITHDRAW_CURRENTACCOUNT' + }, + { + name: 'Add Charge', + taskPermissionName: 'CREATE_CURRENTACCOUNTCHARGE' + }, + { + name: 'Delete', + taskPermissionName: 'DELETE_CURRENTACCOUNT' + } + ]; + break; + case 'Approved': + default: + this.optionArray = []; + } + } + + addOption(option: { name: string; taskPermissionName: string }) { + this.optionArray.push(option); + } +} diff --git a/src/app/current-accounts/current-account-view/current-accounts-documents-tab/current-accounts-documents-tab.component.html b/src/app/current-accounts/current-account-view/current-accounts-documents-tab/current-accounts-documents-tab.component.html new file mode 100644 index 000000000..fa6a16892 --- /dev/null +++ b/src/app/current-accounts/current-account-view/current-accounts-documents-tab/current-accounts-documents-tab.component.html @@ -0,0 +1,16 @@ + + + + diff --git a/src/app/current-accounts/current-account-view/current-accounts-documents-tab/current-accounts-documents-tab.component.scss b/src/app/current-accounts/current-account-view/current-accounts-documents-tab/current-accounts-documents-tab.component.scss new file mode 100644 index 000000000..af9f4c075 --- /dev/null +++ b/src/app/current-accounts/current-account-view/current-accounts-documents-tab/current-accounts-documents-tab.component.scss @@ -0,0 +1,7 @@ +/** + * 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/. + */ diff --git a/src/app/current-accounts/current-account-view/current-accounts-documents-tab/current-accounts-documents-tab.component.ts b/src/app/current-accounts/current-account-view/current-accounts-documents-tab/current-accounts-documents-tab.component.ts new file mode 100644 index 000000000..26a721241 --- /dev/null +++ b/src/app/current-accounts/current-account-view/current-accounts-documents-tab/current-accounts-documents-tab.component.ts @@ -0,0 +1,85 @@ +/** + * 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 { MatDialog } from '@angular/material/dialog'; +import { ActivatedRoute } from '@angular/router'; +import { CurrentAccountsService } from 'app/current-accounts/current-accounts.service'; +import { SettingsService } from 'app/settings/settings.service'; +import { environment } from '../../../../environments/environment'; +import { EntityDocumentsTabComponent } from '../../../shared/tabs/entity-documents-tab/entity-documents-tab.component'; +import { STANDALONE_SHARED_IMPORTS } from 'app/standalone-shared.module'; + +@Component({ + selector: 'mifosx-current-account-documents-tab', + templateUrl: './current-accounts-documents-tab.component.html', + styleUrls: ['./current-accounts-documents-tab.component.scss'], + imports: [ + ...STANDALONE_SHARED_IMPORTS, + EntityDocumentsTabComponent + ] +}) +export class CurrentAccountDocumentsTabComponent { + private route = inject(ActivatedRoute); + private currentAccountsService = inject(CurrentAccountsService); + private settingsService = inject(SettingsService); + dialog = inject(MatDialog); + + /** Stores the resolved current account documents data */ + entityDocuments: any; + /** Stores the current account Account Id */ + entityId: string; + entityType = 'currentaccounts'; + + /** + * Retrieves the current account data from `resolve`. + * @param {ActivatedRoute} route Activated Route. + */ + constructor() { + this.route.data.subscribe((data: { currentAccountDocuments: any }) => { + this.setCurrentAccountDocumentsData(data.currentAccountDocuments); + }); + this.entityId = this.route.parent.snapshot.paramMap.get('currentAccountId'); + } + + setCurrentAccountDocumentsData(data: any) { + data.forEach((ele: any) => { + ele.docUrl = + this.settingsService.serverUrl + + '/currentaccounts/' + + ele.parentEntityId + + '/documents/' + + ele.id + + '/attachment?tenantIdentifier=' + + environment.fineractPlatformTenantId; + if (ele.fileName) { + if ( + ele.fileName.toLowerCase().indexOf('.jpg') !== -1 || + ele.fileName.toLowerCase().indexOf('.jpeg') !== -1 || + ele.fileName.toLowerCase().indexOf('.png') !== -1 + ) { + ele.fileIsImage = true; + } + } + if (ele.type) { + if (ele.type.toLowerCase().indexOf('image') !== -1) { + ele.fileIsImage = true; + } + } + }); + this.entityDocuments = data; + } + + uploadDocument(formData: FormData): any { + return this.currentAccountsService.loadCurrentAccountDocument(this.entityId, formData); + } + + deleteDocument(documentId: any) { + this.currentAccountsService.deleteCurrentAccountDocument(this.entityId, documentId).subscribe((res: any) => {}); + } +} diff --git a/src/app/current-accounts/current-account-view/custom-dialogs/calculate-interest-dialog/calculate-interest-dialog.component.html b/src/app/current-accounts/current-account-view/custom-dialogs/calculate-interest-dialog/calculate-interest-dialog.component.html new file mode 100644 index 000000000..84de34b4b --- /dev/null +++ b/src/app/current-accounts/current-account-view/custom-dialogs/calculate-interest-dialog/calculate-interest-dialog.component.html @@ -0,0 +1,18 @@ + + +

{{ 'labels.heading.Calculate Interest' | translate }}

+
+

{{ 'labels.text.Are you sure you want to calculate interest' | translate }} ?

+
+ + + + diff --git a/src/app/current-accounts/current-account-view/custom-dialogs/calculate-interest-dialog/calculate-interest-dialog.component.scss b/src/app/current-accounts/current-account-view/custom-dialogs/calculate-interest-dialog/calculate-interest-dialog.component.scss new file mode 100644 index 000000000..af9f4c075 --- /dev/null +++ b/src/app/current-accounts/current-account-view/custom-dialogs/calculate-interest-dialog/calculate-interest-dialog.component.scss @@ -0,0 +1,7 @@ +/** + * 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/. + */ diff --git a/src/app/current-accounts/current-account-view/custom-dialogs/calculate-interest-dialog/calculate-interest-dialog.component.ts b/src/app/current-accounts/current-account-view/custom-dialogs/calculate-interest-dialog/calculate-interest-dialog.component.ts new file mode 100644 index 000000000..1f8aa9307 --- /dev/null +++ b/src/app/current-accounts/current-account-view/custom-dialogs/calculate-interest-dialog/calculate-interest-dialog.component.ts @@ -0,0 +1,39 @@ +/** + * 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, inject } from '@angular/core'; +import { + MatDialogRef, + MatDialogTitle, + MatDialogContent, + MatDialogActions, + MatDialogClose +} from '@angular/material/dialog'; +import { CdkScrollable } from '@angular/cdk/scrolling'; +import { STANDALONE_SHARED_IMPORTS } from 'app/standalone-shared.module'; + +/** + * Calculate interest dialog component. + */ +@Component({ + selector: 'mifosx-calculate-interest-dialog', + templateUrl: './calculate-interest-dialog.component.html', + styleUrls: ['./calculate-interest-dialog.component.scss'], + imports: [ + ...STANDALONE_SHARED_IMPORTS, + MatDialogTitle, + CdkScrollable, + MatDialogContent, + MatDialogActions, + MatDialogClose + ] +}) +export class CalculateInterestDialogComponent { + dialogRef = inject>(MatDialogRef); +} diff --git a/src/app/current-accounts/current-account-view/custom-dialogs/inactivate-charge-dialog/inactivate-charge-dialog.component.html b/src/app/current-accounts/current-account-view/custom-dialogs/inactivate-charge-dialog/inactivate-charge-dialog.component.html new file mode 100644 index 000000000..05f3c5885 --- /dev/null +++ b/src/app/current-accounts/current-account-view/custom-dialogs/inactivate-charge-dialog/inactivate-charge-dialog.component.html @@ -0,0 +1,18 @@ + + +

{{ 'tooltips.Inactivate Charge' | translate }}

+
+

{{ 'labels.text.Are you sure you want to inactivate charge with id' | translate }}: {{ data.id }} ?

+
+ + + + diff --git a/src/app/current-accounts/current-account-view/custom-dialogs/inactivate-charge-dialog/inactivate-charge-dialog.component.scss b/src/app/current-accounts/current-account-view/custom-dialogs/inactivate-charge-dialog/inactivate-charge-dialog.component.scss new file mode 100644 index 000000000..af9f4c075 --- /dev/null +++ b/src/app/current-accounts/current-account-view/custom-dialogs/inactivate-charge-dialog/inactivate-charge-dialog.component.scss @@ -0,0 +1,7 @@ +/** + * 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/. + */ diff --git a/src/app/current-accounts/current-account-view/custom-dialogs/inactivate-charge-dialog/inactivate-charge-dialog.component.ts b/src/app/current-accounts/current-account-view/custom-dialogs/inactivate-charge-dialog/inactivate-charge-dialog.component.ts new file mode 100644 index 000000000..abbfb1300 --- /dev/null +++ b/src/app/current-accounts/current-account-view/custom-dialogs/inactivate-charge-dialog/inactivate-charge-dialog.component.ts @@ -0,0 +1,41 @@ +/** + * 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, inject } from '@angular/core'; +import { + MatDialogRef, + MAT_DIALOG_DATA, + MatDialogTitle, + MatDialogContent, + MatDialogActions, + MatDialogClose +} from '@angular/material/dialog'; +import { CdkScrollable } from '@angular/cdk/scrolling'; +import { STANDALONE_SHARED_IMPORTS } from 'app/standalone-shared.module'; + +/** + * Inactivate charge dialog component. + */ +@Component({ + selector: 'mifosx-inactivate-charge-dialog', + templateUrl: './inactivate-charge-dialog.component.html', + styleUrls: ['./inactivate-charge-dialog.component.scss'], + imports: [ + ...STANDALONE_SHARED_IMPORTS, + MatDialogTitle, + CdkScrollable, + MatDialogContent, + MatDialogActions, + MatDialogClose + ] +}) +export class InactivateChargeDialogComponent { + dialogRef = inject>(MatDialogRef); + data = inject(MAT_DIALOG_DATA); +} diff --git a/src/app/current-accounts/current-account-view/custom-dialogs/post-interest-dialog/post-interest-dialog.component.html b/src/app/current-accounts/current-account-view/custom-dialogs/post-interest-dialog/post-interest-dialog.component.html new file mode 100644 index 000000000..1c1c9d51c --- /dev/null +++ b/src/app/current-accounts/current-account-view/custom-dialogs/post-interest-dialog/post-interest-dialog.component.html @@ -0,0 +1,18 @@ + + +

{{ 'labels.heading.Post Interest' | translate }}

+
+

{{ 'labels.text.Are you sure you want to post interest' | translate }} ?

+
+ + + + diff --git a/src/app/current-accounts/current-account-view/custom-dialogs/post-interest-dialog/post-interest-dialog.component.scss b/src/app/current-accounts/current-account-view/custom-dialogs/post-interest-dialog/post-interest-dialog.component.scss new file mode 100644 index 000000000..af9f4c075 --- /dev/null +++ b/src/app/current-accounts/current-account-view/custom-dialogs/post-interest-dialog/post-interest-dialog.component.scss @@ -0,0 +1,7 @@ +/** + * 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/. + */ diff --git a/src/app/current-accounts/current-account-view/custom-dialogs/post-interest-dialog/post-interest-dialog.component.ts b/src/app/current-accounts/current-account-view/custom-dialogs/post-interest-dialog/post-interest-dialog.component.ts new file mode 100644 index 000000000..9f3f2c752 --- /dev/null +++ b/src/app/current-accounts/current-account-view/custom-dialogs/post-interest-dialog/post-interest-dialog.component.ts @@ -0,0 +1,39 @@ +/** + * 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, inject } from '@angular/core'; +import { + MatDialogRef, + MatDialogTitle, + MatDialogContent, + MatDialogActions, + MatDialogClose +} from '@angular/material/dialog'; +import { CdkScrollable } from '@angular/cdk/scrolling'; +import { STANDALONE_SHARED_IMPORTS } from 'app/standalone-shared.module'; + +/** + * Post interest dialog component. + */ +@Component({ + selector: 'mifosx-post-interest-dialog', + templateUrl: './post-interest-dialog.component.html', + styleUrls: ['./post-interest-dialog.component.scss'], + imports: [ + ...STANDALONE_SHARED_IMPORTS, + MatDialogTitle, + CdkScrollable, + MatDialogContent, + MatDialogActions, + MatDialogClose + ] +}) +export class PostInterestDialogComponent { + dialogRef = inject>(MatDialogRef); +} diff --git a/src/app/current-accounts/current-account-view/custom-dialogs/release-amount-dialog/release-amount-dialog.component.html b/src/app/current-accounts/current-account-view/custom-dialogs/release-amount-dialog/release-amount-dialog.component.html new file mode 100644 index 000000000..5e3af77b2 --- /dev/null +++ b/src/app/current-accounts/current-account-view/custom-dialogs/release-amount-dialog/release-amount-dialog.component.html @@ -0,0 +1,18 @@ + + +

{{ 'labels.heading.Release Amount' | translate }}

+
+

{{ 'labels.text.Are you sure you want to release this amount' | translate }}

+
+ + + + diff --git a/src/app/current-accounts/current-account-view/custom-dialogs/release-amount-dialog/release-amount-dialog.component.scss b/src/app/current-accounts/current-account-view/custom-dialogs/release-amount-dialog/release-amount-dialog.component.scss new file mode 100644 index 000000000..af9f4c075 --- /dev/null +++ b/src/app/current-accounts/current-account-view/custom-dialogs/release-amount-dialog/release-amount-dialog.component.scss @@ -0,0 +1,7 @@ +/** + * 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/. + */ diff --git a/src/app/current-accounts/current-account-view/custom-dialogs/release-amount-dialog/release-amount-dialog.component.ts b/src/app/current-accounts/current-account-view/custom-dialogs/release-amount-dialog/release-amount-dialog.component.ts new file mode 100644 index 000000000..259845230 --- /dev/null +++ b/src/app/current-accounts/current-account-view/custom-dialogs/release-amount-dialog/release-amount-dialog.component.ts @@ -0,0 +1,35 @@ +/** + * 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 { + MatDialogRef, + MatDialogTitle, + MatDialogContent, + MatDialogActions, + MatDialogClose +} from '@angular/material/dialog'; +import { CdkScrollable } from '@angular/cdk/scrolling'; +import { STANDALONE_SHARED_IMPORTS } from 'app/standalone-shared.module'; + +@Component({ + selector: 'mifosx-release-amount-dialog', + templateUrl: './release-amount-dialog.component.html', + styleUrls: ['./release-amount-dialog.component.scss'], + imports: [ + ...STANDALONE_SHARED_IMPORTS, + MatDialogTitle, + CdkScrollable, + MatDialogContent, + MatDialogActions, + MatDialogClose + ] +}) +export class ReleaseAmountDialogComponent { + dialogRef = inject>(MatDialogRef); +} diff --git a/src/app/current-accounts/current-account-view/custom-dialogs/toggle-withhold-tax-dialog/toggle-withhold-tax-dialog.component.html b/src/app/current-accounts/current-account-view/custom-dialogs/toggle-withhold-tax-dialog/toggle-withhold-tax-dialog.component.html new file mode 100644 index 000000000..36f358761 --- /dev/null +++ b/src/app/current-accounts/current-account-view/custom-dialogs/toggle-withhold-tax-dialog/toggle-withhold-tax-dialog.component.html @@ -0,0 +1,25 @@ + + +

+ {{ (data.isEnable ? 'labels.heading.Enable' : 'labels.heading.Disable') | translate }} + {{ 'labels.heading.Withhold Tax' | translate }} +

+
+

+ {{ 'labels.text.Are you sure you want to' | translate }} + {{ (data.isEnable ? 'labels.heading.Enable' : 'labels.heading.Disable') | translate }} + {{ 'labels.text.withhold tax for this account' | translate }} ? +

+
+ + + + diff --git a/src/app/current-accounts/current-account-view/custom-dialogs/toggle-withhold-tax-dialog/toggle-withhold-tax-dialog.component.scss b/src/app/current-accounts/current-account-view/custom-dialogs/toggle-withhold-tax-dialog/toggle-withhold-tax-dialog.component.scss new file mode 100644 index 000000000..af9f4c075 --- /dev/null +++ b/src/app/current-accounts/current-account-view/custom-dialogs/toggle-withhold-tax-dialog/toggle-withhold-tax-dialog.component.scss @@ -0,0 +1,7 @@ +/** + * 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/. + */ diff --git a/src/app/current-accounts/current-account-view/custom-dialogs/toggle-withhold-tax-dialog/toggle-withhold-tax-dialog.component.ts b/src/app/current-accounts/current-account-view/custom-dialogs/toggle-withhold-tax-dialog/toggle-withhold-tax-dialog.component.ts new file mode 100644 index 000000000..67dd3ffe9 --- /dev/null +++ b/src/app/current-accounts/current-account-view/custom-dialogs/toggle-withhold-tax-dialog/toggle-withhold-tax-dialog.component.ts @@ -0,0 +1,41 @@ +/** + * 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, inject } from '@angular/core'; +import { + MatDialogRef, + MAT_DIALOG_DATA, + MatDialogTitle, + MatDialogContent, + MatDialogActions, + MatDialogClose +} from '@angular/material/dialog'; +import { CdkScrollable } from '@angular/cdk/scrolling'; +import { STANDALONE_SHARED_IMPORTS } from 'app/standalone-shared.module'; + +/** + * Toggle withhold tax dialog dialog component. + */ +@Component({ + selector: 'mifosx-toggle-withhold-tax-dialog', + templateUrl: './toggle-withhold-tax-dialog.component.html', + styleUrls: ['./toggle-withhold-tax-dialog.component.scss'], + imports: [ + ...STANDALONE_SHARED_IMPORTS, + MatDialogTitle, + CdkScrollable, + MatDialogContent, + MatDialogActions, + MatDialogClose + ] +}) +export class ToggleWithholdTaxDialogComponent { + dialogRef = inject>(MatDialogRef); + data = inject(MAT_DIALOG_DATA); +} diff --git a/src/app/current-accounts/current-account-view/custom-dialogs/undo-transaction-dialog/undo-transaction-dialog.component.html b/src/app/current-accounts/current-account-view/custom-dialogs/undo-transaction-dialog/undo-transaction-dialog.component.html new file mode 100644 index 000000000..eec964e33 --- /dev/null +++ b/src/app/current-accounts/current-account-view/custom-dialogs/undo-transaction-dialog/undo-transaction-dialog.component.html @@ -0,0 +1,18 @@ + + +

{{ 'labels.heading.Undo Transaction' | translate }}

+
+

{{ 'labels.text.Are you sure you want to undo this transaction' | translate }}

+
+ + + + diff --git a/src/app/current-accounts/current-account-view/custom-dialogs/undo-transaction-dialog/undo-transaction-dialog.component.scss b/src/app/current-accounts/current-account-view/custom-dialogs/undo-transaction-dialog/undo-transaction-dialog.component.scss new file mode 100644 index 000000000..af9f4c075 --- /dev/null +++ b/src/app/current-accounts/current-account-view/custom-dialogs/undo-transaction-dialog/undo-transaction-dialog.component.scss @@ -0,0 +1,7 @@ +/** + * 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/. + */ diff --git a/src/app/current-accounts/current-account-view/custom-dialogs/undo-transaction-dialog/undo-transaction-dialog.component.ts b/src/app/current-accounts/current-account-view/custom-dialogs/undo-transaction-dialog/undo-transaction-dialog.component.ts new file mode 100644 index 000000000..9e329c63f --- /dev/null +++ b/src/app/current-accounts/current-account-view/custom-dialogs/undo-transaction-dialog/undo-transaction-dialog.component.ts @@ -0,0 +1,38 @@ +/** + * 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, inject } from '@angular/core'; +import { + MatDialogRef, + MatDialogTitle, + MatDialogContent, + MatDialogActions, + MatDialogClose +} from '@angular/material/dialog'; +import { CdkScrollable } from '@angular/cdk/scrolling'; +import { STANDALONE_SHARED_IMPORTS } from 'app/standalone-shared.module'; +/** + * Undo transaction dialog component. + */ +@Component({ + selector: 'mifosx-undo-transaction-dialog', + templateUrl: './undo-transaction-dialog.component.html', + styleUrls: ['./undo-transaction-dialog.component.scss'], + imports: [ + ...STANDALONE_SHARED_IMPORTS, + MatDialogTitle, + CdkScrollable, + MatDialogContent, + MatDialogActions, + MatDialogClose + ] +}) +export class UndoTransactionDialogComponent { + dialogRef = inject>(MatDialogRef); +} diff --git a/src/app/current-accounts/current-account-view/custom-dialogs/waive-charge-dialog/waive-charge-dialog.component.html b/src/app/current-accounts/current-account-view/custom-dialogs/waive-charge-dialog/waive-charge-dialog.component.html new file mode 100644 index 000000000..d1cb118ea --- /dev/null +++ b/src/app/current-accounts/current-account-view/custom-dialogs/waive-charge-dialog/waive-charge-dialog.component.html @@ -0,0 +1,18 @@ + + +

{{ 'labels.buttons.Waive Charge' | translate }}

+
+

{{ 'labels.text.Are you sure you want to waive charge with id' | translate }}: {{ data.id }} ?

+
+ + + + diff --git a/src/app/current-accounts/current-account-view/custom-dialogs/waive-charge-dialog/waive-charge-dialog.component.scss b/src/app/current-accounts/current-account-view/custom-dialogs/waive-charge-dialog/waive-charge-dialog.component.scss new file mode 100644 index 000000000..af9f4c075 --- /dev/null +++ b/src/app/current-accounts/current-account-view/custom-dialogs/waive-charge-dialog/waive-charge-dialog.component.scss @@ -0,0 +1,7 @@ +/** + * 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/. + */ diff --git a/src/app/current-accounts/current-account-view/custom-dialogs/waive-charge-dialog/waive-charge-dialog.component.ts b/src/app/current-accounts/current-account-view/custom-dialogs/waive-charge-dialog/waive-charge-dialog.component.ts new file mode 100644 index 000000000..9c7f66841 --- /dev/null +++ b/src/app/current-accounts/current-account-view/custom-dialogs/waive-charge-dialog/waive-charge-dialog.component.ts @@ -0,0 +1,41 @@ +/** + * 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, inject } from '@angular/core'; +import { + MatDialogRef, + MAT_DIALOG_DATA, + MatDialogTitle, + MatDialogContent, + MatDialogActions, + MatDialogClose +} from '@angular/material/dialog'; +import { CdkScrollable } from '@angular/cdk/scrolling'; +import { STANDALONE_SHARED_IMPORTS } from 'app/standalone-shared.module'; + +/** + * Waive charge dialog component. + */ +@Component({ + selector: 'mifosx-waive-charge-dialog', + templateUrl: './waive-charge-dialog.component.html', + styleUrls: ['./waive-charge-dialog.component.scss'], + imports: [ + ...STANDALONE_SHARED_IMPORTS, + MatDialogTitle, + CdkScrollable, + MatDialogContent, + MatDialogActions, + MatDialogClose + ] +}) +export class WaiveChargeDialogComponent { + dialogRef = inject>(MatDialogRef); + data = inject(MAT_DIALOG_DATA); +} diff --git a/src/app/current-accounts/current-account-view/datatable-tabs/datatable-tabs.component.html b/src/app/current-accounts/current-account-view/datatable-tabs/datatable-tabs.component.html new file mode 100644 index 000000000..e90f1d1de --- /dev/null +++ b/src/app/current-accounts/current-account-view/datatable-tabs/datatable-tabs.component.html @@ -0,0 +1,17 @@ + + +
+ + +
diff --git a/src/app/current-accounts/current-account-view/datatable-tabs/datatable-tabs.component.scss b/src/app/current-accounts/current-account-view/datatable-tabs/datatable-tabs.component.scss new file mode 100644 index 000000000..af9f4c075 --- /dev/null +++ b/src/app/current-accounts/current-account-view/datatable-tabs/datatable-tabs.component.scss @@ -0,0 +1,7 @@ +/** + * 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/. + */ diff --git a/src/app/current-accounts/current-account-view/datatable-tabs/datatable-tabs.component.ts b/src/app/current-accounts/current-account-view/datatable-tabs/datatable-tabs.component.ts new file mode 100644 index 000000000..2ffa44192 --- /dev/null +++ b/src/app/current-accounts/current-account-view/datatable-tabs/datatable-tabs.component.ts @@ -0,0 +1,48 @@ +/** + * 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, inject } from '@angular/core'; +import { ActivatedRoute } from '@angular/router'; +import { EntityDatatableTabComponent } from '../../../shared/tabs/entity-datatable-tab/entity-datatable-tab.component'; +import { STANDALONE_SHARED_IMPORTS } from 'app/standalone-shared.module'; + +/** + * Current Account Datatable Tabs Component + */ +@Component({ + selector: 'mifosx-datatable-tabs', + templateUrl: './datatable-tabs.component.html', + styleUrls: ['./datatable-tabs.component.scss'], + imports: [ + ...STANDALONE_SHARED_IMPORTS, + EntityDatatableTabComponent + ] +}) +export class DatatableTabsComponent { + private route = inject(ActivatedRoute); + + entityId: string; + /** Current Account Datatable */ + entityDatatable: any; + /** Multi Row Datatable Flag */ + multiRowDatatableFlag: boolean; + + /** + * Fetches Current Account and datatables data from `resolve` + * @param {ActivatedRoute} route Activated Route. + */ + constructor() { + this.entityId = this.route.parent.parent.snapshot.paramMap.get('currentAccountId'); + + this.route.data.subscribe((data: { currentAccountDatatable: any }) => { + this.entityDatatable = data.currentAccountDatatable; + this.multiRowDatatableFlag = this.entityDatatable.columnHeaders[0].columnName === 'id' ? true : false; + }); + } +} diff --git a/src/app/current-accounts/current-account-view/general-tab/general-tab.component.html b/src/app/current-accounts/current-account-view/general-tab/general-tab.component.html new file mode 100644 index 000000000..7bae16664 --- /dev/null +++ b/src/app/current-accounts/current-account-view/general-tab/general-tab.component.html @@ -0,0 +1,367 @@ + + +@if (isLoading) { +
+
+
+} + +@if (!isLoading) { + +} diff --git a/src/app/current-accounts/current-account-view/general-tab/general-tab.component.scss b/src/app/current-accounts/current-account-view/general-tab/general-tab.component.scss new file mode 100644 index 000000000..2221f0329 --- /dev/null +++ b/src/app/current-accounts/current-account-view/general-tab/general-tab.component.scss @@ -0,0 +1,242 @@ +/** + * 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/. + */ + +$bar: 160px; +$foo: $bar * 8; +$color: #e2e4ec; + +.loader { + margin-bottom: 39px; + overflow: auto; +} + +.sidenav { + overflow-x: hidden; +} + +@keyframes move { + 0% { + left: 0; + } + + 16.7% { + left: 0; + } + + 33.3% { + left: calc($foo / 2); + } + + 50% { + left: $foo - $bar; + } + + 66.7% { + left: calc($foo / 2); + } + + 83.3% { + left: 0; + } + + 100% { + left: 0; + } +} + +@keyframes grow { + 0% { + width: $bar; + } + + 16.7% { + width: calc($foo / 2); + } + + 33.3% { + width: calc($foo / 2); + } + + 50% { + width: $bar; + } + + 66.7% { + width: calc($foo / 2); + } + + 83.3% { + width: calc($foo / 2); + } + + 100% { + width: $bar; + } +} + +.general-tab-container { + padding: 16px 20px; + width: 100%; + box-sizing: border-box; + position: relative; + background-color: var(--background-color, #f8f9fa); + color: var(--text-color, #333); + transition: + background-color 0.3s ease, + color 0.3s ease; + + .page-header { + margin-bottom: 24px; + padding: 16px 0; + position: relative; + border-bottom: 1px solid var(--border-color, #e9ecef); + + .header-title { + font-size: 1.75rem; + font-weight: 600; + margin: 0 0 8px; + line-height: 1.2; + color: var(--primary-color, #007bff); + position: relative; + } + + .header-subtitle { + font-size: 0.95rem; + margin: 0; + line-height: 1.4; + color: var(--text-secondary, #6c757d); + position: relative; + } + } + + .content-wrapper { + position: relative; + width: 100%; + } + + .account-info-card { + background: var(--card-background, #fff); + border-radius: 8px; + box-shadow: 0 2px 4px rgb(0 0 0 / 10%); + overflow: hidden; + position: relative; + width: 100%; + max-width: 600px; + transition: + box-shadow 0.3s ease, + background-color 0.3s ease; + + .card-header { + padding: 16px 20px; + background: var(--card-header-bg, #f8f9fa); + border-bottom: 1px solid var(--border-color, #e9ecef); + position: relative; + + h3 { + margin: 0; + font-size: 1.1rem; + font-weight: 600; + line-height: 1.3; + color: var(--text-color, #333); + } + } + + .card-content { + padding: 20px; + position: relative; + + .info-row { + display: flex; + justify-content: space-between; + align-items: baseline; + padding: 10px 0; + border-bottom: 1px solid var(--border-light, #f1f3f4); + position: relative; + min-height: 24px; + + &:last-child { + border-bottom: none; + } + + .label { + font-weight: 500; + color: var(--text-secondary, #6c757d); + flex: 0 0 auto; + margin-right: 16px; + white-space: nowrap; + line-height: 1.4; + } + + .value { + font-weight: 600; + color: var(--text-color, #333); + text-align: right; + flex: 1 1 auto; + line-height: 1.4; + + &.currency { + color: var(--success-color, #28a745); + font-size: 1.05rem; + } + } + } + } + } +} + +// Dark mode styles +[data-theme='dark'] .general-tab-container, +.dark-theme .general-tab-container { + --background-color: #1a1a1a; + --text-color: #e9ecef; + --text-secondary: #adb5bd; + --primary-color: #4dabf7; + --card-background: #2d3748; + --card-header-bg: #374151; + --border-color: #4a5568; + --border-light: #2d3748; + --success-color: #68d391; +} + +// Responsive adjustments for mobile +@media (width <= 768px) { + .general-tab-container { + padding: 12px 16px; + + .page-header { + margin-bottom: 20px; + padding: 12px 0; + + .header-title { + font-size: 1.5rem; + } + + .header-subtitle { + font-size: 0.9rem; + } + } + + .account-info-card { + .card-header { + padding: 14px 16px; + } + + .card-content { + padding: 16px; + + .info-row { + padding: 8px 0; + + .label { + margin-right: 12px; + } + } + } + } + } +} diff --git a/src/app/current-accounts/current-account-view/general-tab/general-tab.component.ts b/src/app/current-accounts/current-account-view/general-tab/general-tab.component.ts new file mode 100644 index 000000000..6569991d6 --- /dev/null +++ b/src/app/current-accounts/current-account-view/general-tab/general-tab.component.ts @@ -0,0 +1,59 @@ +/** + * 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 { Currency } from 'app/shared/models/general.model'; +import { CurrencyPipe } from '@angular/common'; +import { ExternalIdentifierComponent } from '../../../shared/external-identifier/external-identifier.component'; +import { DateFormatPipe } from '../../../pipes/date-format.pipe'; +import { FormatNumberPipe } from '../../../pipes/format-number.pipe'; +import { YesnoPipe } from '../../../pipes/yesno.pipe'; +import { STANDALONE_SHARED_IMPORTS } from 'app/standalone-shared.module'; + +@Component({ + selector: 'mifosx-general-tab', + templateUrl: './general-tab.component.html', + styleUrls: ['./general-tab.component.scss'], + imports: [ + ...STANDALONE_SHARED_IMPORTS, + ExternalIdentifierComponent, + CurrencyPipe, + DateFormatPipe, + FormatNumberPipe, + YesnoPipe + ] +}) +export class GeneralTabComponent { + private route = inject(ActivatedRoute); + private router = inject(Router); + + isLoading = true; + isActive = false; + entityType: string; + + currentAccountData: any; + currency: Currency; + + constructor() { + this.route.parent.parent.data.subscribe((data: { currentAccountData: any }) => { + this.currentAccountData = data.currentAccountData; + this.currency = this.currentAccountData.currency; + this.isLoading = false; + const status = this.currentAccountData.status.value; + this.isActive = status === 'Active'; + }); + if (this.router.url.includes('clients')) { + this.entityType = 'Client'; + } else if (this.router.url.includes('groups')) { + this.entityType = 'Group'; + } else if (this.router.url.includes('centers')) { + this.entityType = 'Center'; + } + } +} diff --git a/src/app/current-accounts/current-account-view/notes-tab/notes-tab.component.html b/src/app/current-accounts/current-account-view/notes-tab/notes-tab.component.html new file mode 100644 index 000000000..af5c2b546 --- /dev/null +++ b/src/app/current-accounts/current-account-view/notes-tab/notes-tab.component.html @@ -0,0 +1,16 @@ + + + + diff --git a/src/app/current-accounts/current-account-view/notes-tab/notes-tab.component.scss b/src/app/current-accounts/current-account-view/notes-tab/notes-tab.component.scss new file mode 100644 index 000000000..af9f4c075 --- /dev/null +++ b/src/app/current-accounts/current-account-view/notes-tab/notes-tab.component.scss @@ -0,0 +1,7 @@ +/** + * 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/. + */ diff --git a/src/app/current-accounts/current-account-view/notes-tab/notes-tab.component.ts b/src/app/current-accounts/current-account-view/notes-tab/notes-tab.component.ts new file mode 100644 index 000000000..1493f27d2 --- /dev/null +++ b/src/app/current-accounts/current-account-view/notes-tab/notes-tab.component.ts @@ -0,0 +1,65 @@ +/** + * 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 } from '@angular/router'; +import { AuthenticationService } from 'app/core/authentication/authentication.service'; +import { CurrentAccountsService } from 'app/current-accounts/current-accounts.service'; +import { EntityNotesTabComponent } from '../../../shared/tabs/entity-notes-tab/entity-notes-tab.component'; +import { STANDALONE_SHARED_IMPORTS } from 'app/standalone-shared.module'; + +@Component({ + selector: 'mifosx-notes-tab', + templateUrl: './notes-tab.component.html', + styleUrls: ['./notes-tab.component.scss'], + imports: [ + ...STANDALONE_SHARED_IMPORTS, + EntityNotesTabComponent + ] +}) +export class NotesTabComponent { + private route = inject(ActivatedRoute); + private currentAccountsService = inject(CurrentAccountsService); + private authenticationService = inject(AuthenticationService); + + entityId: string; + username: string; + entityNotes: any; + + constructor() { + const savedCredentials = this.authenticationService.getCredentials(); + this.username = savedCredentials.username; + this.entityId = this.route.parent.snapshot.params['currentAccountId']; + this.route.data.subscribe((data: { currentAccountNotes: any }) => { + this.entityNotes = data.currentAccountNotes; + }); + } + + addNote(noteContent: any) { + this.currentAccountsService.createCurrentAccountNote(this.entityId, noteContent).subscribe((response: any) => { + this.entityNotes.push({ + id: response.resourceId, + createdByUsername: this.username, + createdOn: new Date(), + note: noteContent.note + }); + }); + } + + editNote(noteId: string, noteContent: any, index: number) { + this.currentAccountsService.editCurrentAccountNote(this.entityId, noteId, noteContent).subscribe(() => { + this.entityNotes[index].note = noteContent.note; + }); + } + + deleteNote(noteId: string, index: number) { + this.currentAccountsService.deleteCurrentAccountNote(this.entityId, noteId).subscribe(() => { + this.entityNotes.splice(index, 1); + }); + } +} diff --git a/src/app/current-accounts/current-account-view/passbook-tab/passbook-tab.component.html b/src/app/current-accounts/current-account-view/passbook-tab/passbook-tab.component.html new file mode 100644 index 000000000..8b4a9dbbd --- /dev/null +++ b/src/app/current-accounts/current-account-view/passbook-tab/passbook-tab.component.html @@ -0,0 +1,151 @@ + + +
+ +
+
+ + {{ 'labels.inputs.From Date' | translate }} + + + + + + + {{ 'labels.inputs.To Date' | translate }} + + + + + + + + @if (isFiltered) { + + } + + + + +
+
+ + +
+
+

{{ 'labels.heading.Current Account Passbook' | translate }}

+

{{ productName }}

+
+ + +
+
+
{{ 'labels.inputs.Account Number' | translate }}
+
{{ accountNo }}
+
+
+
{{ 'labels.inputs.Account Holder' | translate }}
+
{{ clientName }}
+
+
+
{{ 'labels.inputs.Current Account Product' | translate }}
+
{{ productName }}
+
+
+
{{ 'labels.inputs.Opening Date' | translate }}
+
{{ activatedOnDate | dateFormat }}
+
+
+
{{ 'labels.inputs.Status' | translate }}
+
{{ status }}
+
+
+
{{ 'labels.inputs.Interest Rate' | translate }}
+
{{ interestRate }}%
+
+
+ + +
+
+ {{ 'labels.inputs.Current Balance' | translate }}: + {{ accountBalance | currency: currency?.code : 'symbol-narrow' : '1.2-2' }} +
+
+ {{ 'labels.inputs.Available Balance' | translate }}: + {{ availableBalance | currency: currency?.code : 'symbol-narrow' : '1.2-2' }} +
+
+ + + @if (filteredTransactions.length > 0) { + + + + + + + + + + + + + @for (txn of filteredTransactions; track txn.id; let i = $index) { + + + + + + + + + } + +
#{{ 'labels.inputs.Date' | translate }}{{ 'labels.inputs.Description' | translate }}{{ 'labels.inputs.Debit' | translate }}{{ 'labels.inputs.Credit' | translate }}{{ 'labels.inputs.Balance' | translate }}
{{ i + 1 }}{{ txn.date | dateFormat }}{{ txn.transactionType.value }} + @if (isDebit(txn.transactionType)) { + {{ txn.amount | currency: currency?.code : 'symbol-narrow' : '1.2-2' }} + } + + @if (!isDebit(txn.transactionType)) { + {{ txn.amount | currency: currency?.code : 'symbol-narrow' : '1.2-2' }} + } + + {{ txn.runningBalance | currency: currency?.code : 'symbol-narrow' : '1.2-2' }} +
+ } @else { +
+ info_outline + {{ 'labels.text.No transactions found for the selected date range.' | translate }} +
+ } + + + +
+
diff --git a/src/app/current-accounts/current-account-view/passbook-tab/passbook-tab.component.scss b/src/app/current-accounts/current-account-view/passbook-tab/passbook-tab.component.scss new file mode 100644 index 000000000..21d657212 --- /dev/null +++ b/src/app/current-accounts/current-account-view/passbook-tab/passbook-tab.component.scss @@ -0,0 +1,206 @@ +/** + * 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/. + */ + +.passbook-container { + padding: 16px; +} + +// Filter Bar +.passbook-filter-bar { + margin-bottom: 16px; +} + +.passbook-filter-form { + display: flex; + align-items: center; + gap: 12px; + flex-wrap: wrap; +} + +.passbook-date-field { + width: 180px; +} + +.passbook-filter-spacer { + flex: 1; +} + +// Passbook Preview +.passbook-preview { + border: 1px solid #e0e0e0; + border-radius: 8px; + padding: 24px; + background: #fff; +} + +.passbook-header { + text-align: center; + margin-bottom: 20px; + border-bottom: 2px solid #1976d2; + padding-bottom: 12px; + + h2 { + color: #1976d2; + font-size: 20px; + margin: 0 0 4px; + font-weight: 600; + } + + h3 { + font-size: 14px; + font-weight: 400; + color: #666; + margin: 0; + } +} + +.passbook-info-grid { + display: grid; + grid-template-columns: repeat(3, 1fr); + gap: 12px 20px; + margin-bottom: 16px; + padding: 12px; + background: #f5f5f5; + border-radius: 6px; +} + +.passbook-info-item { + .label { + font-size: 10px; + font-weight: 600; + color: #888; + text-transform: uppercase; + letter-spacing: 0.5px; + } + + .value { + font-size: 13px; + color: #333; + margin-top: 2px; + } +} + +.passbook-balance-row { + display: flex; + justify-content: space-between; + margin-bottom: 16px; + padding: 10px 14px; + background: #e3f2fd; + border-radius: 6px; + font-size: 14px; + + .balance-item { + b { + color: #1976d2; + } + } +} + +// Transactions Table +table { + width: 100%; + border-collapse: collapse; + font-size: 13px; + + th { + background: #1976d2; + color: #fff; + padding: 8px 10px; + text-align: left; + font-weight: 600; + font-size: 12px; + } + + td { + padding: 6px 10px; + border-bottom: 1px solid #e0e0e0; + } + + tr:nth-child(even) { + background: #fafafa; + } + + tr:hover { + background: #f0f0f0; + } + + .reversed { + text-decoration: line-through; + color: #999; + } +} + +.text-right { + text-align: right; +} + +.passbook-no-data { + display: flex; + align-items: center; + justify-content: center; + gap: 8px; + padding: 32px; + color: #888; + font-size: 14px; + + mat-icon { + color: #bbb; + } +} + +.passbook-footer { + margin-top: 16px; + font-size: 11px; + color: #999; + text-align: center; + border-top: 1px solid #e0e0e0; + padding-top: 10px; +} + +// Responsive +@media (width <= 768px) { + .passbook-info-grid { + grid-template-columns: 1fr 1fr; + } + + .passbook-balance-row { + flex-direction: column; + gap: 8px; + } + + .passbook-filter-form { + flex-direction: column; + align-items: stretch; + } + + .passbook-date-field { + width: 100%; + } + + .passbook-filter-spacer { + display: none; + } +} + +@media (width <= 480px) { + .passbook-info-grid { + grid-template-columns: 1fr; + } +} + +// Print-specific styles +@media print { + .passbook-filter-bar { + display: none !important; + } + + .passbook-preview { + border: none; + padding: 0; + } +} diff --git a/src/app/current-accounts/current-account-view/passbook-tab/passbook-tab.component.ts b/src/app/current-accounts/current-account-view/passbook-tab/passbook-tab.component.ts new file mode 100644 index 000000000..4c84c1205 --- /dev/null +++ b/src/app/current-accounts/current-account-view/passbook-tab/passbook-tab.component.ts @@ -0,0 +1,186 @@ +/** + * 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, OnInit, OnDestroy, inject, ElementRef, ViewChild } from '@angular/core'; +import { ActivatedRoute } from '@angular/router'; +import { UntypedFormBuilder, Validators } from '@angular/forms'; +import { CurrencyPipe } from '@angular/common'; +import { Subject, takeUntil } from 'rxjs'; + +/** Custom Services */ +import { SettingsService } from 'app/settings/settings.service'; +import { Dates } from 'app/core/utils/dates'; +import { Currency } from 'app/shared/models/general.model'; +import { + CurrentAccountTransaction, + CurrentAccountTransactionType +} from 'app/current-accounts/models/current-account-transaction.model'; +import { MatIcon } from '@angular/material/icon'; +import { FaIconComponent } from '@fortawesome/angular-fontawesome'; +import { DateFormatPipe } from '../../../pipes/date-format.pipe'; +import { STANDALONE_SHARED_IMPORTS } from 'app/standalone-shared.module'; + +@Component({ + selector: 'mifosx-passbook-tab', + templateUrl: './passbook-tab.component.html', + styleUrls: ['./passbook-tab.component.scss'], + standalone: true, + imports: [ + ...STANDALONE_SHARED_IMPORTS, + MatIcon, + FaIconComponent, + DateFormatPipe, + CurrencyPipe + ] +}) +export class PassbookTabComponent implements OnInit, OnDestroy { + private route = inject(ActivatedRoute); + private formBuilder = inject(UntypedFormBuilder); + private settingsService = inject(SettingsService); + private dateUtils = inject(Dates); + + private destroy$ = new Subject(); + + @ViewChild('passbookContent') passbookContent: ElementRef; + + /** Account data */ + accountNo: string = ''; + clientName: string = ''; + productName: string = ''; + currency: Currency | null = null; + status: string = ''; + interestRate: number = 0; + accountBalance: number = 0; + availableBalance: number = 0; + activatedOnDate: any = null; + + /** Transactions */ + allTransactions: CurrentAccountTransaction[] = []; + filteredTransactions: CurrentAccountTransaction[] = []; + + /** Date range form */ + minDate = new Date(2000, 0, 1); + maxDate = new Date(); + dateRangeForm: any; + isFiltered = false; + + ngOnInit(): void { + this.maxDate = this.settingsService.businessDate; + + this.route.parent.data.pipe(takeUntil(this.destroy$)).subscribe((data: { currentAccountData: any }) => { + const account = data.currentAccountData; + this.accountNo = account.accountNo || ''; + this.clientName = account.clientName || account.groupName || ''; + this.productName = account.currentAccountProductName || ''; + this.currency = account.currency || null; + this.status = account.status?.value || ''; + this.interestRate = account.nominalAnnualInterestRate || 0; + this.accountBalance = account.summary?.accountBalance || 0; + this.availableBalance = account.summary?.availableBalance || 0; + this.activatedOnDate = account.timeline?.activatedOnDate || null; + this.allTransactions = account.transactions || []; + this.filteredTransactions = [...this.allTransactions]; + }); + + this.dateRangeForm = this.formBuilder.group({ + fromDate: [ + '', + Validators.required + ], + toDate: [ + '', + Validators.required + ] + }); + } + + isDebit(transactionType: CurrentAccountTransactionType): boolean { + return ( + transactionType.withdrawal === true || + transactionType.feeDeduction === true || + transactionType.overdraftInterest === true || + transactionType.withholdTax === true + ); + } + + filterTransactions(): void { + const fromDate = this.dateRangeForm.value.fromDate; + const toDate = this.dateRangeForm.value.toDate; + + if (!fromDate || !toDate) return; + + const from = new Date(fromDate).getTime(); + const to = new Date(toDate).getTime(); + + this.filteredTransactions = this.allTransactions.filter((txn: CurrentAccountTransaction) => { + const txnDate = this.getTransactionDateAsTime(txn.date); + return txnDate >= from && txnDate <= to; + }); + this.isFiltered = true; + } + + clearFilter(): void { + this.dateRangeForm.reset(); + this.filteredTransactions = [...this.allTransactions]; + this.isFiltered = false; + } + + private getTransactionDateAsTime(date: any): number { + if (Array.isArray(date) && date.length >= 3) { + return new Date(date[0], date[1] - 1, date[2]).getTime(); + } + return new Date(date).getTime(); + } + + printPassbook(): void { + const content = this.passbookContent?.nativeElement?.innerHTML; + if (!content) return; + + const printWindow = window.open('', 'Passbook', 'height=700,width=900'); + printWindow.document.write(` + + + Current Account Passbook - ${this.accountNo} + + + + ${content} + + + `); + printWindow.document.close(); + printWindow.focus(); + printWindow.print(); + printWindow.close(); + } + + ngOnDestroy(): void { + this.destroy$.next(); + this.destroy$.complete(); + } +} diff --git a/src/app/current-accounts/current-account-view/standing-instructions-tab/standing-instructions-tab.component.html b/src/app/current-accounts/current-account-view/standing-instructions-tab/standing-instructions-tab.component.html new file mode 100644 index 000000000..b517656ff --- /dev/null +++ b/src/app/current-accounts/current-account-view/standing-instructions-tab/standing-instructions-tab.component.html @@ -0,0 +1,101 @@ + + +
+
+

{{ 'labels.heading.All Standing Instructions' | translate }}

+
+ +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
{{ 'labels.inputs.Client' | translate }} + {{ instruction.fromClient.displayName }}-{{ instruction.fromClient.id }} + {{ 'labels.inputs.From Account' | translate }} + {{ instruction.fromAccount.accountNo }} ({{ instruction.fromAccountType.value }}) + {{ 'labels.inputs.Beneficiary' | translate }}{{ instruction.toClient.displayName }}{{ 'labels.inputs.To Account' | translate }} + {{ instruction.toAccount.accountNo }} ({{ instruction.toAccountType.value }}) + {{ 'labels.inputs.Amount' | translate }}{{ instruction.instructionType.value }}/{{ instruction.amount }}{{ 'labels.inputs.Validity' | translate }} + {{ instruction.validFrom | dateFormat }} {{ 'labels.inputs.To' | translate }} + {{ instruction.validTill | dateFormat }} + {{ 'labels.inputs.Actions' | translate }} + @if (instruction.status.value !== 'Deleted') { + + + + } + @if (instruction.status.value !== 'Deleted') { + + + + } + +
+
+
diff --git a/src/app/current-accounts/current-account-view/standing-instructions-tab/standing-instructions-tab.component.scss b/src/app/current-accounts/current-account-view/standing-instructions-tab/standing-instructions-tab.component.scss new file mode 100644 index 000000000..e05802c27 --- /dev/null +++ b/src/app/current-accounts/current-account-view/standing-instructions-tab/standing-instructions-tab.component.scss @@ -0,0 +1,31 @@ +/** + * 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/. + */ + +.tab-container { + padding: 1%; + margin: 1%; + + h3 { + margin: 1% auto; + } + + .action-button { + margin-left: auto; + } + + table { + width: 100%; + + .account-action-button { + min-width: 26px; + padding: 0 6px; + margin: 4px; + line-height: 25px; + } + } +} diff --git a/src/app/current-accounts/current-account-view/standing-instructions-tab/standing-instructions-tab.component.ts b/src/app/current-accounts/current-account-view/standing-instructions-tab/standing-instructions-tab.component.ts new file mode 100644 index 000000000..f67be7eaf --- /dev/null +++ b/src/app/current-accounts/current-account-view/standing-instructions-tab/standing-instructions-tab.component.ts @@ -0,0 +1,131 @@ +/** + * 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, OnInit, ViewChild, inject } from '@angular/core'; +import { MatDialog } from '@angular/material/dialog'; +import { + MatTableDataSource, + MatTable, + MatColumnDef, + MatHeaderCellDef, + MatHeaderCell, + MatCellDef, + MatCell, + MatHeaderRowDef, + MatHeaderRow, + MatRowDef, + MatRow +} from '@angular/material/table'; +import { ActivatedRoute } from '@angular/router'; + +/** Custom Services */ +import { CurrentAccountsService } from 'app/current-accounts/current-accounts.service'; +import { AccountTransfersService } from 'app/account-transfers/account-transfers.service'; +import { SettingsService } from 'app/settings/settings.service'; + +/** Dialog Components */ +import { DeleteDialogComponent } from 'app/shared/delete-dialog/delete-dialog.component'; +import { MatTooltip } from '@angular/material/tooltip'; +import { DateFormatPipe } from '../../../pipes/date-format.pipe'; +import { STANDALONE_SHARED_IMPORTS } from 'app/standalone-shared.module'; + +/** + * Current Account Standing Instructions Tab + */ +@Component({ + selector: 'mifosx-standing-instructions-tab', + templateUrl: './standing-instructions-tab.component.html', + styleUrls: ['./standing-instructions-tab.component.scss'], + imports: [ + ...STANDALONE_SHARED_IMPORTS, + MatTable, + MatColumnDef, + MatHeaderCellDef, + MatHeaderCell, + MatCellDef, + MatCell, + MatTooltip, + MatHeaderRowDef, + MatHeaderRow, + MatRowDef, + MatRow, + DateFormatPipe + ] +}) +export class StandingInstructionsTabComponent implements OnInit { + private route = inject(ActivatedRoute); + private currentAccountsService = inject(CurrentAccountsService); + private dialog = inject(MatDialog); + private accountTransfersService = inject(AccountTransfersService); + private settingsService = inject(SettingsService); + + /** Current Account Data */ + currentAccountData: any; + /** Instructions Data */ + instructionsData: any[]; + /** Data source for instructions table. */ + dataSource = new MatTableDataSource(); + /** Columns to be displayed in instructions table. */ + displayedColumns: string[] = [ + 'client', + 'fromAccount', + 'beneficiary', + 'toAccount', + 'amount', + 'validity', + 'actions' + ]; + + /** Instruction Table Reference */ + @ViewChild('instructionsTable', { static: true }) instructionTableRef: MatTable; + + /** + * Retrieves Current Account Data from `resolve`. + * @param {ActivatedRoute} route Activated Route. + * @param {SettingsService} settingsService Setting service + */ + constructor() { + this.route.parent.data.subscribe((data: { currentAccountData: any }) => { + this.currentAccountData = data.currentAccountData; + }); + } + + ngOnInit() { + this.getStandingInstructions(); + } + + /** + * Retrieves standing instructions and initializes instructions table. + */ + getStandingInstructions() { + const clientId = this.currentAccountData.clientId; + const clientName = this.currentAccountData.clientName; + const accountId = this.currentAccountData.id; + const locale = this.settingsService.language.code; + const dateFormat = this.settingsService.dateFormat; + this.currentAccountsService + .getStandingInstructions(clientId, clientName, accountId, locale, dateFormat) + .subscribe((response: any) => { + this.instructionsData = response.pageItems; + this.dataSource.data = this.instructionsData; + this.instructionTableRef.renderRows(); + }); + } + + deleteStandingInstruction(instructionId: any) { + const deleteStandingInstructionDialogRef = this.dialog.open(DeleteDialogComponent, { + data: { deleteContext: `standing instruction id: ${instructionId}` } + }); + deleteStandingInstructionDialogRef.afterClosed().subscribe((response: any) => { + if (response.delete) { + this.accountTransfersService.deleteStandingInstrucions(instructionId).subscribe(() => {}); + } + }); + } +} diff --git a/src/app/current-accounts/current-account-view/transactions-tab/export-transactions/export-transactions.component.html b/src/app/current-accounts/current-account-view/transactions-tab/export-transactions/export-transactions.component.html new file mode 100644 index 000000000..6881feb92 --- /dev/null +++ b/src/app/current-accounts/current-account-view/transactions-tab/export-transactions/export-transactions.component.html @@ -0,0 +1,58 @@ + + +
+
+ + {{ 'labels.inputs.From Date' | translate }} + + + + + {{ 'labels.inputs.From Date' | translate }} {{ 'labels.commons.is' | translate }} + {{ 'labels.commons.required' | translate }} + + + + + {{ 'labels.inputs.To Date' | translate }} + + + + + {{ 'labels.inputs.To Date' | translate }} {{ 'labels.commons.is' | translate }} + {{ 'labels.commons.required' | translate }} + + +
+ +
+ + +
+
+ +@if (!hideOutput) { + +} diff --git a/src/app/current-accounts/current-account-view/transactions-tab/export-transactions/export-transactions.component.scss b/src/app/current-accounts/current-account-view/transactions-tab/export-transactions/export-transactions.component.scss new file mode 100644 index 000000000..dab071f33 --- /dev/null +++ b/src/app/current-accounts/current-account-view/transactions-tab/export-transactions/export-transactions.component.scss @@ -0,0 +1,13 @@ +/** + * 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/. + */ + +.generate-button { + max-height: 2%; + padding: 1% 0 2% 6%; + align-self: center; +} diff --git a/src/app/current-accounts/current-account-view/transactions-tab/export-transactions/export-transactions.component.ts b/src/app/current-accounts/current-account-view/transactions-tab/export-transactions/export-transactions.component.ts new file mode 100644 index 000000000..6bdbd07d5 --- /dev/null +++ b/src/app/current-accounts/current-account-view/transactions-tab/export-transactions/export-transactions.component.ts @@ -0,0 +1,120 @@ +/** + * 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, OnInit, inject } from '@angular/core'; +import { DomSanitizer } from '@angular/platform-browser'; +import { UntypedFormBuilder, Validators, ReactiveFormsModule } from '@angular/forms'; +import { ActivatedRoute, RouterLink } from '@angular/router'; + +/** Custom Services */ +import { ReportsService } from 'app/reports/reports.service'; +import { SettingsService } from 'app/settings/settings.service'; +import { Dates } from 'app/core/utils/dates'; +import { FaIconComponent } from '@fortawesome/angular-fontawesome'; +import { STANDALONE_SHARED_IMPORTS } from 'app/standalone-shared.module'; + +/** + * Export Client Current Account Transactions Component + */ +@Component({ + selector: 'mifosx-export-transactions', + templateUrl: './export-transactions.component.html', + styleUrls: ['./export-transactions.component.scss'], + imports: [ + ...STANDALONE_SHARED_IMPORTS, + FaIconComponent + ] +}) +export class ExportTransactionsComponent implements OnInit { + private sanitizer = inject(DomSanitizer); + private reportsService = inject(ReportsService); + private formBuilder = inject(UntypedFormBuilder); + private dateUtils = inject(Dates); + private route = inject(ActivatedRoute); + private settingsService = inject(SettingsService); + + /** Minimum date allowed. */ + minDate = new Date(2000, 0, 1); + /** Maximum date allowed. */ + maxDate = new Date(); + /** Transactions Report Form */ + transactionsReportForm: any; + /** substitute for resolver */ + hideOutput = true; + /** trusted resource url for pentaho output */ + pentahoUrl: any; + /** Current Account Id */ + currentAccountId: any; + + /** + * Fetches current account data from grandparent's `resolve` + * @param {DomSanitizer} sanitizer DOM Sanitizer + * @param {ReportsService} reportsService Reports Service + * @param {FormBuilder} formBuilder Form Builder + * @param {Dates} dateUtils Date Utils + * @param {ActivatedRoute} route Activated Route + * @param {SettingsService} settingsService Settings Service + */ + constructor() { + this.route.parent.parent.data.subscribe((data: { currentAccountData: any }) => { + this.currentAccountId = data.currentAccountData.accountNo; + }); + } + + ngOnInit() { + this.maxDate = this.settingsService.businessDate; + this.createTransactionsReportForm(); + } + + /** + * Creates the transactions report form. + */ + createTransactionsReportForm() { + this.transactionsReportForm = this.formBuilder.group({ + fromDate: [ + '', + Validators.required + ], + toDate: [ + '', + Validators.required + ] + }); + } + + /** + * Generates client current account transactions report. + */ + generate() { + const data = { + 'output-type': 'PDF', + R_startDate: this.dateUtils.formatDate( + this.transactionsReportForm.value.fromDate, + this.settingsService.dateFormat + ), + R_endDate: this.dateUtils.formatDate(this.transactionsReportForm.value.toDate, this.settingsService.dateFormat), + R_currentAccountId: this.currentAccountId + }; + this.reportsService + .getPentahoRunReportData( + 'Client Current Account Transactions', + data, + 'default', + this.settingsService.language.code, + this.settingsService.dateFormat + ) + .subscribe((res: any) => { + const contentType = res.headers.get('Content-Type'); + const file = new Blob([res.body], { type: contentType }); + const filecontent = URL.createObjectURL(file); + this.pentahoUrl = this.sanitizer.bypassSecurityTrustResourceUrl(filecontent); + this.hideOutput = false; + }); + } +} diff --git a/src/app/current-accounts/current-account-view/transactions-tab/transactions-tab.component.html b/src/app/current-accounts/current-account-view/transactions-tab/transactions-tab.component.html new file mode 100644 index 000000000..91b3c19ac --- /dev/null +++ b/src/app/current-accounts/current-account-view/transactions-tab/transactions-tab.component.html @@ -0,0 +1,185 @@ + + +
+
+
+ @if (checkStatus()) { +
+ + {{ 'labels.inputs.Hide Reversed' | translate }} + + {{ 'labels.inputs.Hide Accruals' | translate }} + +
+ } +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
# + {{ idx + 1 }} + {{ 'labels.inputs.Id' | translate }} + {{ transaction.id }} + + {{ 'labels.inputs.Transaction Date' | translate }} + + {{ transaction.date | dateFormat }} + {{ 'labels.inputs.External Id' | translate }} + + {{ 'labels.inputs.Transaction Type' | translate }} + {{ transaction.transactionType.value | translateKey: 'catalogs' }} + {{ 'labels.inputs.Debit' | translate }} + {{ + isDebit(transaction.transactionType) + ? (transaction.amount | currency: currency?.code || 'USD' : 'symbol-narrow' : '1.2-2') + : 'N/A' + }} + {{ 'labels.inputs.Credit' | translate }} + {{ + !isDebit(transaction.transactionType) + ? (transaction.amount | currency: currency?.code || 'USD' : 'symbol-narrow' : '1.2-2') + : 'N/A' + }} + {{ 'labels.inputs.Balance' | translate }} + {{ transaction.runningBalance | currency: currency?.code || 'USD' : 'symbol-narrow' : '1.2-2' }} + {{ 'labels.inputs.Actions' | translate }} + + + + @if (!transaction.reversed) { + + + + } + + + +
+ + +
+ +
+
+ + {{ 'labels.text.No transaction was found' | translate }} +
+
+
diff --git a/src/app/current-accounts/current-account-view/transactions-tab/transactions-tab.component.scss b/src/app/current-accounts/current-account-view/transactions-tab/transactions-tab.component.scss new file mode 100644 index 000000000..1aea1e37b --- /dev/null +++ b/src/app/current-accounts/current-account-view/transactions-tab/transactions-tab.component.scss @@ -0,0 +1,42 @@ +/** + * 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/. + */ + +@use 'assets/styles/helper'; + +.tab-container { + padding: 1%; + margin: 1%; + + h3 { + margin: 1% auto; + } + + .action-button { + margin-left: auto; + } + + table { + width: 100%; + + .account-action-button { + min-width: 26px; + padding: 0 0 3px; + margin: 0 10%; + line-height: 25px; + + .accruals { + padding-top: 1%; + } + } + } +} + +.select-row:hover { + cursor: pointer; + font-weight: 800; +} diff --git a/src/app/current-accounts/current-account-view/transactions-tab/transactions-tab.component.ts b/src/app/current-accounts/current-account-view/transactions-tab/transactions-tab.component.ts new file mode 100644 index 000000000..408167c62 --- /dev/null +++ b/src/app/current-accounts/current-account-view/transactions-tab/transactions-tab.component.ts @@ -0,0 +1,270 @@ +/** + * 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, OnInit, ViewChild, inject } from '@angular/core'; +import { UntypedFormControl, ReactiveFormsModule } from '@angular/forms'; +import { MatPaginator } from '@angular/material/paginator'; +import { MatSort, MatSortHeader } from '@angular/material/sort'; +import { CurrencyPipe, NgClass } from '@angular/common'; +import { + MatTableDataSource, + MatTable, + MatColumnDef, + MatHeaderCellDef, + MatHeaderCell, + MatCellDef, + MatCell, + MatHeaderRowDef, + MatHeaderRow, + MatRowDef, + MatRow +} from '@angular/material/table'; +import { ActivatedRoute, Router, RouterLink } from '@angular/router'; +import { Dates } from 'app/core/utils/dates'; +import { + CurrentAccountTransaction, + CurrentAccountTransactionType +} from 'app/current-accounts/models/current-account-transaction.model'; +import { CurrentAccountsService } from 'app/current-accounts/current-accounts.service'; +import { SettingsService } from 'app/settings/settings.service'; +import { UndoTransactionDialogComponent } from '../custom-dialogs/undo-transaction-dialog/undo-transaction-dialog.component'; +import { MatDialog } from '@angular/material/dialog'; +import { Currency } from 'app/shared/models/general.model'; +import { MatCheckbox } from '@angular/material/checkbox'; +import { MatButton, MatIconButton } from '@angular/material/button'; +import { ExternalIdentifierComponent } from '../../../shared/external-identifier/external-identifier.component'; +import { MatMenuTrigger, MatMenu, MatMenuItem } from '@angular/material/menu'; +import { MatIcon } from '@angular/material/icon'; +import { FaIconComponent } from '@fortawesome/angular-fontawesome'; +import { DateFormatPipe } from '../../../pipes/date-format.pipe'; +import { STANDALONE_SHARED_IMPORTS } from 'app/standalone-shared.module'; + +/** + * Transactions Tab Component. + */ +@Component({ + selector: 'mifosx-transactions-tab', + templateUrl: './transactions-tab.component.html', + styleUrls: ['./transactions-tab.component.scss'], + imports: [ + ...STANDALONE_SHARED_IMPORTS, + MatCheckbox, + MatTable, + MatSort, + MatColumnDef, + MatHeaderCellDef, + MatHeaderCell, + MatSortHeader, + MatCellDef, + MatCell, + NgClass, + ExternalIdentifierComponent, + MatIconButton, + MatMenuTrigger, + MatIcon, + MatMenu, + MatMenuItem, + FaIconComponent, + MatHeaderRowDef, + MatHeaderRow, + MatRowDef, + MatRow, + MatPaginator, + DateFormatPipe, + CurrencyPipe + ] +}) +export class TransactionsTabComponent implements OnInit { + private route = inject(ActivatedRoute); + private router = inject(Router); + private currentAccountsService = inject(CurrentAccountsService); + private settingsService = inject(SettingsService); + private dialog = inject(MatDialog); + private dateUtils = inject(Dates); + + /** Current Account Status */ + status: any; + /** Currency */ + currency: Currency | null = null; + /** Transactions Data */ + transactionsData: CurrentAccountTransaction[] = []; + /** Form control to handle accural parameter */ + hideAccrualsParam: UntypedFormControl; + hideReversedParam: UntypedFormControl; + /** Columns to be displayed in transactions table. */ + displayedColumns: string[] = [ + 'row', + 'id', + 'date', + 'externalId', + 'transactionType', + 'debit', + 'credit', + 'balance', + 'actions' + ]; + /** Data source for transactions table. */ + dataSource: MatTableDataSource; + @ViewChild(MatPaginator, { static: true }) paginator: MatPaginator; + @ViewChild(MatSort, { static: true }) sort: MatSort; + + accountWithTransactions = false; + + accountId: string; + + /** + * Retrieves current account data from `resolve`. + * @param {ActivatedRoute} route Activated Route. + */ + constructor() { + this.route.parent.parent.data.subscribe((data: { currentAccountData: any }) => { + this.transactionsData = data.currentAccountData.transactions; + this.status = data.currentAccountData.status.value; + this.currency = data.currentAccountData.currency || null; + }); + this.accountId = this.route.parent.parent.snapshot.params['currentAccountId']; + } + + ngOnInit() { + this.hideAccrualsParam = new UntypedFormControl(false); + this.hideReversedParam = new UntypedFormControl(false); + this.setTransactions(); + } + + setTransactions(): void { + this.dataSource = new MatTableDataSource(this.transactionsData); + this.accountWithTransactions = this.transactionsData && this.transactionsData.length > 0; + this.dataSource.paginator = this.paginator; + this.dataSource.sort = this.sort; + } + + /** + * Checks if transaction is debit. + * @param {any} transactionType Transaction Type + */ + isDebit(transactionType: CurrentAccountTransactionType) { + return ( + transactionType.withdrawal === true || + transactionType.feeDeduction === true || + transactionType.overdraftInterest === true || + transactionType.withholdTax === true + ); + } + + isAccrual(transactionType: CurrentAccountTransactionType): boolean { + return transactionType.accrual || transactionType.code === 'currentAccountTransactionType.accrual'; + } + + /** + * Checks transaction status. + */ + checkStatus() { + if ( + this.status === 'Active' || + this.status === 'Closed' || + this.status === 'Transfer in progress' || + this.status === 'Transfer on hold' || + this.status === 'Premature Closed' || + this.status === 'Matured' + ) { + return true; + } + return false; + } + + /** + * Show Transactions Details + * @param transactionsData Transactions Data + */ + showTransactions(transactionsData: CurrentAccountTransaction) { + if (transactionsData.transfer) { + this.router.navigate([`../transfer-funds/account-transfers/${transactionsData.transfer.id}`], { + relativeTo: this.route + }); + } else { + this.router.navigate( + [ + transactionsData.id, + 'general' + ], + { relativeTo: this.route } + ); + } + } + + /** + * Stops the propagation to view pages. + * @param $event Mouse Event + */ + routeEdit($event: MouseEvent) { + $event.stopPropagation(); + } + + hideAccruals() { + this.filterTransactions(this.hideReversedParam.value, this.hideAccrualsParam.value); + } + + hideReversed() { + this.filterTransactions(this.hideReversedParam.value, this.hideAccrualsParam.value); + } + + filterTransactions(hideReversed: boolean, hideAccrual: boolean): void { + let transactions: CurrentAccountTransaction[] = this.transactionsData; + + if (hideAccrual || hideReversed) { + transactions = this.transactionsData.filter((t: CurrentAccountTransaction) => { + return !(hideReversed && t.reversed) && !(hideAccrual && t.transactionType.accrual); + }); + } + this.dataSource = new MatTableDataSource(transactions); + this.dataSource.paginator = this.paginator; + this.dataSource.sort = this.sort; + } + + currentAccountTransactionColor(transaction: CurrentAccountTransaction): string { + if (transaction.reversed) { + return 'strike'; + } else if (transaction.transfer) { + return 'transfer'; + } else if (transaction.transactionType.accrual) { + return 'accrual'; + } else { + return ''; + } + } + + undoTransaction(transactionData: CurrentAccountTransaction): void { + const undoTransactionAccountDialogRef = this.dialog.open(UndoTransactionDialogComponent); + undoTransactionAccountDialogRef.afterClosed().subscribe((response: any) => { + if (response.confirm) { + const locale = this.settingsService.language.code; + const dateFormat = this.settingsService.dateFormat; + const data = { + transactionDate: this.dateUtils.parseDate(transactionData.date), + transactionAmount: 0, + dateFormat, + locale + }; + this.currentAccountsService + .executeCurrentAccountTransactionsCommand(this.accountId, 'undo', data, transactionData.id) + .subscribe(() => { + this.reload(); + }); + } + }); + } + + private reload() { + const clientId = this.route.parent.parent.snapshot.params['clientId']; + const url: string = this.router.url; + this.router + .navigateByUrl(`/clients/${clientId}/current-accounts`, { skipLocationChange: true }) + .then(() => this.router.navigate([url])); + } +} diff --git a/src/app/current-accounts/current-account-view/transactions/edit-transaction/edit-transaction.component.html b/src/app/current-accounts/current-account-view/transactions/edit-transaction/edit-transaction.component.html new file mode 100644 index 000000000..c3bf3eb64 --- /dev/null +++ b/src/app/current-accounts/current-account-view/transactions/edit-transaction/edit-transaction.component.html @@ -0,0 +1,108 @@ + + +
+ +
+ + + {{ 'labels.inputs.Transaction Date' | translate }} + + + + @if (editTransactionForm.controls.transactionDate.hasError('required')) { + + {{ 'labels.inputs.Transaction Date' | translate }} {{ 'labels.commons.is' | translate }} + {{ 'labels.commons.required' | translate }} + + } + + + + + + + {{ 'labels.inputs.Payment Type' | translate }} + + @for (paymentType of paymentTypeOptions; track paymentType) { + + {{ paymentType.name }} + + } + + + +
+ + +
+ {{ 'labels.inputs.Show Payment Details' | translate }} +
+
+
+
+ + @if (editTransactionForm.controls['accountNumber']) { + + {{ 'labels.inputs.Account Number' | translate }} + + + } + + @if (editTransactionForm.controls['checkNumber']) { + + {{ 'labels.inputs.Cheque' | translate }} + + + } + + @if (editTransactionForm.controls['routingCode']) { + + {{ 'labels.inputs.Routing Code' | translate }} + + + } + + @if (editTransactionForm.controls['receiptNumber']) { + + {{ 'labels.inputs.Receipt Number' | translate }} + + + } + + @if (editTransactionForm.controls['bankNumber']) { + + {{ 'labels.inputs.Bank' | translate }} + + + } + + + + + +
+
+
+
diff --git a/src/app/current-accounts/current-account-view/transactions/edit-transaction/edit-transaction.component.scss b/src/app/current-accounts/current-account-view/transactions/edit-transaction/edit-transaction.component.scss new file mode 100644 index 000000000..e1ea46f9f --- /dev/null +++ b/src/app/current-accounts/current-account-view/transactions/edit-transaction/edit-transaction.component.scss @@ -0,0 +1,11 @@ +/** + * 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/. + */ + +.container { + width: 37rem; +} diff --git a/src/app/current-accounts/current-account-view/transactions/edit-transaction/edit-transaction.component.ts b/src/app/current-accounts/current-account-view/transactions/edit-transaction/edit-transaction.component.ts new file mode 100644 index 000000000..98d907228 --- /dev/null +++ b/src/app/current-accounts/current-account-view/transactions/edit-transaction/edit-transaction.component.ts @@ -0,0 +1,164 @@ +/** + * 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, OnInit, inject } from '@angular/core'; +import { + UntypedFormGroup, + UntypedFormBuilder, + Validators, + UntypedFormControl, + ReactiveFormsModule +} from '@angular/forms'; +import { Router, ActivatedRoute, RouterLink } from '@angular/router'; +import { Dates } from 'app/core/utils/dates'; + +/** Custom Services */ +import { CurrentAccountsService } from 'app/current-accounts/current-accounts.service'; +import { SettingsService } from 'app/settings/settings.service'; +import { Currency } from 'app/shared/models/general.model'; +import { InputAmountComponent } from '../../../../shared/input-amount/input-amount.component'; +import { MatSlideToggle } from '@angular/material/slide-toggle'; +import { STANDALONE_SHARED_IMPORTS } from 'app/standalone-shared.module'; + +/** + * Edit Transaction component. + */ +@Component({ + selector: 'mifosx-edit-transaction', + templateUrl: './edit-transaction.component.html', + styleUrls: ['./edit-transaction.component.scss'], + imports: [ + ...STANDALONE_SHARED_IMPORTS, + InputAmountComponent, + MatSlideToggle + ] +}) +export class EditTransactionComponent implements OnInit { + private formBuilder = inject(UntypedFormBuilder); + private route = inject(ActivatedRoute); + private router = inject(Router); + private dateUtils = inject(Dates); + private currentAccountsService = inject(CurrentAccountsService); + private settingsService = inject(SettingsService); + + /** Minimum Due Date allowed. */ + minDate = new Date(2000, 0, 1); + /** Maximum Due Date allowed. */ + maxDate = new Date(); + /** Current account transaction form. */ + editTransactionForm: UntypedFormGroup; + /** current account transaction payment options. */ + paymentTypeOptions: { + id: number; + name: string; + description: string; + isCashPayment: boolean; + position: number; + }[]; + /** Flag to enable payment details fields. */ + showPaymentDetails: Boolean = false; + /** current account's Id */ + currentAccountId: string; + /** Transaction Template */ + transactionTemplateData: any; + currency: Currency; + + /** + * Retrieves the Current Account transaction template data from `resolve`. + * @param {FormBuilder} formBuilder Form Builder. + * @param {CurrentAccountsService} currentAccountsService Current Accounts Service. + * @param {ActivatedRoute} route Activated Route. + * @param {Dates} dateUtils Date Utils. + * @param {Router} router Router for navigation. + * @param {SettingsService} settingsService Setting service + */ + constructor() { + this.route.data.subscribe((data: { currentAccountTransactionTemplate: any }) => { + this.transactionTemplateData = data.currentAccountTransactionTemplate; + if (data.currentAccountTransactionTemplate.currency) { + this.currency = data.currentAccountTransactionTemplate.currency; + } + this.paymentTypeOptions = this.transactionTemplateData.paymentTypeOptions; + }); + this.currentAccountId = this.route.snapshot.params['currentAccountId']; + } + + /** + * Creates the Current Account account transaction form when component loads. + */ + ngOnInit() { + this.maxDate = this.settingsService.businessDate; + this.createEditTransactionForm(); + this.editTransactionForm.patchValue({ + transactionDate: this.transactionTemplateData.date && new Date(this.transactionTemplateData.date), + transactionAmount: this.transactionTemplateData.amount, + paymentTypeId: this.transactionTemplateData.paymentTypeId + }); + } + + /** + * Method to create the Current Account Transaction Form. + */ + createEditTransactionForm() { + this.editTransactionForm = this.formBuilder.group({ + transactionDate: [ + '', + Validators.required + ], + transactionAmount: [ + '', + Validators.required + ], + paymentTypeId: [''] + }); + } + + /** + * Method to add payment detail fields to the UI. + */ + addPaymentDetails() { + this.showPaymentDetails = !this.showPaymentDetails; + if (this.showPaymentDetails) { + this.editTransactionForm.addControl('accountNumber', new UntypedFormControl('')); + this.editTransactionForm.addControl('checkNumber', new UntypedFormControl('')); + this.editTransactionForm.addControl('routingCode', new UntypedFormControl('')); + this.editTransactionForm.addControl('receiptNumber', new UntypedFormControl('')); + this.editTransactionForm.addControl('bankNumber', new UntypedFormControl('')); + } else { + this.editTransactionForm.removeControl('accountNumber'); + this.editTransactionForm.removeControl('checkNumber'); + this.editTransactionForm.removeControl('routingCode'); + this.editTransactionForm.removeControl('receiptNumber'); + this.editTransactionForm.removeControl('bankNumber'); + } + } + + /** + * Method to submit the transaction details. + */ + submit() { + const editTransactionFormData = this.editTransactionForm.value; + const locale = this.settingsService.language.code; + const dateFormat = this.settingsService.dateFormat; + const prevTransactionDate: Date = this.editTransactionForm.value.transactionDate; + if (editTransactionFormData.transactionDate instanceof Date) { + editTransactionFormData.transactionDate = this.dateUtils.formatDate(prevTransactionDate, dateFormat); + } + const data = { + ...editTransactionFormData, + dateFormat, + locale + }; + this.currentAccountsService + .executeCurrentAccountTransactionsCommand(this.currentAccountId, 'modify', data, this.transactionTemplateData.id) + .subscribe((res) => { + this.router.navigate(['../'], { relativeTo: this.route }); + }); + } +} diff --git a/src/app/current-accounts/current-account-view/transactions/view-reciept/view-reciept.component.html b/src/app/current-accounts/current-account-view/transactions/view-reciept/view-reciept.component.html new file mode 100644 index 000000000..a25238587 --- /dev/null +++ b/src/app/current-accounts/current-account-view/transactions/view-reciept/view-reciept.component.html @@ -0,0 +1,33 @@ + + + +
+ +
+ +
+ +
+ + +
+

Unable to Load Receipt

+

The transaction receipt could not be loaded. This may be due to:

+
    +
  • Missing or incomplete report configuration
  • +
  • Report generation service unavailable
  • +
  • Invalid transaction data
  • +
+

Please contact your system administrator for assistance.

+

Technical details may be available in the browser console (F12).

+
+
+
diff --git a/src/app/current-accounts/current-account-view/transactions/view-reciept/view-reciept.component.scss b/src/app/current-accounts/current-account-view/transactions/view-reciept/view-reciept.component.scss new file mode 100644 index 000000000..20b9898e4 --- /dev/null +++ b/src/app/current-accounts/current-account-view/transactions/view-reciept/view-reciept.component.scss @@ -0,0 +1,58 @@ +/** + * 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/. + */ + +.container { + max-width: 50rem; + margin: 0 auto; + + .back-button { + margin-bottom: 1rem; + } + + .pdf-container { + width: 100%; + height: calc(100vh - 250px); + min-height: 600px; + display: flex; + justify-content: center; + align-items: center; + background-color: #525659; + + .pdf-iframe { + width: 100%; + height: 100%; + border: none; + } + } + + .error-message { + padding: 2rem; + text-align: center; + background-color: #fff3cd; + border: 1px solid #ffc107; + border-radius: 4px; + margin: 1rem 0; + + h3 { + color: #856404; + margin-bottom: 1rem; + } + + ul { + text-align: left; + display: inline-block; + margin: 1rem 0; + } + + .console-hint { + font-size: 0.875rem; + color: #6c757d; + margin-top: 1rem; + } + } +} diff --git a/src/app/current-accounts/current-account-view/transactions/view-reciept/view-reciept.component.ts b/src/app/current-accounts/current-account-view/transactions/view-reciept/view-reciept.component.ts new file mode 100644 index 000000000..f3c21dca8 --- /dev/null +++ b/src/app/current-accounts/current-account-view/transactions/view-reciept/view-reciept.component.ts @@ -0,0 +1,72 @@ +/** + * 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, OnInit, OnDestroy, inject } from '@angular/core'; +import { DomSanitizer, SafeResourceUrl } from '@angular/platform-browser'; +import { ActivatedRoute, RouterLink } from '@angular/router'; +import { FaIconComponent } from '@fortawesome/angular-fontawesome'; +import { STANDALONE_SHARED_IMPORTS } from 'app/standalone-shared.module'; +import { HttpResponse } from '@angular/common/http'; + +/** + * View Transaction Reciept Component + */ +@Component({ + selector: 'mifosx-view-reciept', + templateUrl: './view-reciept.component.html', + styleUrls: ['./view-reciept.component.scss'], + imports: [ + ...STANDALONE_SHARED_IMPORTS, + FaIconComponent + ] +}) +export class ViewRecieptComponent implements OnInit, OnDestroy { + private sanitizer = inject(DomSanitizer); + private route = inject(ActivatedRoute); + + /** trusted resource url for pentaho output */ + pentahoUrl: SafeResourceUrl | null = null; + /** Transaction Reciept Data */ + transactionRecieptData: HttpResponse; + /** Blob URL for cleanup */ + private blobUrl: string | null = null; + + /** + * Fetches transaction reciept `resolve` + * @param {DomSanitizer} sanitizer DOM Sanitizer + * @param {ActivatedRoute} route Activated Route + */ + constructor() { + this.route.data.subscribe((data: { currentAccountTransactionReciept: HttpResponse }) => { + this.transactionRecieptData = data.currentAccountTransactionReciept; + }); + } + + ngOnInit() { + if (!this.transactionRecieptData || !this.transactionRecieptData.body) { + return; + } + + const contentType = this.transactionRecieptData.headers.get('Content-Type'); + const file = new Blob([this.transactionRecieptData.body], { type: contentType }); + + if (file.size === 0) { + return; + } + + this.blobUrl = URL.createObjectURL(file); + this.pentahoUrl = this.sanitizer.bypassSecurityTrustResourceUrl(this.blobUrl); + } + + ngOnDestroy() { + if (this.blobUrl) { + URL.revokeObjectURL(this.blobUrl); + } + } +} diff --git a/src/app/current-accounts/current-account-view/transactions/view-transaction/current-accounts-transaction-datatable-tab/current-accounts-transaction-datatable-tab.component.html b/src/app/current-accounts/current-account-view/transactions/view-transaction/current-accounts-transaction-datatable-tab/current-accounts-transaction-datatable-tab.component.html new file mode 100644 index 000000000..d504bcc56 --- /dev/null +++ b/src/app/current-accounts/current-account-view/transactions/view-transaction/current-accounts-transaction-datatable-tab/current-accounts-transaction-datatable-tab.component.html @@ -0,0 +1,9 @@ + + +

{{ 'labels.heading.current-account-transaction-datatable-tab works' | translate }}!

diff --git a/src/app/current-accounts/current-account-view/transactions/view-transaction/current-accounts-transaction-datatable-tab/current-accounts-transaction-datatable-tab.component.scss b/src/app/current-accounts/current-account-view/transactions/view-transaction/current-accounts-transaction-datatable-tab/current-accounts-transaction-datatable-tab.component.scss new file mode 100644 index 000000000..af9f4c075 --- /dev/null +++ b/src/app/current-accounts/current-account-view/transactions/view-transaction/current-accounts-transaction-datatable-tab/current-accounts-transaction-datatable-tab.component.scss @@ -0,0 +1,7 @@ +/** + * 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/. + */ diff --git a/src/app/current-accounts/current-account-view/transactions/view-transaction/current-accounts-transaction-datatable-tab/current-accounts-transaction-datatable-tab.component.ts b/src/app/current-accounts/current-account-view/transactions/view-transaction/current-accounts-transaction-datatable-tab/current-accounts-transaction-datatable-tab.component.ts new file mode 100644 index 000000000..59a129971 --- /dev/null +++ b/src/app/current-accounts/current-account-view/transactions/view-transaction/current-accounts-transaction-datatable-tab/current-accounts-transaction-datatable-tab.component.ts @@ -0,0 +1,22 @@ +/** + * Copyright since 2025 Mifos Initiative + * + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. + */ + +import { Component } from '@angular/core'; +import { STANDALONE_SHARED_IMPORTS } from 'app/standalone-shared.module'; + +@Component({ + selector: 'mifosx-current-account-transaction-datatable-tab', + templateUrl: './current-accounts-transaction-datatable-tab.component.html', + styleUrls: ['./current-accounts-transaction-datatable-tab.component.scss'], + imports: [ + ...STANDALONE_SHARED_IMPORTS + ] +}) +export class CurrentAccountTransactionDatatableTabComponent { + constructor() {} +} diff --git a/src/app/current-accounts/current-account-view/transactions/view-transaction/current-accounts-transaction-general-tab/current-accounts-transaction-general-tab.component.html b/src/app/current-accounts/current-account-view/transactions/view-transaction/current-accounts-transaction-general-tab/current-accounts-transaction-general-tab.component.html new file mode 100644 index 000000000..21db3b8d3 --- /dev/null +++ b/src/app/current-accounts/current-account-view/transactions/view-transaction/current-accounts-transaction-general-tab/current-accounts-transaction-general-tab.component.html @@ -0,0 +1,128 @@ + + +
+ @if ( + !( + transactionData.interestedPostedAsOn === false && + (transactionData.transactionType.id === 17 || transactionData.transactionType.id === 3) + ) + ) { + + + @if ( + transactionData.transactionType.value === 'Transfer' || + transactionData.reversed === 'true' || + transactionData.transactionType.id === 3 || + transactionData.transactionType.id === 17 + ) { + + } + + + } + @if (!transactionData.reversed && transactionData.transactionType.amountHold) { + + + + } + @if (allowUndo()) { + + + + } +
+ +
+ + +
+
+ +
+ {{ 'labels.inputs.Id' | translate }} +
+ +
+ {{ transactionData.id }} +
+ +
+ {{ 'labels.inputs.Transaction Type' | translate }} +
+ +
+ {{ transactionData.transactionType.value | translateKey: 'catalogs' }} +
+ +
+ {{ 'labels.inputs.Transaction Date' | translate }} +
+ +
+ {{ transactionData.date | dateFormat }} +
+ +
+ {{ 'labels.inputs.Currency' | translate }} +
+ +
+ {{ transactionData.currency.displayLabel }} +
+ +
+ {{ 'labels.inputs.Amount' | translate }} +
+ +
+ {{ transactionData.amount | currency: transactionData.currency.code : 'symbol-narrow' : '1.2-2' }} +
+ + @if (transactionData.note) { +
+ {{ 'labels.inputs.Note' | translate }} +
+ } + + @if (transactionData.note) { +
+ {{ transactionData.note }} +
+ } + + @if (transactionData.paymentDetailData) { + + + } +
+ +
+ +
+
+
+
diff --git a/src/app/current-accounts/current-account-view/transactions/view-transaction/current-accounts-transaction-general-tab/current-accounts-transaction-general-tab.component.scss b/src/app/current-accounts/current-account-view/transactions/view-transaction/current-accounts-transaction-general-tab/current-accounts-transaction-general-tab.component.scss new file mode 100644 index 000000000..29e681005 --- /dev/null +++ b/src/app/current-accounts/current-account-view/transactions/view-transaction/current-accounts-transaction-general-tab/current-accounts-transaction-general-tab.component.scss @@ -0,0 +1,45 @@ +/** + * 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/. + */ + +@use 'assets/styles/helper'; +@use 'assets/styles/colours' as *; + +.container { + max-width: 37rem; + + .card-content { + div { + margin: 1rem 0; + word-wrap: break-word; + } + + mifosx-transaction-payment-detail { + width: 100%; + margin-bottom: 20px; + } + } +} + +.transaction-buttons { + margin-top: 3%; +} + +.active { + background-color: $status-active; + height: 4px; +} + +.linked { + background-color: $status-approved; + height: 4px; +} + +.undo { + background-color: $status-active-overdue; + height: 4px; +} diff --git a/src/app/current-accounts/current-account-view/transactions/view-transaction/current-accounts-transaction-general-tab/current-accounts-transaction-general-tab.component.ts b/src/app/current-accounts/current-account-view/transactions/view-transaction/current-accounts-transaction-general-tab/current-accounts-transaction-general-tab.component.ts new file mode 100644 index 000000000..c476d6c42 --- /dev/null +++ b/src/app/current-accounts/current-account-view/transactions/view-transaction/current-accounts-transaction-general-tab/current-accounts-transaction-general-tab.component.ts @@ -0,0 +1,106 @@ +/** + * 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 { MatDialog } from '@angular/material/dialog'; +import { ActivatedRoute, Router, RouterLink } from '@angular/router'; +import { Dates } from 'app/core/utils/dates'; +import { ReleaseAmountDialogComponent } from 'app/current-accounts/current-account-view/custom-dialogs/release-amount-dialog/release-amount-dialog.component'; +import { UndoTransactionDialogComponent } from 'app/current-accounts/current-account-view/custom-dialogs/undo-transaction-dialog/undo-transaction-dialog.component'; +import { CurrentAccountsService } from 'app/current-accounts/current-accounts.service'; +import { SettingsService } from 'app/settings/settings.service'; +import { NgClass, CurrencyPipe } from '@angular/common'; +import { FaIconComponent } from '@fortawesome/angular-fontawesome'; +import { TransactionPaymentDetailComponent } from '../../../../../shared/transaction-payment-detail/transaction-payment-detail.component'; +import { DateFormatPipe } from '../../../../../pipes/date-format.pipe'; +import { STANDALONE_SHARED_IMPORTS } from 'app/standalone-shared.module'; + +@Component({ + selector: 'mifosx-current-account-transaction-general-tab', + templateUrl: './current-accounts-transaction-general-tab.component.html', + styleUrls: ['./current-accounts-transaction-general-tab.component.scss'], + imports: [ + ...STANDALONE_SHARED_IMPORTS, + FaIconComponent, + NgClass, + TransactionPaymentDetailComponent, + CurrencyPipe, + DateFormatPipe + ] +}) +export class CurrentAccountTransactionGeneralTabComponent { + private currentAccountsService = inject(CurrentAccountsService); + private route = inject(ActivatedRoute); + private dateUtils = inject(Dates); + private router = inject(Router); + dialog = inject(MatDialog); + private settingsService = inject(SettingsService); + + accountId: string; + transactionId: string; + transactionData: any; + + constructor() { + this.route.data.subscribe((data: { currentAccountTransaction: any }) => { + this.accountId = this.route.parent.snapshot.params['currentAccountId']; + this.transactionData = data.currentAccountTransaction; + }); + } + + allowUndo(): boolean { + if (this.transactionData.reversed && this.transactionData.transactionType.amountHold) { + return false; + } + return !this.transactionData.reversed; + } + + releaseAmount(): void { + const releaseAmountDialogRef = this.dialog.open(ReleaseAmountDialogComponent); + releaseAmountDialogRef.afterClosed().subscribe((response: any) => { + if (response.confirm) { + const data = {}; + this.currentAccountsService + .executeCurrentAccountTransactionsCommand(this.accountId, 'releaseAmount', data, this.transactionData.id) + .subscribe(() => { + this.router.navigate(['../..'], { relativeTo: this.route }); + }); + } + }); + } + + undoTransaction(): void { + const undoTransactionAccountDialogRef = this.dialog.open(UndoTransactionDialogComponent); + undoTransactionAccountDialogRef.afterClosed().subscribe((response: any) => { + if (response.confirm) { + const locale = this.settingsService.language.code; + const dateFormat = this.settingsService.dateFormat; + const data = { + transactionDate: this.dateUtils.formatDate( + this.transactionData.date && new Date(this.transactionData.date), + dateFormat + ), + transactionAmount: 0, + dateFormat, + locale + }; + this.currentAccountsService + .executeCurrentAccountTransactionsCommand(this.accountId, 'undo', data, this.transactionData.id) + .subscribe(() => { + this.router.navigate(['../..'], { relativeTo: this.route }); + }); + } + }); + } + + transactionColor(): string { + if (this.transactionData.reversed) { + return 'undo'; + } + return 'active'; + } +} diff --git a/src/app/current-accounts/current-account-view/transactions/view-transaction/datatable-transaction-tab/datatable-transaction-tab.component.html b/src/app/current-accounts/current-account-view/transactions/view-transaction/datatable-transaction-tab/datatable-transaction-tab.component.html new file mode 100644 index 000000000..46c2a7452 --- /dev/null +++ b/src/app/current-accounts/current-account-view/transactions/view-transaction/datatable-transaction-tab/datatable-transaction-tab.component.html @@ -0,0 +1,17 @@ + + +
+ + +
diff --git a/src/app/current-accounts/current-account-view/transactions/view-transaction/datatable-transaction-tab/datatable-transaction-tab.component.scss b/src/app/current-accounts/current-account-view/transactions/view-transaction/datatable-transaction-tab/datatable-transaction-tab.component.scss new file mode 100644 index 000000000..af9f4c075 --- /dev/null +++ b/src/app/current-accounts/current-account-view/transactions/view-transaction/datatable-transaction-tab/datatable-transaction-tab.component.scss @@ -0,0 +1,7 @@ +/** + * 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/. + */ diff --git a/src/app/current-accounts/current-account-view/transactions/view-transaction/datatable-transaction-tab/datatable-transaction-tab.component.ts b/src/app/current-accounts/current-account-view/transactions/view-transaction/datatable-transaction-tab/datatable-transaction-tab.component.ts new file mode 100644 index 000000000..a17ec4b0d --- /dev/null +++ b/src/app/current-accounts/current-account-view/transactions/view-transaction/datatable-transaction-tab/datatable-transaction-tab.component.ts @@ -0,0 +1,39 @@ +/** + * 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 { ActivatedRoute } from '@angular/router'; +import { EntityDatatableTabComponent } from '../../../../../shared/tabs/entity-datatable-tab/entity-datatable-tab.component'; +import { STANDALONE_SHARED_IMPORTS } from 'app/standalone-shared.module'; + +@Component({ + selector: 'mifosx-datatable-transaction-tab', + templateUrl: './datatable-transaction-tab.component.html', + styleUrls: ['./datatable-transaction-tab.component.scss'], + imports: [ + ...STANDALONE_SHARED_IMPORTS, + EntityDatatableTabComponent + ] +}) +export class DatatableTransactionTabComponent { + private route = inject(ActivatedRoute); + + /** Transaction Id */ + entityId: string; + /** Current Account Datatable */ + entityDatatable: any; + /** Multi Row Datatable Flag */ + multiRowDatatableFlag: boolean; + constructor() { + this.entityId = this.route.parent.parent.snapshot.paramMap.get('id'); + this.route.data.subscribe((data: { transactionDatatable: any }) => { + this.entityDatatable = data.transactionDatatable; + this.multiRowDatatableFlag = this.entityDatatable.columnHeaders[0].columnName === 'id' ? true : false; + }); + } +} diff --git a/src/app/current-accounts/current-account-view/transactions/view-transaction/view-transaction.component.html b/src/app/current-accounts/current-account-view/transactions/view-transaction/view-transaction.component.html new file mode 100644 index 000000000..53c760c00 --- /dev/null +++ b/src/app/current-accounts/current-account-view/transactions/view-transaction/view-transaction.component.html @@ -0,0 +1,37 @@ + + +
+ + + + + +
diff --git a/src/app/current-accounts/current-account-view/transactions/view-transaction/view-transaction.component.scss b/src/app/current-accounts/current-account-view/transactions/view-transaction/view-transaction.component.scss new file mode 100644 index 000000000..e4166fc69 --- /dev/null +++ b/src/app/current-accounts/current-account-view/transactions/view-transaction/view-transaction.component.scss @@ -0,0 +1,18 @@ +/** + * 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/. + */ + +.container { + max-width: 37rem; + + .content { + div { + margin: 1rem 0; + word-wrap: break-word; + } + } +} diff --git a/src/app/current-accounts/current-account-view/transactions/view-transaction/view-transaction.component.ts b/src/app/current-accounts/current-account-view/transactions/view-transaction/view-transaction.component.ts new file mode 100644 index 000000000..51b458476 --- /dev/null +++ b/src/app/current-accounts/current-account-view/transactions/view-transaction/view-transaction.component.ts @@ -0,0 +1,50 @@ +/** + * 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, inject } from '@angular/core'; +import { ActivatedRoute, RouterLinkActive, RouterLink, RouterOutlet } from '@angular/router'; +import { MatDialog } from '@angular/material/dialog'; +import { MatTabNav, MatTabLink, MatTabNavPanel } from '@angular/material/tabs'; +import { STANDALONE_SHARED_IMPORTS } from 'app/standalone-shared.module'; + +@Component({ + selector: 'mifosx-view-transaction', + templateUrl: './view-transaction.component.html', + styleUrls: ['./view-transaction.component.scss'], + imports: [ + ...STANDALONE_SHARED_IMPORTS, + MatTabNav, + MatTabLink, + RouterLinkActive, + MatTabNavPanel, + RouterOutlet + ] +}) +export class ViewTransactionComponent { + private route = inject(ActivatedRoute); + dialog = inject(MatDialog); + + /** Transaction data. */ + transactionData: any; + + accountId: any; + /** Transaction Data Tables */ + entityDatatables: any; + + /** + * @param {Router} router Router for navigation. + * @param {MatDialog} dialog Dialog reference. + */ + constructor() { + this.route.data.subscribe((data: { transactionDatatables: any }) => { + this.accountId = this.route.snapshot.params['currentAccountId']; + this.entityDatatables = data.transactionDatatables; + }); + } +} diff --git a/src/app/current-accounts/current-account-view/view-charge/view-charge.component.html b/src/app/current-accounts/current-account-view/view-charge/view-charge.component.html new file mode 100644 index 000000000..5b1007bf8 --- /dev/null +++ b/src/app/current-accounts/current-account-view/view-charge/view-charge.component.html @@ -0,0 +1,155 @@ + + +@if (currentAccountData.status.value === 'Active' && chargeData.amountOutstanding !== 0) { +
+ + + @if (isRecurringCharge()) { + + + + } +
+} + +@if (currentAccountData.status.value === 'Submitted and pending approval') { +
+ + +
+} + +
+ + +
+
+ {{ 'labels.inputs.Name' | translate }} +
+ +
+ {{ chargeData.name }} +
+ +
+ {{ 'labels.inputs.Charge Type' | translate }} +
+ +
+ {{ chargeData.penalty ? 'Penalty' : ('Fee' | translateKey: 'catalogs') }} +
+ +
+ {{ 'labels.inputs.Currency' | translate }} +
+ +
+ {{ chargeData.currency.displayLabel }} +
+ +
+ {{ 'labels.inputs.Payment due at' | translate }} +
+ +
+ {{ chargeData.chargeTimeType.value | translateKey: 'catalogs' }} +
+ +
+ {{ 'labels.inputs.Payment due as of' | translate }} +
+ +
+ {{ chargeData.dueDate | dateFormat }} +
+ +
+ {{ 'labels.inputs.Calculation Type' | translate }} +
+ +
+ {{ chargeData.chargeCalculationType.value | translateKey: 'catalogs' }} +
+ +
+ {{ 'labels.inputs.Due' | translate }} +
+ +
+ {{ chargeData.amount | formatNumber }} +
+ +
+ {{ 'labels.inputs.Paid' | translate }} +
+ +
+ {{ chargeData.amountPaid | formatNumber }} +
+ +
+ {{ 'labels.inputs.Waived' | translate }} +
+ +
+ {{ chargeData.amountWaived | formatNumber }} +
+ +
+ {{ 'labels.inputs.Outstanding' | translate }} +
+ +
+ {{ chargeData.amountOutstanding | formatNumber }} +
+
+ +
+ +
+
+
+
diff --git a/src/app/current-accounts/current-account-view/view-charge/view-charge.component.scss b/src/app/current-accounts/current-account-view/view-charge/view-charge.component.scss new file mode 100644 index 000000000..e4166fc69 --- /dev/null +++ b/src/app/current-accounts/current-account-view/view-charge/view-charge.component.scss @@ -0,0 +1,18 @@ +/** + * 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/. + */ + +.container { + max-width: 37rem; + + .content { + div { + margin: 1rem 0; + word-wrap: break-word; + } + } +} diff --git a/src/app/current-accounts/current-account-view/view-charge/view-charge.component.ts b/src/app/current-accounts/current-account-view/view-charge/view-charge.component.ts new file mode 100644 index 000000000..7c2ca5fd2 --- /dev/null +++ b/src/app/current-accounts/current-account-view/view-charge/view-charge.component.ts @@ -0,0 +1,233 @@ +/** + * 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, inject } from '@angular/core'; +import { ActivatedRoute, Router, RouterLink } from '@angular/router'; +import { MatDialog } from '@angular/material/dialog'; + +/** Custom Services */ +import { CurrentAccountsService } from 'app/current-accounts/current-accounts.service'; +import { SettingsService } from 'app/settings/settings.service'; + +/** Custom Dialogs */ +import { FormDialogComponent } from 'app/shared/form-dialog/form-dialog.component'; +import { WaiveChargeDialogComponent } from '../custom-dialogs/waive-charge-dialog/waive-charge-dialog.component'; +import { InactivateChargeDialogComponent } from '../custom-dialogs/inactivate-charge-dialog/inactivate-charge-dialog.component'; +import { DeleteDialogComponent } from 'app/shared/delete-dialog/delete-dialog.component'; + +/** Custom Models */ +import { FormfieldBase } from 'app/shared/form-dialog/formfield/model/formfield-base'; +import { InputBase } from 'app/shared/form-dialog/formfield/model/input-base'; +import { DatepickerBase } from 'app/shared/form-dialog/formfield/model/datepicker-base'; +import { Dates } from 'app/core/utils/dates'; +import { FaIconComponent } from '@fortawesome/angular-fontawesome'; +import { DateFormatPipe } from '../../../pipes/date-format.pipe'; +import { FormatNumberPipe } from '../../../pipes/format-number.pipe'; +import { STANDALONE_SHARED_IMPORTS } from 'app/standalone-shared.module'; + +/** + * View Charge Component. + */ +@Component({ + selector: 'mifosx-view-charge', + templateUrl: './view-charge.component.html', + styleUrls: ['./view-charge.component.scss'], + imports: [ + ...STANDALONE_SHARED_IMPORTS, + FaIconComponent, + DateFormatPipe, + FormatNumberPipe + ] +}) +export class ViewChargeComponent { + private currentAccountsService = inject(CurrentAccountsService); + private route = inject(ActivatedRoute); + private dateUtils = inject(Dates); + private router = inject(Router); + dialog = inject(MatDialog); + private settingsService = inject(SettingsService); + + /** Charge data. */ + chargeData: any; + /** Current Account Data */ + currentAccountData: any; + + /** + * Retrieves the Charge data from `resolve`. + * @param {CurrentAccountsService} currentAccountsService Current Accounts Service + * @param {ActivatedRoute} route Activated Route. + * @param {Router} router Router for navigation. + * @param {MatDialog} dialog Dialog reference. + * @param {Dates} dateUtils Date Utils. + * @param {SettingsService} settingsService Setting service + */ + constructor() { + this.route.data.subscribe((data: { currentAccountCharge: any }) => { + this.chargeData = data.currentAccountCharge; + }); + this.route.data.subscribe((data: { currentAccountData: any }) => { + this.currentAccountData = data.currentAccountData; + }); + } + + /** + * Pays the charge. + */ + payCharge() { + const formfields: FormfieldBase[] = [ + new InputBase({ + controlName: 'amount', + label: 'Amount', + value: '', + type: 'number', + required: true + }), + new DatepickerBase({ + controlName: 'dueDate', + label: 'Payment Date', + value: '', + type: 'date', + required: true + }) + ]; + const data = { + title: 'Pay Charge', + layout: { addButtonText: 'Confirm' }, + formfields: formfields + }; + const payChargeDialogRef = this.dialog.open(FormDialogComponent, { data }); + payChargeDialogRef.afterClosed().subscribe((response: any) => { + if (response.data) { + const locale = this.settingsService.language.code; + const dateFormat = this.settingsService.dateFormat; + const dataObject = { + ...response.data.value, + dueDate: this.dateUtils.formatDate(response.data.value.dueDate, dateFormat), + dateFormat, + locale + }; + this.currentAccountsService + .executeCurrentAccountChargesCommand(this.chargeData.accountId, 'pay', dataObject, this.chargeData.id) + .subscribe(() => { + this.reload(); + }); + } + }); + } + + /** + * Waive's the charge + */ + waiveCharge() { + const waiveChargeDialogRef = this.dialog.open(WaiveChargeDialogComponent, { data: { id: this.chargeData.id } }); + waiveChargeDialogRef.afterClosed().subscribe((response: any) => { + if (response.confirm) { + this.currentAccountsService + .executeCurrentAccountChargesCommand(this.chargeData.accountId, 'waive', {}, this.chargeData.id) + .subscribe(() => { + this.reload(); + }); + } + }); + } + + /** + * Inactivate's the charge + */ + inactivateCharge() { + const inactivateChargeDialogRef = this.dialog.open(InactivateChargeDialogComponent, { + data: { id: this.chargeData.id } + }); + inactivateChargeDialogRef.afterClosed().subscribe((response: any) => { + if (response.confirm) { + this.currentAccountsService + .executeCurrentAccountChargesCommand(this.chargeData.accountId, 'inactivate', {}, this.chargeData.id) + .subscribe(() => { + this.reload(); + }); + } + }); + } + + /** + * Edits the charge + */ + editCharge() { + const formfields: FormfieldBase[] = [ + new InputBase({ + controlName: 'amount', + label: 'Amount', + value: this.chargeData.amount || this.chargeData.amountOrPercentage, + type: 'number', + required: true + }) + ]; + const data = { + title: 'Edit Charge', + layout: { addButtonText: 'Confirm' }, + formfields: formfields + }; + const editChargeDialogRef = this.dialog.open(FormDialogComponent, { data }); + editChargeDialogRef.afterClosed().subscribe((response: any) => { + if (response.data) { + const locale = this.settingsService.language.code; + const dateFormat = this.settingsService.dateFormat; + const dataObject = { + ...response.data.value, + dateFormat, + locale + }; + this.currentAccountsService + .editCurrentAccountCharge(this.chargeData.accountId, dataObject, this.chargeData.id) + .subscribe(() => { + this.reload(); + }); + } + }); + } + + /** + * Deletes the charge + */ + deleteCharge() { + const deleteChargeDialogRef = this.dialog.open(DeleteDialogComponent, { + data: { deleteContext: `charge id:${this.chargeData.id}` } + }); + deleteChargeDialogRef.afterClosed().subscribe((response: any) => { + if (response.delete) { + this.currentAccountsService + .deleteCurrentAccountCharge(this.chargeData.accountId, this.chargeData.id) + .subscribe(() => { + this.reload(); + }); + } + }); + } + + /** + * Checks if charge is recurring + */ + isRecurringCharge() { + const chargeTimeType = this.chargeData.chargeTimeType.value; + return chargeTimeType === 'Monthly Fee' || chargeTimeType === 'Annual Fee' || chargeTimeType === 'Weekly Fee'; + } + + /** + * Refetches data fot the component + * TODO: Replace by a custom reload component instead of hard-coded back-routing. + */ + private reload() { + const url: string = this.router.url.replace(`/${this.chargeData.id}`, ''); + const refreshUrl: string = this.router.url.slice( + 0, + this.router.url.indexOf('current-accounts') + 'current-accounts'.length + ); + this.router.navigateByUrl(refreshUrl, { skipLocationChange: true }).then(() => this.router.navigate([url])); + } +} diff --git a/src/app/current-accounts/current-accounts-routing.module.ts b/src/app/current-accounts/current-accounts-routing.module.ts new file mode 100644 index 000000000..5827180df --- /dev/null +++ b/src/app/current-accounts/current-accounts-routing.module.ts @@ -0,0 +1,272 @@ +/** + * 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 { NgModule } from '@angular/core'; +import { Routes, RouterModule } from '@angular/router'; + +/** Custom Components */ +import { CurrentAccountViewComponent } from './current-account-view/current-account-view.component'; +import { TransactionsTabComponent } from './current-account-view/transactions-tab/transactions-tab.component'; +import { DatatableTransactionTabComponent } from './current-account-view/transactions/view-transaction/datatable-transaction-tab/datatable-transaction-tab.component'; +import { CurrentAccountActionsComponent } from './current-account-actions/current-account-actions.component'; +import { ChargesTabComponent } from './current-account-view/charges-tab/charges-tab.component'; +import { StandingInstructionsTabComponent } from './current-account-view/standing-instructions-tab/standing-instructions-tab.component'; +import { DatatableTabsComponent } from './current-account-view/datatable-tabs/datatable-tabs.component'; +import { CreateCurrentAccountComponent } from './create-current-account/create-current-account.component'; +import { EditCurrentAccountComponent } from './edit-current-account/edit-current-account.component'; +import { ViewTransactionComponent } from './current-account-view/transactions/view-transaction/view-transaction.component'; +import { ViewChargeComponent } from './current-account-view/view-charge/view-charge.component'; +import { ViewRecieptComponent } from './current-account-view/transactions/view-reciept/view-reciept.component'; +import { ExportTransactionsComponent } from './current-account-view/transactions-tab/export-transactions/export-transactions.component'; +import { EditTransactionComponent } from './current-account-view/transactions/edit-transaction/edit-transaction.component'; + +/** Custom Resolvers */ +import { CurrentAccountViewResolver } from './common-resolvers/current-account-view.resolver'; +import { CurrentAccountDatatableResolver } from './common-resolvers/current-accounts-datatable.resolver'; +import { CurrentAccountDatatablesResolver } from './common-resolvers/current-accounts-datatables.resolver'; +import { TransactionDatatableResolver } from './common-resolvers/transaction-datatable.resolver'; +import { TransactionDatatablesResolver } from './common-resolvers/transaction-datatables.resolver'; +import { CurrentAccountTemplateResolver } from './common-resolvers/current-account-template.resolver'; +import { CurrentAccountAndTemplateResolver } from './common-resolvers/current-account-and-template.resolver'; +import { CurrentAccountTransactionResolver } from './common-resolvers/current-account-transaction.resolver'; +import { CurrentAccountChargeResolver } from './common-resolvers/current-account-charge.resolver'; +import { CurrentAccountActionsResolver } from './common-resolvers/current-account-actions.resolver'; +import { CurrentAccountTransactionRecieptResolver } from './common-resolvers/current-accounts-transaction-reciept.resolver'; +import { CurrentAccountTransactionTemplateResolver } from './common-resolvers/current-account-transaction-template.resolver'; +import { CurrentAccountDocumentsTabComponent } from './current-account-view/current-accounts-documents-tab/current-accounts-documents-tab.component'; +import { NotesTabComponent } from './current-account-view/notes-tab/notes-tab.component'; +import { CurrentAccountNotesResolver } from './common-resolvers/current-accounts-notes.resolver'; +import { CurrentAccountDocumentsResolver } from './common-resolvers/current-accounts-documents.resolver'; +import { CurrentAccountTransactionGeneralTabComponent } from './current-account-view/transactions/view-transaction/current-accounts-transaction-general-tab/current-accounts-transaction-general-tab.component'; +import { GeneralTabComponent } from './current-account-view/general-tab/general-tab.component'; +import { PassbookTabComponent } from './current-account-view/passbook-tab/passbook-tab.component'; + +/** Current Account Routes */ +const routes: Routes = [ + { + path: '', + data: { + title: 'All Current Accounts', + breadcrumb: 'Current Accounts', + routeParamBreadcrumb: false, + addBreadcrumbLink: false + }, + children: [ + { + path: 'create', + data: { title: 'Create Current Account', breadcrumb: 'Create Current Account' }, + component: CreateCurrentAccountComponent, + resolve: { + currentAccountTemplate: CurrentAccountTemplateResolver + } + }, + { + path: ':currentAccountId', + data: { title: 'Current Account View', routeParamBreadcrumb: 'currentAccountId' }, + component: CurrentAccountViewComponent, + resolve: { + currentAccountData: CurrentAccountViewResolver, + currentAccountDatatables: CurrentAccountDatatablesResolver + }, + children: [ + { + path: '', + redirectTo: 'general', + pathMatch: 'full' + }, + { + path: 'general', + data: { title: 'Current Account Details', breadcrumb: 'General', routeParamBreadcrumb: false }, + children: [ + { + path: '', + component: GeneralTabComponent + } + ] + }, + { + path: 'transactions', + data: { title: 'Current Account Transactions', breadcrumb: 'Transactions', routeParamBreadcrumb: false }, + children: [ + { + path: '', + component: TransactionsTabComponent + }, + { + path: 'export', + component: ExportTransactionsComponent + } + ] + }, + { + path: 'passbook', + component: PassbookTabComponent, + data: { title: 'Passbook', breadcrumb: 'Passbook', routeParamBreadcrumb: false } + }, + { + path: 'charges', + component: ChargesTabComponent, + data: { title: 'Current Account Charges', breadcrumb: 'Charges', routeParamBreadcrumb: false } + }, + { + path: 'standing-instructions', + component: StandingInstructionsTabComponent, + data: { title: 'Current Account SIH', breadcrumb: 'Standing Instructions', routeParamBreadcrumb: false } + }, + { + path: 'notes', + component: NotesTabComponent, + data: { title: 'Current Account Notes', breadcrumb: 'Notes', routeParamBreadcrumb: false }, + resolve: { + currentAccountNotes: CurrentAccountNotesResolver + } + }, + { + path: 'documents', + component: CurrentAccountDocumentsTabComponent, + data: { title: 'Current Account Documents', breadcrumb: 'Documents', routeParamBreadcrumb: false }, + resolve: { + currentAccountDocuments: CurrentAccountDocumentsResolver + } + }, + { + path: 'datatables', + children: [ + { + path: ':datatableName', + component: DatatableTabsComponent, + data: { title: 'View Data Table', routeParamBreadcrumb: 'datatableName' }, + resolve: { + currentAccountDatatable: CurrentAccountDatatableResolver + } + } + ] + } + ] + }, + { + path: ':currentAccountId/edit', + data: { title: 'Edit Current Account', breadcrumb: 'Edit', routeParamBreadcrumb: false }, + component: EditCurrentAccountComponent, + resolve: { + currentAccountAndTemplate: CurrentAccountAndTemplateResolver + } + }, + { + path: ':currentAccountId/transactions/:id', + data: { title: 'Current Account Transactions', breadcrumb: 'Transactions', routeParamBreadcrumb: false }, + children: [ + { + path: '', + component: ViewTransactionComponent, + resolve: { + transactionDatatables: TransactionDatatablesResolver + }, + children: [ + { + path: '', + redirectTo: 'general', + pathMatch: 'full' + }, + { + path: 'general', + component: CurrentAccountTransactionGeneralTabComponent, + resolve: { + currentAccountTransaction: CurrentAccountTransactionResolver + } + }, + { + path: 'datatables', + children: [ + { + path: ':datatableName', + component: DatatableTransactionTabComponent, + data: { title: 'View Data table', routeParamBreadcrumb: 'datatableName' }, + resolve: { + transactionDatatable: TransactionDatatableResolver + } + } + ] + } + ] + }, + { + path: 'edit', + component: EditTransactionComponent, + data: { breadcrumb: 'Edit', routeParamBreadcrumb: false }, + resolve: { + currentAccountTransactionTemplate: CurrentAccountTransactionTemplateResolver + } + }, + { + path: 'reciept', + component: ViewRecieptComponent, + data: { breadcrumb: 'Reciept', routeParamBreadcrumb: false }, + resolve: { + currentAccountTransactionReciept: CurrentAccountTransactionRecieptResolver + } + } + ] + }, + { + path: ':currentAccountId/charges', + data: { title: 'Current Account Charges', breadcrumb: 'Charges', routeParamBreadcrumb: false }, + children: [ + { + path: '', + redirectTo: '../charges', + pathMatch: 'prefix' + }, + { + path: ':id', + data: { routeParamBreadcrumb: 'id' }, + component: ViewChargeComponent, + resolve: { + currentAccountData: CurrentAccountViewResolver, + currentAccountCharge: CurrentAccountChargeResolver + } + } + ] + }, + { + path: ':currentAccountId/actions/:name', + data: { title: 'Current Account Actions', breadcrumb: 'Current Account Actions', routeParamBreadcrumb: 'name' }, + component: CurrentAccountActionsComponent, + resolve: { + currentAccountActionData: CurrentAccountActionsResolver + } + }, + { + path: ':currentAccountId/transfer-funds', + loadChildren: () => + import('../account-transfers/account-transfers.module').then((m) => m.AccountTransfersModule) + } + ] + } +]; +@NgModule({ + imports: [RouterModule.forChild(routes)], + exports: [RouterModule], + declarations: [], + providers: [ + CurrentAccountViewResolver, + CurrentAccountDatatablesResolver, + CurrentAccountDatatableResolver, + TransactionDatatableResolver, + TransactionDatatablesResolver, + CurrentAccountTemplateResolver, + CurrentAccountAndTemplateResolver, + CurrentAccountTransactionResolver, + CurrentAccountChargeResolver, + CurrentAccountActionsResolver, + CurrentAccountTransactionRecieptResolver, + CurrentAccountTransactionTemplateResolver + ] +}) +export class CurrentAccountsRoutingModule {} diff --git a/src/app/current-accounts/current-accounts.module.ts b/src/app/current-accounts/current-accounts.module.ts new file mode 100644 index 000000000..5c7d9a284 --- /dev/null +++ b/src/app/current-accounts/current-accounts.module.ts @@ -0,0 +1,124 @@ +/** + * 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 { NgModule } from '@angular/core'; + +/** Custom Modules */ +import { CurrentAccountsRoutingModule } from './current-accounts-routing.module'; +import { SharedModule } from 'app/shared/shared.module'; +import { PipesModule } from '../pipes/pipes.module'; +import { DirectivesModule } from '../directives/directives.module'; + +/** Custom Components */ +import { CurrentAccountActionsComponent } from './current-account-actions/current-account-actions.component'; +import { CurrentAccountTransactionsComponent } from './current-account-actions/current-account-transactions/current-account-transactions.component'; +import { TransactionsTabComponent } from './current-account-view/transactions-tab/transactions-tab.component'; +import { CurrentAccountViewComponent } from './current-account-view/current-account-view.component'; +import { ChargesTabComponent } from './current-account-view/charges-tab/charges-tab.component'; +import { StandingInstructionsTabComponent } from './current-account-view/standing-instructions-tab/standing-instructions-tab.component'; +import { DatatableTabsComponent } from './current-account-view/datatable-tabs/datatable-tabs.component'; +import { CreateCurrentAccountComponent } from './create-current-account/create-current-account.component'; +import { CurrentAccountDetailsStepComponent } from './current-account-stepper/current-account-details-step/current-account-details-step.component'; +import { CurrentAccountChargesStepComponent } from './current-account-stepper/current-account-charges-step/current-account-charges-step.component'; +import { CurrentAccountTermsStepComponent } from './current-account-stepper/current-account-terms-step/current-account-terms-step.component'; +import { CurrentAccountPreviewStepComponent } from './current-account-stepper/current-account-preview-step/current-account-preview-step.component'; +import { EditCurrentAccountComponent } from './edit-current-account/edit-current-account.component'; +import { ApproveCurrentAccountComponent } from './current-account-actions/approve-current-account/approve-current-account.component'; +import { RejectCurrentAccountComponent } from './current-account-actions/reject-current-account/reject-current-account.component'; +import { ActivateCurrentAccountComponent } from './current-account-actions/activate-current-account/activate-current-account.component'; +import { UndoApprovalCurrentAccountComponent } from './current-account-actions/undo-approval-current-account/undo-approval-current-account.component'; +import { PostInterestAsOnCurrentAccountComponent } from './current-account-actions/post-interest-as-on-current-account/post-interest-as-on-current-account.component'; +import { CurrentAccountAssignStaffComponent } from './current-account-actions/current-account-assign-staff/current-account-assign-staff.component'; +import { CurrentAccountUnassignStaffComponent } from './current-account-actions/current-account-unassign-staff/current-account-unassign-staff.component'; +import { CalculateInterestDialogComponent } from './current-account-view/custom-dialogs/calculate-interest-dialog/calculate-interest-dialog.component'; +import { PostInterestDialogComponent } from './current-account-view/custom-dialogs/post-interest-dialog/post-interest-dialog.component'; +import { ViewTransactionComponent } from './current-account-view/transactions/view-transaction/view-transaction.component'; +import { UndoTransactionDialogComponent } from './current-account-view/custom-dialogs/undo-transaction-dialog/undo-transaction-dialog.component'; +import { ViewChargeComponent } from './current-account-view/view-charge/view-charge.component'; +import { WaiveChargeDialogComponent } from './current-account-view/custom-dialogs/waive-charge-dialog/waive-charge-dialog.component'; +import { InactivateChargeDialogComponent } from './current-account-view/custom-dialogs/inactivate-charge-dialog/inactivate-charge-dialog.component'; +import { WithdrawByClientCurrentAccountComponent } from './current-account-actions/withdraw-by-client-current-account/withdraw-by-client-current-account.component'; +import { AddChargeCurrentAccountComponent } from './current-account-actions/add-charge-current-account/add-charge-current-account.component'; +import { CloseCurrentAccountComponent } from './current-account-actions/close-current-account/close-current-account.component'; +import { ToggleWithholdTaxDialogComponent } from './current-account-view/custom-dialogs/toggle-withhold-tax-dialog/toggle-withhold-tax-dialog.component'; +import { ViewRecieptComponent } from './current-account-view/transactions/view-reciept/view-reciept.component'; +import { ExportTransactionsComponent } from './current-account-view/transactions-tab/export-transactions/export-transactions.component'; +import { EditTransactionComponent } from './current-account-view/transactions/edit-transaction/edit-transaction.component'; +import { ApplyAnnualFeesCurrentAccountComponent } from './current-account-actions/apply-annual-fees-current-account/apply-annual-fees-current-account.component'; +import { ManageCurrentAccountComponent } from './current-account-actions/manage-current-account/manage-current-account.component'; +import { ReleaseAmountDialogComponent } from './current-account-view/custom-dialogs/release-amount-dialog/release-amount-dialog.component'; +import { CurrentAccountDocumentsTabComponent } from './current-account-view/current-accounts-documents-tab/current-accounts-documents-tab.component'; +import { NotesTabComponent } from './current-account-view/notes-tab/notes-tab.component'; +import { DatatableTransactionTabComponent } from './current-account-view/transactions/view-transaction/datatable-transaction-tab/datatable-transaction-tab.component'; +import { CurrentAccountTransactionGeneralTabComponent } from './current-account-view/transactions/view-transaction/current-accounts-transaction-general-tab/current-accounts-transaction-general-tab.component'; +import { CurrentAccountTransactionDatatableTabComponent } from './current-account-view/transactions/view-transaction/current-accounts-transaction-datatable-tab/current-accounts-transaction-datatable-tab.component'; +import { GeneralTabComponent } from './current-account-view/general-tab/general-tab.component'; +import { CurrentAccountActiveClientMembersComponent } from './current-account-stepper/current-accounts-active-client-members/current-accounts-active-client-members.component'; +import { PassbookTabComponent } from './current-account-view/passbook-tab/passbook-tab.component'; + +/** + * Current Accounts Module + * + * All components related to Current Account functions should be declared here. + */ +@NgModule({ + imports: [ + SharedModule, + PipesModule, + DirectivesModule, + CurrentAccountsRoutingModule, + CurrentAccountActionsComponent, + CurrentAccountTransactionsComponent, + TransactionsTabComponent, + CurrentAccountViewComponent, + ChargesTabComponent, + StandingInstructionsTabComponent, + DatatableTabsComponent, + CreateCurrentAccountComponent, + CurrentAccountDetailsStepComponent, + CurrentAccountChargesStepComponent, + CurrentAccountTermsStepComponent, + CurrentAccountPreviewStepComponent, + EditCurrentAccountComponent, + ApproveCurrentAccountComponent, + RejectCurrentAccountComponent, + ActivateCurrentAccountComponent, + UndoApprovalCurrentAccountComponent, + PostInterestAsOnCurrentAccountComponent, + CurrentAccountAssignStaffComponent, + CurrentAccountUnassignStaffComponent, + CalculateInterestDialogComponent, + PostInterestDialogComponent, + ViewTransactionComponent, + UndoTransactionDialogComponent, + ViewChargeComponent, + WaiveChargeDialogComponent, + InactivateChargeDialogComponent, + WithdrawByClientCurrentAccountComponent, + AddChargeCurrentAccountComponent, + CloseCurrentAccountComponent, + ToggleWithholdTaxDialogComponent, + ViewRecieptComponent, + ExportTransactionsComponent, + EditTransactionComponent, + ApplyAnnualFeesCurrentAccountComponent, + ManageCurrentAccountComponent, + ReleaseAmountDialogComponent, + CurrentAccountDocumentsTabComponent, + NotesTabComponent, + DatatableTransactionTabComponent, + CurrentAccountTransactionGeneralTabComponent, + CurrentAccountTransactionDatatableTabComponent, + GeneralTabComponent, + CurrentAccountActiveClientMembersComponent, + PassbookTabComponent + ], + providers: [] +}) +export class CurrentAccountsModule {} diff --git a/src/app/current-accounts/current-accounts.service.ts b/src/app/current-accounts/current-accounts.service.ts new file mode 100644 index 000000000..6267da6b3 --- /dev/null +++ b/src/app/current-accounts/current-accounts.service.ts @@ -0,0 +1,368 @@ +/** + * 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 { Injectable, inject } from '@angular/core'; +import { HttpClient, HttpParams } from '@angular/common/http'; + +/** rxjs Imports */ +import { Observable } from 'rxjs'; + +/** + * Current Accounts Service. + */ +@Injectable({ + providedIn: 'root' +}) +export class CurrentAccountsService { + private http = inject(HttpClient); + + /** + * @param {string} currentAccountId is current account's Id. + * @returns {Observable} + */ + getCurrentAccountTransactionTemplateResource(currentAccountId: string): Observable { + return this.http.get(`/currentaccounts/${currentAccountId}/transactions/template`); + } + + /** + * @param {string} currentAccountId current account id. + * @returns {Observable} + */ + getCurrentAccountChargeTemplateResource(currentAccountId: string): Observable { + return this.http.get(`/currentaccounts/${currentAccountId}/charges/template`); + } + + /** + * @param {any} currentAccountCharge to apply on a current account Account. + * @returns {Observable} + */ + createCurrentAccountCharge( + currentAccountId: string, + resourceType: string, + currentAccountCharge: any + ): Observable { + return this.http.post(`/currentaccounts/${currentAccountId}/${resourceType}`, currentAccountCharge); + } + + /** + * @param {string} chargeId Charge ID of charge. + * @returns {Observable} Charge. + */ + getChargeTemplate(chargeId: string): Observable { + const params = { template: 'true' }; + return this.http.get(`/charges/${chargeId}`, { params: params }); + } + + /** + * @param accountId Current Account Id of account to get data for. + * @returns {Observable} Current Account data. + */ + getCurrentAccountData(accountId: string): Observable { + const httpParams = new HttpParams().set('associations', 'all'); + return this.http.get(`/currentaccounts/${accountId}`, { params: httpParams }); + } + + /** + * @param accountId Current Account Id of account to get data for. + * @returns {Observable} Current account and template. + */ + getCurrentAccountAndTemplate(accountId: string, template: boolean): Observable { + const httpParams = new HttpParams().set('template', template.toString()).set('associations', 'charges'); + return this.http.get(`/currentaccounts/${accountId}`, { params: httpParams }); + } + + /** + * @param clientId Client Id + * @param clientName Client Name + * @param fromAccountId Account Id + * @param locale Locale + * @param dateFormat Date Format + * @returns {Observable} Standing Instructions + */ + getStandingInstructions( + clientId: string, + clientName: string, + fromAccountId: string, + locale: string, + dateFormat: string + ): Observable { + const httpParams = new HttpParams() + .set('clientId', clientId) + .set('clientName', clientName) + .set('fromAccountId', fromAccountId) + .set('fromAccountType', '2') + .set('locale', locale) + .set('dateFormat', dateFormat); + return this.http.get(`/standinginstructions`, { params: httpParams }); + } + + /** + * @returns {Observable} + */ + getCurrentAccountDatatables(): Observable { + const httpParams = new HttpParams().set('apptable', 'm_current_account'); + return this.http.get(`/datatables`, { params: httpParams }); + } + + /** + * @param accountId account Id of current account to get datatable for. + * @param datatableName Data table name. + * @returns {Observable} + */ + getCurrentAccountDatatable(accountId: string, datatableName: string): Observable { + const httpParams = new HttpParams().set('genericResultSet', 'true'); + return this.http.get(`/datatables/${datatableName}/${accountId}`, { params: httpParams }); + } + + /** + * @returns {Observable} + */ + getCurrentAccountTransactionDatatables(): Observable { + const httpParams = new HttpParams().set('apptable', 'm_current_account_transaction'); + return this.http.get(`/datatables`, { params: httpParams }); + } + + /** + * @param accountId account Id of current account to get datatable for. + * @param datatableName Data table name. + * @returns {Observable} + */ + getCurrentAccountTransactionDatatable(transactionId: string, datatableName: string): Observable { + const httpParams = new HttpParams().set('genericResultSet', 'true'); + return this.http.get(`/datatables/${datatableName}/${transactionId}`, { params: httpParams }); + } + + /** + * @param accountId account Id of current account to get add datatable entry for. + * @param datatableName Data Table name. + * @param data Data. + * @returns {Observable} + */ + addCurrentAccountDatatableEntry(accountId: string, datatableName: string, data: any): Observable { + const httpParams = new HttpParams().set('genericResultSet', 'true'); + return this.http.post(`/datatables/${datatableName}/${accountId}`, data, { params: httpParams }); + } + + /** + * @param accountId account Id of current account to get add datatable entry for. + * @param datatableName Data Table name. + * @param data Data. + * @returns {Observable} + */ + editCurrentAccountDatatableEntry(accountId: string, datatableName: string, data: any): Observable { + const httpParams = new HttpParams().set('genericResultSet', 'true'); + return this.http.put(`/datatables/${datatableName}/${accountId}`, data, { params: httpParams }); + } + + /** + * @param accountId account Id of current account to get add datatable entry for. + * @param datatableName Data Table name. + * @returns {Observable} + */ + deleteDatatableContent(accountId: string, datatableName: string): Observable { + const httpParams = new HttpParams().set('genericResultSet', 'true'); + return this.http.delete(`/datatables/${datatableName}/${accountId}`, { params: httpParams }); + } + + /** + * @param entityId Entity Id assosciated with current account. + * @returns {Observable} Current account template. + */ + getCurrentAccountTemplate(entityId: string, productId?: string, isGroup?: boolean): Observable { + let httpParams = new HttpParams().set(isGroup ? 'groupId' : 'clientId', entityId); + httpParams = productId ? httpParams.set('productId', productId) : httpParams; + return this.http.get('/currentaccounts/template', { params: httpParams }); + } + + /** + * @param {any} currentAccount Current Account + * @returns {Observable} + */ + createCurrentAccount(currentAccount: any): Observable { + return this.http.post('/currentaccounts', currentAccount); + } + + /** + * @param {any} currentAccount Current Account + * @returns {Observable} + */ + updateCurrentAccount(accountId: string, currentAccount: any): Observable { + return this.http.put(`/currentaccounts/${accountId}`, currentAccount); + } + + /** + * @param {string} accountId current account Id + * @returns {Observable} + */ + deleteCurrentAccount(accountId: string): Observable { + return this.http.delete(`/currentaccounts/${accountId}`); + } + + /** + * @param {string} accountId Current Account Id + * @param {string} command Command + * @param {any} data Data + * @returns {Observable} + */ + executeCurrentAccountCommand(accountId: string, command: string, data: any): Observable { + const httpParams = new HttpParams().set('command', command); + return this.http.post(`/currentaccounts/${accountId}`, data, { params: httpParams }); + } + + /** + * @param {string} accountId Current Account Id + * @param {string} command Command + * @param {any} data Data + * @returns {Observable} + */ + executeCurrentAccountUpdateCommand(accountId: string, command: string, data: any): Observable { + const httpParams = new HttpParams().set('command', command); + return this.http.put(`/currentaccounts/${accountId}`, data, { params: httpParams }); + } + + /** + * @param {string} accountId Current Account Id + * @param {string} transactionId Transaction Id + * @returns {Observable} + */ + getCurrentAccountTransaction(accountId: string, transactionId: string): Observable { + return this.http.get(`/currentaccounts/${accountId}/transactions/${transactionId}`); + } + + /** + * @param {string} accountId Current Account Id + * @param {string} transactionId Transaction Id + * @returns {Observable} + */ + getCurrentAccountTransactionTemplate(accountId: string, transactionId: string): Observable { + const httpParams = new HttpParams().set('template', 'true'); + return this.http.get(`/currentaccounts/${accountId}/transactions/${transactionId}`, { params: httpParams }); + } + + /** + * @param {string} accountId Current Account Id + * @param {string} command Command + * @param {any} data Data + * @param {string} transactionId Transaction Id + * @returns {Observable} + */ + executeCurrentAccountTransactionsCommand( + accountId: string, + command: string, + data: any, + transactionId?: any + ): Observable { + const httpParams = new HttpParams().set('command', command); + if (transactionId) { + return this.http.post(`/currentaccounts/${accountId}/transactions/${transactionId}`, data, { + params: httpParams + }); + } + return this.http.post(`/currentaccounts/${accountId}/transactions`, data, { params: httpParams }); + } + + /** + * @param {string} accountId current account Id + * @param {string} chargeId current account charge Id + * @returns {Observable} + */ + getCurrentAccountCharge(accountId: string, chargeId: string): Observable { + return this.http.get(`/currentaccounts/${accountId}/charges/${chargeId}`); + } + + /** + * @param {string} accountId Current Account Id + * @param {string} command Command + * @param {any} data Data + * @param {string} chargeId Charge Id + * @returns {Observable} + */ + executeCurrentAccountChargesCommand(accountId: string, command: string, data: any, chargeId: any): Observable { + const httpParams = new HttpParams().set('command', command); + return this.http.post(`/currentaccounts/${accountId}/charges/${chargeId}`, data, { params: httpParams }); + } + + /** + * @param {string} accountId Current Account Id + * @param {any} data Charge Data + * @param {any} chargeId Charge Id + * @returns {Observable} + */ + editCurrentAccountCharge(accountId: string, data: any, chargeId: any): Observable { + return this.http.put(`/currentaccounts/${accountId}/charges/${chargeId}`, data); + } + + /** + * @param {string} accountId Current Account Id + * @param {any} chargeId Charge Id + * @returns {Observable} + */ + deleteCurrentAccountCharge(accountId: string, chargeId: any): Observable { + return this.http.delete(`/currentaccounts/${accountId}/charges/${chargeId}`); + } + + /** + * @param currentAccountId Current Account Id + * @returns The notes for particular loan + */ + getCurrentAccountNotes(currentAccountId: string): Observable { + return this.http.get(`/currentaccounts/${currentAccountId}/notes`); + } + + /** + * Adds a note to the particular Current Account Id + * @param currentAccountId Current Account ID + * @param noteData Note Data to be added + * @returns {Observable} + */ + createCurrentAccountNote(currentAccountId: string, noteData: any): Observable { + return this.http.post(`/currentaccounts/${currentAccountId}/notes`, noteData); + } + + /** + * Edits the Current Account Note + * @param currentAccountId Current Account ID + * @param noteId Note ID + * @param noteData Note Data + */ + editCurrentAccountNote(currentAccountId: string, noteId: string, noteData: any) { + return this.http.put(`/currentaccounts/${currentAccountId}/notes/${noteId}`, noteData); + } + + /** + * Deletes the particular Note + * @param currentAccountId Current Account ID + * @param noteId Note ID + */ + deleteCurrentAccountNote(currentAccountId: string, noteId: string) { + return this.http.delete(`/currentaccounts/${currentAccountId}/notes/${noteId}`); + } + + /** + * @param currentAccountId Current Account Id of account to get data for. + * @returns {Observable} Current Account data. + */ + getCurrentAccountDocuments(currentAccountId: any): Observable { + return this.http.get(`/currentaccounts/${currentAccountId}/documents`); + } + + downloadCurrentAccountDocument(parentEntityId: string, documentId: string) { + return this.http.get(`/currentaccounts/${parentEntityId}/documents/${documentId}/attachment`, { + responseType: 'blob' + }); + } + + deleteCurrentAccountDocument(currentAccountId: any, documentId: any): Observable { + return this.http.delete(`/currentaccounts/${currentAccountId}/documents/${documentId}`); + } + + loadCurrentAccountDocument(currentAccountId: any, data: any): Observable { + return this.http.post(`/currentaccounts/${currentAccountId}/documents`, data); + } +} diff --git a/src/app/current-accounts/edit-current-account/edit-current-account.component.html b/src/app/current-accounts/edit-current-account/edit-current-account.component.html new file mode 100644 index 000000000..a23533b44 --- /dev/null +++ b/src/app/current-accounts/edit-current-account/edit-current-account.component.html @@ -0,0 +1,81 @@ + + +
+ + + + + + + + + + + + + + + + + + + + + + + {{ 'labels.inputs.DETAILS' | translate }} + + + + + + + {{ 'labels.inputs.TERMS' | translate }} + + + + + + {{ 'labels.inputs.CHARGES' | translate }} + + + + + + @if (currentAccountFormValidAndNotPristine) { + + {{ 'labels.inputs.PREVIEW' | translate }} + + + + } + +
diff --git a/src/app/current-accounts/edit-current-account/edit-current-account.component.scss b/src/app/current-accounts/edit-current-account/edit-current-account.component.scss new file mode 100644 index 000000000..af9f4c075 --- /dev/null +++ b/src/app/current-accounts/edit-current-account/edit-current-account.component.scss @@ -0,0 +1,7 @@ +/** + * 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/. + */ diff --git a/src/app/current-accounts/edit-current-account/edit-current-account.component.ts b/src/app/current-accounts/edit-current-account/edit-current-account.component.ts new file mode 100644 index 000000000..836824166 --- /dev/null +++ b/src/app/current-accounts/edit-current-account/edit-current-account.component.ts @@ -0,0 +1,162 @@ +/** + * 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'; + +/** Custom Components */ +import { CurrentAccountDetailsStepComponent } from '../current-account-stepper/current-account-details-step/current-account-details-step.component'; +import { CurrentAccountTermsStepComponent } from '../current-account-stepper/current-account-terms-step/current-account-terms-step.component'; +import { CurrentAccountChargesStepComponent } from '../current-account-stepper/current-account-charges-step/current-account-charges-step.component'; + +/** Custom Services */ +import { CurrentAccountsService } from '../current-accounts.service'; +import { SettingsService } from 'app/settings/settings.service'; +import { Dates } from 'app/core/utils/dates'; +import { MatStepper, MatStepperIcon, MatStep, MatStepLabel } from '@angular/material/stepper'; +import { FaIconComponent } from '@fortawesome/angular-fontawesome'; +import { CurrentAccountPreviewStepComponent } from '../current-account-stepper/current-account-preview-step/current-account-preview-step.component'; +import { STANDALONE_SHARED_IMPORTS } from 'app/standalone-shared.module'; + +/** + * Edit Current Account Component + */ +@Component({ + selector: 'mifosx-edit-current-account-account', + templateUrl: './edit-current-account.component.html', + styleUrls: ['./edit-current-account.component.scss'], + imports: [ + ...STANDALONE_SHARED_IMPORTS, + MatStepper, + MatStepperIcon, + FaIconComponent, + MatStep, + MatStepLabel, + CurrentAccountDetailsStepComponent, + CurrentAccountTermsStepComponent, + CurrentAccountChargesStepComponent, + CurrentAccountPreviewStepComponent + ] +}) +export class EditCurrentAccountComponent { + private route = inject(ActivatedRoute); + private router = inject(Router); + private dateUtils = inject(Dates); + private currentAccountsService = inject(CurrentAccountsService); + private settingsService = inject(SettingsService); + + /** Current Account Template */ + currentAccountAndTemplate: any; + /** Current Account Product Template */ + currentAccountProductTemplate: any; + + /** Current Account Details Step */ + @ViewChild(CurrentAccountDetailsStepComponent, { static: true }) + currentAccountDetailsStep: CurrentAccountDetailsStepComponent; + /** Current Account Terms Step */ + @ViewChild(CurrentAccountTermsStepComponent, { static: true }) + currentAccountTermsStep: CurrentAccountTermsStepComponent; + /** Current Account Charges Step */ + @ViewChild(CurrentAccountChargesStepComponent, { static: true }) + currentAccountChargesStep: CurrentAccountChargesStepComponent; + + /** + * Fetches current account template from `resolve` + * @param {ActivatedRoute} route Activated Route + * @param {Router} router Router + * @param {Dates} dateUtils Date Utils + * @param {CurrentAccountsService} currentAccountsService Current Accounts Service + * @param {SettingsService} settingsService Settings Service + */ + constructor() { + this.route.data.subscribe((data: { currentAccountAndTemplate: any }) => { + this.currentAccountAndTemplate = data.currentAccountAndTemplate; + }); + } + + /** + * Sets current account product template. + * @param {any} $event API response + */ + setTemplate($event: any) { + this.currentAccountProductTemplate = $event; + } + + /** + * Retrieves current account details form. + */ + get currentAccountDetailsForm() { + return this.currentAccountDetailsStep.currentAccountDetailsForm; + } + + /** + * Retrieves current account terms form. + */ + get currentAccountTermsForm() { + return this.currentAccountTermsStep.currentAccountTermsForm; + } + + /** + * Checks validity and pristinity of overall current account form . + */ + get currentAccountFormValidAndNotPristine() { + return ( + this.currentAccountDetailsForm.valid && + this.currentAccountTermsForm.valid && + (!this.currentAccountDetailsForm.pristine || + !this.currentAccountTermsForm.pristine || + !this.currentAccountChargesStep.pristine) + ); + } + + /** + * Retrieves current account object. + */ + get currentAccount() { + return { + ...this.currentAccountDetailsStep.currentAccountDetails, + ...this.currentAccountTermsStep.currentAccountTerms, + ...this.currentAccountChargesStep.currentAccountCharges + }; + } + + /** + * Creates a new share account. + */ + submit() { + // TODO: Update once language and date settings are setup + const locale = this.settingsService.language.code; + const dateFormat = this.settingsService.dateFormat; + const monthDayFormat = 'dd MMMM'; + const currentAccount = { + ...this.currentAccount, + charges: this.currentAccount.charges.map((charge: any) => ({ + chargeId: charge.id, + amount: charge.amount, + dueDate: this.dateUtils.formatDate(charge.dueDate, dateFormat), + feeOnMonthDay: this.dateUtils.formatDate(charge.feeOnMonthDay, dateFormat), + feeInterval: charge.feeInterval + })), + submittedOnDate: this.dateUtils.formatDate(this.currentAccount.submittedOnDate, dateFormat), + dateFormat, + monthDayFormat, + locale + }; + if (this.currentAccountAndTemplate.clientId) { + currentAccount.clientId = this.currentAccountAndTemplate.clientId; + } else { + currentAccount.groupId = this.currentAccountAndTemplate.groupId; + } + this.currentAccountsService + .updateCurrentAccount(this.currentAccountAndTemplate.id, currentAccount) + .subscribe((response: any) => { + this.router.navigate(['../'], { relativeTo: this.route }); + }); + } +} diff --git a/src/app/current-accounts/models/current-account-transaction.model.ts b/src/app/current-accounts/models/current-account-transaction.model.ts new file mode 100644 index 000000000..6b2327466 --- /dev/null +++ b/src/app/current-accounts/models/current-account-transaction.model.ts @@ -0,0 +1,64 @@ +/** + * 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 { Currency } from 'app/shared/models/general.model'; + +export interface CurrentAccountTransaction { + id: number; + transactionType: CurrentAccountTransactionType; + entryType: string; + accountId: number; + accountNo: string; + date: number[]; + currency: Currency; + amount: number; + runningBalance: number; + reversed: boolean; + transfer: Transfer; + submittedOnDate: number[]; + interestedPostedAsOn: boolean; + submittedByUsername: string; + isManualTransaction: boolean; + isReversal: boolean; + originalTransactionId: number; + lienTransaction: boolean; + releaseTransactionId: number; + chargesPaidByData: any[]; +} + +export interface CurrentAccountTransactionType { + id: number; + code: string; + value: string; + deposit: boolean; + dividendPayout: boolean; + withdrawal: boolean; + interestPosting: boolean; + feeDeduction: boolean; + initiateTransfer: boolean; + approveTransfer: boolean; + withdrawTransfer: boolean; + rejectTransfer: boolean; + overdraftInterest: boolean; + writtenoff: boolean; + overdraftFee: boolean; + withholdTax: boolean; + escheat: boolean; + amountHold: boolean; + amountRelease: boolean; + accrual: boolean; +} + +export interface Transfer { + id: number; + reversed: boolean; + currency: Currency; + transferAmount: number; + transferDate: number[]; + transferDescription: string; +} diff --git a/src/app/products/charges/create-charge/create-charge.component.ts b/src/app/products/charges/create-charge/create-charge.component.ts index 7dc3c121f..5c925963d 100644 --- a/src/app/products/charges/create-charge/create-charge.component.ts +++ b/src/app/products/charges/create-charge/create-charge.component.ts @@ -170,6 +170,14 @@ export class CreateChargeComponent implements OnInit { this.chargeCalculationTypeData = this.chargesTemplateData.shareChargeCalculationTypeOptions; this.chargeTimeTypeData = this.chargesTemplateData.shareChargeTimeTypeOptions; break; + case 5: // Current Account — uses same options as Savings + this.chargeCalculationTypeData = + this.chargesTemplateData.currentAccountChargeCalculationTypeOptions || + this.chargesTemplateData.savingsChargeCalculationTypeOptions; + this.chargeTimeTypeData = + this.chargesTemplateData.currentAccountChargeTimeTypeOptions || + this.chargesTemplateData.savingsChargeTimeTypeOptions; + break; } }); } @@ -188,7 +196,7 @@ export class CreateChargeComponent implements OnInit { if (this.chargeForm.get('chargeTimeType').value !== 12 && chargeCalculationType.id === 5) { return false; } - if (this.chargeForm.get('chargeAppliesTo').value === 2) { + if (this.chargeForm.get('chargeAppliesTo').value === 2 || this.chargeForm.get('chargeAppliesTo').value === 5) { if ( !( this.chargeForm.get('chargeTimeType').value === 5 || @@ -216,7 +224,7 @@ export class CreateChargeComponent implements OnInit { chargeCalculationType === 4 || chargeCalculationType === 5 ); - } else if (chargeAppliesTo === 2) { + } else if (chargeAppliesTo === 2 || chargeAppliesTo === 5) { return (chargeTimeType === 16 || chargeTimeType === 5) && chargeCalculationType === 2; } else if (chargeAppliesTo === 4) { return (chargeTimeType === 14 || chargeTimeType === 15) && chargeCalculationType === 2; @@ -248,6 +256,10 @@ export class CreateChargeComponent implements OnInit { this.chargeForm.removeControl('incomeAccountId'); this.chargeForm.get('penalty').setValue(false); break; + case 5: // Current Account — same as Savings + this.chargeForm.removeControl('chargePaymentMode'); + this.chargeForm.removeControl('incomeAccountId'); + break; } this.chargeForm.get('chargeCalculationType').reset(); this.chargeForm.get('chargeTimeType').reset(); diff --git a/src/app/products/charges/edit-charge/edit-charge.component.ts b/src/app/products/charges/edit-charge/edit-charge.component.ts index 7a757efc5..cf839ffa8 100644 --- a/src/app/products/charges/edit-charge/edit-charge.component.ts +++ b/src/app/products/charges/edit-charge/edit-charge.component.ts @@ -154,6 +154,15 @@ export class EditChargeComponent implements OnInit { this.addFeeFrequency = false; break; } + case 'Current Account': { + this.chargeTimeTypeOptions = + this.chargeData.currentAccountChargeTimeTypeOptions || this.chargeData.savingsChargeTimeTypeOptions; + this.chargeCalculationTypeOptions = + this.chargeData.currentAccountChargeCalculationTypeOptions || + this.chargeData.savingsChargeCalculationTypeOptions; + this.addFeeFrequency = false; + break; + } case 'Shares': { this.chargeTimeTypeOptions = this.chargeData.shareChargeTimeTypeOptions; this.chargeCalculationTypeOptions = this.chargeData.shareChargeCalculationTypeOptions; diff --git a/src/app/products/charges/models/charge.model.ts b/src/app/products/charges/models/charge.model.ts index 1a8d73d6c..597ed5bd1 100644 --- a/src/app/products/charges/models/charge.model.ts +++ b/src/app/products/charges/models/charge.model.ts @@ -36,6 +36,7 @@ export enum ChargeAppliesToCode { ChargeAppliesToLoan = 'chargeAppliesTo.loan', ChargeAppliesToSavings = 'chargeAppliesTo.savings', ChargeAppliesToShares = 'chargeAppliesTo.shares', + ChargeAppliesToCurrentAccount = 'chargeAppliesTo.currentAccount', ChargeCalculationTypeFlat = 'chargeCalculationType.flat', ChargeCalculationTypePercentOfAmount = 'chargeCalculationType.percent.of.amount', ChargeCalculationTypePercentOfAmountAndInterest = 'chargeCalculationType.percent.of.amount.and.interest', diff --git a/src/app/products/current-products/create-current-product/create-current-product.component.html b/src/app/products/current-products/create-current-product/create-current-product.component.html new file mode 100644 index 000000000..a197848ae --- /dev/null +++ b/src/app/products/current-products/create-current-product/create-current-product.component.html @@ -0,0 +1,67 @@ +
+ + + + + + + + + {{ 'labels.inputs.DETAILS' | translate }} + + + + + {{ 'labels.inputs.CURRENCY' | translate }} + + + + + {{ 'labels.inputs.TERMS' | translate }} + + + + + {{ 'labels.inputs.SETTINGS' | translate }} + + + + + {{ 'labels.inputs.CHARGES' | translate }} + + + + + {{ 'labels.inputs.ACCOUNTING' | translate }} + + + + @if (formValid) { + + {{ 'labels.inputs.PREVIEW' | translate }} + + + } + +
diff --git a/src/app/products/current-products/create-current-product/create-current-product.component.scss b/src/app/products/current-products/create-current-product/create-current-product.component.scss new file mode 100644 index 000000000..fbc6592ec --- /dev/null +++ b/src/app/products/current-products/create-current-product/create-current-product.component.scss @@ -0,0 +1,4 @@ +fa-icon { + position: relative; + left: 5%; +} diff --git a/src/app/products/current-products/create-current-product/create-current-product.component.ts b/src/app/products/current-products/create-current-product/create-current-product.component.ts new file mode 100644 index 000000000..cbe6ad729 --- /dev/null +++ b/src/app/products/current-products/create-current-product/create-current-product.component.ts @@ -0,0 +1,116 @@ +import { Component, ViewChild, inject } from '@angular/core'; +import { ActivatedRoute, Router } from '@angular/router'; +import { SavingProductDetailsStepComponent } from '../../saving-products/saving-product-stepper/saving-product-details-step/saving-product-details-step.component'; +import { SavingProductCurrencyStepComponent } from '../../saving-products/saving-product-stepper/saving-product-currency-step/saving-product-currency-step.component'; +import { SavingProductTermsStepComponent } from '../../saving-products/saving-product-stepper/saving-product-terms-step/saving-product-terms-step.component'; +import { SavingProductSettingsStepComponent } from '../../saving-products/saving-product-stepper/saving-product-settings-step/saving-product-settings-step.component'; +import { SavingProductChargesStepComponent } from '../../saving-products/saving-product-stepper/saving-product-charges-step/saving-product-charges-step.component'; +import { SavingProductAccountingStepComponent } from '../../saving-products/saving-product-stepper/saving-product-accounting-step/saving-product-accounting-step.component'; +import { SavingProductPreviewStepComponent } from '../../saving-products/saving-product-stepper/saving-product-preview-step/saving-product-preview-step.component'; +import { ProductsService } from 'app/products/products.service'; +import { SettingsService } from 'app/settings/settings.service'; +import { Accounting } from 'app/core/utils/accounting'; +import { MatStepper, MatStepperIcon, MatStep, MatStepLabel } from '@angular/material/stepper'; +import { FaIconComponent } from '@fortawesome/angular-fontawesome'; +import { STANDALONE_SHARED_IMPORTS } from 'app/standalone-shared.module'; + +@Component({ + selector: 'mifosx-create-current-product', + templateUrl: './create-current-product.component.html', + styleUrls: ['./create-current-product.component.scss'], + imports: [ + ...STANDALONE_SHARED_IMPORTS, + MatStepper, + MatStepperIcon, + FaIconComponent, + MatStep, + MatStepLabel, + SavingProductDetailsStepComponent, + SavingProductCurrencyStepComponent, + SavingProductTermsStepComponent, + SavingProductSettingsStepComponent, + SavingProductChargesStepComponent, + SavingProductAccountingStepComponent, + SavingProductPreviewStepComponent + ] +}) +export class CreateCurrentProductComponent { + private route = inject(ActivatedRoute); + private productsService = inject(ProductsService); + private router = inject(Router); + private settingsService = inject(SettingsService); + private accounting = inject(Accounting); + + @ViewChild(SavingProductDetailsStepComponent, { static: true }) detailsStep: SavingProductDetailsStepComponent; + @ViewChild(SavingProductCurrencyStepComponent, { static: true }) currencyStep: SavingProductCurrencyStepComponent; + @ViewChild(SavingProductTermsStepComponent, { static: true }) termsStep: SavingProductTermsStepComponent; + @ViewChild(SavingProductSettingsStepComponent, { static: true }) settingsStep: SavingProductSettingsStepComponent; + @ViewChild(SavingProductChargesStepComponent, { static: true }) chargesStep: SavingProductChargesStepComponent; + @ViewChild(SavingProductAccountingStepComponent, { static: true }) + accountingStep: SavingProductAccountingStepComponent; + + currentProductsTemplate: any; + accountingRuleData: string[] = []; + + constructor() { + this.route.data.subscribe((data: { currentProductsTemplate: any }) => { + this.currentProductsTemplate = data.currentProductsTemplate; + }); + this.accountingRuleData = this.accounting.getAccountingRulesForSavings(); + } + + get detailsForm() { + return this.detailsStep.savingProductDetailsForm; + } + get currencyForm() { + return this.currencyStep.savingProductCurrencyForm; + } + get termsForm() { + return this.termsStep.savingProductTermsForm; + } + get settingsForm() { + return this.settingsStep.savingProductSettingsForm; + } + get accountingForm() { + return this.accountingStep.savingProductAccountingForm; + } + + get formValid() { + return ( + this.detailsForm.valid && + this.currencyForm.valid && + this.termsForm.valid && + this.settingsForm.valid && + this.accountingForm.valid + ); + } + + get currentProduct() { + return { + ...this.detailsStep.savingProductDetails, + ...this.currencyStep.savingProductCurrency, + ...this.termsStep.savingProductTerms, + ...this.settingsStep.savingProductSettings, + ...this.chargesStep.savingProductCharges, + ...this.accountingStep.savingProductAccounting + }; + } + + submit() { + const product = { + ...this.currentProduct, + charges: this.currentProduct.charges.map((charge: any) => ({ id: charge.id })), + locale: this.settingsService.language.code + }; + delete product.advancedAccountingRules; + this.productsService.createCurrentProduct(product).subscribe((response: any) => { + this.router.navigate( + [ + '../', + response.resourceId + ], + { relativeTo: this.route } + ); + }); + } +} diff --git a/src/app/products/current-products/current-product.resolver.ts b/src/app/products/current-products/current-product.resolver.ts new file mode 100644 index 000000000..e63fc88bd --- /dev/null +++ b/src/app/products/current-products/current-product.resolver.ts @@ -0,0 +1,14 @@ +import { Injectable, inject } from '@angular/core'; +import { ActivatedRouteSnapshot } from '@angular/router'; +import { Observable } from 'rxjs'; +import { ProductsService } from '../products.service'; + +@Injectable() +export class CurrentProductResolver { + private productsService = inject(ProductsService); + + resolve(route: ActivatedRouteSnapshot): Observable { + const productId = route.parent.paramMap.get('productId'); + return this.productsService.getCurrentProduct(productId); + } +} diff --git a/src/app/products/current-products/current-products-template.resolver.ts b/src/app/products/current-products/current-products-template.resolver.ts new file mode 100644 index 000000000..44d4653df --- /dev/null +++ b/src/app/products/current-products/current-products-template.resolver.ts @@ -0,0 +1,12 @@ +import { Injectable, inject } from '@angular/core'; +import { Observable } from 'rxjs'; +import { ProductsService } from '../products.service'; + +@Injectable() +export class CurrentProductsTemplateResolver { + private productsService = inject(ProductsService); + + resolve(): Observable { + return this.productsService.getCurrentProductsTemplate(); + } +} diff --git a/src/app/products/current-products/current-products.component.html b/src/app/products/current-products/current-products.component.html new file mode 100644 index 000000000..4e0f19183 --- /dev/null +++ b/src/app/products/current-products/current-products.component.html @@ -0,0 +1,36 @@ +
+
+ +
+
+ +
+
+ + {{ 'labels.inputs.Filter' | translate }} + + +
+ +
+ + + + + + + + + + + + + +
{{ 'labels.inputs.Name' | translate }}{{ product.name }}{{ 'labels.inputs.Short Name' | translate }}{{ product.shortName }}
+ + +
+
diff --git a/src/app/products/current-products/current-products.component.scss b/src/app/products/current-products/current-products.component.scss new file mode 100644 index 000000000..24edb63bb --- /dev/null +++ b/src/app/products/current-products/current-products.component.scss @@ -0,0 +1,7 @@ +table { + width: 100%; + + .select-row:hover { + cursor: pointer; + } +} diff --git a/src/app/products/current-products/current-products.component.ts b/src/app/products/current-products/current-products.component.ts new file mode 100644 index 000000000..b2eb80a43 --- /dev/null +++ b/src/app/products/current-products/current-products.component.ts @@ -0,0 +1,71 @@ +import { Component, OnInit, ViewChild, inject } from '@angular/core'; +import { MatPaginator } from '@angular/material/paginator'; +import { MatSort, MatSortHeader } from '@angular/material/sort'; +import { + MatTableDataSource, + MatTable, + MatColumnDef, + MatHeaderCellDef, + MatHeaderCell, + MatCellDef, + MatCell, + MatHeaderRowDef, + MatHeaderRow, + MatRowDef, + MatRow +} from '@angular/material/table'; +import { ActivatedRoute, RouterLink } from '@angular/router'; +import { FaIconComponent } from '@fortawesome/angular-fontawesome'; +import { STANDALONE_SHARED_IMPORTS } from 'app/standalone-shared.module'; + +@Component({ + selector: 'mifosx-current-products', + templateUrl: './current-products.component.html', + styleUrls: ['./current-products.component.scss'], + imports: [ + ...STANDALONE_SHARED_IMPORTS, + FaIconComponent, + MatTable, + MatSort, + MatColumnDef, + MatHeaderCellDef, + MatHeaderCell, + MatSortHeader, + MatCellDef, + MatCell, + MatHeaderRowDef, + MatHeaderRow, + MatRowDef, + MatRow, + MatPaginator + ] +}) +export class CurrentProductsComponent implements OnInit { + private route = inject(ActivatedRoute); + + currentProductsData: any; + displayedColumns: string[] = [ + 'name', + 'shortName' + ]; + dataSource: MatTableDataSource; + + @ViewChild(MatPaginator, { static: true }) paginator: MatPaginator; + @ViewChild(MatSort, { static: true }) sort: MatSort; + + constructor() { + this.route.data.subscribe((data: { currentProducts: any }) => { + this.currentProductsData = data.currentProducts; + }); + } + + ngOnInit() { + this.dataSource = new MatTableDataSource(this.currentProductsData); + this.dataSource.paginator = this.paginator; + this.dataSource.sort = this.sort; + } + + applyFilter(filterValue: string) { + this.dataSource.filter = filterValue.trim().toLowerCase(); + } +} diff --git a/src/app/products/current-products/current-products.resolver.ts b/src/app/products/current-products/current-products.resolver.ts new file mode 100644 index 000000000..bf442d094 --- /dev/null +++ b/src/app/products/current-products/current-products.resolver.ts @@ -0,0 +1,12 @@ +import { Injectable, inject } from '@angular/core'; +import { Observable } from 'rxjs'; +import { ProductsService } from '../products.service'; + +@Injectable() +export class CurrentProductsResolver { + private productsService = inject(ProductsService); + + resolve(): Observable { + return this.productsService.getCurrentProducts(); + } +} diff --git a/src/app/products/current-products/edit-current-product/current-product-and-template.resolver.ts b/src/app/products/current-products/edit-current-product/current-product-and-template.resolver.ts new file mode 100644 index 000000000..10ad654ed --- /dev/null +++ b/src/app/products/current-products/edit-current-product/current-product-and-template.resolver.ts @@ -0,0 +1,14 @@ +import { Injectable, inject } from '@angular/core'; +import { ActivatedRouteSnapshot } from '@angular/router'; +import { Observable } from 'rxjs'; +import { ProductsService } from '../../products.service'; + +@Injectable() +export class CurrentProductAndTemplateResolver { + private productsService = inject(ProductsService); + + resolve(route: ActivatedRouteSnapshot): Observable { + const productId = route.paramMap.get('productId'); + return this.productsService.getCurrentProduct(productId, true); + } +} diff --git a/src/app/products/current-products/edit-current-product/edit-current-product.component.html b/src/app/products/current-products/edit-current-product/edit-current-product.component.html new file mode 100644 index 000000000..a8ab8c762 --- /dev/null +++ b/src/app/products/current-products/edit-current-product/edit-current-product.component.html @@ -0,0 +1,69 @@ +
+ + + + + + + + + {{ 'labels.inputs.DETAILS' | translate }} + + + + + {{ 'labels.inputs.CURRENCY' | translate }} + + + + + {{ 'labels.inputs.TERMS' | translate }} + + + + + {{ 'labels.inputs.SETTINGS' | translate }} + + + + + {{ 'labels.inputs.CHARGES' | translate }} + + + + + {{ 'labels.inputs.ACCOUNTING' | translate }} + + + + @if (formValidAndNotPristine) { + + {{ 'labels.inputs.PREVIEW' | translate }} + + + } + +
diff --git a/src/app/products/current-products/edit-current-product/edit-current-product.component.scss b/src/app/products/current-products/edit-current-product/edit-current-product.component.scss new file mode 100644 index 000000000..fbc6592ec --- /dev/null +++ b/src/app/products/current-products/edit-current-product/edit-current-product.component.scss @@ -0,0 +1,4 @@ +fa-icon { + position: relative; + left: 5%; +} diff --git a/src/app/products/current-products/edit-current-product/edit-current-product.component.ts b/src/app/products/current-products/edit-current-product/edit-current-product.component.ts new file mode 100644 index 000000000..0083121a2 --- /dev/null +++ b/src/app/products/current-products/edit-current-product/edit-current-product.component.ts @@ -0,0 +1,122 @@ +import { Component, ViewChild, inject } from '@angular/core'; +import { ActivatedRoute, Router } from '@angular/router'; +import { SavingProductDetailsStepComponent } from '../../saving-products/saving-product-stepper/saving-product-details-step/saving-product-details-step.component'; +import { SavingProductCurrencyStepComponent } from '../../saving-products/saving-product-stepper/saving-product-currency-step/saving-product-currency-step.component'; +import { SavingProductTermsStepComponent } from '../../saving-products/saving-product-stepper/saving-product-terms-step/saving-product-terms-step.component'; +import { SavingProductSettingsStepComponent } from '../../saving-products/saving-product-stepper/saving-product-settings-step/saving-product-settings-step.component'; +import { SavingProductChargesStepComponent } from '../../saving-products/saving-product-stepper/saving-product-charges-step/saving-product-charges-step.component'; +import { SavingProductAccountingStepComponent } from '../../saving-products/saving-product-stepper/saving-product-accounting-step/saving-product-accounting-step.component'; +import { SavingProductPreviewStepComponent } from '../../saving-products/saving-product-stepper/saving-product-preview-step/saving-product-preview-step.component'; +import { ProductsService } from 'app/products/products.service'; +import { SettingsService } from 'app/settings/settings.service'; +import { Accounting } from 'app/core/utils/accounting'; +import { MatStepper, MatStepperIcon, MatStep, MatStepLabel } from '@angular/material/stepper'; +import { FaIconComponent } from '@fortawesome/angular-fontawesome'; +import { STANDALONE_SHARED_IMPORTS } from 'app/standalone-shared.module'; + +@Component({ + selector: 'mifosx-edit-current-product', + templateUrl: './edit-current-product.component.html', + styleUrls: ['./edit-current-product.component.scss'], + imports: [ + ...STANDALONE_SHARED_IMPORTS, + MatStepper, + MatStepperIcon, + FaIconComponent, + MatStep, + MatStepLabel, + SavingProductDetailsStepComponent, + SavingProductCurrencyStepComponent, + SavingProductTermsStepComponent, + SavingProductSettingsStepComponent, + SavingProductChargesStepComponent, + SavingProductAccountingStepComponent, + SavingProductPreviewStepComponent + ] +}) +export class EditCurrentProductComponent { + private route = inject(ActivatedRoute); + private productsService = inject(ProductsService); + private router = inject(Router); + private settingsService = inject(SettingsService); + private accounting = inject(Accounting); + + @ViewChild(SavingProductDetailsStepComponent, { static: true }) detailsStep: SavingProductDetailsStepComponent; + @ViewChild(SavingProductCurrencyStepComponent, { static: true }) currencyStep: SavingProductCurrencyStepComponent; + @ViewChild(SavingProductTermsStepComponent, { static: true }) termsStep: SavingProductTermsStepComponent; + @ViewChild(SavingProductSettingsStepComponent, { static: true }) settingsStep: SavingProductSettingsStepComponent; + @ViewChild(SavingProductChargesStepComponent, { static: true }) chargesStep: SavingProductChargesStepComponent; + @ViewChild(SavingProductAccountingStepComponent, { static: true }) + accountingStep: SavingProductAccountingStepComponent; + + currentProductAndTemplate: any; + accountingRuleData: string[] = []; + + constructor() { + this.route.data.subscribe((data: { currentProductAndTemplate: any }) => { + this.currentProductAndTemplate = data.currentProductAndTemplate; + }); + this.accountingRuleData = this.accounting.getAccountingRulesForSavings(); + } + + get detailsForm() { + return this.detailsStep.savingProductDetailsForm; + } + get currencyForm() { + return this.currencyStep.savingProductCurrencyForm; + } + get termsForm() { + return this.termsStep.savingProductTermsForm; + } + get settingsForm() { + return this.settingsStep.savingProductSettingsForm; + } + get accountingForm() { + return this.accountingStep.savingProductAccountingForm; + } + + get formValidAndNotPristine() { + return ( + this.detailsForm.valid && + this.currencyForm.valid && + this.termsForm.valid && + this.settingsForm.valid && + this.accountingForm.valid && + (!this.detailsForm.pristine || + !this.currencyForm.pristine || + !this.termsForm.pristine || + !this.settingsForm.pristine || + !this.chargesStep.pristine || + !this.accountingForm.pristine) + ); + } + + get currentProduct() { + return { + ...this.detailsStep.savingProductDetails, + ...this.currencyStep.savingProductCurrency, + ...this.termsStep.savingProductTerms, + ...this.settingsStep.savingProductSettings, + ...this.chargesStep.savingProductCharges, + ...this.accountingStep.savingProductAccounting + }; + } + + submit() { + const product = { + ...this.currentProduct, + charges: this.currentProduct.charges.map((charge: any) => ({ id: charge.id })), + locale: this.settingsService.language.code + }; + delete product.advancedAccountingRules; + this.productsService.updateCurrentProduct(this.currentProductAndTemplate.id, product).subscribe((response: any) => { + this.router.navigate( + [ + '../../', + response.resourceId + ], + { relativeTo: this.route } + ); + }); + } +} diff --git a/src/app/products/current-products/view-current-product/current-product-general-tab/current-product-general-tab.component.html b/src/app/products/current-products/view-current-product/current-product-general-tab/current-product-general-tab.component.html new file mode 100644 index 000000000..18491561c --- /dev/null +++ b/src/app/products/current-products/view-current-product/current-product-general-tab/current-product-general-tab.component.html @@ -0,0 +1,102 @@ +
+ +
+ +
+
+

{{ currentProduct.name }}

+ + +

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

+ + +
+ {{ 'labels.inputs.Short Name' | translate }}: + {{ currentProduct.shortName }} +
+ + @if (currentProduct.description) { +
+ {{ 'labels.inputs.Description' | translate }}: + {{ currentProduct.description }} +
+ } + +

{{ 'labels.inputs.Currency' | translate }}

+ + +
+ {{ 'labels.inputs.Currency' | translate }}: + {{ currentProduct.currency.name }} +
+ +
+ {{ 'labels.inputs.Decimal Places' | translate }}: + {{ currentProduct.currency.decimalPlaces }} +
+ +

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

+ + +
+ {{ 'labels.inputs.Nominal Annual Interest' | translate }}: + {{ currentProduct.nominalAnnualInterestRate | formatNumber }} % +
+ +
+ {{ 'labels.inputs.Interest Compounding Period' | translate }}: + {{ currentProduct.interestCompoundingPeriodType?.value | translateKey: 'catalogs' }} +
+ +
+ {{ 'labels.inputs.Interest Posting Period' | translate }}: + {{ currentProduct.interestPostingPeriodType?.value | translateKey: 'catalogs' }} +
+ + @if (currentProduct.charges?.length) { +
+

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

+ + + + + + + + + + + + + + + + + + + + +
{{ 'labels.inputs.Name' | translate }}{{ charge.name + ', ' + charge.currency.displaySymbol }}{{ 'labels.inputs.Type' | translate }} + {{ charge.chargeCalculationType.value | translateKey: 'catalogs' }} + {{ 'labels.inputs.Amount' | translate }}{{ charge.amount | formatNumber }}{{ 'labels.inputs.Collected On' | translate }}{{ charge.chargeTimeType.value | translateKey: 'catalogs' }}
+
+ } + +

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

+ + + +
+
diff --git a/src/app/products/current-products/view-current-product/current-product-general-tab/current-product-general-tab.component.scss b/src/app/products/current-products/view-current-product/current-product-general-tab/current-product-general-tab.component.scss new file mode 100644 index 000000000..b4c371673 --- /dev/null +++ b/src/app/products/current-products/view-current-product/current-product-general-tab/current-product-general-tab.component.scss @@ -0,0 +1,36 @@ +@use 'assets/styles/helper'; +@use 'assets/styles/colours' as *; + +.tab-container { + padding: 1%; + margin: 1%; +} + +.mat-h3 { + margin-top: 10px !important; + font-weight: 300; + color: $status-approved; +} + +h2, +h3, +h4 { + margin: 0; + font-weight: 500; +} + +table { + width: 100%; +} + +.mat-elevation-z1 { + margin: 1em 0 1.5em; +} + +span { + margin: 0.5em 0; +} + +mat-divider { + margin: 0 0 0.5em; +} diff --git a/src/app/products/current-products/view-current-product/current-product-general-tab/current-product-general-tab.component.ts b/src/app/products/current-products/view-current-product/current-product-general-tab/current-product-general-tab.component.ts new file mode 100644 index 000000000..54b58f02c --- /dev/null +++ b/src/app/products/current-products/view-current-product/current-product-general-tab/current-product-general-tab.component.ts @@ -0,0 +1,68 @@ +import { Component, inject } from '@angular/core'; +import { ActivatedRoute, RouterLink } from '@angular/router'; +import { Accounting } from 'app/core/utils/accounting'; +import { FaIconComponent } from '@fortawesome/angular-fontawesome'; +import { MatDivider } from '@angular/material/divider'; +import { + MatTable, + MatColumnDef, + MatHeaderCellDef, + MatHeaderCell, + MatCellDef, + MatCell, + MatHeaderRowDef, + MatHeaderRow, + MatRowDef, + MatRow +} from '@angular/material/table'; +import { ViewSavingsAccountingDetailsComponent } from '../../../../shared/accounting/view-savings-accounting-details/view-savings-accounting-details.component'; +import { FormatNumberPipe } from '../../../../pipes/format-number.pipe'; +import { YesnoPipe } from '../../../../pipes/yesno.pipe'; +import { STANDALONE_SHARED_IMPORTS } from 'app/standalone-shared.module'; + +@Component({ + selector: 'mifosx-current-product-general-tab', + templateUrl: './current-product-general-tab.component.html', + styleUrls: ['./current-product-general-tab.component.scss'], + imports: [ + ...STANDALONE_SHARED_IMPORTS, + FaIconComponent, + MatDivider, + MatTable, + MatColumnDef, + MatHeaderCellDef, + MatHeaderCell, + MatCellDef, + MatCell, + MatHeaderRowDef, + MatHeaderRow, + MatRowDef, + MatRow, + ViewSavingsAccountingDetailsComponent, + FormatNumberPipe, + YesnoPipe + ] +}) +export class CurrentProductGeneralTabComponent { + private route = inject(ActivatedRoute); + private accounting = inject(Accounting); + + currentProduct: any; + + chargesDisplayedColumns: string[] = [ + 'name', + 'chargeCalculationType', + 'amount', + 'chargeTimeType' + ]; + + constructor() { + this.route.data.subscribe((data: { currentProduct: any }) => { + this.currentProduct = data.currentProduct; + }); + } + + isCashOrAccrualAccounting(): boolean { + return this.accounting.isCashOrAccrualAccounting(this.currentProduct.accountingRule); + } +} diff --git a/src/app/products/current-products/view-current-product/view-current-product.component.html b/src/app/products/current-products/view-current-product/view-current-product.component.html new file mode 100644 index 000000000..5285675e9 --- /dev/null +++ b/src/app/products/current-products/view-current-product/view-current-product.component.html @@ -0,0 +1,18 @@ + + + + + + + + diff --git a/src/app/products/current-products/view-current-product/view-current-product.component.scss b/src/app/products/current-products/view-current-product/view-current-product.component.scss new file mode 100644 index 000000000..c8c153b25 --- /dev/null +++ b/src/app/products/current-products/view-current-product/view-current-product.component.scss @@ -0,0 +1,14 @@ +.product-card { + margin: 0 auto; + max-width: 80rem; + width: 90%; + padding: 0; + + .navigation-tabs { + overflow: auto; + } + + i:hover { + cursor: pointer; + } +} diff --git a/src/app/products/current-products/view-current-product/view-current-product.component.ts b/src/app/products/current-products/view-current-product/view-current-product.component.ts new file mode 100644 index 000000000..4d4c1c6d0 --- /dev/null +++ b/src/app/products/current-products/view-current-product/view-current-product.component.ts @@ -0,0 +1,23 @@ +import { Component, inject } from '@angular/core'; +import { ActivatedRoute, RouterLinkActive, RouterLink, RouterOutlet } from '@angular/router'; +import { MatTabNav, MatTabLink, MatTabNavPanel } from '@angular/material/tabs'; +import { STANDALONE_SHARED_IMPORTS } from 'app/standalone-shared.module'; + +@Component({ + selector: 'mifosx-view-current-product', + templateUrl: './view-current-product.component.html', + styleUrls: ['./view-current-product.component.scss'], + imports: [ + ...STANDALONE_SHARED_IMPORTS, + MatTabNav, + MatTabLink, + RouterLinkActive, + MatTabNavPanel, + RouterOutlet + ] +}) +export class ViewCurrentProductComponent { + private route = inject(ActivatedRoute); + + constructor() {} +} diff --git a/src/app/products/products-routing.module.ts b/src/app/products/products-routing.module.ts index dc8caea60..0c49649fd 100644 --- a/src/app/products/products-routing.module.ts +++ b/src/app/products/products-routing.module.ts @@ -135,6 +135,17 @@ import { ShareProductDatatablesResolver } from './share-products/share-product-d import { ShareProductDatatableResolver } from './share-products/share-product-datatable.resolver'; import { GlobalConfigurationsResolver } from 'app/system/configurations/global-configurations-tab/global-configurations.resolver'; +/** Current Products */ +import { CurrentProductsComponent } from './current-products/current-products.component'; +import { CreateCurrentProductComponent } from './current-products/create-current-product/create-current-product.component'; +import { ViewCurrentProductComponent } from './current-products/view-current-product/view-current-product.component'; +import { EditCurrentProductComponent } from './current-products/edit-current-product/edit-current-product.component'; +import { CurrentProductGeneralTabComponent } from './current-products/view-current-product/current-product-general-tab/current-product-general-tab.component'; +import { CurrentProductsResolver } from './current-products/current-products.resolver'; +import { CurrentProductsTemplateResolver } from './current-products/current-products-template.resolver'; +import { CurrentProductResolver } from './current-products/current-product.resolver'; +import { CurrentProductAndTemplateResolver } from './current-products/edit-current-product/current-product-and-template.resolver'; + /** Products Routes */ const routes: Routes = [ Route.withShell([ @@ -283,6 +294,60 @@ const routes: Routes = [ } ] }, + { + path: 'current-products', + data: { title: 'Current Products', breadcrumb: 'Current Products' }, + children: [ + { + path: '', + component: CurrentProductsComponent, + resolve: { + currentProducts: CurrentProductsResolver + } + }, + { + path: 'create', + component: CreateCurrentProductComponent, + data: { title: 'Create Current Product', breadcrumb: 'Create' }, + resolve: { + currentProductsTemplate: CurrentProductsTemplateResolver + } + }, + { + path: ':productId', + data: { title: 'View Current Product', breadcrumb: 'productId', routeParamBreadcrumb: 'productId' }, + children: [ + { + path: '', + component: ViewCurrentProductComponent, + children: [ + { + path: '', + redirectTo: 'general', + pathMatch: 'full' + }, + { + path: 'general', + data: { title: 'General', breadcrumb: 'General', routeParamBreadcrumb: false }, + component: CurrentProductGeneralTabComponent, + resolve: { + currentProduct: CurrentProductResolver + } + } + ] + }, + { + path: 'edit', + component: EditCurrentProductComponent, + data: { title: 'Edit Current Product', breadcrumb: 'Edit', routeParamBreadcrumb: false }, + resolve: { + currentProductAndTemplate: CurrentProductAndTemplateResolver + } + } + ] + } + ] + }, { path: 'share-products', data: { title: 'Share Products', breadcrumb: 'Share Products' }, @@ -951,7 +1016,11 @@ const routes: Routes = [ CollateralsResolver, CollateralTemplateResolver, DelinquencyRangeComponentsResolver, - DelinquencyBucketComponentsResolver + DelinquencyBucketComponentsResolver, + CurrentProductsResolver, + CurrentProductsTemplateResolver, + CurrentProductResolver, + CurrentProductAndTemplateResolver ] }) export class ProductsRoutingModule {} diff --git a/src/app/products/products.component.html b/src/app/products/products.component.html index ffbd60378..7e96b4550 100644 --- a/src/app/products/products.component.html +++ b/src/app/products/products.component.html @@ -83,6 +83,21 @@
+
+ + + +
+
+
+ + + + + +
+ + OpenProject + {{ opFilteredWorkPackages.length }} +
+
+ +
+ +
+ +

OpenProject Work Items

+

Fetch work items from your OpenProject instance to view and track project tasks.

+ +
+ + +
+ +

Loading OpenProject work items...

+
+ + +
+ +

{{ opError }}

+ +
+ + +
+ +
+ + Search work items... + + + + +
+ + +
+
+
+ #{{ wp.id }} + {{ wp.type }} +
+

{{ wp.subject }}

+
+ + {{ wp.status }} + + + {{ wp.priority }} + +
+
+ {{ wp.assignee }} + {{ formatOpDate(wp.dueDate) }} + {{ wp.project }} +
+
+ + {{ wp.percentageDone }}% +
+
+ + +
+
+
+ +
Showing {{ opFilteredWorkPackages.length }} of {{ opWorkPackages.length }} work items
+
+ + +
+
+
+

+ #{{ opSelectedWorkPackage.id }} + {{ opSelectedWorkPackage.subject }} +

+ +
+
+
+ Status + + {{ opSelectedWorkPackage.status }} + +
+
+ Type + {{ opSelectedWorkPackage.type }} +
+
+ Priority + + {{ opSelectedWorkPackage.priority }} + +
+
+ Project + {{ opSelectedWorkPackage.project }} +
+
+ Assignee + {{ opSelectedWorkPackage.assignee }} +
+
+ Created + {{ formatOpDate(opSelectedWorkPackage.createdAt) }} +
+
+ Updated + {{ formatOpDate(opSelectedWorkPackage.updatedAt) }} +
+
+ Due Date + {{ formatOpDate(opSelectedWorkPackage.dueDate) }} +
+
+ Progress +
+ + {{ opSelectedWorkPackage.percentageDone }}% +
+
+
+ Description +
{{ opSelectedWorkPackage.description }}
+
+
+ +
+
+ + +
+
+ +

Fetching work item details...

+
diff --git a/src/app/rbi-circulars/rbi-circulars.component.scss b/src/app/rbi-circulars/rbi-circulars.component.scss index 539ac24c0..c38517fb5 100644 --- a/src/app/rbi-circulars/rbi-circulars.component.scss +++ b/src/app/rbi-circulars/rbi-circulars.component.scss @@ -293,6 +293,365 @@ } } +// OpenProject column in Notification tab +.col-op-details { + width: 240px; + vertical-align: top; +} + +.op-no-link { + color: #ccc; + font-size: 0.85rem; +} + +.op-fetch-btn { + font-size: 0.75rem; + white-space: nowrap; +} + +.op-row-loading { + display: flex; + justify-content: center; + padding: 4px 0; +} + +.op-row-result { + display: flex; + flex-direction: column; + gap: 4px; + font-size: 0.8rem; +} + +.op-row-summary { + display: flex; + align-items: center; + gap: 6px; +} + +.op-row-id { + font-weight: 700; + color: #6a1b9a; + font-size: 0.75rem; +} + +.op-row-subject { + font-weight: 500; + color: #333; + line-height: 1.3; + display: -webkit-box; + -webkit-line-clamp: 2; + -webkit-box-orient: vertical; + overflow: hidden; +} + +.op-row-meta { + display: flex; + gap: 10px; + color: #777; + font-size: 0.75rem; + + .op-info-icon { + font-size: 0.65rem; + color: #aaa; + margin-right: 3px; + } +} + +.op-row-actions { + display: flex; + gap: 2px; + margin-top: 2px; +} + +.op-view-detail-btn { + font-size: 0.7rem; + padding: 0 6px; + line-height: 24px; + height: 24px; +} + +// OpenProject Tab +.op-start-section { + display: flex; + flex-direction: column; + align-items: center; + padding: 48px 0; + text-align: center; + gap: 8px; + + .op-start-icon { + font-size: 2.5rem; + color: #6a1b9a; + margin-bottom: 8px; + } + + h3 { + margin: 0; + color: #333; + } + + p { + color: #888; + font-size: 0.85rem; + margin: 0 0 16px; + } +} + +.op-toolbar { + display: flex; + align-items: center; + gap: 12px; + margin-bottom: 16px; + flex-wrap: wrap; + + .op-search-field { + flex: 1; + min-width: 220px; + max-width: 400px; + } +} + +.op-grid { + display: grid; + grid-template-columns: repeat(auto-fill, minmax(340px, 1fr)); + gap: 16px; +} + +.op-card { + background: white; + border: 1px solid #e8e8e8; + border-radius: 10px; + padding: 16px; + cursor: pointer; + transition: box-shadow 0.2s, border-color 0.2s; + + &:hover { + box-shadow: 0 4px 12px rgb(0 0 0 / 10%); + border-color: #6a1b9a40; + } +} + +.op-card-header { + display: flex; + justify-content: space-between; + align-items: center; + margin-bottom: 8px; +} + +.op-card-id { + font-size: 0.8rem; + font-weight: 700; + color: #6a1b9a; +} + +.op-card-type { + font-size: 0.75rem; + font-weight: 600; + text-transform: uppercase; + letter-spacing: 0.5px; +} + +.op-card-subject { + margin: 0 0 10px; + font-size: 0.95rem; + font-weight: 500; + color: #222; + line-height: 1.4; +} + +.op-card-meta { + display: flex; + gap: 8px; + margin-bottom: 10px; + flex-wrap: wrap; +} + +.op-status-chip { + display: inline-block; + font-size: 0.7rem; + font-weight: 600; + color: white; + padding: 2px 10px; + border-radius: 12px; + letter-spacing: 0.3px; +} + +.op-priority-chip { + display: inline-block; + font-size: 0.7rem; + font-weight: 600; + padding: 2px 10px; + border-radius: 12px; + border: 1.5px solid; + background: transparent; + letter-spacing: 0.3px; +} + +.op-card-info { + display: flex; + flex-wrap: wrap; + gap: 12px; + font-size: 0.8rem; + color: #666; + margin-bottom: 8px; + + .op-info-icon { + font-size: 0.7rem; + color: #aaa; + margin-right: 4px; + } +} + +.op-progress { + display: flex; + align-items: center; + gap: 8px; + margin-bottom: 8px; + + mat-progress-bar { + flex: 1; + } + + .op-progress-label { + font-size: 0.75rem; + font-weight: 600; + color: #555; + min-width: 35px; + } +} + +.op-card-actions { + display: flex; + gap: 4px; + margin-top: 8px; + border-top: 1px solid #f0f0f0; + padding-top: 8px; + + button { + font-size: 0.75rem; + } +} + +// OpenProject Detail Overlay +.op-detail-overlay { + position: fixed; + inset: 0; + background: rgb(0 0 0 / 40%); + display: flex; + align-items: center; + justify-content: center; + z-index: 1000; +} + +.op-detail-panel { + background: white; + border-radius: 12px; + width: 90%; + max-width: 600px; + max-height: 85vh; + overflow-y: auto; + box-shadow: 0 8px 32px rgb(0 0 0 / 20%); +} + +.op-detail-header { + display: flex; + justify-content: space-between; + align-items: flex-start; + padding: 20px 24px 12px; + border-bottom: 1px solid #f0f0f0; + + h3 { + margin: 0; + font-size: 1.1rem; + font-weight: 600; + color: #222; + line-height: 1.4; + + .op-card-id { + margin-right: 8px; + } + } +} + +.op-detail-body { + padding: 16px 24px; +} + +.op-detail-row { + display: flex; + align-items: center; + gap: 12px; + padding: 8px 0; + border-bottom: 1px solid #f8f8f8; +} + +.op-detail-label { + font-size: 0.8rem; + font-weight: 600; + color: #888; + text-transform: uppercase; + letter-spacing: 0.5px; + min-width: 90px; +} + +.op-detail-value { + font-size: 0.9rem; + color: #333; +} + +.op-detail-progress { + display: flex; + align-items: center; + gap: 8px; + flex: 1; + + mat-progress-bar { + flex: 1; + } + + span { + font-size: 0.8rem; + font-weight: 600; + color: #555; + } +} + +.op-detail-description { + margin-top: 12px; + + .op-description-content { + margin-top: 8px; + font-size: 0.85rem; + color: #444; + line-height: 1.6; + padding: 12px; + background: #f9f9f9; + border-radius: 6px; + } +} + +.op-detail-footer { + display: flex; + gap: 12px; + justify-content: flex-end; + padding: 16px 24px; + border-top: 1px solid #f0f0f0; +} + +.op-detail-loading { + background: white; + border-radius: 12px; + padding: 48px; + display: flex; + flex-direction: column; + align-items: center; + gap: 16px; + + p { + color: #666; + } +} + // Responsive @media screen and (width <= 768px) { .rbi-container { @@ -322,4 +681,22 @@ .col-sno { width: 40px; } + + .op-grid { + grid-template-columns: 1fr; + } + + .op-toolbar { + flex-direction: column; + align-items: stretch; + + .op-search-field { + max-width: 100%; + } + } + + .op-detail-panel { + width: 95%; + max-height: 90vh; + } } diff --git a/src/app/rbi-circulars/rbi-circulars.component.ts b/src/app/rbi-circulars/rbi-circulars.component.ts index 3ad2e2a54..dec51c67e 100644 --- a/src/app/rbi-circulars/rbi-circulars.component.ts +++ b/src/app/rbi-circulars/rbi-circulars.component.ts @@ -6,16 +6,19 @@ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ -import { Component, OnInit, inject } from '@angular/core'; +import { Component, OnInit, ViewChildren, QueryList, AfterViewInit, inject } from '@angular/core'; import { RbiCircularsService, RbiFeedItem } from './rbi-circulars.service'; +import { OpenProjectService, OpenProjectWorkPackage } from './openproject.service'; import { STANDALONE_SHARED_IMPORTS } from 'app/standalone-shared.module'; import { FaIconComponent } from '@fortawesome/angular-fontawesome'; import { MatProgressSpinner } from '@angular/material/progress-spinner'; import { MatTooltip } from '@angular/material/tooltip'; import { MatTabsModule } from '@angular/material/tabs'; import { MatTableModule, MatTableDataSource } from '@angular/material/table'; -import { MatPaginatorModule } from '@angular/material/paginator'; +import { MatPaginatorModule, MatPaginator } from '@angular/material/paginator'; import { MatSortModule } from '@angular/material/sort'; +import { MatChipsModule } from '@angular/material/chips'; +import { MatProgressBarModule } from '@angular/material/progress-bar'; interface CategoryTab { key: string; @@ -38,11 +41,15 @@ interface CategoryTab { MatTabsModule, MatTableModule, MatPaginatorModule, - MatSortModule + MatSortModule, + MatChipsModule, + MatProgressBarModule ] }) -export class RbiCircularsComponent implements OnInit { +export class RbiCircularsComponent implements OnInit, AfterViewInit { + @ViewChildren(MatPaginator) paginators!: QueryList; private rbiService = inject(RbiCircularsService); + private openProjectService = inject(OpenProjectService); feedItems: RbiFeedItem[] = []; isLoading = true; @@ -57,8 +64,29 @@ export class RbiCircularsComponent implements OnInit { 'actions' ]; + notificationColumns: string[] = [ + 'sno', + 'title', + 'pubDate', + 'actions', + 'opDetails' + ]; + tabs: CategoryTab[] = []; + /** OpenProject state */ + openProjectEnabled = this.openProjectService.isEnabled; + opWorkPackages: OpenProjectWorkPackage[] = []; + opFilteredWorkPackages: OpenProjectWorkPackage[] = []; + opLoading = false; + opError: string | null = null; + opSelectedWorkPackage: OpenProjectWorkPackage | null = null; + opDetailLoading = false; + + /** Per-row OpenProject detail fetch state (keyed by RBI feed item id) */ + opRowLoading: Record = {}; + opRowDetails: Record = {}; + /** Fixed category config */ private categoryConfig: Record = { ALL: { label: 'All Feeds', icon: 'list', color: '#1a237e' }, @@ -74,6 +102,20 @@ export class RbiCircularsComponent implements OnInit { this.loadFeeds(); } + ngAfterViewInit() { + this.connectPaginators(); + this.paginators.changes.subscribe(() => this.connectPaginators()); + } + + private connectPaginators() { + const paginatorArr = this.paginators.toArray(); + this.tabs.forEach((tab, i) => { + if (paginatorArr[i]) { + tab.dataSource.paginator = paginatorArr[i]; + } + }); + } + loadFeeds() { this.isLoading = true; this.errorMessage = null; @@ -169,6 +211,101 @@ export class RbiCircularsComponent implements OnInit { return tab.dataSource.filteredData.indexOf(row) + 1; } + /** Fetch OpenProject details for a notification row */ + fetchOpDetailsForRow(row: RbiFeedItem) { + if (!row.openProjectWpId) return; + this.opRowLoading[row.id] = true; + this.opRowDetails[row.id] = null; + this.openProjectService.getWorkPackageById(row.openProjectWpId).subscribe({ + next: (wp) => { + this.opRowDetails[row.id] = wp; + this.opRowLoading[row.id] = false; + }, + error: () => { + this.opRowLoading[row.id] = false; + this.opRowDetails[row.id] = null; + } + }); + } + + getColumnsForTab(tab: CategoryTab): string[] { + return tab.key === 'NOTIFICATION' ? this.notificationColumns : this.displayedColumns; + } + + /** OpenProject Methods */ + loadWorkPackages() { + this.opLoading = true; + this.opError = null; + this.openProjectService.getWorkPackages().subscribe({ + next: (items) => { + this.opWorkPackages = items; + this.opFilteredWorkPackages = items; + this.opLoading = false; + }, + error: () => { + this.opLoading = false; + this.opError = 'Failed to load OpenProject work items. Please check your configuration.'; + } + }); + } + + viewWorkPackageDetails(id: number) { + this.opDetailLoading = true; + this.opSelectedWorkPackage = null; + this.openProjectService.getWorkPackageById(id).subscribe({ + next: (wp) => { + this.opSelectedWorkPackage = wp; + this.opDetailLoading = false; + }, + error: () => { + this.opDetailLoading = false; + } + }); + } + + closeWorkPackageDetails() { + this.opSelectedWorkPackage = null; + } + + openInOpenProject(wp: OpenProjectWorkPackage) { + if (wp.openProjectUrl) { + window.open(wp.openProjectUrl, '_blank'); + } + } + + filterWorkPackages(query: string) { + if (!query.trim()) { + this.opFilteredWorkPackages = this.opWorkPackages; + return; + } + const q = query.toLowerCase(); + this.opFilteredWorkPackages = this.opWorkPackages.filter( + (wp) => wp.subject?.toLowerCase().includes(q) || wp.status?.toLowerCase().includes(q) || wp.type?.toLowerCase().includes(q) + ); + } + + getStatusColor(status: string): string { + const s = status?.toLowerCase() || ''; + if (s.includes('closed') || s.includes('done') || s.includes('resolved')) return '#2e7d32'; + if (s.includes('in progress') || s.includes('active')) return '#1976d2'; + if (s.includes('rejected') || s.includes('blocked')) return '#c62828'; + return '#f57c00'; + } + + getPriorityColor(priority: string): string { + const p = priority?.toLowerCase() || ''; + if (p.includes('immediate') || p.includes('urgent')) return '#c62828'; + if (p.includes('high')) return '#e65100'; + if (p.includes('low')) return '#2e7d32'; + return '#1976d2'; + } + + formatOpDate(dateStr: string): string { + if (!dateStr) return '—'; + const d = new Date(dateStr); + return isNaN(d.getTime()) ? '—' : d.toLocaleDateString('en-IN', { day: '2-digit', month: 'short', year: 'numeric' }); + } + private buildTabs() { const categoryKeys = [ 'ALL', @@ -205,6 +342,8 @@ export class RbiCircularsComponent implements OnInit { tab.dataSource.filter = this.searchText.toLowerCase().trim(); } }); + // Reconnect paginators after data update + setTimeout(() => this.connectPaginators()); } private sortByDate(items: RbiFeedItem[]): RbiFeedItem[] { diff --git a/src/app/rbi-circulars/rbi-circulars.service.ts b/src/app/rbi-circulars/rbi-circulars.service.ts index cecf15760..20a610c14 100644 --- a/src/app/rbi-circulars/rbi-circulars.service.ts +++ b/src/app/rbi-circulars/rbi-circulars.service.ts @@ -18,6 +18,9 @@ export interface RbiFeedItem { pubDate: any; // Fineract returns [year,month,day,hour,min,sec] array category: string; fetchedOn?: any; + openProjectWpId?: number; // OpenProject work package ID + openProjectWpStatus?: string; + openProjectWpProgress?: number; } @Injectable({ diff --git a/src/app/reports/reports-routing.module.ts b/src/app/reports/reports-routing.module.ts index 860f9ba46..bdcf03fb0 100644 --- a/src/app/reports/reports-routing.module.ts +++ b/src/app/reports/reports-routing.module.ts @@ -16,6 +16,7 @@ import { Route } from '../core/route/route.service'; /** Custom Components */ import { ReportsComponent } from './reports.component'; import { RunReportComponent } from './run-report/run-report.component'; +import { SanctionsLogsComponent } from './sanctions-logs/sanctions-logs.component'; /** Custom Resolvers */ import { ReportsResolver } from './common-resolvers/reports.resolver'; @@ -37,6 +38,11 @@ const routes: Routes = [ path: '', component: ReportsComponent }, + { + path: 'sanctions-logs', + data: { title: 'Sanctions Logs', breadcrumb: 'Sanctions Logs' }, + component: SanctionsLogsComponent + }, { path: ':filter', data: { routeParamBreadcrumb: 'filter' }, diff --git a/src/app/reports/reports.module.ts b/src/app/reports/reports.module.ts index 74e0adf01..266455270 100644 --- a/src/app/reports/reports.module.ts +++ b/src/app/reports/reports.module.ts @@ -20,6 +20,7 @@ import { RunReportComponent } from './run-report/run-report.component'; import { TableAndSmsComponent } from './run-report/table-and-sms/table-and-sms.component'; import { ChartComponent } from './run-report/chart/chart.component'; import { PentahoComponent } from './run-report/pentaho/pentaho.component'; +import { SanctionsLogsComponent } from './sanctions-logs/sanctions-logs.component'; /** * Reports Module @@ -34,7 +35,8 @@ import { PentahoComponent } from './run-report/pentaho/pentaho.component'; RunReportComponent, TableAndSmsComponent, ChartComponent, - PentahoComponent + PentahoComponent, + SanctionsLogsComponent ], providers: [DecimalPipe] }) diff --git a/src/app/reports/sanctions-logs/sanctions-logs.component.html b/src/app/reports/sanctions-logs/sanctions-logs.component.html new file mode 100644 index 000000000..8581ef8e0 --- /dev/null +++ b/src/app/reports/sanctions-logs/sanctions-logs.component.html @@ -0,0 +1,99 @@ +
+ + +
+ + Search by Name + + + + +
+ + + + @if (searchName || activeFilter) { + + } +
+
+ + @if (loading) { + + } + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + +
Screened Name{{ log.screenedName }}Result + + @if (log.screeningResult === 'CLEAR') { + + } @else { + + } + {{ log.screeningResult }} + + Matches{{ log.matchCount || 0 }}Screened By{{ log.screenedBy }}Screened At{{ log.screenedAt | date: 'medium' }}
+ + @if (!loading && dataSource.data.length === 0) { +
+ +

No screening logs found.

+
+ } + + +
+
diff --git a/src/app/reports/sanctions-logs/sanctions-logs.component.scss b/src/app/reports/sanctions-logs/sanctions-logs.component.scss new file mode 100644 index 000000000..05c32eddb --- /dev/null +++ b/src/app/reports/sanctions-logs/sanctions-logs.component.scss @@ -0,0 +1,120 @@ +.page-header { + display: flex; + align-items: center; + gap: 10px; + margin-bottom: 16px; + + .header-icon { + font-size: 24px; + color: #1074b9; + } + + h2 { + margin: 0; + font-size: 20px; + font-weight: 600; + color: #333; + } +} + +.filters-row { + display: flex; + align-items: center; + gap: 16px; + margin-bottom: 16px; + flex-wrap: wrap; + + .search-field { + flex: 1; + min-width: 250px; + max-width: 400px; + } + + .search-icon { + color: #999; + margin-right: 4px; + } +} + +.filter-chips { + display: flex; + align-items: center; + gap: 8px; + + button { + font-size: 13px; + border-radius: 20px; + } + + .chip-icon { + margin-right: 4px; + font-size: 12px; + } + + .active-filter { + background-color: #1074b9; + color: #fff; + border-color: #1074b9; + } + + .chip-clear.active-filter { + background-color: #388e3c; + border-color: #388e3c; + } + + .chip-flagged.active-filter { + background-color: #d32f2f; + border-color: #d32f2f; + } +} + +.result-badge { + display: inline-flex; + align-items: center; + gap: 4px; + padding: 3px 10px; + border-radius: 12px; + font-size: 12px; + font-weight: 600; + text-transform: uppercase; +} + +.badge-clear { + background: #e8f5e9; + color: #2e7d32; +} + +.badge-flagged { + background: #ffebee; + color: #c62828; +} + +.select-row { + cursor: pointer; + + &:hover { + background: #f5f5f5; + } +} + +.no-data { + display: flex; + flex-direction: column; + align-items: center; + padding: 40px 0; + color: #999; + + .no-data-icon { + font-size: 40px; + margin-bottom: 8px; + } + + p { + font-size: 14px; + margin: 0; + } +} + +mat-progress-bar { + margin-bottom: 8px; +} diff --git a/src/app/reports/sanctions-logs/sanctions-logs.component.ts b/src/app/reports/sanctions-logs/sanctions-logs.component.ts new file mode 100644 index 000000000..6328fbce7 --- /dev/null +++ b/src/app/reports/sanctions-logs/sanctions-logs.component.ts @@ -0,0 +1,115 @@ +import { Component, OnInit, ViewChild, inject } from '@angular/core'; +import { MatPaginator } from '@angular/material/paginator'; +import { MatSort, MatSortHeader } from '@angular/material/sort'; +import { + MatTableDataSource, + MatTable, + MatColumnDef, + MatHeaderCellDef, + MatHeaderCell, + MatCellDef, + MatCell, + MatHeaderRowDef, + MatHeaderRow, + MatRowDef, + MatRow +} from '@angular/material/table'; +import { MatChipsModule } from '@angular/material/chips'; +import { MatProgressBarModule } from '@angular/material/progress-bar'; +import { MatTooltip } from '@angular/material/tooltip'; +import { NgClass, DatePipe } from '@angular/common'; +import { FaIconComponent } from '@fortawesome/angular-fontawesome'; +import { STANDALONE_SHARED_IMPORTS } from 'app/standalone-shared.module'; +import { SanctionsLogsService } from './sanctions-logs.service'; + +@Component({ + selector: 'mifosx-sanctions-logs', + templateUrl: './sanctions-logs.component.html', + styleUrls: ['./sanctions-logs.component.scss'], + imports: [ + ...STANDALONE_SHARED_IMPORTS, + MatTable, + MatSort, + MatColumnDef, + MatHeaderCellDef, + MatHeaderCell, + MatSortHeader, + MatCellDef, + MatCell, + MatHeaderRowDef, + MatHeaderRow, + MatRowDef, + MatRow, + MatPaginator, + MatChipsModule, + MatProgressBarModule, + MatTooltip, + NgClass, + DatePipe, + FaIconComponent + ] +}) +export class SanctionsLogsComponent implements OnInit { + private sanctionsLogsService = inject(SanctionsLogsService); + + displayedColumns: string[] = [ + 'screenedName', + 'screeningResult', + 'matchCount', + 'screenedBy', + 'screenedAt' + ]; + + dataSource = new MatTableDataSource(); + searchName = ''; + activeFilter: string | null = null; + loading = false; + + @ViewChild(MatPaginator, { static: true }) paginator: MatPaginator; + @ViewChild(MatSort, { static: true }) sort: MatSort; + + ngOnInit() { + this.loadLogs(); + } + + loadLogs() { + this.loading = true; + const filters: any = {}; + if (this.activeFilter) { + filters.result = this.activeFilter; + } + if (this.searchName?.trim()) { + filters.name = this.searchName.trim(); + } + this.sanctionsLogsService.getLogs(filters).subscribe({ + next: (data: any) => { + this.dataSource = new MatTableDataSource(data || []); + this.dataSource.paginator = this.paginator; + this.dataSource.sort = this.sort; + this.loading = false; + }, + error: () => { + this.dataSource = new MatTableDataSource([]); + this.dataSource.paginator = this.paginator; + this.dataSource.sort = this.sort; + this.loading = false; + } + }); + } + + applySearch(value: string) { + this.searchName = value; + this.loadLogs(); + } + + filterByResult(result: string | null) { + this.activeFilter = this.activeFilter === result ? null : result; + this.loadLogs(); + } + + clearFilters() { + this.searchName = ''; + this.activeFilter = null; + this.loadLogs(); + } +} diff --git a/src/app/reports/sanctions-logs/sanctions-logs.service.ts b/src/app/reports/sanctions-logs/sanctions-logs.service.ts new file mode 100644 index 000000000..4f16e5fdd --- /dev/null +++ b/src/app/reports/sanctions-logs/sanctions-logs.service.ts @@ -0,0 +1,19 @@ +import { Injectable, inject } from '@angular/core'; +import { HttpClient, HttpParams } from '@angular/common/http'; +import { Observable } from 'rxjs'; + +@Injectable({ providedIn: 'root' }) +export class SanctionsLogsService { + private http = inject(HttpClient); + + getLogs(filters?: { result?: string; name?: string }): Observable { + let params = new HttpParams(); + if (filters?.result) { + params = params.set('result', filters.result); + } + if (filters?.name) { + params = params.set('name', filters.name); + } + return this.http.get('/sanctions/logs', { params }); + } +} diff --git a/src/app/shared/icons.module.ts b/src/app/shared/icons.module.ts index 2b95f32e8..fe4459da5 100644 --- a/src/app/shared/icons.module.ts +++ b/src/app/shared/icons.module.ts @@ -23,6 +23,7 @@ import { faBuilding, faCalendar, faCalendarAlt, + faCaretDown, faChartBar, faCheck, faCheckCircle, @@ -142,7 +143,11 @@ import { faFingerprint, faNewspaper, faRss, - faFilter + faFilter, + faProjectDiagram, + faFolder, + faSatelliteDish, + faMobileAlt } from '@fortawesome/free-solid-svg-icons'; /** @@ -173,6 +178,7 @@ export class IconsModule { faCalendar, faCalendarAlt, faCalendarCheck, + faCaretDown, faChartBar, faCheck, faCheckCircle, @@ -291,7 +297,11 @@ export class IconsModule { faFingerprint, faNewspaper, faRss, - faFilter + faFilter, + faProjectDiagram, + faFolder, + faSatelliteDish, + faMobileAlt ); } } diff --git a/src/assets/translations/en-US.json b/src/assets/translations/en-US.json index 2188ba1a7..5837b9acb 100644 --- a/src/assets/translations/en-US.json +++ b/src/assets/translations/en-US.json @@ -503,6 +503,7 @@ "Month": "Month", "More": "More", "New Cashier": "New Cashier", + "New Current Account": "New Current Account", "New Fixed Deposits Account": "New Fixed Deposits Account", "New Loan Account": "New Loan Account", "New Recurring Deposit Account": "New Recurring Deposit Account", @@ -991,6 +992,7 @@ "Beneficiary": "Beneficiary", "Beneficiary Selection": "Beneficiary Selection", "Block Savings Account": "Block Savings Account", + "Block Current Account": "Block Current Account", "Bulk Import": "Bulk Import", "Bulk Loan Reassignment": "Bulk Loan Reassignment", "Business Rule Parameters": "Business Rule Parameters", @@ -1046,6 +1048,10 @@ "Create recurring deposit product": "Create recurring deposit product", "Create Savings Product": "Create Savings Product", "Create share product": "Create share product", + "Current Account": "Current Account", + "Current Account Details": "Current Account Details", + "Current Account Passbook": "Current Account Passbook", + "Current Products": "Current Products", "Currency": "Currency", "Currency Configuration": "Currency Configuration", "CKYC Verification": "CKYC Verification", @@ -1062,6 +1068,7 @@ "Delinquency Ranges": "Delinquency Ranges", "Deposit Frequency": "Deposit Frequency", "Deposit Money To Saving Account": "Deposit Money To Saving Account", + "Deposit Money To Current Account": "Deposit Money To Current Account", "Deposit Period": "Deposit Period", "Details": "Details", "Disable": "Disable", @@ -1341,6 +1348,7 @@ "Welcome to Mifos": "Welcome to Nexora®", "WriteOff reasons to Expense accounts": "WriteOff reasons to Expense accounts", "Withdraw Money From Saving Account": "Withdraw Money From Saving Account", + "Withdraw Money From Current Account": "Withdraw Money From Current Account", "Why Open Source": "Why Open Source", "Withhold Tax": "Withhold Tax", "Working Days": "Working Days", @@ -1693,6 +1701,7 @@ "Credit Tags": "Credit Tags", "Cron Expression": "Cron Expression", "Ctrl": "Ctrl", + "Current Account Product": "Current Account Product", "Currency": "Currency", "Currency Code": "Currency Code", "Currency Configuration": "Currency Configuration", @@ -2681,6 +2690,7 @@ "To Loan Officer": "To Loan Officer", "To Office": "To Office", "To Savings Officer": "To Savings Officer", + "To Current Account Officer": "To Current Account Officer", "To loan officer": "To loan officer", "Current Price": "Current Price", "Topup closure amount": "Topup closure amount", @@ -3080,6 +3090,7 @@ "Are you sure you want to activate": "Are you sure you want to activate ", "holiday": "holiday", "this Savings Account": " this Savings Account", + "this Current Account": " this Current Account", "Are you sure you want to approve checker": "Are you sure you want to approve checker", "Are you sure you want to reject checker": "Are you sure you want to reject checker", "Are you sure you want to delete checker": "Are you sure you want to delete checker", @@ -3255,6 +3266,7 @@ "Create SMS Campaign": "Create SMS Campaign", "Create Savings Product": "Create Savings Product", "Create Savings Account": "Create Savings Account", + "Create Current Product": "Create Current Product", "Create Self Service User": "Create Self Service User", "Create Share Product": "Create Share Product", "Create Shares Account": "Create Shares Account", @@ -4018,6 +4030,7 @@ "Analytics": "Analytics Dashboard (Apache Superset)", "RBI Circulars": "RBI Circulars & Notifications", "CKYC": "Central KYC Management", + "Current Account": "Current Account", "Print Loan Application": "Print Loan Application", "Adjust Charge": "Adjust Charge", "After the minimum deposit term has passed": "After the minimum deposit term has passed, additional deposit durations may be specified.", diff --git a/src/environments/.env.ts b/src/environments/.env.ts index 98253421b..8eaa2f0aa 100644 --- a/src/environments/.env.ts +++ b/src/environments/.env.ts @@ -1,17 +1,9 @@ -/** - * 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/. - */ - // IMPORTANT: THIS FILE IS AUTO GENERATED! DO NOT MANUALLY EDIT OR CHECKIN! /* tslint:disable */ export default { 'mifos_x': { - 'version': '260310', - 'hash': 'g07f701f65' + 'version': '260317', + 'hash': 'g73aa69ee0' }, 'allow_switching_backend_instance': true }; diff --git a/src/environments/environment.prod.ts b/src/environments/environment.prod.ts index c31ced528..b1ad9662c 100644 --- a/src/environments/environment.prod.ts +++ b/src/environments/environment.prod.ts @@ -122,6 +122,10 @@ export const environment = { productionModeEnableRBAC: loadedEnv['productionModeEnableRBAC'] === 'true' || loadedEnv['productionModeEnableRBAC'] === true || false, + /** OpenProject Integration (via Fineract backend) */ + openProjectEnabled: + loadedEnv['openProjectEnabled'] === 'true' || loadedEnv['openProjectEnabled'] === true || false, + /** Apache Superset Analytics Integration */ supersetEnabled: loadedEnv['supersetEnabled'] === 'true' || loadedEnv['supersetEnabled'] === true || false, supersetUrl: loadedEnv['supersetUrl'] || '', diff --git a/src/environments/environment.ts b/src/environments/environment.ts index dd4413a7f..ba0a339ee 100644 --- a/src/environments/environment.ts +++ b/src/environments/environment.ts @@ -126,6 +126,9 @@ export const environment = { productionModeEnableRBAC: loadedEnv.productionModeEnableRBAC === 'true' || loadedEnv.productionModeEnableRBAC === true || false, + /** OpenProject Integration (via Fineract backend) */ + openProjectEnabled: loadedEnv.openProjectEnabled === 'true' || loadedEnv.openProjectEnabled === true || false, + /** Apache Superset Analytics Integration */ supersetEnabled: loadedEnv.supersetEnabled === 'true' || loadedEnv.supersetEnabled === true || true, supersetUrl: loadedEnv.supersetUrl || 'https://dashboard.solwio.in',