WEB-506 feat: add Remittance Module with 7-step payout wizard (#3200)
WEB-506 fix: address CodeRabbit review (security, validation) WEB-506 fix: make API gateway header name configurable (MIFOS_REMITTANCE_API_HEADER) WEB-506 fix: remove duplicate translation keys (Personal Information, Transaction Details)
This commit is contained in:
@@ -387,6 +387,29 @@ For more detailed configuration options, refer to the `env.sample` file in the r
|
||||
| MIFOS_INTERBANK_TRANSFERS_API_VERSION | The Interbank server api version | /v1.0 |
|
||||
| MIFOS_INTERBANK_TRANSFERS_ENABLED | If the Interbank feature is enabled | true |
|
||||
|
||||
#### Remittance Module Settings
|
||||
|
||||
These variables configure the Remittance Module, which provides a 7-step wizard for processing remittance payouts (search, validate recipient, assign payout, confirm payment, and generate receipt).
|
||||
|
||||
| Variable | Description | Default Value |
|
||||
| ----------------------------- | -------------------------------------- | ---------------------------- |
|
||||
| MIFOS_REMITTANCE_API_URL | The Remittance Microservice server URL | https://apis.mifos.community |
|
||||
| MIFOS_REMITTANCE_API_PROVIDER | The Remittance server endpoint | /1.0/remittance |
|
||||
| MIFOS_REMITTANCE_API_VERSION | The Remittance server API version | /v1 |
|
||||
| MIFOS_REMITTANCE_ENABLED | If the Remittance feature is enabled | false |
|
||||
| MIFOS_REMITTANCE_API_HEADER | API gateway auth header name | X-Gravitee-Api-Key |
|
||||
| MIFOS_REMITTANCE_API_KEY | API key for the Remittance gateway | |
|
||||
|
||||
When `MIFOS_REMITTANCE_ENABLED` is set to `true`, a "Remittances" menu item appears in the sidenav and the `/remittances` route becomes accessible. The module supports the following workflow:
|
||||
|
||||
1. **Search Remittance** — Look up a transaction by vendor and external reference ID
|
||||
2. **Remittance Details** — Review transaction details and status
|
||||
3. **Search Beneficiary** — Validate the recipient's identity documents
|
||||
4. **Beneficiary Details** — Review validated recipient info and assign to a Mifos client
|
||||
5. **Transactional Profile** — Register the transactional profile and assign payout
|
||||
6. **Confirm Payment** — Final confirmation before payout
|
||||
7. **Payment Receipt** — View and print the payment receipt
|
||||
|
||||
### Client Data Masking Example
|
||||
|
||||
When `MIFOS_COMPLIANCE_HIDE_CLIENT_DATA=false` (default):
|
||||
|
||||
@@ -1,6 +1,9 @@
|
||||
# Rate limiting zone for external National ID API (2 requests/second per IP)
|
||||
limit_req_zone $binary_remote_addr zone=external_nationalid:10m rate=2r/s;
|
||||
|
||||
# Rate limiting zone for Remittance API (2 requests/second per IP)
|
||||
limit_req_zone $binary_remote_addr zone=remittance_api:10m rate=2r/s;
|
||||
|
||||
server {
|
||||
listen 80;
|
||||
server_name _;
|
||||
@@ -46,6 +49,30 @@ server {
|
||||
# CORS is not needed since requests come from the same origin
|
||||
}
|
||||
|
||||
# Remittance API proxy
|
||||
location /remittance-api {
|
||||
resolver 127.0.0.11 valid=30s;
|
||||
|
||||
# Rate limit to prevent abuse of the upstream Remittance API
|
||||
limit_req zone=remittance_api burst=5 nodelay;
|
||||
|
||||
set $remittance_target "${MIFOS_REMITTANCE_API_URL}";
|
||||
|
||||
# Rewrite path: /remittance-api/... -> /1.0/remittance/...
|
||||
rewrite ^/remittance-api(.*)$ /1.0/remittance$1 break;
|
||||
|
||||
proxy_pass $remittance_target;
|
||||
proxy_set_header Host $proxy_host;
|
||||
proxy_set_header X-Real-IP $remote_addr;
|
||||
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
||||
proxy_set_header X-Forwarded-Proto $scheme;
|
||||
|
||||
# Inject API key server-side so it is never exposed to the browser
|
||||
proxy_set_header ${MIFOS_REMITTANCE_API_HEADER} "${MIFOS_REMITTANCE_API_KEY}";
|
||||
|
||||
proxy_pass_request_headers on;
|
||||
}
|
||||
|
||||
# Angular app - serve index.html for all routes
|
||||
location / {
|
||||
try_files $uri $uri/ /index.html;
|
||||
|
||||
+37
-5
@@ -51,12 +51,12 @@ const proxyConfig = [
|
||||
changeOrigin: true,
|
||||
secure: true,
|
||||
logLevel: 'debug',
|
||||
headers: {
|
||||
...(process.env.EXTERNAL_NATIONAL_ID_SYSTEM_API_KEY
|
||||
? { 'X-Gravitee-Api-Key': process.env.EXTERNAL_NATIONAL_ID_SYSTEM_API_KEY }
|
||||
: {})
|
||||
},
|
||||
onProxyReq: function (proxyReq, req, res) {
|
||||
// Inject API key server-side (same as nginx proxy_set_header in production)
|
||||
const apiKey = process.env.EXTERNAL_NATIONAL_ID_SYSTEM_API_KEY || '';
|
||||
if (apiKey) {
|
||||
proxyReq.setHeader('X-Gravitee-Api-Key', apiKey);
|
||||
}
|
||||
const rewrittenPath = (req.url || '').replace(/^\/external-nationalid/, '/1.0/nationalid');
|
||||
console.log('[Proxy] Proxying:', req.method, req.url, '->', this.target + rewrittenPath);
|
||||
},
|
||||
@@ -75,6 +75,38 @@ const proxyConfig = [
|
||||
res.end('Proxy error: ' + (err && err.message ? err.message : 'Unknown error'));
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
context: ['/remittance-api'],
|
||||
target: 'https://apis.mifos.community',
|
||||
pathRewrite: { '^/remittance-api': '/1.0/remittance' },
|
||||
changeOrigin: true,
|
||||
secure: true,
|
||||
logLevel: 'debug',
|
||||
headers: {
|
||||
...(process.env.MIFOS_REMITTANCE_API_KEY
|
||||
? { [process.env.MIFOS_REMITTANCE_API_HEADER || 'X-Gravitee-Api-Key']: process.env.MIFOS_REMITTANCE_API_KEY }
|
||||
: {})
|
||||
},
|
||||
onProxyReq: function (proxyReq, req, res) {
|
||||
const rewrittenPath = (req.url || '').replace(/^\/remittance-api/, '/1.0/remittance');
|
||||
console.log('[Proxy] Proxying:', req.method, req.url, '->', this.target + rewrittenPath);
|
||||
},
|
||||
onError: function (err, req, res) {
|
||||
console.error(
|
||||
'[Proxy] Error while proxying request:',
|
||||
req && req.method,
|
||||
req && req.url,
|
||||
'->',
|
||||
this.target,
|
||||
'-',
|
||||
err && err.message
|
||||
);
|
||||
if (res && !res.headersSent) {
|
||||
res.writeHead(502, { 'Content-Type': 'text/plain' });
|
||||
res.end('Proxy error: ' + (err && err.message ? err.message : 'Unknown error'));
|
||||
}
|
||||
}
|
||||
}
|
||||
// For local development use `proxy.localhost.conf js` .
|
||||
];
|
||||
|
||||
@@ -72,5 +72,38 @@ module.exports = [
|
||||
res.end('Proxy error: ' + (err && err.message ? err.message : 'Unknown error'));
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
context: ['/remittance-api'],
|
||||
target: 'http://54.225.231.146:8080',
|
||||
pathRewrite: { '^/remittance-api': '/1.0/remittance' },
|
||||
changeOrigin: true,
|
||||
secure: false,
|
||||
logLevel: 'debug',
|
||||
onProxyReq: function (proxyReq, req, res) {
|
||||
// Inject API key server-side (same as nginx proxy_set_header in production)
|
||||
const apiKey = process.env.MIFOS_REMITTANCE_API_KEY || '';
|
||||
const apiHeader = process.env.MIFOS_REMITTANCE_API_HEADER || 'X-Gravitee-Api-Key';
|
||||
if (apiKey) {
|
||||
proxyReq.setHeader(apiHeader, apiKey);
|
||||
}
|
||||
const rewrittenPath = (req.url || '').replace(/^\/remittance-api/, '/1.0/remittance');
|
||||
console.log('[Proxy] Proxying:', req.method, req.url, '->', this.target + rewrittenPath);
|
||||
},
|
||||
onError: function (err, req, res) {
|
||||
console.error(
|
||||
'[Proxy] Error while proxying request:',
|
||||
req && req.method,
|
||||
req && req.url,
|
||||
'->',
|
||||
this.target,
|
||||
'-',
|
||||
err && err.message
|
||||
);
|
||||
if (res && !res.headersSent) {
|
||||
res.writeHead(502, { 'Content-Type': 'text/plain' });
|
||||
res.end('Proxy error: ' + (err && err.message ? err.message : 'Unknown error'));
|
||||
}
|
||||
}
|
||||
}
|
||||
];
|
||||
|
||||
@@ -48,6 +48,7 @@ import { CollectionsModule } from './collections/collections.module';
|
||||
import { ProfileModule } from './profile/profile.module';
|
||||
import { TasksModule } from './tasks/tasks.module';
|
||||
import { ConfigurationWizardModule } from './configuration-wizard/configuration-wizard.module';
|
||||
import { RemittancesModule } from './remittances/remittances.module';
|
||||
import { PortalModule } from '@angular/cdk/portal';
|
||||
|
||||
/** Main Routing Module */
|
||||
@@ -127,6 +128,7 @@ export function HttpLoaderFactory(http: HttpClient) {
|
||||
CollectionsModule,
|
||||
TasksModule,
|
||||
ConfigurationWizardModule,
|
||||
RemittancesModule,
|
||||
AppRoutingModule,
|
||||
NotFoundComponent,
|
||||
CallbackComponent,
|
||||
|
||||
@@ -41,6 +41,10 @@ export class AuthenticationInterceptor implements HttpInterceptor {
|
||||
* Intercepts a Http request and sets the request headers.
|
||||
*/
|
||||
intercept(request: HttpRequest<any>, next: HttpHandler): Observable<HttpEvent<any>> {
|
||||
// Skip Fineract auth headers for external API calls (e.g. remittance, national ID)
|
||||
if (request.url.startsWith('http://') || request.url.startsWith('https://')) {
|
||||
return next.handle(request);
|
||||
}
|
||||
if (this.settingsService.tenantIdentifier) {
|
||||
httpOptions.headers['Fineract-Platform-TenantId'] = this.settingsService.tenantIdentifier;
|
||||
}
|
||||
|
||||
@@ -132,6 +132,19 @@
|
||||
</mat-icon>
|
||||
<a matLine>{{ 'labels.menus.Individual Collection Sheet' | translate }}</a>
|
||||
</mat-list-item>
|
||||
<mat-list-item
|
||||
*ngIf="mifosRemittanceEnabled"
|
||||
[routerLink]="['/remittances/process']"
|
||||
[matTooltipPosition]="tooltipPosition"
|
||||
matTooltip="{{ 'tooltips.Process Remittance' | translate }}"
|
||||
routerLinkActive="active-menu"
|
||||
[routerLinkActiveOptions]="{ exact: false }"
|
||||
>
|
||||
<mat-icon matListIcon>
|
||||
<fa-icon icon="money-bill-wave" size="sm"></fa-icon>
|
||||
</mat-icon>
|
||||
<a matLine>{{ 'labels.menus.Remittances' | translate }}</a>
|
||||
</mat-list-item>
|
||||
<mat-list-item
|
||||
[routerLink]="['/notifications']"
|
||||
[matTooltipPosition]="tooltipPosition"
|
||||
|
||||
@@ -32,6 +32,7 @@ import { MatNavList, MatListItem } from '@angular/material/list';
|
||||
import { MatIcon } from '@angular/material/icon';
|
||||
import { MatLine } from '@angular/material/grid-list';
|
||||
import { STANDALONE_SHARED_IMPORTS } from 'app/standalone-shared.module';
|
||||
import { remittanceConfig } from '../../../remittances/remittance.config';
|
||||
|
||||
import { catchError, finalize, of, take } from 'rxjs';
|
||||
|
||||
@@ -77,6 +78,8 @@ export class SidenavComponent implements OnInit, AfterViewInit {
|
||||
mappedActivities: any[] = [];
|
||||
/** Collection of possible frequent activities */
|
||||
frequentActivities: any[] = frequentActivities;
|
||||
/** Whether remittance feature is enabled */
|
||||
mifosRemittanceEnabled = remittanceConfig.isRemittanceEnabled;
|
||||
|
||||
/* Refernce of logo */
|
||||
@ViewChild('logo') logo: ElementRef<any>;
|
||||
|
||||
@@ -58,6 +58,7 @@ const activities: any[] = [
|
||||
{ activity: 'audit', path: '/products/audit-trails' },
|
||||
{ activity: 'create accounting closure', path: '' },
|
||||
{ activity: 'navigation', path: '/navigation' },
|
||||
{ activity: 'remittances', path: '/remittances/process' },
|
||||
{ activity: '', path: 'home' }
|
||||
];
|
||||
|
||||
|
||||
@@ -0,0 +1,66 @@
|
||||
/**
|
||||
* 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/.
|
||||
*/
|
||||
|
||||
/**
|
||||
* Recipient validation request body.
|
||||
* POST /v1/remittances/:tenant/:vendor/transactions/:id/recipient
|
||||
*/
|
||||
export interface ValidateRecipientRequest {
|
||||
givenName: string;
|
||||
lastName: string;
|
||||
motherMaidenName?: string;
|
||||
dateOfBirth?: string;
|
||||
phone?: string;
|
||||
address?: {
|
||||
country?: string;
|
||||
state?: string;
|
||||
city?: string;
|
||||
postalCode?: string;
|
||||
line1?: string;
|
||||
};
|
||||
primaryDocument: {
|
||||
documentType: string;
|
||||
documentNumber: string;
|
||||
issuingCountry?: string;
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Validated recipient response from the API.
|
||||
* Maps to all 17 beneficiary fields required by the spec.
|
||||
*/
|
||||
export interface ValidatedRecipient {
|
||||
valid?: boolean;
|
||||
givenName?: string;
|
||||
firstName?: string;
|
||||
lastName: string;
|
||||
motherMaidenName?: string;
|
||||
dateOfBirth?: string;
|
||||
phone?: string;
|
||||
identityCard?: string;
|
||||
address?: {
|
||||
country?: string;
|
||||
state?: string;
|
||||
city?: string;
|
||||
postalCode?: string;
|
||||
line1?: string;
|
||||
line2?: string;
|
||||
};
|
||||
primaryDocument?: {
|
||||
documentType?: string;
|
||||
documentNumber?: string;
|
||||
issuingCountry?: string;
|
||||
};
|
||||
occupation?: string;
|
||||
economicActivity?: string;
|
||||
/** Additional fields from spec (may be populated via additionalInfo or extended API) */
|
||||
municipality?: string;
|
||||
locality?: string;
|
||||
newLocality?: string;
|
||||
entityMember?: string;
|
||||
}
|
||||
@@ -0,0 +1,43 @@
|
||||
/**
|
||||
* 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/.
|
||||
*/
|
||||
|
||||
export enum RemittanceStatus {
|
||||
PENDING = 'PENDING',
|
||||
READY_FOR_PAYOUT = 'READY_FOR_PAYOUT',
|
||||
PAID = 'PAID',
|
||||
REJECTED = 'REJECTED',
|
||||
CANCELED = 'CANCELED',
|
||||
EXPIRED = 'EXPIRED',
|
||||
UNKNOWN = 'UNKNOWN'
|
||||
}
|
||||
|
||||
/**
|
||||
* Determines whether the given status represents a terminal (final) state.
|
||||
*/
|
||||
export function isTerminalStatus(status: RemittanceStatus): boolean {
|
||||
return [
|
||||
RemittanceStatus.PAID,
|
||||
RemittanceStatus.REJECTED,
|
||||
RemittanceStatus.CANCELED,
|
||||
RemittanceStatus.EXPIRED
|
||||
].includes(status);
|
||||
}
|
||||
|
||||
/**
|
||||
* Maps a raw status string from the API to the RemittanceStatus enum.
|
||||
* Handles both CANCELED (US) and CANCELLED (UK) spellings.
|
||||
*/
|
||||
export function parseRemittanceStatus(raw: string): RemittanceStatus {
|
||||
if (!raw) return RemittanceStatus.UNKNOWN;
|
||||
let normalized = raw.toUpperCase().replace(/-/g, '_').trim();
|
||||
if (normalized === 'CANCELLED') {
|
||||
normalized = 'CANCELED';
|
||||
}
|
||||
const match = Object.values(RemittanceStatus).find((s) => s === normalized);
|
||||
return match ?? RemittanceStatus.UNKNOWN;
|
||||
}
|
||||
@@ -0,0 +1,105 @@
|
||||
/**
|
||||
* 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/.
|
||||
*/
|
||||
|
||||
/**
|
||||
* Represents a remittance transaction returned by the Find-Remittance endpoint.
|
||||
* GET /v1/remittances/:tenant/:vendor/transactions/:id
|
||||
*/
|
||||
export interface RemittanceTransaction {
|
||||
sendingPartner: string;
|
||||
externalTransactionId: string;
|
||||
pin: string;
|
||||
created: string;
|
||||
processedAt: string;
|
||||
senderName: string;
|
||||
recipientName: string;
|
||||
receivingAmount: number;
|
||||
receivingCurrency: string;
|
||||
recipientAddress: string;
|
||||
status: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* Payout assignment response (Endpoint 3).
|
||||
* POST /v1/remittances/:tenant/:vendor/transactions/:id/payout-assignment
|
||||
*/
|
||||
export interface PayoutAssignmentResponse {
|
||||
sendingPartner: string;
|
||||
remittanceKey: string;
|
||||
created: string;
|
||||
receivingAmount: number;
|
||||
receivingCurrency: string;
|
||||
trackingNumber: string;
|
||||
sender: string;
|
||||
recipient: string;
|
||||
status: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* Payout confirmation response (Endpoint 4).
|
||||
* POST /v1/remittances/:tenant/:vendor/transactions/:id/payout-confirmation/:clientId
|
||||
*/
|
||||
export interface PayoutConfirmationResponse {
|
||||
transaction: {
|
||||
receivingAmount: number;
|
||||
receivingCurrency: string;
|
||||
status: string;
|
||||
sender: PersonName;
|
||||
recipient: RecipientDetails;
|
||||
};
|
||||
}
|
||||
|
||||
export interface PersonName {
|
||||
firstName: string;
|
||||
lastName: string;
|
||||
middleName?: string;
|
||||
mothersMaidenName?: string;
|
||||
}
|
||||
|
||||
export interface RecipientAddress {
|
||||
country: string;
|
||||
state: string;
|
||||
city: string;
|
||||
postalCode: string;
|
||||
line1: string;
|
||||
line2?: string;
|
||||
}
|
||||
|
||||
export interface RecipientDocument {
|
||||
documentType: string;
|
||||
documentNumber: string;
|
||||
issuingCountry: string;
|
||||
}
|
||||
|
||||
export interface RecipientDetails extends PersonName {
|
||||
phone?: string;
|
||||
email?: string;
|
||||
nationality?: string;
|
||||
dateOfBirth?: string;
|
||||
homeAddress?: RecipientAddress;
|
||||
primaryDocument?: RecipientDocument;
|
||||
occupation?: string;
|
||||
economicActivity?: string;
|
||||
}
|
||||
|
||||
/** Vendor item from the GET /v1/remittances/:tenant/vendors endpoint */
|
||||
export interface RemittanceVendor {
|
||||
name: string;
|
||||
headerValue: string;
|
||||
}
|
||||
|
||||
/** Request body for payout assignment */
|
||||
export interface PayoutAssignmentRequest {
|
||||
agent?: string;
|
||||
additionalInfo?: Record<string, string>;
|
||||
}
|
||||
|
||||
/** Request body for payout confirmation */
|
||||
export interface PayoutConfirmationRequest {
|
||||
additionalInfo?: Record<string, string>;
|
||||
}
|
||||
@@ -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/.
|
||||
-->
|
||||
|
||||
<div class="container">
|
||||
<mat-card>
|
||||
<mat-card-content>
|
||||
<div class="header">
|
||||
<h2>{{ 'labels.heading.Process Remittance' | translate }}</h2>
|
||||
</div>
|
||||
|
||||
<mat-stepper #stepper orientation="vertical" [linear]="true">
|
||||
<!-- Step 1: Search Remittance -->
|
||||
<mat-step [editable]="!remittanceData" [completed]="!!remittanceData">
|
||||
<ng-template matStepLabel>{{ 'labels.heading.Search Remittance' | translate }}</ng-template>
|
||||
<mifosx-search-remittance-step (remittanceFound)="onRemittanceFound($event)"> </mifosx-search-remittance-step>
|
||||
</mat-step>
|
||||
|
||||
<!-- Step 2: Remittance Details -->
|
||||
<mat-step [editable]="false" [completed]="detailsReviewed">
|
||||
<ng-template matStepLabel>{{ 'labels.heading.Remittance Details' | translate }}</ng-template>
|
||||
<mifosx-remittance-details-step
|
||||
[remittanceData]="remittanceData"
|
||||
[parsedStatus]="parsedStatus"
|
||||
(detailsNext)="onDetailsNext()"
|
||||
>
|
||||
</mifosx-remittance-details-step>
|
||||
</mat-step>
|
||||
|
||||
<!-- Step 3: Search Beneficiary (only for PENDING) -->
|
||||
@if (showBeneficiarySteps) {
|
||||
<mat-step [editable]="false" [completed]="!!recipientData">
|
||||
<ng-template matStepLabel>{{ 'labels.heading.Search Beneficiary' | translate }}</ng-template>
|
||||
<mifosx-search-beneficiary-step
|
||||
[vendor]="selectedVendor"
|
||||
[transactionId]="transactionId"
|
||||
(recipientValidated)="onRecipientValidated($event)"
|
||||
>
|
||||
</mifosx-search-beneficiary-step>
|
||||
</mat-step>
|
||||
|
||||
<!-- Step 4: Beneficiary Details -->
|
||||
<mat-step [editable]="false" [completed]="beneficiaryReviewed">
|
||||
<ng-template matStepLabel>{{ 'labels.heading.Beneficiary Details' | translate }}</ng-template>
|
||||
<mifosx-beneficiary-details-step
|
||||
[recipientData]="recipientData"
|
||||
(beneficiaryNext)="onBeneficiaryDetailsNext($event)"
|
||||
>
|
||||
</mifosx-beneficiary-details-step>
|
||||
</mat-step>
|
||||
|
||||
<!-- Step 5: Transactional Profile / Assign Payout -->
|
||||
<mat-step [editable]="false" [completed]="!!payoutAssignment">
|
||||
<ng-template matStepLabel>{{ 'labels.heading.Transactional Profile' | translate }}</ng-template>
|
||||
<mifosx-transactional-profile-step
|
||||
[vendor]="selectedVendor"
|
||||
[transactionId]="transactionId"
|
||||
[remittanceData]="remittanceData"
|
||||
[recipientData]="recipientData"
|
||||
(payoutAssigned)="onPayoutAssigned($event)"
|
||||
>
|
||||
</mifosx-transactional-profile-step>
|
||||
</mat-step>
|
||||
}
|
||||
|
||||
<!-- Step 6: Confirm Payment -->
|
||||
@if (showConfirmStep) {
|
||||
<mat-step [editable]="false" [completed]="!!payoutConfirmation">
|
||||
<ng-template matStepLabel>{{ 'labels.heading.Confirm Payment' | translate }}</ng-template>
|
||||
<mifosx-confirm-payment-step
|
||||
[vendor]="selectedVendor"
|
||||
[transactionId]="transactionId"
|
||||
[remittanceData]="remittanceData"
|
||||
[recipientData]="recipientData"
|
||||
[payoutAssignment]="payoutAssignment"
|
||||
[clientId]="selectedClientId"
|
||||
(payoutConfirmed)="onPayoutConfirmed($event)"
|
||||
>
|
||||
</mifosx-confirm-payment-step>
|
||||
</mat-step>
|
||||
}
|
||||
|
||||
<!-- Step 7: Payment Receipt (always shown) -->
|
||||
<mat-step [editable]="false">
|
||||
<ng-template matStepLabel>{{ 'labels.heading.Payment Receipt' | translate }}</ng-template>
|
||||
<mifosx-payment-receipt-step
|
||||
[remittanceData]="remittanceData"
|
||||
[recipientData]="recipientData"
|
||||
[payoutConfirmation]="payoutConfirmation"
|
||||
[payoutAssignment]="payoutAssignment"
|
||||
[parsedStatus]="parsedStatus"
|
||||
(resetWorkflow)="resetWorkflow()"
|
||||
>
|
||||
</mifosx-payment-receipt-step>
|
||||
</mat-step>
|
||||
</mat-stepper>
|
||||
</mat-card-content>
|
||||
</mat-card>
|
||||
</div>
|
||||
@@ -0,0 +1,20 @@
|
||||
/**
|
||||
* 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 {
|
||||
padding: 16px;
|
||||
}
|
||||
|
||||
.header {
|
||||
margin-bottom: 16px;
|
||||
|
||||
h2 {
|
||||
margin: 0;
|
||||
font-weight: 500;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,217 @@
|
||||
/**
|
||||
* Copyright since 2025 Mifos Initiative
|
||||
*
|
||||
* This Source Code Form is subject to the terms of the Mozilla Public
|
||||
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
* file, You can obtain one at http://mozilla.org/MPL/2.0/.
|
||||
*/
|
||||
|
||||
import { Component, ViewChild, ChangeDetectorRef, inject } from '@angular/core';
|
||||
import { MatStepper, MatStep, MatStepLabel } from '@angular/material/stepper';
|
||||
import { STANDALONE_SHARED_IMPORTS } from '../../standalone-shared.module';
|
||||
|
||||
import {
|
||||
RemittanceTransaction,
|
||||
PayoutAssignmentResponse,
|
||||
PayoutConfirmationResponse,
|
||||
RemittanceVendor
|
||||
} from '../models/remittance.model';
|
||||
import { ValidatedRecipient } from '../models/beneficiary.model';
|
||||
import { RemittanceStatus, parseRemittanceStatus, isTerminalStatus } from '../models/remittance-status.enum';
|
||||
|
||||
import { SearchRemittanceStepComponent } from '../remittance-stepper/search-remittance-step/search-remittance-step.component';
|
||||
import { RemittanceDetailsStepComponent } from '../remittance-stepper/remittance-details-step/remittance-details-step.component';
|
||||
import { SearchBeneficiaryStepComponent } from '../remittance-stepper/search-beneficiary-step/search-beneficiary-step.component';
|
||||
import { BeneficiaryDetailsStepComponent } from '../remittance-stepper/beneficiary-details-step/beneficiary-details-step.component';
|
||||
import { TransactionalProfileStepComponent } from '../remittance-stepper/transactional-profile-step/transactional-profile-step.component';
|
||||
import { ConfirmPaymentStepComponent } from '../remittance-stepper/confirm-payment-step/confirm-payment-step.component';
|
||||
import { PaymentReceiptStepComponent } from '../remittance-stepper/payment-receipt-step/payment-receipt-step.component';
|
||||
|
||||
@Component({
|
||||
selector: 'mifosx-process-remittance',
|
||||
standalone: true,
|
||||
imports: [
|
||||
...STANDALONE_SHARED_IMPORTS,
|
||||
MatStepper,
|
||||
MatStep,
|
||||
MatStepLabel,
|
||||
SearchRemittanceStepComponent,
|
||||
RemittanceDetailsStepComponent,
|
||||
SearchBeneficiaryStepComponent,
|
||||
BeneficiaryDetailsStepComponent,
|
||||
TransactionalProfileStepComponent,
|
||||
ConfirmPaymentStepComponent,
|
||||
PaymentReceiptStepComponent
|
||||
],
|
||||
templateUrl: './process-remittance.component.html',
|
||||
styleUrls: ['./process-remittance.component.scss']
|
||||
})
|
||||
export class ProcessRemittanceComponent {
|
||||
@ViewChild('stepper') stepper!: MatStepper;
|
||||
|
||||
private cdr = inject(ChangeDetectorRef);
|
||||
|
||||
/** Shared state across steps */
|
||||
remittanceData: RemittanceTransaction | null = null;
|
||||
recipientData: ValidatedRecipient | null = null;
|
||||
payoutAssignment: PayoutAssignmentResponse | null = null;
|
||||
payoutConfirmation: PayoutConfirmationResponse | null = null;
|
||||
selectedVendor: RemittanceVendor | null = null;
|
||||
transactionId = '';
|
||||
parsedStatus: RemittanceStatus = RemittanceStatus.UNKNOWN;
|
||||
selectedClientId: number | null = null;
|
||||
|
||||
/** Step completion flags */
|
||||
detailsReviewed = false;
|
||||
beneficiaryReviewed = false;
|
||||
|
||||
/** Step visibility control - explicit properties for better change detection */
|
||||
showBeneficiarySteps = false;
|
||||
showConfirmStep = false;
|
||||
|
||||
private updateStepVisibility(): void {
|
||||
// Beneficiary steps (3-5) only show for PENDING.
|
||||
// Per spec: READY_FOR_PAYOUT skips directly to Confirm Payment.
|
||||
// IMPORTANT: Once shown, never hide — prevents mid-flow step destruction
|
||||
// when assignPayout changes status from PENDING → READY_FOR_PAYOUT.
|
||||
this.showBeneficiarySteps = this.showBeneficiarySteps || this.parsedStatus === RemittanceStatus.PENDING;
|
||||
|
||||
// Confirm step only shows for READY_FOR_PAYOUT (the only actionable confirm state).
|
||||
// Terminal statuses (PAID, REJECTED, etc.) skip directly to Payment Receipt.
|
||||
this.showConfirmStep = this.showConfirmStep || this.parsedStatus === RemittanceStatus.READY_FOR_PAYOUT;
|
||||
}
|
||||
|
||||
/**
|
||||
* Called when Step 1 (Search) finds a remittance.
|
||||
*/
|
||||
onRemittanceFound(event: {
|
||||
transaction: RemittanceTransaction;
|
||||
transactionId: string;
|
||||
vendor: RemittanceVendor;
|
||||
}): void {
|
||||
this.remittanceData = event.transaction;
|
||||
this.transactionId = event.transactionId;
|
||||
this.selectedVendor = event.vendor;
|
||||
this.parsedStatus = parseRemittanceStatus(event.transaction.status);
|
||||
|
||||
// Update step visibility based on new status
|
||||
this.updateStepVisibility();
|
||||
this.cdr.detectChanges();
|
||||
|
||||
setTimeout(() => {
|
||||
this.cdr.detectChanges();
|
||||
this.stepper.next();
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Called after Step 2 (Remittance Details) user clicks Next.
|
||||
* Routes based on status.
|
||||
*/
|
||||
onDetailsNext(): void {
|
||||
this.detailsReviewed = true;
|
||||
this.cdr.detectChanges();
|
||||
|
||||
if (isTerminalStatus(this.parsedStatus)) {
|
||||
this.goToLastStep();
|
||||
} else {
|
||||
this.stepper.next();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Called when Step 3 (Search Beneficiary) validates a recipient.
|
||||
*/
|
||||
onRecipientValidated(recipient: ValidatedRecipient): void {
|
||||
this.recipientData = recipient;
|
||||
this.cdr.detectChanges();
|
||||
this.stepper.next();
|
||||
}
|
||||
|
||||
/**
|
||||
* Called after Step 4 (Beneficiary Details) user selects client and clicks Next.
|
||||
*/
|
||||
onBeneficiaryDetailsNext(clientId: number): void {
|
||||
this.selectedClientId = clientId;
|
||||
this.beneficiaryReviewed = true;
|
||||
this.cdr.detectChanges();
|
||||
this.stepper.next();
|
||||
}
|
||||
|
||||
/**
|
||||
* Called when Step 5 (Transactional Profile) assigns payout.
|
||||
* Status typically changes from PENDING → READY_FOR_PAYOUT here.
|
||||
*/
|
||||
onPayoutAssigned(assignment: PayoutAssignmentResponse): void {
|
||||
this.payoutAssignment = assignment;
|
||||
this.parsedStatus = parseRemittanceStatus(assignment.status);
|
||||
|
||||
// Update step visibility — showConfirmStep will become true
|
||||
// showBeneficiarySteps stays true (never contracts once expanded)
|
||||
this.updateStepVisibility();
|
||||
|
||||
this.cdr.detectChanges();
|
||||
|
||||
// Navigate to confirm step (next visible step after transactional profile)
|
||||
if (isTerminalStatus(this.parsedStatus)) {
|
||||
this.goToLastStep();
|
||||
} else {
|
||||
this.goToConfirmStep();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Called when Step 6 (Confirm Payment) confirms payout.
|
||||
*/
|
||||
onPayoutConfirmed(confirmation: PayoutConfirmationResponse): void {
|
||||
this.payoutConfirmation = confirmation;
|
||||
this.parsedStatus = RemittanceStatus.PAID;
|
||||
this.cdr.detectChanges();
|
||||
this.stepper.next();
|
||||
}
|
||||
|
||||
/**
|
||||
* Reset the entire workflow.
|
||||
*/
|
||||
resetWorkflow(): void {
|
||||
this.remittanceData = null;
|
||||
this.recipientData = null;
|
||||
this.payoutAssignment = null;
|
||||
this.payoutConfirmation = null;
|
||||
this.selectedVendor = null;
|
||||
this.transactionId = '';
|
||||
this.parsedStatus = RemittanceStatus.UNKNOWN;
|
||||
this.selectedClientId = null;
|
||||
this.detailsReviewed = false;
|
||||
this.beneficiaryReviewed = false;
|
||||
|
||||
// Reset step visibility
|
||||
this.showBeneficiarySteps = false;
|
||||
this.showConfirmStep = false;
|
||||
|
||||
this.stepper.reset();
|
||||
}
|
||||
|
||||
private goToConfirmStep(): void {
|
||||
// Use setTimeout to ensure the DOM is updated after status change
|
||||
setTimeout(() => {
|
||||
const steps = this.stepper.steps.toArray();
|
||||
|
||||
// Confirm step is always second to last (before Receipt)
|
||||
const confirmIndex = steps.length - 2;
|
||||
|
||||
if (confirmIndex >= 0 && confirmIndex < steps.length) {
|
||||
this.stepper.selectedIndex = confirmIndex;
|
||||
} else {
|
||||
this.goToLastStep();
|
||||
}
|
||||
}, 0);
|
||||
}
|
||||
|
||||
private goToLastStep(): void {
|
||||
// Use setTimeout to ensure the DOM is updated
|
||||
setTimeout(() => {
|
||||
this.stepper.selectedIndex = this.stepper.steps.length - 1;
|
||||
}, 0);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,26 @@
|
||||
/**
|
||||
* 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 { inject } from '@angular/core';
|
||||
import { Router, CanActivateFn } from '@angular/router';
|
||||
import { remittanceConfig } from './remittance.config';
|
||||
|
||||
/**
|
||||
* Guard to prevent access to the Remittance module if it is disabled in the config.
|
||||
*/
|
||||
export const remittanceEnabledGuard: CanActivateFn = () => {
|
||||
const router = inject(Router);
|
||||
|
||||
if (remittanceConfig.isRemittanceEnabled) {
|
||||
return true;
|
||||
}
|
||||
|
||||
// If disabled, redirect to home or a 404 page
|
||||
router.navigate(['/home'], { replaceUrl: true });
|
||||
return false;
|
||||
};
|
||||
+234
@@ -0,0 +1,234 @@
|
||||
<!--
|
||||
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/.
|
||||
-->
|
||||
|
||||
@if (recipientData) {
|
||||
<div class="step-content">
|
||||
<h3>{{ 'labels.heading.Validated Recipient Information' | translate }}</h3>
|
||||
|
||||
<!-- Section: Identification -->
|
||||
<div class="section-header">
|
||||
<mat-icon>badge</mat-icon>
|
||||
<h4>{{ 'labels.heading.Identification' | translate }}</h4>
|
||||
</div>
|
||||
<div class="details-grid">
|
||||
@if (recipientData.primaryDocument?.documentType) {
|
||||
<div class="detail-item">
|
||||
<span class="label">{{ 'labels.inputs.Document Type' | translate }}</span>
|
||||
<span class="value">{{ recipientData.primaryDocument?.documentType }}</span>
|
||||
</div>
|
||||
}
|
||||
|
||||
@if (recipientData.primaryDocument?.documentNumber) {
|
||||
<div class="detail-item">
|
||||
<span class="label">{{ 'labels.inputs.Document Number' | translate }}</span>
|
||||
<span class="value">{{ recipientData.primaryDocument?.documentNumber }}</span>
|
||||
</div>
|
||||
}
|
||||
|
||||
@if (recipientData.identityCard) {
|
||||
<div class="detail-item">
|
||||
<span class="label">{{ 'labels.inputs.National Identity Card Number' | translate }}</span>
|
||||
<span class="value">{{ recipientData.identityCard }}</span>
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
|
||||
<!-- Section: Personal Information -->
|
||||
<div class="section-header">
|
||||
<mat-icon>person</mat-icon>
|
||||
<h4>{{ 'labels.heading.Personal Information' | translate }}</h4>
|
||||
</div>
|
||||
<div class="details-grid">
|
||||
<div class="detail-item">
|
||||
<span class="label">{{ 'labels.inputs.First Name' | translate }}</span>
|
||||
<span class="value">{{ recipientData.givenName }}</span>
|
||||
</div>
|
||||
|
||||
<div class="detail-item">
|
||||
<span class="label">{{ 'labels.inputs.Last Name' | translate }}</span>
|
||||
<span class="value">{{ recipientData.lastName }}</span>
|
||||
</div>
|
||||
|
||||
@if (recipientData.motherMaidenName) {
|
||||
<div class="detail-item">
|
||||
<span class="label">{{ 'labels.inputs.Mothers Maiden Name' | translate }}</span>
|
||||
<span class="value">{{ recipientData.motherMaidenName }}</span>
|
||||
</div>
|
||||
}
|
||||
|
||||
@if (recipientData.dateOfBirth) {
|
||||
<div class="detail-item">
|
||||
<span class="label">{{ 'labels.inputs.Date of Birth' | translate }}</span>
|
||||
<span class="value">{{ recipientData.dateOfBirth | date: 'mediumDate' }}</span>
|
||||
</div>
|
||||
}
|
||||
|
||||
@if (recipientData.phone) {
|
||||
<div class="detail-item">
|
||||
<span class="label">{{ 'labels.inputs.Phone Number' | translate }}</span>
|
||||
<span class="value">{{ recipientData.phone }}</span>
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
|
||||
<!-- Section: Address -->
|
||||
<div class="section-header">
|
||||
<mat-icon>location_on</mat-icon>
|
||||
<h4>{{ 'labels.inputs.Address' | translate }}</h4>
|
||||
</div>
|
||||
<div class="details-grid">
|
||||
@if (recipientData.address?.line1) {
|
||||
<div class="detail-item">
|
||||
<span class="label">{{ 'labels.inputs.Address Line' | translate }}</span>
|
||||
<span class="value">{{ recipientData.address?.line1 }}</span>
|
||||
</div>
|
||||
}
|
||||
|
||||
@if (recipientData.address?.postalCode) {
|
||||
<div class="detail-item">
|
||||
<span class="label">{{ 'labels.inputs.Postal Code' | translate }}</span>
|
||||
<span class="value">{{ recipientData.address?.postalCode }}</span>
|
||||
</div>
|
||||
}
|
||||
|
||||
@if (recipientData.address?.state) {
|
||||
<div class="detail-item">
|
||||
<span class="label">{{ 'labels.inputs.State' | translate }}</span>
|
||||
<span class="value">{{ recipientData.address?.state }}</span>
|
||||
</div>
|
||||
}
|
||||
|
||||
@if (recipientData.municipality || recipientData.address?.city) {
|
||||
<div class="detail-item">
|
||||
<span class="label">{{ 'labels.inputs.Municipality' | translate }}</span>
|
||||
<span class="value">{{ recipientData.municipality || recipientData.address?.city }}</span>
|
||||
</div>
|
||||
}
|
||||
|
||||
@if (recipientData.locality) {
|
||||
<div class="detail-item">
|
||||
<span class="label">{{ 'labels.inputs.Locality' | translate }}</span>
|
||||
<span class="value">{{ recipientData.locality }}</span>
|
||||
</div>
|
||||
}
|
||||
|
||||
@if (recipientData.newLocality) {
|
||||
<div class="detail-item">
|
||||
<span class="label">{{ 'labels.inputs.New Locality' | translate }}</span>
|
||||
<span class="value">{{ recipientData.newLocality }}</span>
|
||||
</div>
|
||||
}
|
||||
|
||||
@if (recipientData.address?.country) {
|
||||
<div class="detail-item">
|
||||
<span class="label">{{ 'labels.inputs.Country' | translate }}</span>
|
||||
<span class="value">{{ recipientData.address?.country }}</span>
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
|
||||
<!-- Section: Professional Information -->
|
||||
<div class="section-header">
|
||||
<mat-icon>work</mat-icon>
|
||||
<h4>{{ 'labels.heading.Professional Information' | translate }}</h4>
|
||||
</div>
|
||||
<div class="details-grid">
|
||||
@if (recipientData.occupation) {
|
||||
<div class="detail-item">
|
||||
<span class="label">{{ 'labels.inputs.Occupation' | translate }}</span>
|
||||
<span class="value">{{ recipientData.occupation }}</span>
|
||||
</div>
|
||||
}
|
||||
|
||||
@if (recipientData.economicActivity) {
|
||||
<div class="detail-item">
|
||||
<span class="label">{{ 'labels.inputs.Economic Activity' | translate }}</span>
|
||||
<span class="value">{{ recipientData.economicActivity }}</span>
|
||||
</div>
|
||||
}
|
||||
|
||||
@if (recipientData.entityMember) {
|
||||
<div class="detail-item">
|
||||
<span class="label">{{ 'labels.inputs.Entity Member' | translate }}</span>
|
||||
<span class="value">{{ recipientData.entityMember }}</span>
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
|
||||
<!-- Section: Assign to Mifos Client -->
|
||||
<div class="section-header">
|
||||
<mat-icon>assignment_ind</mat-icon>
|
||||
<h4>{{ 'labels.heading.Assign to Mifos Client' | translate }}</h4>
|
||||
</div>
|
||||
<div class="client-search-section">
|
||||
<p class="search-instruction">
|
||||
{{ 'labels.text.Search and assign this beneficiary to an existing Mifos client account' | translate }}
|
||||
</p>
|
||||
|
||||
<mat-form-field appearance="outline" class="client-search-field">
|
||||
<mat-label>{{ 'labels.inputs.Search Client' | translate }}</mat-label>
|
||||
<input
|
||||
matInput
|
||||
[formControl]="clientSearchControl"
|
||||
[matAutocomplete]="clientAuto"
|
||||
placeholder="{{ 'labels.inputs.Type client name or account number' | translate }}"
|
||||
/>
|
||||
<mat-icon matIconPrefix>search</mat-icon>
|
||||
<mat-autocomplete
|
||||
#clientAuto="matAutocomplete"
|
||||
[displayWith]="displayFn"
|
||||
(optionSelected)="onClientSelected($event.option.value)"
|
||||
>
|
||||
@for (client of filteredClients$ | async; track client.id) {
|
||||
<mat-option [value]="client">
|
||||
<div class="client-option">
|
||||
<span class="client-name">{{ client.displayName }}</span>
|
||||
<span class="client-details">
|
||||
@if (client.accountNo) {
|
||||
<span class="account-no">{{ 'labels.inputs.Account' | translate }}: {{ client.accountNo }}</span>
|
||||
}
|
||||
@if (client.externalId) {
|
||||
<span class="external-id"
|
||||
>{{ 'labels.inputs.External ID' | translate }}: {{ client.externalId }}</span
|
||||
>
|
||||
}
|
||||
</span>
|
||||
</div>
|
||||
</mat-option>
|
||||
}
|
||||
</mat-autocomplete>
|
||||
</mat-form-field>
|
||||
|
||||
@if (searchError) {
|
||||
<div class="error-message">
|
||||
<mat-icon>error</mat-icon>
|
||||
<span>{{ 'labels.text.Failed to search clients. Please try again.' | translate }}</span>
|
||||
</div>
|
||||
}
|
||||
|
||||
@if (selectedClient) {
|
||||
<div class="selected-client-info">
|
||||
<mat-icon color="primary">check_circle</mat-icon>
|
||||
<div class="client-info">
|
||||
<strong>{{ selectedClient.displayName }}</strong>
|
||||
@if (selectedClient.accountNo) {
|
||||
<span class="subdued">{{ 'labels.inputs.Account' | translate }}: {{ selectedClient.accountNo }}</span>
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
|
||||
<div class="step-actions">
|
||||
<button mat-raised-button color="primary" (click)="onNext()" [disabled]="!selectedClient">
|
||||
{{ 'labels.buttons.Next' | translate }}
|
||||
<mat-icon>arrow_forward</mat-icon>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
+161
@@ -0,0 +1,161 @@
|
||||
/**
|
||||
* 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/.
|
||||
*/
|
||||
|
||||
.step-content {
|
||||
padding: 16px 0;
|
||||
|
||||
h3 {
|
||||
margin: 0 0 16px;
|
||||
font-weight: 500;
|
||||
}
|
||||
}
|
||||
|
||||
.section-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
margin: 20px 0 12px;
|
||||
padding-bottom: 8px;
|
||||
border-bottom: 1px solid #e0e0e0;
|
||||
|
||||
mat-icon {
|
||||
font-size: 20px;
|
||||
width: 20px;
|
||||
height: 20px;
|
||||
}
|
||||
|
||||
h4 {
|
||||
margin: 0;
|
||||
font-weight: 500;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
&:first-of-type {
|
||||
margin-top: 0;
|
||||
}
|
||||
}
|
||||
|
||||
.details-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fill, minmax(240px, 100%));
|
||||
gap: 16px;
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
|
||||
.detail-item {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 4px;
|
||||
|
||||
.label {
|
||||
font-size: 12px;
|
||||
font-weight: 500;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.5px;
|
||||
}
|
||||
|
||||
.value {
|
||||
font-size: 14px;
|
||||
}
|
||||
}
|
||||
|
||||
.step-actions {
|
||||
margin-top: 16px;
|
||||
}
|
||||
|
||||
.client-search-section {
|
||||
margin: 16px 0;
|
||||
padding: 16px;
|
||||
border-radius: 8px;
|
||||
background-color: var(--mdc-theme-surface, rgb(0 0 0 / 4%));
|
||||
|
||||
.search-instruction {
|
||||
margin: 0 0 12px;
|
||||
font-size: 13px;
|
||||
opacity: 0.7;
|
||||
}
|
||||
|
||||
.client-search-field {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.error-message {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
margin-top: 8px;
|
||||
padding: 8px 12px;
|
||||
background-color: var(--mdc-theme-error-container, #ffebee);
|
||||
border-radius: 4px;
|
||||
color: var(--mdc-theme-on-error-container, #c62828);
|
||||
font-size: 13px;
|
||||
|
||||
mat-icon {
|
||||
font-size: 18px;
|
||||
width: 18px;
|
||||
height: 18px;
|
||||
}
|
||||
}
|
||||
|
||||
.selected-client-info {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 12px;
|
||||
margin-top: 12px;
|
||||
padding: 12px;
|
||||
background-color: var(--mdc-theme-primary-container, rgb(76 175 80 / 12%));
|
||||
border-radius: 4px;
|
||||
border: 1px solid var(--mdc-theme-primary, rgb(76 175 80 / 30%));
|
||||
|
||||
mat-icon {
|
||||
font-size: 24px;
|
||||
width: 24px;
|
||||
height: 24px;
|
||||
color: var(--mdc-theme-primary, #4caf50);
|
||||
}
|
||||
|
||||
.client-info {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 4px;
|
||||
|
||||
strong {
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.subdued {
|
||||
font-size: 12px;
|
||||
opacity: 0.7;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.client-option {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 4px;
|
||||
padding: 4px 0;
|
||||
|
||||
.client-name {
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.client-details {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 12px;
|
||||
font-size: 12px;
|
||||
color: #666;
|
||||
|
||||
.account-no,
|
||||
.external-id {
|
||||
white-space: nowrap;
|
||||
}
|
||||
}
|
||||
}
|
||||
+98
@@ -0,0 +1,98 @@
|
||||
/**
|
||||
* 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, EventEmitter, Input, Output, inject, OnInit } from '@angular/core';
|
||||
import { STANDALONE_SHARED_IMPORTS } from '../../../standalone-shared.module';
|
||||
import { MatIcon } from '@angular/material/icon';
|
||||
import { FormControl, ReactiveFormsModule } from '@angular/forms';
|
||||
import { MatAutocompleteModule } from '@angular/material/autocomplete';
|
||||
import { Observable, of } from 'rxjs';
|
||||
import { debounceTime, distinctUntilChanged, switchMap, startWith, catchError, map } from 'rxjs/operators';
|
||||
|
||||
import { ValidatedRecipient } from '../../models/beneficiary.model';
|
||||
import { ClientsService } from '../../../clients/clients.service';
|
||||
|
||||
interface ClientSearchResult {
|
||||
id: number;
|
||||
displayName: string;
|
||||
accountNo: string;
|
||||
externalId: string;
|
||||
}
|
||||
|
||||
@Component({
|
||||
selector: 'mifosx-beneficiary-details-step',
|
||||
standalone: true,
|
||||
imports: [
|
||||
...STANDALONE_SHARED_IMPORTS,
|
||||
MatIcon,
|
||||
ReactiveFormsModule,
|
||||
MatAutocompleteModule
|
||||
],
|
||||
templateUrl: './beneficiary-details-step.component.html',
|
||||
styleUrls: ['./beneficiary-details-step.component.scss']
|
||||
})
|
||||
export class BeneficiaryDetailsStepComponent implements OnInit {
|
||||
@Input() recipientData: ValidatedRecipient | null = null;
|
||||
@Output() beneficiaryNext = new EventEmitter<number>();
|
||||
|
||||
private clientsService = inject(ClientsService);
|
||||
|
||||
clientSearchControl = new FormControl<string | ClientSearchResult>('');
|
||||
filteredClients$!: Observable<ClientSearchResult[]>;
|
||||
selectedClient: ClientSearchResult | null = null;
|
||||
searchError = false;
|
||||
|
||||
ngOnInit(): void {
|
||||
this.filteredClients$ = this.clientSearchControl.valueChanges.pipe(
|
||||
startWith(''),
|
||||
debounceTime(300),
|
||||
distinctUntilChanged(),
|
||||
switchMap((searchValue) => {
|
||||
const searchText = typeof searchValue === 'string' ? searchValue : '';
|
||||
if (!searchText || searchText.trim().length < 2) {
|
||||
return of([]);
|
||||
}
|
||||
return this.searchClients(searchText.trim());
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
private searchClients(searchText: string): Observable<ClientSearchResult[]> {
|
||||
this.searchError = false;
|
||||
return this.clientsService.searchByText(searchText, 0, 20, '', '').pipe(
|
||||
map((response: any) => {
|
||||
const clients = response?.content || [];
|
||||
return clients.map((client: any) => ({
|
||||
id: client.id,
|
||||
displayName: client.displayName || '',
|
||||
accountNo: client.accountNumber || '',
|
||||
externalId: client.externalId || ''
|
||||
}));
|
||||
}),
|
||||
catchError(() => {
|
||||
this.searchError = true;
|
||||
return of([]);
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
onClientSelected(client: ClientSearchResult): void {
|
||||
this.selectedClient = client;
|
||||
this.clientSearchControl.setValue(client.displayName);
|
||||
}
|
||||
|
||||
displayFn = (value: string | ClientSearchResult): string => {
|
||||
return typeof value === 'string' ? value : value?.displayName || '';
|
||||
};
|
||||
|
||||
onNext(): void {
|
||||
if (this.selectedClient) {
|
||||
this.beneficiaryNext.emit(this.selectedClient.id);
|
||||
}
|
||||
}
|
||||
}
|
||||
+107
@@ -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/.
|
||||
-->
|
||||
|
||||
<div class="step-content">
|
||||
<h3>{{ 'labels.heading.Confirm Payout' | translate }}</h3>
|
||||
<p class="description">
|
||||
{{ 'labels.text.Please review the details carefully before confirming the payment.' | translate }}
|
||||
</p>
|
||||
|
||||
<div class="confirmation-card">
|
||||
<!-- Transaction info -->
|
||||
@if (remittanceData) {
|
||||
<div class="card-section">
|
||||
<h4>{{ 'labels.heading.Transaction' | translate }}</h4>
|
||||
@if (officeName) {
|
||||
<div class="info-row">
|
||||
<span class="label">{{ 'labels.inputs.Office' | translate }}</span>
|
||||
<span class="value">{{ officeName }}</span>
|
||||
</div>
|
||||
}
|
||||
<div class="info-row">
|
||||
<span class="label">{{ 'labels.inputs.Code' | translate }}</span>
|
||||
<span class="value">{{ remittanceData.pin }}</span>
|
||||
</div>
|
||||
<div class="info-row">
|
||||
<span class="label">{{ 'labels.inputs.Date' | translate }}</span>
|
||||
<span class="value">{{ remittanceData.created | date: 'medium' }}</span>
|
||||
</div>
|
||||
<div class="info-row">
|
||||
<span class="label">{{ 'labels.inputs.Payout Amount' | translate }}</span>
|
||||
<span class="value amount">{{
|
||||
remittanceData.receivingAmount | currency: remittanceData.receivingCurrency
|
||||
}}</span>
|
||||
</div>
|
||||
<div class="info-row">
|
||||
<span class="label">{{ 'labels.inputs.Sender' | translate }}</span>
|
||||
<span class="value">{{ remittanceData.senderName }}</span>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
|
||||
<mat-divider></mat-divider>
|
||||
|
||||
<!-- Recipient info -->
|
||||
<div class="card-section">
|
||||
<h4>{{ 'labels.heading.Recipient' | translate }}</h4>
|
||||
<div class="info-row">
|
||||
<span class="label">{{ 'labels.inputs.Name' | translate }}</span>
|
||||
@if (recipientData?.givenName) {
|
||||
<span class="value">{{ recipientData.givenName }} {{ recipientData.lastName }}</span>
|
||||
} @else {
|
||||
<span class="value">{{ remittanceData?.recipientName }}</span>
|
||||
}
|
||||
</div>
|
||||
@if (recipientData?.primaryDocument?.documentType) {
|
||||
<div class="info-row">
|
||||
<span class="label">{{ 'labels.inputs.Document' | translate }}</span>
|
||||
<span class="value"
|
||||
>{{ recipientData.primaryDocument?.documentType }} -
|
||||
{{ recipientData.primaryDocument?.documentNumber }}</span
|
||||
>
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
|
||||
@if (payoutAssignment) {
|
||||
<mat-divider></mat-divider>
|
||||
|
||||
<!-- Assignment info -->
|
||||
<div class="card-section">
|
||||
<h4>{{ 'labels.heading.Payout Assignment' | translate }}</h4>
|
||||
<div class="info-row">
|
||||
<span class="label">{{ 'labels.inputs.Status' | translate }}</span>
|
||||
<span class="value">{{ payoutAssignment.status }}</span>
|
||||
</div>
|
||||
@if (payoutAssignment?.trackingNumber) {
|
||||
<div class="info-row">
|
||||
<span class="label">{{ 'labels.inputs.Reference Number' | translate }}</span>
|
||||
<span class="value mono">{{ payoutAssignment.trackingNumber }}</span>
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
|
||||
@if (errorMessage) {
|
||||
<div class="error-banner">
|
||||
<mat-icon>error_outline</mat-icon>
|
||||
<span>{{ errorMessage }}</span>
|
||||
</div>
|
||||
}
|
||||
|
||||
<div class="step-actions">
|
||||
<button mat-raised-button color="warn" (click)="confirmPayout()" [disabled]="isLoading || !vendor">
|
||||
@if (isLoading) {
|
||||
<mat-spinner diameter="20"></mat-spinner>
|
||||
} @else {
|
||||
<span><mat-icon>check_circle</mat-icon> {{ 'labels.buttons.Confirm Payment' | translate }}</span>
|
||||
}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
+99
@@ -0,0 +1,99 @@
|
||||
/**
|
||||
* 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/.
|
||||
*/
|
||||
|
||||
.step-content {
|
||||
padding: 16px 0;
|
||||
|
||||
h3 {
|
||||
margin: 0 0 8px;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.description {
|
||||
margin: 0 0 24px;
|
||||
font-size: 14px;
|
||||
}
|
||||
}
|
||||
|
||||
.confirmation-card {
|
||||
border: 1px solid #e0e0e0;
|
||||
border-radius: 8px;
|
||||
overflow: hidden;
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
|
||||
.card-section {
|
||||
padding: 16px;
|
||||
|
||||
h4 {
|
||||
margin: 0 0 12px;
|
||||
font-weight: 500;
|
||||
font-size: 13px;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.5px;
|
||||
}
|
||||
}
|
||||
|
||||
.info-row {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
padding: 4px 0;
|
||||
|
||||
.label {
|
||||
font-size: 13px;
|
||||
}
|
||||
|
||||
.value {
|
||||
font-size: 14px;
|
||||
font-weight: 500;
|
||||
|
||||
&.amount {
|
||||
font-size: 18px;
|
||||
font-weight: 700;
|
||||
color: #2e7d32;
|
||||
}
|
||||
|
||||
&.mono {
|
||||
font-family: monospace;
|
||||
font-size: 12px;
|
||||
background: #f5f5f5;
|
||||
padding: 2px 8px;
|
||||
border-radius: 4px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.error-banner {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
padding: 12px 16px;
|
||||
margin: 8px 0;
|
||||
background-color: #fdecea;
|
||||
border-radius: 4px;
|
||||
color: #611a15;
|
||||
|
||||
mat-icon {
|
||||
color: #f44336;
|
||||
font-size: 20px;
|
||||
width: 20px;
|
||||
height: 20px;
|
||||
}
|
||||
}
|
||||
|
||||
.step-actions {
|
||||
margin-top: 16px;
|
||||
|
||||
button {
|
||||
mat-spinner {
|
||||
display: inline-block;
|
||||
margin-right: 8px;
|
||||
}
|
||||
}
|
||||
}
|
||||
+88
@@ -0,0 +1,88 @@
|
||||
/**
|
||||
* 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, EventEmitter, Input, Output, inject } from '@angular/core';
|
||||
import { STANDALONE_SHARED_IMPORTS } from '../../../standalone-shared.module';
|
||||
import { MatProgressSpinner } from '@angular/material/progress-spinner';
|
||||
import { MatIcon } from '@angular/material/icon';
|
||||
import { MatDivider } from '@angular/material/divider';
|
||||
import { TranslateService } from '@ngx-translate/core';
|
||||
|
||||
import { RemittancesService } from '../../remittances.service';
|
||||
import {
|
||||
RemittanceTransaction,
|
||||
RemittanceVendor,
|
||||
PayoutAssignmentResponse,
|
||||
PayoutConfirmationResponse
|
||||
} from '../../models/remittance.model';
|
||||
import { ValidatedRecipient } from '../../models/beneficiary.model';
|
||||
import { AuthenticationService } from '../../../core/authentication/authentication.service';
|
||||
|
||||
@Component({
|
||||
selector: 'mifosx-confirm-payment-step',
|
||||
standalone: true,
|
||||
imports: [
|
||||
...STANDALONE_SHARED_IMPORTS,
|
||||
MatProgressSpinner,
|
||||
MatIcon,
|
||||
MatDivider
|
||||
],
|
||||
templateUrl: './confirm-payment-step.component.html',
|
||||
styleUrls: ['./confirm-payment-step.component.scss']
|
||||
})
|
||||
export class ConfirmPaymentStepComponent {
|
||||
@Input() vendor: RemittanceVendor | null = null;
|
||||
@Input() transactionId = '';
|
||||
@Input() remittanceData: RemittanceTransaction | null = null;
|
||||
@Input() recipientData: ValidatedRecipient | null = null;
|
||||
@Input() payoutAssignment: PayoutAssignmentResponse | null = null;
|
||||
@Input() clientId: number | null = null;
|
||||
@Output() payoutConfirmed = new EventEmitter<PayoutConfirmationResponse>();
|
||||
|
||||
private remittancesService = inject(RemittancesService);
|
||||
private authService = inject(AuthenticationService);
|
||||
private translateService = inject(TranslateService);
|
||||
|
||||
isLoading = false;
|
||||
errorMessage = '';
|
||||
|
||||
get officeName(): string {
|
||||
return this.authService.getCredentials()?.officeName || '';
|
||||
}
|
||||
|
||||
confirmPayout(): void {
|
||||
if (!this.vendor || !this.recipientData || !this.clientId) {
|
||||
this.errorMessage = this.translateService.instant('labels.text.Missing required information. Please try again.');
|
||||
return;
|
||||
}
|
||||
|
||||
this.isLoading = true;
|
||||
this.errorMessage = '';
|
||||
|
||||
this.remittancesService
|
||||
.confirmPayout(this.vendor.headerValue, this.transactionId, this.clientId, {
|
||||
additionalInfo: {
|
||||
comment: 'Payout confirmation from web-app'
|
||||
}
|
||||
})
|
||||
.subscribe({
|
||||
next: (response) => {
|
||||
this.isLoading = false;
|
||||
this.payoutConfirmed.emit(response);
|
||||
},
|
||||
error: (err) => {
|
||||
this.isLoading = false;
|
||||
this.errorMessage =
|
||||
err?.error?.message ||
|
||||
this.translateService.instant(
|
||||
'labels.text.Failed to confirm payment. Please try again or contact support.'
|
||||
);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
+103
@@ -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/.
|
||||
-->
|
||||
|
||||
<div class="step-content">
|
||||
<div class="receipt-header" [ngClass]="{ success: isPaid, terminal: isTerminal }">
|
||||
<mat-icon class="receipt-icon">{{ receiptIcon }}</mat-icon>
|
||||
<h3>{{ receiptTitle }}</h3>
|
||||
</div>
|
||||
|
||||
@if (remittanceData) {
|
||||
<div class="receipt-body">
|
||||
<div class="receipt-section">
|
||||
<h4>{{ 'labels.heading.Transaction Details' | translate }}</h4>
|
||||
@if (officeName) {
|
||||
<div class="receipt-row">
|
||||
<span class="label">{{ 'labels.inputs.Office' | translate }}</span>
|
||||
<span class="value">{{ officeName }}</span>
|
||||
</div>
|
||||
}
|
||||
<div class="receipt-row">
|
||||
<span class="label">{{ 'labels.inputs.External ID' | translate }}</span>
|
||||
<span class="value">{{ remittanceData.pin }}</span>
|
||||
</div>
|
||||
<div class="receipt-row">
|
||||
<span class="label">{{ 'labels.inputs.Status' | translate }}</span>
|
||||
<span class="value status-text">{{ payoutConfirmation?.transaction?.status || remittanceData.status }}</span>
|
||||
</div>
|
||||
<div class="receipt-row">
|
||||
<span class="label">{{ 'labels.inputs.Payout Amount' | translate }}</span>
|
||||
<span class="value amount">{{
|
||||
remittanceData.receivingAmount | currency: remittanceData.receivingCurrency
|
||||
}}</span>
|
||||
</div>
|
||||
<div class="receipt-row">
|
||||
<span class="label">{{ 'labels.inputs.Sender' | translate }}</span>
|
||||
<span class="value">{{ remittanceData.senderName }}</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="receipt-section">
|
||||
<h4>{{ 'labels.heading.Recipient Details' | translate }}</h4>
|
||||
<div class="receipt-row">
|
||||
<span class="label">{{ 'labels.inputs.Name' | translate }}</span>
|
||||
@if (recipientData?.givenName) {
|
||||
<span class="value">{{ recipientData.givenName }} {{ recipientData.lastName }}</span>
|
||||
} @else {
|
||||
<span class="value">{{ remittanceData?.recipientName }}</span>
|
||||
}
|
||||
</div>
|
||||
@if (recipientData?.primaryDocument?.documentType) {
|
||||
<div class="receipt-row">
|
||||
<span class="label">{{ 'labels.inputs.Document' | translate }}</span>
|
||||
<span class="value"
|
||||
>{{ recipientData.primaryDocument.documentType }} -
|
||||
{{ recipientData.primaryDocument.documentNumber }}</span
|
||||
>
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
|
||||
@if (payoutConfirmation) {
|
||||
<div class="receipt-section">
|
||||
<h4>{{ 'labels.heading.Confirmation Details' | translate }}</h4>
|
||||
@if (payoutConfirmation?.transaction) {
|
||||
<div class="receipt-row">
|
||||
<span class="label">{{ 'labels.inputs.Status' | translate }}</span>
|
||||
<span class="value">{{ payoutConfirmation.transaction.status }}</span>
|
||||
</div>
|
||||
@if (payoutAssignment?.trackingNumber) {
|
||||
<div class="receipt-row">
|
||||
<span class="label">{{ 'labels.inputs.Reference Number' | translate }}</span>
|
||||
<span class="value mono">{{ payoutAssignment.trackingNumber }}</span>
|
||||
</div>
|
||||
}
|
||||
<div class="receipt-row">
|
||||
<span class="label">{{ 'labels.inputs.Payout Amount' | translate }}</span>
|
||||
<span class="value">{{
|
||||
payoutConfirmation.transaction.receivingAmount
|
||||
| currency: payoutConfirmation.transaction.receivingCurrency
|
||||
}}</span>
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
}
|
||||
|
||||
<div class="step-actions">
|
||||
<button mat-raised-button (click)="printReceipt()">
|
||||
<mat-icon>print</mat-icon>
|
||||
{{ 'labels.buttons.Print Receipt' | translate }}
|
||||
</button>
|
||||
<button mat-raised-button color="primary" (click)="onNewRemittance()">
|
||||
<mat-icon>add</mat-icon>
|
||||
{{ 'labels.buttons.New Remittance' | translate }}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
+192
@@ -0,0 +1,192 @@
|
||||
/**
|
||||
* Copyright since 2025 Mifos Initiative
|
||||
*
|
||||
* This Source Code Form is subject to the terms of the Mozilla Public
|
||||
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
* file, You can obtain one at http://mozilla.org/MPL/2.0/.
|
||||
*/
|
||||
|
||||
.step-content {
|
||||
padding: 16px 0;
|
||||
}
|
||||
|
||||
.receipt-header {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
padding: 24px;
|
||||
margin-bottom: 24px;
|
||||
border-radius: 8px;
|
||||
text-align: center;
|
||||
|
||||
&.success {
|
||||
background-color: #e8f5e9;
|
||||
|
||||
.receipt-icon {
|
||||
color: #2e7d32;
|
||||
}
|
||||
|
||||
h3 {
|
||||
color: #2e7d32;
|
||||
}
|
||||
}
|
||||
|
||||
&.terminal {
|
||||
background-color: #fff3e0;
|
||||
|
||||
.receipt-icon {
|
||||
color: #e65100;
|
||||
}
|
||||
|
||||
h3 {
|
||||
color: #e65100;
|
||||
}
|
||||
}
|
||||
|
||||
.receipt-icon {
|
||||
font-size: 48px;
|
||||
width: 48px;
|
||||
height: 48px;
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
|
||||
h3 {
|
||||
margin: 0;
|
||||
font-weight: 500;
|
||||
}
|
||||
}
|
||||
|
||||
.receipt-body {
|
||||
border: 1px solid #e0e0e0;
|
||||
border-radius: 8px;
|
||||
overflow: hidden;
|
||||
margin-bottom: 24px;
|
||||
}
|
||||
|
||||
.receipt-section {
|
||||
padding: 16px;
|
||||
|
||||
&:not(:last-child) {
|
||||
border-bottom: 1px solid #e0e0e0;
|
||||
}
|
||||
|
||||
h4 {
|
||||
margin: 0 0 12px;
|
||||
font-weight: 500;
|
||||
font-size: 13px;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.5px;
|
||||
}
|
||||
}
|
||||
|
||||
.receipt-row {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
padding: 6px 0;
|
||||
|
||||
.label {
|
||||
font-size: 13px;
|
||||
}
|
||||
|
||||
.value {
|
||||
font-size: 14px;
|
||||
font-weight: 500;
|
||||
|
||||
&.amount {
|
||||
font-size: 18px;
|
||||
font-weight: 700;
|
||||
color: #2e7d32;
|
||||
}
|
||||
|
||||
&.status-text {
|
||||
text-transform: uppercase;
|
||||
font-size: 12px;
|
||||
letter-spacing: 0.5px;
|
||||
}
|
||||
|
||||
&.mono {
|
||||
font-family: monospace;
|
||||
font-size: 12px;
|
||||
background: #f5f5f5;
|
||||
padding: 2px 8px;
|
||||
border-radius: 4px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.step-actions {
|
||||
display: flex;
|
||||
gap: 12px;
|
||||
margin-top: 16px;
|
||||
}
|
||||
|
||||
@media print {
|
||||
.step-actions {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.step-content {
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
.receipt-header {
|
||||
border: none;
|
||||
padding: 16px;
|
||||
margin-bottom: 16px;
|
||||
|
||||
&.success {
|
||||
background-color: transparent;
|
||||
border-bottom: 2px solid #000;
|
||||
|
||||
.receipt-icon {
|
||||
color: #000;
|
||||
}
|
||||
|
||||
h3 {
|
||||
color: #000;
|
||||
}
|
||||
}
|
||||
|
||||
&.terminal {
|
||||
background-color: transparent;
|
||||
border-bottom: 2px solid #000;
|
||||
|
||||
.receipt-icon {
|
||||
color: #000;
|
||||
}
|
||||
|
||||
h3 {
|
||||
color: #000;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.receipt-body {
|
||||
border: 1px solid #000;
|
||||
}
|
||||
|
||||
.receipt-section {
|
||||
border-color: #000 !important;
|
||||
|
||||
h4 {
|
||||
color: #000;
|
||||
}
|
||||
}
|
||||
|
||||
.receipt-row {
|
||||
.label,
|
||||
.value {
|
||||
color: #000;
|
||||
}
|
||||
|
||||
.value.amount {
|
||||
color: #000;
|
||||
}
|
||||
|
||||
.value.mono {
|
||||
background: transparent;
|
||||
border: 1px solid #ccc;
|
||||
}
|
||||
}
|
||||
}
|
||||
+76
@@ -0,0 +1,76 @@
|
||||
/**
|
||||
* 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, EventEmitter, Input, Output, inject } from '@angular/core';
|
||||
import { MatIcon } from '@angular/material/icon';
|
||||
import { STANDALONE_SHARED_IMPORTS } from '../../../standalone-shared.module';
|
||||
|
||||
import {
|
||||
RemittanceTransaction,
|
||||
PayoutAssignmentResponse,
|
||||
PayoutConfirmationResponse
|
||||
} from '../../models/remittance.model';
|
||||
import { ValidatedRecipient } from '../../models/beneficiary.model';
|
||||
import { RemittanceStatus, isTerminalStatus } from '../../models/remittance-status.enum';
|
||||
import { AuthenticationService } from '../../../core/authentication/authentication.service';
|
||||
import { TranslateService } from '@ngx-translate/core';
|
||||
|
||||
@Component({
|
||||
selector: 'mifosx-payment-receipt-step',
|
||||
standalone: true,
|
||||
imports: [
|
||||
...STANDALONE_SHARED_IMPORTS,
|
||||
MatIcon
|
||||
],
|
||||
templateUrl: './payment-receipt-step.component.html',
|
||||
styleUrls: ['./payment-receipt-step.component.scss']
|
||||
})
|
||||
export class PaymentReceiptStepComponent {
|
||||
private authService = inject(AuthenticationService);
|
||||
private translateService = inject(TranslateService);
|
||||
|
||||
@Input() remittanceData: RemittanceTransaction | null = null;
|
||||
@Input() recipientData: ValidatedRecipient | null = null;
|
||||
@Input() payoutConfirmation: PayoutConfirmationResponse | null = null;
|
||||
@Input() payoutAssignment: PayoutAssignmentResponse | null = null;
|
||||
@Input() parsedStatus: RemittanceStatus = RemittanceStatus.UNKNOWN;
|
||||
@Output() resetWorkflow = new EventEmitter<void>();
|
||||
|
||||
get officeName(): string {
|
||||
return this.authService.getCredentials()?.officeName || '';
|
||||
}
|
||||
|
||||
get isPaid(): boolean {
|
||||
return this.parsedStatus === RemittanceStatus.PAID;
|
||||
}
|
||||
|
||||
get isTerminal(): boolean {
|
||||
return isTerminalStatus(this.parsedStatus) && !this.isPaid;
|
||||
}
|
||||
|
||||
get receiptIcon(): string {
|
||||
if (this.isPaid) return 'check_circle';
|
||||
if (this.isTerminal) return 'cancel';
|
||||
return 'info';
|
||||
}
|
||||
|
||||
get receiptTitle(): string {
|
||||
if (this.isPaid) return this.translateService.instant('labels.heading.Payment Successful');
|
||||
if (this.isTerminal)
|
||||
return this.translateService.instant('labels.heading.Transaction') + ' ' + (this.remittanceData?.status || '');
|
||||
return this.translateService.instant('labels.heading.Transaction Status');
|
||||
}
|
||||
|
||||
onNewRemittance(): void {
|
||||
this.resetWorkflow.emit();
|
||||
}
|
||||
|
||||
printReceipt(): void {
|
||||
window.print();
|
||||
}
|
||||
}
|
||||
+92
@@ -0,0 +1,92 @@
|
||||
<!--
|
||||
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/.
|
||||
-->
|
||||
|
||||
@if (remittanceData) {
|
||||
<div class="step-content">
|
||||
<div class="status-row">
|
||||
<span class="status-badge" [ngClass]="statusBadgeClass">{{ remittanceData.status }}</span>
|
||||
</div>
|
||||
|
||||
<div class="details-grid">
|
||||
@if (remittanceData.sendingPartner) {
|
||||
<div class="detail-item">
|
||||
<span class="label">{{ 'labels.inputs.Sent By' | translate }}</span>
|
||||
<span class="value">{{ remittanceData.sendingPartner }}</span>
|
||||
</div>
|
||||
}
|
||||
|
||||
<div class="detail-item">
|
||||
<span class="label">{{ 'labels.inputs.External ID' | translate }}</span>
|
||||
<span class="value">{{ remittanceData.pin }}</span>
|
||||
</div>
|
||||
|
||||
<div class="detail-item">
|
||||
<span class="label">{{ 'labels.inputs.Created At' | translate }}</span>
|
||||
@if (isValidDate(remittanceData.created)) {
|
||||
<span class="value">{{ remittanceData.created | date: 'medium' }}</span>
|
||||
} @else {
|
||||
<span class="value">{{ remittanceData.created }}</span>
|
||||
}
|
||||
</div>
|
||||
|
||||
@if (remittanceData.processedAt && isValidDate(remittanceData.processedAt)) {
|
||||
<div class="detail-item">
|
||||
<span class="label">{{ 'labels.inputs.Deposited At' | translate }}</span>
|
||||
<span class="value">{{ remittanceData.processedAt | date: 'medium' }}</span>
|
||||
</div>
|
||||
}
|
||||
|
||||
<div class="detail-item">
|
||||
<span class="label">{{ 'labels.inputs.Sender Name' | translate }}</span>
|
||||
<span class="value">{{ remittanceData.senderName }}</span>
|
||||
</div>
|
||||
|
||||
<div class="detail-item">
|
||||
<span class="label">{{ 'labels.inputs.Payout Amount' | translate }}</span>
|
||||
<span class="value">{{ remittanceData.receivingAmount | currency: remittanceData.receivingCurrency }}</span>
|
||||
</div>
|
||||
|
||||
<div class="detail-item">
|
||||
<span class="label">{{ 'labels.inputs.Recipient Name' | translate }}</span>
|
||||
<span class="value">{{ remittanceData.recipientName }}</span>
|
||||
</div>
|
||||
|
||||
@if (remittanceData.recipientAddress) {
|
||||
<div class="detail-item">
|
||||
<span class="label">{{ 'labels.inputs.Address' | translate }}</span>
|
||||
<span class="value">{{ remittanceData.recipientAddress }}</span>
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
|
||||
@if (isPaid) {
|
||||
<div class="terminal-info paid-info">
|
||||
<mat-icon>check_circle</mat-icon>
|
||||
<span>{{ 'labels.text.This remittance has already been paid successfully.' | translate }}</span>
|
||||
</div>
|
||||
} @else if (isTerminal) {
|
||||
<div class="terminal-warning">
|
||||
<mat-icon>info</mat-icon>
|
||||
<span>{{
|
||||
'labels.text.This remittance has a terminal status and cannot be processed further.' | translate
|
||||
}}</span>
|
||||
</div>
|
||||
}
|
||||
|
||||
<div class="step-actions">
|
||||
<button mat-raised-button color="primary" (click)="onNext()">
|
||||
@if (isTerminal) {
|
||||
{{ 'labels.buttons.View Receipt' | translate }}
|
||||
} @else {
|
||||
{{ 'labels.buttons.Next' | translate }}
|
||||
}
|
||||
<mat-icon>arrow_forward</mat-icon>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
+104
@@ -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/.
|
||||
*/
|
||||
|
||||
.step-content {
|
||||
padding: 16px 0;
|
||||
}
|
||||
|
||||
.status-row {
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
|
||||
.status-badge {
|
||||
display: inline-block;
|
||||
padding: 4px 12px;
|
||||
border-radius: 16px;
|
||||
font-size: 12px;
|
||||
font-weight: 600;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.5px;
|
||||
}
|
||||
|
||||
.badge-pending {
|
||||
background-color: #fff3e0;
|
||||
color: #e65100;
|
||||
}
|
||||
|
||||
.badge-ready {
|
||||
background-color: #e3f2fd;
|
||||
color: #1565c0;
|
||||
}
|
||||
|
||||
.badge-paid {
|
||||
background-color: #e8f5e9;
|
||||
color: #2e7d32;
|
||||
}
|
||||
|
||||
.badge-terminal {
|
||||
background-color: #ffebee;
|
||||
color: #c62828;
|
||||
}
|
||||
|
||||
.badge-unknown {
|
||||
background-color: #f5f5f5;
|
||||
color: #616161;
|
||||
}
|
||||
|
||||
.details-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fill, minmax(240px, 100%));
|
||||
gap: 16px;
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
|
||||
.detail-item {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 4px;
|
||||
|
||||
.label {
|
||||
font-size: 12px;
|
||||
font-weight: 500;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.5px;
|
||||
}
|
||||
|
||||
.value {
|
||||
font-size: 14px;
|
||||
}
|
||||
}
|
||||
|
||||
.terminal-warning,
|
||||
.terminal-info {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
padding: 12px 16px;
|
||||
margin: 16px 0;
|
||||
border-radius: 4px;
|
||||
|
||||
mat-icon {
|
||||
font-size: 20px;
|
||||
width: 20px;
|
||||
height: 20px;
|
||||
}
|
||||
}
|
||||
|
||||
.terminal-warning {
|
||||
background-color: #fff3e0;
|
||||
color: #e65100;
|
||||
}
|
||||
|
||||
.paid-info {
|
||||
background-color: #e8f5e9;
|
||||
color: #2e7d32;
|
||||
}
|
||||
|
||||
.step-actions {
|
||||
margin-top: 16px;
|
||||
}
|
||||
+65
@@ -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, EventEmitter, Input, Output } from '@angular/core';
|
||||
import { STANDALONE_SHARED_IMPORTS } from '../../../standalone-shared.module';
|
||||
import { MatIcon } from '@angular/material/icon';
|
||||
|
||||
import { RemittanceTransaction } from '../../models/remittance.model';
|
||||
import { RemittanceStatus, isTerminalStatus } from '../../models/remittance-status.enum';
|
||||
|
||||
@Component({
|
||||
selector: 'mifosx-remittance-details-step',
|
||||
standalone: true,
|
||||
imports: [
|
||||
...STANDALONE_SHARED_IMPORTS,
|
||||
MatIcon
|
||||
],
|
||||
templateUrl: './remittance-details-step.component.html',
|
||||
styleUrls: ['./remittance-details-step.component.scss']
|
||||
})
|
||||
export class RemittanceDetailsStepComponent {
|
||||
@Input() remittanceData: RemittanceTransaction | null = null;
|
||||
@Input() parsedStatus: RemittanceStatus = RemittanceStatus.UNKNOWN;
|
||||
@Output() detailsNext = new EventEmitter<void>();
|
||||
|
||||
get isTerminal(): boolean {
|
||||
return isTerminalStatus(this.parsedStatus);
|
||||
}
|
||||
|
||||
get isPaid(): boolean {
|
||||
return this.parsedStatus === RemittanceStatus.PAID;
|
||||
}
|
||||
|
||||
get statusBadgeClass(): string {
|
||||
switch (this.parsedStatus) {
|
||||
case RemittanceStatus.PENDING:
|
||||
return 'badge-pending';
|
||||
case RemittanceStatus.READY_FOR_PAYOUT:
|
||||
return 'badge-ready';
|
||||
case RemittanceStatus.PAID:
|
||||
return 'badge-paid';
|
||||
case RemittanceStatus.REJECTED:
|
||||
case RemittanceStatus.CANCELED:
|
||||
case RemittanceStatus.EXPIRED:
|
||||
return 'badge-terminal';
|
||||
default:
|
||||
return 'badge-unknown';
|
||||
}
|
||||
}
|
||||
|
||||
isValidDate(value: string | undefined): boolean {
|
||||
if (!value) return false;
|
||||
const timestamp = Date.parse(value);
|
||||
return !isNaN(timestamp);
|
||||
}
|
||||
|
||||
onNext(): void {
|
||||
this.detailsNext.emit();
|
||||
}
|
||||
}
|
||||
+77
@@ -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/.
|
||||
-->
|
||||
|
||||
<div class="step-content">
|
||||
<form [formGroup]="beneficiaryForm" (ngSubmit)="validateRecipient()">
|
||||
<div class="form-fields">
|
||||
<mat-form-field appearance="outline">
|
||||
<mat-label>{{ 'labels.inputs.First Name' | translate }}</mat-label>
|
||||
<input
|
||||
matInput
|
||||
formControlName="givenName"
|
||||
placeholder="{{ 'labels.placeholders.Enter first name (include middle name)' | translate }}"
|
||||
/>
|
||||
@if (beneficiaryForm.get('givenName')?.hasError('required') && beneficiaryForm.get('givenName')?.touched) {
|
||||
<mat-error>{{ 'labels.inputs.First name is required' | translate }}</mat-error>
|
||||
}
|
||||
</mat-form-field>
|
||||
|
||||
<mat-form-field appearance="outline">
|
||||
<mat-label>{{ 'labels.inputs.Last Name' | translate }}</mat-label>
|
||||
<input
|
||||
matInput
|
||||
formControlName="lastName"
|
||||
placeholder="{{ 'labels.placeholders.Enter last name' | translate }}"
|
||||
/>
|
||||
@if (beneficiaryForm.get('lastName')?.hasError('required') && beneficiaryForm.get('lastName')?.touched) {
|
||||
<mat-error>{{ 'labels.inputs.Last name is required' | translate }}</mat-error>
|
||||
}
|
||||
</mat-form-field>
|
||||
|
||||
<mat-form-field appearance="outline">
|
||||
<mat-label>{{ 'labels.inputs.Document Type' | translate }}</mat-label>
|
||||
<mat-select formControlName="documentType">
|
||||
@for (doc of documentTypes; track doc.value) {
|
||||
<mat-option [value]="doc.value">{{ doc.label | translate }}</mat-option>
|
||||
}
|
||||
</mat-select>
|
||||
</mat-form-field>
|
||||
|
||||
<mat-form-field appearance="outline">
|
||||
<mat-label>{{ 'labels.inputs.Document Number' | translate }}</mat-label>
|
||||
<input
|
||||
matInput
|
||||
formControlName="documentNumber"
|
||||
placeholder="{{ 'labels.placeholders.Enter document number' | translate }}"
|
||||
/>
|
||||
@if (
|
||||
beneficiaryForm.get('documentNumber')?.hasError('required') && beneficiaryForm.get('documentNumber')?.touched
|
||||
) {
|
||||
<mat-error>{{ 'labels.inputs.Document number is required' | translate }}</mat-error>
|
||||
}
|
||||
</mat-form-field>
|
||||
</div>
|
||||
|
||||
@if (errorMessage) {
|
||||
<div class="error-banner">
|
||||
<mat-icon>error_outline</mat-icon>
|
||||
<span>{{ errorMessage }}</span>
|
||||
</div>
|
||||
}
|
||||
|
||||
<div class="step-actions">
|
||||
<button mat-raised-button color="primary" type="submit" [disabled]="beneficiaryForm.invalid || isLoading">
|
||||
@if (isLoading) {
|
||||
<mat-spinner diameter="20"></mat-spinner>
|
||||
} @else {
|
||||
<span><mat-icon>person_search</mat-icon> {{ 'labels.buttons.Validate Recipient' | translate }}</span>
|
||||
}
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
+47
@@ -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/.
|
||||
*/
|
||||
|
||||
.step-content {
|
||||
padding: 16px 0;
|
||||
}
|
||||
|
||||
.form-fields {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 8px;
|
||||
max-width: 480px;
|
||||
}
|
||||
|
||||
.error-banner {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
padding: 12px 16px;
|
||||
margin: 8px 0;
|
||||
background-color: #fdecea;
|
||||
border-radius: 4px;
|
||||
color: #611a15;
|
||||
|
||||
mat-icon {
|
||||
color: #f44336;
|
||||
font-size: 20px;
|
||||
width: 20px;
|
||||
height: 20px;
|
||||
}
|
||||
}
|
||||
|
||||
.step-actions {
|
||||
margin-top: 16px;
|
||||
|
||||
button {
|
||||
mat-spinner {
|
||||
display: inline-block;
|
||||
margin-right: 8px;
|
||||
}
|
||||
}
|
||||
}
|
||||
+125
@@ -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/.
|
||||
*/
|
||||
|
||||
import { Component, EventEmitter, Input, OnInit, Output, inject } from '@angular/core';
|
||||
import { FormBuilder, FormGroup, Validators } from '@angular/forms';
|
||||
import { STANDALONE_SHARED_IMPORTS } from '../../../standalone-shared.module';
|
||||
import { MatProgressSpinner } from '@angular/material/progress-spinner';
|
||||
import { MatIcon } from '@angular/material/icon';
|
||||
import { TranslateService } from '@ngx-translate/core';
|
||||
|
||||
import { RemittancesService } from '../../remittances.service';
|
||||
import { RemittanceVendor } from '../../models/remittance.model';
|
||||
import { ValidatedRecipient } from '../../models/beneficiary.model';
|
||||
|
||||
@Component({
|
||||
selector: 'mifosx-search-beneficiary-step',
|
||||
standalone: true,
|
||||
imports: [
|
||||
...STANDALONE_SHARED_IMPORTS,
|
||||
MatProgressSpinner,
|
||||
MatIcon
|
||||
],
|
||||
templateUrl: './search-beneficiary-step.component.html',
|
||||
styleUrls: ['./search-beneficiary-step.component.scss']
|
||||
})
|
||||
export class SearchBeneficiaryStepComponent implements OnInit {
|
||||
@Input() vendor: RemittanceVendor | null = null;
|
||||
@Input() transactionId = '';
|
||||
@Output() recipientValidated = new EventEmitter<ValidatedRecipient>();
|
||||
|
||||
private fb = inject(FormBuilder);
|
||||
private remittancesService = inject(RemittancesService);
|
||||
private translateService = inject(TranslateService);
|
||||
|
||||
beneficiaryForm!: FormGroup;
|
||||
isLoading = false;
|
||||
errorMessage = '';
|
||||
|
||||
documentTypes = [
|
||||
{ value: 'NATIONAL_ID', label: 'labels.inputs.National ID' },
|
||||
{ value: 'PASSPORT', label: 'labels.inputs.Passport' },
|
||||
{ value: 'DRIVERS_LICENSE', label: "labels.inputs.Driver's License" },
|
||||
{ value: 'VOTER_ID', label: 'labels.inputs.Voter ID' }
|
||||
];
|
||||
|
||||
ngOnInit(): void {
|
||||
this.beneficiaryForm = this.fb.group({
|
||||
givenName: [
|
||||
'',
|
||||
Validators.required
|
||||
],
|
||||
lastName: [
|
||||
'',
|
||||
Validators.required
|
||||
],
|
||||
documentType: [
|
||||
'NATIONAL_ID',
|
||||
Validators.required
|
||||
],
|
||||
documentNumber: [
|
||||
'',
|
||||
[
|
||||
Validators.required,
|
||||
Validators.minLength(3)
|
||||
]
|
||||
]
|
||||
});
|
||||
}
|
||||
|
||||
validateRecipient(): void {
|
||||
if (this.beneficiaryForm.invalid || !this.vendor) return;
|
||||
|
||||
this.isLoading = true;
|
||||
this.errorMessage = '';
|
||||
|
||||
const { givenName, lastName, documentType, documentNumber } = this.beneficiaryForm.value;
|
||||
|
||||
this.remittancesService
|
||||
.validateRecipient(this.vendor.headerValue, this.transactionId, {
|
||||
givenName,
|
||||
lastName,
|
||||
primaryDocument: {
|
||||
documentType,
|
||||
documentNumber
|
||||
}
|
||||
})
|
||||
.subscribe({
|
||||
next: (recipient) => {
|
||||
this.isLoading = false;
|
||||
if (recipient && recipient.valid) {
|
||||
// Merge form data into recipient so details show in Confirm step
|
||||
const enrichedRecipient: ValidatedRecipient = {
|
||||
...recipient,
|
||||
firstName: givenName,
|
||||
givenName: givenName,
|
||||
lastName: lastName,
|
||||
primaryDocument: {
|
||||
documentType,
|
||||
documentNumber,
|
||||
issuingCountry: ''
|
||||
}
|
||||
};
|
||||
this.recipientValidated.emit(enrichedRecipient);
|
||||
} else {
|
||||
this.errorMessage = this.translateService.instant(
|
||||
'labels.text.Recipient is not valid for this transaction. Please check the details.'
|
||||
);
|
||||
}
|
||||
},
|
||||
error: (err) => {
|
||||
this.isLoading = false;
|
||||
this.errorMessage =
|
||||
err?.error?.message ||
|
||||
this.translateService.instant(
|
||||
'labels.text.Recipient validation failed. Please check the document details and try again.'
|
||||
);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
+81
@@ -0,0 +1,81 @@
|
||||
<!--
|
||||
Copyright since 2025 Mifos Initiative
|
||||
|
||||
This Source Code Form is subject to the terms of the Mozilla Public
|
||||
License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
file, You can obtain one at http://mozilla.org/MPL/2.0/.
|
||||
-->
|
||||
|
||||
<div class="step-content">
|
||||
<form [formGroup]="searchForm" (ngSubmit)="searchRemittance()">
|
||||
<div class="form-fields">
|
||||
<mat-form-field appearance="outline">
|
||||
<mat-label>{{ 'labels.inputs.Vendor' | translate }}</mat-label>
|
||||
<mat-select formControlName="vendor" [disabled]="vendorLoadError">
|
||||
@for (v of vendors; track v.name) {
|
||||
<mat-option [value]="v.headerValue">{{ v.name }}</mat-option>
|
||||
}
|
||||
</mat-select>
|
||||
@if (isLoadingVendors) {
|
||||
<mat-hint>{{ 'labels.inputs.Loading vendors...' | translate }}</mat-hint>
|
||||
}
|
||||
@if (vendorLoadError) {
|
||||
<mat-error>{{ 'labels.inputs.Failed to load vendors' | translate }}</mat-error>
|
||||
}
|
||||
</mat-form-field>
|
||||
|
||||
<mat-form-field appearance="outline">
|
||||
<mat-label>{{ 'labels.inputs.External Reference ID' | translate }}</mat-label>
|
||||
<input
|
||||
matInput
|
||||
formControlName="externalId"
|
||||
placeholder="{{ 'labels.placeholders.Enter remittance reference number' | translate }}"
|
||||
/>
|
||||
@if (searchForm.get('externalId')?.hasError('required') && searchForm.get('externalId')?.touched) {
|
||||
<mat-error>{{ 'labels.inputs.Reference ID is required' | translate }}</mat-error>
|
||||
}
|
||||
@if (searchForm.get('externalId')?.hasError('minlength')) {
|
||||
<mat-error>{{ 'labels.inputs.Minimum 3 characters' | translate }}</mat-error>
|
||||
}
|
||||
</mat-form-field>
|
||||
</div>
|
||||
|
||||
@if (vendorLoadError) {
|
||||
<div class="vendor-error-banner">
|
||||
<mat-icon>warning</mat-icon>
|
||||
<span>{{ 'labels.text.Unable to load vendors. Please check your connection and try again.' | translate }}</span>
|
||||
<button mat-stroked-button color="primary" type="button" (click)="loadVendors()" [disabled]="isLoadingVendors">
|
||||
@if (isLoadingVendors) {
|
||||
<mat-spinner diameter="16"></mat-spinner>
|
||||
} @else {
|
||||
<mat-icon>refresh</mat-icon>
|
||||
<span>{{ 'labels.buttons.Retry' | translate }}</span>
|
||||
}
|
||||
</button>
|
||||
</div>
|
||||
}
|
||||
|
||||
@if (errorMessage) {
|
||||
<div class="error-banner">
|
||||
<mat-icon>error_outline</mat-icon>
|
||||
<span>{{ errorMessage }}</span>
|
||||
</div>
|
||||
}
|
||||
|
||||
<div class="step-actions">
|
||||
<button
|
||||
mat-raised-button
|
||||
color="primary"
|
||||
type="button"
|
||||
(click)="searchRemittance()"
|
||||
[disabled]="isLoading || vendorLoadError"
|
||||
>
|
||||
@if (isLoading) {
|
||||
<mat-spinner diameter="20"></mat-spinner>
|
||||
} @else {
|
||||
<span><mat-icon>search</mat-icon> {{ 'labels.buttons.Search' | translate }}</span>
|
||||
}
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
+90
@@ -0,0 +1,90 @@
|
||||
/**
|
||||
* 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/.
|
||||
*/
|
||||
|
||||
.step-content {
|
||||
padding: 16px 0;
|
||||
}
|
||||
|
||||
.form-fields {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 8px;
|
||||
max-width: 480px;
|
||||
}
|
||||
|
||||
.error-banner {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
padding: 12px 16px;
|
||||
margin: 8px 0;
|
||||
background-color: #fdecea;
|
||||
border-radius: 4px;
|
||||
color: #611a15;
|
||||
|
||||
mat-icon {
|
||||
color: #f44336;
|
||||
font-size: 20px;
|
||||
width: 20px;
|
||||
height: 20px;
|
||||
}
|
||||
}
|
||||
|
||||
.vendor-error-banner {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 12px;
|
||||
padding: 16px;
|
||||
margin: 12px 0;
|
||||
background-color: #fff3e0;
|
||||
border-radius: 4px;
|
||||
border-left: 4px solid #ff9800;
|
||||
color: #e65100;
|
||||
|
||||
mat-icon {
|
||||
color: #ff9800;
|
||||
font-size: 24px;
|
||||
width: 24px;
|
||||
height: 24px;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
span {
|
||||
flex: 1;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
button {
|
||||
flex-shrink: 0;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
|
||||
mat-spinner {
|
||||
display: inline-block;
|
||||
}
|
||||
|
||||
mat-icon {
|
||||
font-size: 18px;
|
||||
width: 18px;
|
||||
height: 18px;
|
||||
margin: 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.step-actions {
|
||||
margin-top: 16px;
|
||||
|
||||
button {
|
||||
mat-spinner {
|
||||
display: inline-block;
|
||||
margin-right: 8px;
|
||||
}
|
||||
}
|
||||
}
|
||||
+114
@@ -0,0 +1,114 @@
|
||||
/**
|
||||
* 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, EventEmitter, OnInit, Output, inject } from '@angular/core';
|
||||
import { FormBuilder, FormGroup, Validators } from '@angular/forms';
|
||||
import { STANDALONE_SHARED_IMPORTS } from '../../../standalone-shared.module';
|
||||
import { MatProgressSpinner } from '@angular/material/progress-spinner';
|
||||
import { MatIcon } from '@angular/material/icon';
|
||||
|
||||
import { TranslateService } from '@ngx-translate/core';
|
||||
import { RemittancesService } from '../../remittances.service';
|
||||
import { RemittanceTransaction, RemittanceVendor } from '../../models/remittance.model';
|
||||
|
||||
@Component({
|
||||
selector: 'mifosx-search-remittance-step',
|
||||
standalone: true,
|
||||
imports: [
|
||||
...STANDALONE_SHARED_IMPORTS,
|
||||
MatProgressSpinner,
|
||||
MatIcon
|
||||
],
|
||||
templateUrl: './search-remittance-step.component.html',
|
||||
styleUrls: ['./search-remittance-step.component.scss']
|
||||
})
|
||||
export class SearchRemittanceStepComponent implements OnInit {
|
||||
@Output() remittanceFound = new EventEmitter<{
|
||||
transaction: RemittanceTransaction;
|
||||
transactionId: string;
|
||||
vendor: RemittanceVendor;
|
||||
}>();
|
||||
|
||||
private fb = inject(FormBuilder);
|
||||
private remittancesService = inject(RemittancesService);
|
||||
private translateService = inject(TranslateService);
|
||||
|
||||
searchForm!: FormGroup;
|
||||
vendors: RemittanceVendor[] = [];
|
||||
isLoading = false;
|
||||
isLoadingVendors = false;
|
||||
vendorLoadError = false;
|
||||
errorMessage = '';
|
||||
|
||||
ngOnInit(): void {
|
||||
this.searchForm = this.fb.group({
|
||||
vendor: [
|
||||
'',
|
||||
Validators.required
|
||||
],
|
||||
externalId: [
|
||||
'',
|
||||
[
|
||||
Validators.required,
|
||||
Validators.minLength(3)
|
||||
]
|
||||
]
|
||||
});
|
||||
|
||||
this.loadVendors();
|
||||
}
|
||||
|
||||
loadVendors(): void {
|
||||
this.isLoadingVendors = true;
|
||||
this.vendorLoadError = false;
|
||||
this.remittancesService.getVendors().subscribe({
|
||||
next: (response) => {
|
||||
this.vendors = response.vendors;
|
||||
this.isLoadingVendors = false;
|
||||
this.vendorLoadError = false;
|
||||
if (this.vendors.length === 1) {
|
||||
this.searchForm.patchValue({ vendor: this.vendors[0].headerValue });
|
||||
}
|
||||
},
|
||||
error: () => {
|
||||
this.vendorLoadError = true;
|
||||
this.isLoadingVendors = false;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
searchRemittance(): void {
|
||||
this.searchForm.markAllAsTouched();
|
||||
if (this.searchForm.invalid) return;
|
||||
|
||||
this.isLoading = true;
|
||||
this.errorMessage = '';
|
||||
|
||||
const { vendor, externalId } = this.searchForm.value;
|
||||
const selectedVendor = this.vendors.find((v) => v.headerValue === vendor);
|
||||
|
||||
this.remittancesService.findRemittance(vendor, externalId).subscribe({
|
||||
next: (transaction) => {
|
||||
this.isLoading = false;
|
||||
this.remittanceFound.emit({
|
||||
transaction,
|
||||
transactionId: transaction.externalTransactionId || externalId,
|
||||
vendor: selectedVendor || { name: vendor, headerValue: vendor }
|
||||
});
|
||||
},
|
||||
error: (err) => {
|
||||
this.isLoading = false;
|
||||
this.errorMessage =
|
||||
err?.error?.message ||
|
||||
this.translateService.instant(
|
||||
'labels.text.Remittance not found. Please check the reference number and try again.'
|
||||
);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
+145
@@ -0,0 +1,145 @@
|
||||
<!--
|
||||
Copyright since 2025 Mifos Initiative
|
||||
|
||||
This Source Code Form is subject to the terms of the Mozilla Public
|
||||
License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
file, You can obtain one at http://mozilla.org/MPL/2.0/.
|
||||
-->
|
||||
|
||||
<div class="step-content">
|
||||
<h3>{{ 'labels.heading.Transactional Profile' | translate }}</h3>
|
||||
<p class="description">
|
||||
{{
|
||||
'labels.text.Register the transactional profile for the beneficiary. Add entries for individuals or legal entities.'
|
||||
| translate
|
||||
}}
|
||||
</p>
|
||||
|
||||
<!-- Entity Type Toggle -->
|
||||
<div class="entity-type-toggle">
|
||||
<mat-button-toggle-group [(ngModel)]="entityType" aria-label="Entity type">
|
||||
<mat-button-toggle value="individual">
|
||||
<mat-icon>person</mat-icon>
|
||||
{{ 'labels.inputs.Individual' | translate }}
|
||||
</mat-button-toggle>
|
||||
<mat-button-toggle value="legal">
|
||||
<mat-icon>business</mat-icon>
|
||||
{{ 'labels.inputs.Legal Entity' | translate }}
|
||||
</mat-button-toggle>
|
||||
</mat-button-toggle-group>
|
||||
</div>
|
||||
|
||||
<!-- Transactional Profile Data Table -->
|
||||
<div class="table-section">
|
||||
@if (profileData.length > 0) {
|
||||
<table mat-table [dataSource]="profileData" class="profile-table">
|
||||
<!-- # Remittance Column -->
|
||||
<ng-container matColumnDef="remittanceNumber">
|
||||
<th mat-header-cell *matHeaderCellDef>{{ 'labels.inputs.# Remittance' | translate }}</th>
|
||||
<td mat-cell *matCellDef="let row">{{ row.remittanceNumber }}</td>
|
||||
</ng-container>
|
||||
|
||||
<!-- Avg Amount/Transaction Column -->
|
||||
<ng-container matColumnDef="averageAmount">
|
||||
<th mat-header-cell *matHeaderCellDef>{{ 'labels.inputs.Avg Amount/Transaction' | translate }}</th>
|
||||
<td mat-cell *matCellDef="let row">{{ row.averageAmount | currency: row.currency }}</td>
|
||||
</ng-container>
|
||||
|
||||
<!-- # Senders Column -->
|
||||
<ng-container matColumnDef="senderCount">
|
||||
<th mat-header-cell *matHeaderCellDef>{{ 'labels.inputs.# Senders' | translate }}</th>
|
||||
<td mat-cell *matCellDef="let row">{{ row.senderCount }}</td>
|
||||
</ng-container>
|
||||
|
||||
<!-- Sender Registry Column -->
|
||||
<ng-container matColumnDef="senderRegistry">
|
||||
<th mat-header-cell *matHeaderCellDef>{{ 'labels.inputs.Sender Registry' | translate }}</th>
|
||||
<td mat-cell *matCellDef="let row">{{ row.senderRegistry }}</td>
|
||||
</ng-container>
|
||||
|
||||
<tr mat-header-row *matHeaderRowDef="displayedColumns"></tr>
|
||||
<tr
|
||||
mat-row
|
||||
*matRowDef="let row; columns: displayedColumns; let i = index"
|
||||
(click)="removeProfileEntry(i)"
|
||||
matTooltip="{{ 'labels.text.Click to remove' | translate }}"
|
||||
class="clickable-row"
|
||||
></tr>
|
||||
</table>
|
||||
}
|
||||
|
||||
@if (profileData.length === 0) {
|
||||
<div class="empty-table">
|
||||
<mat-icon>table_chart</mat-icon>
|
||||
<p>{{ 'labels.text.No transactional profile entries yet. Add one below.' | translate }}</p>
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
|
||||
<!-- Add Profile Entry Form -->
|
||||
<div class="add-entry-section">
|
||||
<h4>{{ 'labels.heading.Add Profile Entry' | translate }}</h4>
|
||||
<form [formGroup]="profileForm" class="entry-form">
|
||||
<mat-form-field appearance="outline">
|
||||
<mat-label>{{ 'labels.inputs.# Remittance' | translate }}</mat-label>
|
||||
<input
|
||||
matInput
|
||||
formControlName="remittanceNumber"
|
||||
placeholder="{{ 'labels.placeholders.# Remittance' | translate }}"
|
||||
/>
|
||||
</mat-form-field>
|
||||
|
||||
<mat-form-field appearance="outline">
|
||||
<mat-label>{{ 'labels.inputs.Avg Amount/Transaction' | translate }}</mat-label>
|
||||
<input matInput type="number" formControlName="averageAmount" placeholder="0.00" />
|
||||
</mat-form-field>
|
||||
|
||||
<mat-form-field appearance="outline">
|
||||
<mat-label>{{ 'labels.inputs.# Senders' | translate }}</mat-label>
|
||||
<input matInput type="number" formControlName="senderCount" placeholder="0" min="0" />
|
||||
</mat-form-field>
|
||||
|
||||
<mat-form-field appearance="outline">
|
||||
<mat-label>{{ 'labels.inputs.Sender Registry' | translate }}</mat-label>
|
||||
<input
|
||||
matInput
|
||||
formControlName="senderRegistry"
|
||||
placeholder="{{ 'labels.placeholders.Sender name' | translate }}"
|
||||
/>
|
||||
</mat-form-field>
|
||||
|
||||
<button
|
||||
mat-stroked-button
|
||||
color="primary"
|
||||
type="button"
|
||||
(click)="addProfileEntry()"
|
||||
[disabled]="profileForm.invalid"
|
||||
>
|
||||
<mat-icon>add</mat-icon>
|
||||
{{ 'labels.buttons.Add Entry' | translate }}
|
||||
</button>
|
||||
</form>
|
||||
</div>
|
||||
|
||||
@if (errorMessage) {
|
||||
<div class="error-banner">
|
||||
<mat-icon>error_outline</mat-icon>
|
||||
<span>{{ errorMessage }}</span>
|
||||
</div>
|
||||
}
|
||||
|
||||
<div class="step-actions">
|
||||
<button
|
||||
mat-raised-button
|
||||
color="primary"
|
||||
(click)="assignPayout()"
|
||||
[disabled]="isLoading || !vendor || !recipientData || profileData.length === 0"
|
||||
>
|
||||
@if (isLoading) {
|
||||
<mat-spinner diameter="20"></mat-spinner>
|
||||
} @else {
|
||||
<span><mat-icon>assignment_turned_in</mat-icon> {{ 'labels.buttons.Assign Payout' | translate }}</span>
|
||||
}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
+150
@@ -0,0 +1,150 @@
|
||||
/**
|
||||
* 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/.
|
||||
*/
|
||||
|
||||
.step-content {
|
||||
padding: 16px 0;
|
||||
|
||||
h3 {
|
||||
margin: 0 0 8px;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.description {
|
||||
margin: 0 0 24px;
|
||||
font-size: 14px;
|
||||
}
|
||||
}
|
||||
|
||||
.entity-type-toggle {
|
||||
margin-bottom: 20px;
|
||||
|
||||
mat-button-toggle-group {
|
||||
border-radius: 8px;
|
||||
}
|
||||
|
||||
mat-icon {
|
||||
margin-right: 4px;
|
||||
font-size: 18px;
|
||||
width: 18px;
|
||||
height: 18px;
|
||||
vertical-align: middle;
|
||||
}
|
||||
}
|
||||
|
||||
.table-section {
|
||||
margin-bottom: 24px;
|
||||
|
||||
.profile-table {
|
||||
width: 100%;
|
||||
border: 1px solid #e0e0e0;
|
||||
border-radius: 8px;
|
||||
overflow: hidden;
|
||||
|
||||
th.mat-mdc-header-cell {
|
||||
font-weight: 600;
|
||||
font-size: 12px;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.5px;
|
||||
}
|
||||
|
||||
td.mat-mdc-cell {
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.clickable-row {
|
||||
cursor: pointer;
|
||||
|
||||
&:hover {
|
||||
background-color: #fff3f3;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.empty-table {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
padding: 32px 16px;
|
||||
background-color: #fafafa;
|
||||
border: 1px dashed #e0e0e0;
|
||||
border-radius: 8px;
|
||||
|
||||
mat-icon {
|
||||
font-size: 40px;
|
||||
width: 40px;
|
||||
height: 40px;
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
|
||||
p {
|
||||
margin: 0;
|
||||
font-size: 14px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.add-entry-section {
|
||||
margin-bottom: 24px;
|
||||
padding: 16px;
|
||||
border-radius: 8px;
|
||||
border: 1px solid #e0e0e0;
|
||||
|
||||
h4 {
|
||||
margin: 0 0 12px;
|
||||
font-weight: 500;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.entry-form {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 12px;
|
||||
align-items: flex-end;
|
||||
|
||||
mat-form-field {
|
||||
flex: 1 1 180px;
|
||||
min-width: 160px;
|
||||
}
|
||||
|
||||
button {
|
||||
margin-bottom: 3.5em;
|
||||
margin-left: auto;
|
||||
white-space: nowrap;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.error-banner {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
padding: 12px 16px;
|
||||
margin: 8px 0;
|
||||
background-color: #fdecea;
|
||||
border-radius: 4px;
|
||||
color: #611a15;
|
||||
|
||||
mat-icon {
|
||||
color: #f44336;
|
||||
font-size: 20px;
|
||||
width: 20px;
|
||||
height: 20px;
|
||||
}
|
||||
}
|
||||
|
||||
.step-actions {
|
||||
margin-top: 16px;
|
||||
|
||||
button {
|
||||
mat-spinner {
|
||||
display: inline-block;
|
||||
margin-right: 8px;
|
||||
}
|
||||
}
|
||||
}
|
||||
+167
@@ -0,0 +1,167 @@
|
||||
/**
|
||||
* 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, EventEmitter, Input, Output, inject, OnInit } from '@angular/core';
|
||||
import { FormBuilder, FormGroup, Validators, FormsModule } from '@angular/forms';
|
||||
import { STANDALONE_SHARED_IMPORTS } from '../../../standalone-shared.module';
|
||||
import { MatProgressSpinner } from '@angular/material/progress-spinner';
|
||||
import { MatIcon } from '@angular/material/icon';
|
||||
import { MatTableModule } from '@angular/material/table';
|
||||
import { MatButtonToggleModule } from '@angular/material/button-toggle';
|
||||
|
||||
import { TranslateService } from '@ngx-translate/core';
|
||||
import { RemittancesService } from '../../remittances.service';
|
||||
import { RemittanceTransaction, RemittanceVendor, PayoutAssignmentResponse } from '../../models/remittance.model';
|
||||
import { ValidatedRecipient } from '../../models/beneficiary.model';
|
||||
|
||||
/**
|
||||
* Represents a single row in the Transactional Profile data table.
|
||||
* As required by the spec: # Remittance, Avg Amount/Transaction, # Senders, Sender Registry.
|
||||
*/
|
||||
export interface TransactionalProfileRow {
|
||||
remittanceNumber: string;
|
||||
averageAmount: number;
|
||||
currency: string;
|
||||
senderCount: number;
|
||||
senderRegistry: string;
|
||||
}
|
||||
|
||||
@Component({
|
||||
selector: 'mifosx-transactional-profile-step',
|
||||
standalone: true,
|
||||
imports: [
|
||||
...STANDALONE_SHARED_IMPORTS,
|
||||
MatProgressSpinner,
|
||||
MatIcon,
|
||||
MatTableModule,
|
||||
MatButtonToggleModule,
|
||||
FormsModule
|
||||
],
|
||||
templateUrl: './transactional-profile-step.component.html',
|
||||
styleUrls: ['./transactional-profile-step.component.scss']
|
||||
})
|
||||
export class TransactionalProfileStepComponent implements OnInit {
|
||||
@Input() vendor: RemittanceVendor | null = null;
|
||||
@Input() transactionId = '';
|
||||
@Input() remittanceData: RemittanceTransaction | null = null;
|
||||
@Input() recipientData: ValidatedRecipient | null = null;
|
||||
@Output() payoutAssigned = new EventEmitter<PayoutAssignmentResponse>();
|
||||
|
||||
private remittancesService = inject(RemittancesService);
|
||||
private fb = inject(FormBuilder);
|
||||
private translateService = inject(TranslateService);
|
||||
|
||||
isLoading = false;
|
||||
errorMessage = '';
|
||||
|
||||
/** Transactional profile data table */
|
||||
displayedColumns: string[] = [
|
||||
'remittanceNumber',
|
||||
'averageAmount',
|
||||
'senderCount',
|
||||
'senderRegistry'
|
||||
];
|
||||
profileData: TransactionalProfileRow[] = [];
|
||||
|
||||
/** Form for adding new profile entries */
|
||||
profileForm!: FormGroup;
|
||||
|
||||
/** Entity type toggle: 'individual' or 'legal' */
|
||||
entityType: 'individual' | 'legal' = 'individual';
|
||||
|
||||
ngOnInit(): void {
|
||||
this.profileForm = this.fb.group({
|
||||
remittanceNumber: [
|
||||
'',
|
||||
Validators.required
|
||||
],
|
||||
averageAmount: [
|
||||
'',
|
||||
[
|
||||
Validators.required,
|
||||
Validators.min(0)
|
||||
]
|
||||
],
|
||||
senderCount: [
|
||||
0,
|
||||
[
|
||||
Validators.required,
|
||||
Validators.min(0)
|
||||
]
|
||||
],
|
||||
senderRegistry: [
|
||||
'',
|
||||
Validators.required
|
||||
]
|
||||
});
|
||||
|
||||
// Pre-populate from the current transaction if available
|
||||
if (this.remittanceData) {
|
||||
this.profileData = [
|
||||
{
|
||||
remittanceNumber: this.remittanceData.pin,
|
||||
averageAmount: this.remittanceData.receivingAmount,
|
||||
currency: this.remittanceData.receivingCurrency,
|
||||
senderCount: 1,
|
||||
senderRegistry: this.remittanceData.senderName
|
||||
}
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
addProfileEntry(): void {
|
||||
if (this.profileForm.invalid) return;
|
||||
const val = this.profileForm.value;
|
||||
this.profileData = [
|
||||
...this.profileData,
|
||||
{
|
||||
remittanceNumber: val.remittanceNumber,
|
||||
averageAmount: val.averageAmount,
|
||||
currency: this.remittanceData?.receivingCurrency || 'USD',
|
||||
senderCount: Math.max(1, val.senderCount || 1),
|
||||
senderRegistry: val.senderRegistry
|
||||
}
|
||||
];
|
||||
this.profileForm.reset({
|
||||
senderCount: 1
|
||||
});
|
||||
}
|
||||
|
||||
removeProfileEntry(index: number): void {
|
||||
this.profileData = this.profileData.filter((_, i) => i !== index);
|
||||
}
|
||||
|
||||
assignPayout(): void {
|
||||
if (!this.vendor || !this.recipientData) return;
|
||||
|
||||
this.isLoading = true;
|
||||
this.errorMessage = '';
|
||||
|
||||
this.remittancesService
|
||||
.assignPayout(this.vendor.headerValue, this.transactionId, {
|
||||
additionalInfo: {
|
||||
comment: 'Payout assignment from web-app',
|
||||
transactionalProfile: JSON.stringify(this.profileData),
|
||||
entityType: this.entityType,
|
||||
profileEntryCount: this.profileData.length.toString()
|
||||
}
|
||||
})
|
||||
.subscribe({
|
||||
next: (response) => {
|
||||
this.isLoading = false;
|
||||
this.payoutAssigned.emit(response);
|
||||
},
|
||||
error: (err) => {
|
||||
this.isLoading = false;
|
||||
this.errorMessage =
|
||||
err?.error?.message ||
|
||||
this.translateService.instant('labels.text.Failed to assign payout. Please try again.');
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -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/.
|
||||
*/
|
||||
|
||||
import { environment } from '../../environments/environment';
|
||||
|
||||
/**
|
||||
* Global configuration for the Remittance module.
|
||||
*
|
||||
* Reads from environment settings so the feature toggle is centralized.
|
||||
* Set `mifosRemittanceEnabled` in the environment (or window.env) to control this.
|
||||
*/
|
||||
export const remittanceConfig = {
|
||||
isRemittanceEnabled: environment.mifosRemittanceEnabled
|
||||
};
|
||||
@@ -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/.
|
||||
*/
|
||||
|
||||
import { NgModule } from '@angular/core';
|
||||
import { Routes, RouterModule } from '@angular/router';
|
||||
|
||||
import { Route } from '../core/route/route.service';
|
||||
import { ProcessRemittanceComponent } from './process-remittance/process-remittance.component';
|
||||
import { remittanceEnabledGuard } from './remittance-enabled.guard';
|
||||
|
||||
const routes: Routes = [
|
||||
Route.withShell([
|
||||
{
|
||||
path: 'remittances',
|
||||
canActivate: [remittanceEnabledGuard],
|
||||
data: { title: 'Remittances', breadcrumb: 'Remittances' },
|
||||
children: [
|
||||
{
|
||||
path: '',
|
||||
redirectTo: 'process',
|
||||
pathMatch: 'full'
|
||||
},
|
||||
{
|
||||
path: 'process',
|
||||
component: ProcessRemittanceComponent,
|
||||
data: { title: 'Process Remittance', breadcrumb: 'Process' }
|
||||
}
|
||||
]
|
||||
}
|
||||
])
|
||||
];
|
||||
|
||||
@NgModule({
|
||||
imports: [RouterModule.forChild(routes)],
|
||||
exports: [RouterModule]
|
||||
})
|
||||
export class RemittancesRoutingModule {}
|
||||
@@ -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/.
|
||||
*/
|
||||
|
||||
import { NgModule } from '@angular/core';
|
||||
import { SharedModule } from 'app/shared/shared.module';
|
||||
import { PipesModule } from 'app/pipes/pipes.module';
|
||||
import { DirectivesModule } from 'app/directives/directives.module';
|
||||
|
||||
import { RemittancesRoutingModule } from './remittances-routing.module';
|
||||
import { ProcessRemittanceComponent } from './process-remittance/process-remittance.component';
|
||||
import { SearchRemittanceStepComponent } from './remittance-stepper/search-remittance-step/search-remittance-step.component';
|
||||
import { RemittanceDetailsStepComponent } from './remittance-stepper/remittance-details-step/remittance-details-step.component';
|
||||
import { SearchBeneficiaryStepComponent } from './remittance-stepper/search-beneficiary-step/search-beneficiary-step.component';
|
||||
import { BeneficiaryDetailsStepComponent } from './remittance-stepper/beneficiary-details-step/beneficiary-details-step.component';
|
||||
import { TransactionalProfileStepComponent } from './remittance-stepper/transactional-profile-step/transactional-profile-step.component';
|
||||
import { ConfirmPaymentStepComponent } from './remittance-stepper/confirm-payment-step/confirm-payment-step.component';
|
||||
import { PaymentReceiptStepComponent } from './remittance-stepper/payment-receipt-step/payment-receipt-step.component';
|
||||
|
||||
@NgModule({
|
||||
imports: [
|
||||
SharedModule,
|
||||
PipesModule,
|
||||
DirectivesModule,
|
||||
RemittancesRoutingModule,
|
||||
ProcessRemittanceComponent,
|
||||
SearchRemittanceStepComponent,
|
||||
RemittanceDetailsStepComponent,
|
||||
SearchBeneficiaryStepComponent,
|
||||
BeneficiaryDetailsStepComponent,
|
||||
TransactionalProfileStepComponent,
|
||||
ConfirmPaymentStepComponent,
|
||||
PaymentReceiptStepComponent
|
||||
],
|
||||
providers: []
|
||||
})
|
||||
export class RemittancesModule {}
|
||||
@@ -0,0 +1,152 @@
|
||||
/**
|
||||
* Copyright since 2025 Mifos Initiative
|
||||
*
|
||||
* This Source Code Form is subject to the terms of the Mozilla Public
|
||||
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
* file, You can obtain one at http://mozilla.org/MPL/2.0/.
|
||||
*/
|
||||
|
||||
import { Injectable, inject } from '@angular/core';
|
||||
import { HttpClient, HttpHeaders, HttpBackend } from '@angular/common/http';
|
||||
import { Observable } from 'rxjs';
|
||||
|
||||
import { environment } from '../../environments/environment';
|
||||
import {
|
||||
RemittanceTransaction,
|
||||
RemittanceVendor,
|
||||
PayoutAssignmentRequest,
|
||||
PayoutAssignmentResponse,
|
||||
PayoutConfirmationRequest,
|
||||
PayoutConfirmationResponse
|
||||
} from './models/remittance.model';
|
||||
import { ValidateRecipientRequest, ValidatedRecipient } from './models/beneficiary.model';
|
||||
|
||||
/**
|
||||
* Remittances Service
|
||||
*
|
||||
* Handles all HTTP communication with the Mifos Remittance Microservice.
|
||||
* Base URL pattern: {mifosRemittanceApiUrl}{mifosRemittanceApiProvider}{mifosRemittanceApiVersion}/remittances/:tenant/:vendor/...
|
||||
*
|
||||
* Auth: X-Gravitee-Api-Key header (injected server-side by the proxy)
|
||||
*/
|
||||
@Injectable({
|
||||
providedIn: 'root'
|
||||
})
|
||||
export class RemittancesService {
|
||||
private httpBackend = inject(HttpBackend);
|
||||
|
||||
/** Separate HttpClient that bypasses interceptors (avoids Fineract API prefix) */
|
||||
private http = new HttpClient(this.httpBackend);
|
||||
|
||||
private get baseUrl(): string {
|
||||
return `${environment.mifosRemittanceApiUrl}${environment.mifosRemittanceApiProvider}${environment.mifosRemittanceApiVersion}`;
|
||||
}
|
||||
|
||||
private get tenant(): string {
|
||||
return environment.fineractPlatformTenantId || 'default';
|
||||
}
|
||||
|
||||
private getHeaders(): HttpHeaders {
|
||||
return new HttpHeaders({ 'Content-Type': 'application/json' });
|
||||
}
|
||||
|
||||
/**
|
||||
* Endpoint 0: Get available remittance vendors for the tenant.
|
||||
* GET /v1/remittances/:tenant/vendors
|
||||
*/
|
||||
getVendors(): Observable<{ vendors: RemittanceVendor[] }> {
|
||||
return this.http.get<{ vendors: RemittanceVendor[] }>(`${this.baseUrl}/remittances/${this.tenant}/vendors`, {
|
||||
headers: this.getHeaders()
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Endpoint 1: Find remittance by external ID / PIN.
|
||||
* GET /v1/remittances/:tenant/:vendor/transactions/:id
|
||||
*/
|
||||
findRemittance(vendor: string, externalId: string): Observable<RemittanceTransaction> {
|
||||
return this.http.get<RemittanceTransaction>(
|
||||
`${this.baseUrl}/remittances/${this.tenant}/${vendor}/transactions/${externalId}`,
|
||||
{ headers: this.getHeaders() }
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Endpoint 2: Validate recipient for a transaction.
|
||||
* POST /v1/remittances/:tenant/:vendor/transactions/:id/recipient
|
||||
*/
|
||||
validateRecipient(
|
||||
vendor: string,
|
||||
transactionId: string,
|
||||
data: ValidateRecipientRequest
|
||||
): Observable<ValidatedRecipient> {
|
||||
return this.http.post<ValidatedRecipient>(
|
||||
`${this.baseUrl}/remittances/${this.tenant}/${vendor}/transactions/${transactionId}/recipient`,
|
||||
data,
|
||||
{ headers: this.getHeaders() }
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Endpoint 3: Assign transaction ready to payout (lock for payout).
|
||||
* POST /v1/remittances/:tenant/:vendor/transactions/:id/payout-assignment
|
||||
*/
|
||||
assignPayout(
|
||||
vendor: string,
|
||||
transactionId: string,
|
||||
body?: PayoutAssignmentRequest
|
||||
): Observable<PayoutAssignmentResponse> {
|
||||
return this.http.post<PayoutAssignmentResponse>(
|
||||
`${this.baseUrl}/remittances/${this.tenant}/${vendor}/transactions/${transactionId}/payout-assignment`,
|
||||
body || {},
|
||||
{ headers: this.getHeaders() }
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Endpoint 4: Confirm transaction payout.
|
||||
* POST /v1/remittances/:tenant/:vendor/transactions/:id/payout-confirmation/:clientId
|
||||
*/
|
||||
confirmPayout(
|
||||
vendor: string,
|
||||
transactionId: string,
|
||||
clientId: number,
|
||||
body?: PayoutConfirmationRequest
|
||||
): Observable<PayoutConfirmationResponse> {
|
||||
return this.http.post<PayoutConfirmationResponse>(
|
||||
`${this.baseUrl}/remittances/${this.tenant}/${vendor}/transactions/${transactionId}/payout-confirmation/${clientId}`,
|
||||
body || {},
|
||||
{ headers: this.getHeaders() }
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Unassign transaction from payout (state management).
|
||||
* DELETE /v1/remittances/:tenant/:vendor/transactions/:id/payout-assignment
|
||||
*/
|
||||
unassignPayout(vendor: string, transactionId: string, reason: string): Observable<any> {
|
||||
return this.http.request(
|
||||
'DELETE',
|
||||
`${this.baseUrl}/remittances/${this.tenant}/${vendor}/transactions/${transactionId}/payout-assignment`,
|
||||
{
|
||||
headers: this.getHeaders(),
|
||||
body: { reason }
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Reverse payout confirmation (state management).
|
||||
* DELETE /v1/remittances/:tenant/:vendor/transactions/:id/payout-confirmation
|
||||
*/
|
||||
reversePayoutConfirmation(vendor: string, transactionId: string, reason: string): Observable<any> {
|
||||
return this.http.request(
|
||||
'DELETE',
|
||||
`${this.baseUrl}/remittances/${this.tenant}/${vendor}/transactions/${transactionId}/payout-confirmation`,
|
||||
{
|
||||
headers: this.getHeaders(),
|
||||
body: { reason }
|
||||
}
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -64,6 +64,7 @@ import {
|
||||
faMinusCircle,
|
||||
faMinus,
|
||||
faMoneyBillAlt,
|
||||
faMoneyBillWave,
|
||||
faMoneyCheck,
|
||||
faPencilAlt,
|
||||
faPlay,
|
||||
@@ -216,6 +217,7 @@ export class IconsModule {
|
||||
faMinus,
|
||||
faMinusCircle,
|
||||
faMoneyBillAlt,
|
||||
faMoneyBillWave,
|
||||
faMoneyCheck,
|
||||
faPause,
|
||||
faPencilAlt,
|
||||
|
||||
@@ -90,6 +90,14 @@
|
||||
window['env']['mifosInterbankTransfersApiVersion'] = '';
|
||||
window['env']['mifosInterbankTransfersEnabled'] = 'true';
|
||||
|
||||
// Remittance Module Environment variables
|
||||
window['env']['mifosRemittanceApiUrl'] = '';
|
||||
window['env']['mifosRemittanceApiProvider'] = '/remittance-api';
|
||||
window['env']['mifosRemittanceApiVersion'] = '/v1';
|
||||
window['env']['mifosRemittanceEnabled'] = false;
|
||||
window['env']['mifosRemittanceApiHeader'] = 'X-Gravitee-Api-Key';
|
||||
window['env']['mifosRemittanceApiKey'] = '';
|
||||
|
||||
// Enable Role-Based Access Control (RBAC) for menu/button permissions
|
||||
// Set to true to enable RBAC, false (default) for backward compatibility
|
||||
window['env']['productionModeEnableRBAC'] = false;
|
||||
|
||||
@@ -102,6 +102,14 @@
|
||||
window['env']['mifosInterbankTransfersApiVersion'] = '$MIFOS_INTERBANK_TRANSFERS_API_VERSION';
|
||||
window['env']['mifosInterbankTransfersEnabled'] = '$MIFOS_INTERBANK_TRANSFERS_ENABLED';
|
||||
|
||||
// Remittance Module Environment variables
|
||||
window['env']['mifosRemittanceApiUrl'] = '$MIFOS_REMITTANCE_API_URL';
|
||||
window['env']['mifosRemittanceApiProvider'] = '$MIFOS_REMITTANCE_API_PROVIDER';
|
||||
window['env']['mifosRemittanceApiVersion'] = '$MIFOS_REMITTANCE_API_VERSION';
|
||||
window['env']['mifosRemittanceEnabled'] = '$MIFOS_REMITTANCE_ENABLED';
|
||||
window['env']['mifosRemittanceApiHeader'] = '$MIFOS_REMITTANCE_API_HEADER';
|
||||
window['env']['mifosRemittanceApiKey'] = '$MIFOS_REMITTANCE_API_KEY';
|
||||
|
||||
// Enable Role-Based Access Control (RBAC) for menu/button permissions
|
||||
// Set to 'true' to enable RBAC, 'false' (default) for backward compatibility
|
||||
window['env']['productionModeEnableRBAC'] = '$MIFOS_PRODUCTION_MODE_ENABLE_RBAC';
|
||||
|
||||
@@ -502,6 +502,7 @@
|
||||
"Resend OTP": "Resend OTP",
|
||||
"Reset": "Reset",
|
||||
"Reset Password": "Reset Password",
|
||||
"Retry": "Retry",
|
||||
"Retrieve": "Retrieve",
|
||||
"Revert Transaction": "Revert Transaction",
|
||||
"Run and Download Report": "Run and Download Report",
|
||||
@@ -561,7 +562,14 @@
|
||||
"View Active Charges": "View Active Charges",
|
||||
"View Inactive Charges": "View Inactive Charges",
|
||||
"Yes": "Yes",
|
||||
"Is Self Service": "Is Self Service"
|
||||
"Is Self Service": "Is Self Service",
|
||||
"Assign Payout": "Assign Payout",
|
||||
"Confirm Payment": "Confirm Payment",
|
||||
"New Remittance": "New Remittance",
|
||||
"Print Receipt": "Print Receipt",
|
||||
"Validate Recipient": "Validate Recipient",
|
||||
"View Receipt": "View Receipt",
|
||||
"Add Entry": "Add Entry"
|
||||
},
|
||||
"catalogs": {
|
||||
"Interest payment waiver": "Interest Payment Waiver",
|
||||
@@ -1301,7 +1309,31 @@
|
||||
"Date": "Date",
|
||||
"Secure & Reliable": "Secure & Reliable",
|
||||
"Community Driven": "Community Driven",
|
||||
"Global Impact": "Global Impact"
|
||||
"Global Impact": "Global Impact",
|
||||
"Assign Payout": "Assign Payout",
|
||||
"Assign to Mifos Client": "Assign to Mifos Client",
|
||||
"Beneficiary Details": "Beneficiary Details",
|
||||
"Confirm Payment": "Confirm Payment",
|
||||
"Confirm Payout": "Confirm Payout",
|
||||
"Confirmation Details": "Confirmation Details",
|
||||
"Payment Receipt": "Payment Receipt",
|
||||
"Payment Successful": "Payment Successful",
|
||||
"Process Remittance": "Process Remittance",
|
||||
"Recipient Details": "Recipient Details",
|
||||
"Recipient Summary": "Recipient Summary",
|
||||
"Remittance Details": "Remittance Details",
|
||||
"Search Beneficiary": "Search Beneficiary",
|
||||
"Search Remittance": "Search Remittance",
|
||||
"Transaction": "Transaction",
|
||||
"Transaction Status": "Transaction Status",
|
||||
"Transaction Summary": "Transaction Summary",
|
||||
"Transactional Profile": "Transactional Profile",
|
||||
"Validated Recipient Information": "Validated Recipient Information",
|
||||
"Recipient": "Recipient",
|
||||
"Payout Assignment": "Payout Assignment",
|
||||
"Identification": "Identification",
|
||||
"Professional Information": "Professional Information",
|
||||
"Add Profile Entry": "Add Profile Entry"
|
||||
},
|
||||
"inputs": {
|
||||
"Floating rate": "Floating Rate",
|
||||
@@ -1519,6 +1551,7 @@
|
||||
"Client Details": "Client Details",
|
||||
"Client ID": "Client ID",
|
||||
"Client Id": "Client Id",
|
||||
"Confirmation Number": "Confirmation Number",
|
||||
"Client Member": "Client Member",
|
||||
"Client Members": "Client Members",
|
||||
"Client Name": "Client Name",
|
||||
@@ -1707,6 +1740,9 @@
|
||||
"Do not calculate interest on past due principal balances": "Do not calculate interest on past due principal balances",
|
||||
"Documents": "Documents",
|
||||
"Document Key": "Document Key",
|
||||
"Document": "Document",
|
||||
"Document Number": "Document Number",
|
||||
"Document number is required": "Document number is required",
|
||||
"Document Type": "Document Type",
|
||||
"Dormancy Tracking": "Dormancy Tracking",
|
||||
"Download": "Download",
|
||||
@@ -1767,6 +1803,8 @@
|
||||
"Extend Repayment Period": "Extend Repayment Period",
|
||||
"External Asset Owner": "External Asset Owner",
|
||||
"External ID": "External ID",
|
||||
"Failed to load vendors": "Failed to load vendors",
|
||||
"External Reference ID": "External Reference ID",
|
||||
"External Id": "External Id",
|
||||
"External id": "External id",
|
||||
"External ID not found": "External ID not found",
|
||||
@@ -1795,6 +1833,7 @@
|
||||
"Filter By Version": "Filter By Version",
|
||||
"Financial Activity": "Financial Activity",
|
||||
"First Name": "First Name",
|
||||
"First name is required": "First name is required",
|
||||
"First repayment on": "First repayment on",
|
||||
"Fixed Account": "Fixed Account",
|
||||
"Fixed Deposit": "Fixed Deposit",
|
||||
@@ -1986,6 +2025,7 @@
|
||||
"Last Active Transaction Date": "Last Active Transaction Date",
|
||||
"Last Loan Amount": "Last Loan Amount",
|
||||
"Last Name": "Last Name",
|
||||
"Last name is required": "Last name is required",
|
||||
"Last Repayment": "Last Repayment",
|
||||
"Last Repayment Date": "Last Repayment Date",
|
||||
"Late": "Late",
|
||||
@@ -1995,6 +2035,7 @@
|
||||
"Liability Account": "Liability Account",
|
||||
"Liability Account (CR)": "Liability Account (CR)",
|
||||
"Lifted On": "Lifted On",
|
||||
"Loading vendors...": "Loading vendors...",
|
||||
"Link savings": "Link savings",
|
||||
"Linked Savings Account": "Linked Savings Account",
|
||||
"Linked Savings Account(Dividend Posting)": "Linked Savings Account(Dividend Posting)",
|
||||
@@ -2426,6 +2467,7 @@
|
||||
"Schedule Date": "Schedule Date",
|
||||
"Score": "Score",
|
||||
"Screen Report": "Screen Report",
|
||||
"Search Client": "Search Client",
|
||||
"Search Journal Entries": "Search Journal Entries",
|
||||
"Select Action": "Select Action",
|
||||
"Select Client Members for Transfer": "Select Client Members for Transfer",
|
||||
@@ -2570,6 +2612,7 @@
|
||||
"Trigger Type": "Trigger Type",
|
||||
"Tuesday": "Tuesday",
|
||||
"Type": "Type",
|
||||
"Type client name or account number": "Type client name or account number",
|
||||
"Type/Quality": "Type/Quality",
|
||||
"Unassigned": "Unassigned",
|
||||
"Unassignment Date": "Unassignment Date",
|
||||
@@ -2692,7 +2735,45 @@
|
||||
"Overdue": "Overdue",
|
||||
"Overdue Fees": "Overdue Fees",
|
||||
"Select action": "Select action",
|
||||
"Select entity": "Select entity"
|
||||
"Select entity": "Select entity",
|
||||
"Confirmed At": "Confirmed At",
|
||||
"Full Name": "Full Name",
|
||||
"Loading vendors...": "Loading vendors...",
|
||||
"Minimum 3 characters": "Minimum 3 characters",
|
||||
"National ID": "National ID",
|
||||
"Nationality": "Nationality",
|
||||
"Payout Amount": "Payout Amount",
|
||||
"Payout Country": "Payout Country",
|
||||
"Payout Token": "Payout Token",
|
||||
"Recipient Name": "Recipient Name",
|
||||
"Reference ID is required": "Reference ID is required",
|
||||
"Send Amount": "Send Amount",
|
||||
"Send Country": "Send Country",
|
||||
"Sender": "Sender",
|
||||
"Sender Name": "Sender Name",
|
||||
"Sent By": "Sent By",
|
||||
"Deposited At": "Deposited At",
|
||||
"Vendor": "Vendor",
|
||||
"Vendor is required": "Vendor is required",
|
||||
"Voter ID": "Voter ID",
|
||||
"Passport": "Passport",
|
||||
"Driver's License": "Driver's License",
|
||||
"Mothers Maiden Name": "Mother's Maiden Name",
|
||||
"National Identity Card Number": "National Identity Card Number",
|
||||
"State": "State",
|
||||
"Municipality": "Municipality",
|
||||
"Locality": "Locality",
|
||||
"New Locality": "New Locality",
|
||||
"Occupation": "Occupation",
|
||||
"Economic Activity": "Economic Activity",
|
||||
"Entity Member": "Entity Member",
|
||||
"Country": "Country",
|
||||
"# Remittance": "# Remittance",
|
||||
"Avg Amount/Transaction": "Avg Amount/Transaction",
|
||||
"# Senders": "# Senders",
|
||||
"Sender Registry": "Sender Registry",
|
||||
"Individual": "Individual",
|
||||
"Legal Entity": "Legal Entity"
|
||||
},
|
||||
"links": {
|
||||
"Community": "Community",
|
||||
@@ -2782,6 +2863,8 @@
|
||||
"Interbank Transfer": "Interbank Transfer",
|
||||
"Interest Payment Waiver": "Interest Payment Waiver",
|
||||
"Payments": "Payments",
|
||||
"Remittances": "Remittances",
|
||||
"Process Remittance": "Process Remittance",
|
||||
"Payout Refund": "Payout Refund",
|
||||
"Post Interest": "Post Interest",
|
||||
"Post Interest As On": "Post Interest As On",
|
||||
@@ -2833,8 +2916,14 @@
|
||||
"Reject Additional Shares": "Reject Additional Shares"
|
||||
},
|
||||
"placeholders": {
|
||||
"# Remittance": "# Remittance",
|
||||
"Add new server": "Add new server",
|
||||
"Enter document number": "Enter document number",
|
||||
"Enter first name (include middle name)": "Enter first name (include middle name)",
|
||||
"Enter last name": "Enter last name",
|
||||
"Enter remittance reference number": "Enter remittance reference number",
|
||||
"Search Activity": "Search Activity",
|
||||
"Sender name": "Sender name",
|
||||
"Enter your username": "Enter your username",
|
||||
"Enter your password": "Enter your password"
|
||||
},
|
||||
@@ -2949,6 +3038,7 @@
|
||||
"Business Date functionality is not enabled": "The Business Date functionality is not enabled. If you need that, Go to Global Configurations and enable the configuration",
|
||||
"Button for accounting department": "This button opens the menu to access the system tools required by the accounting department.",
|
||||
"Button for individual account": "This button will be used for individual accounts, groups and centers",
|
||||
"Failed to search clients. Please try again.": "Failed to search clients. Please try again.",
|
||||
"Cancel Pending Sale": "Cancel Pending Sale",
|
||||
"Cashier Transactions": "Cashier Transactions",
|
||||
"Cashiers": "Cashiers",
|
||||
@@ -3431,6 +3521,7 @@
|
||||
"Preferences for generating account numbers for client": "Preferences for generating account numbers for client, loan and savings accounts",
|
||||
"Products": "Products",
|
||||
"Products Mix": "Products Mix",
|
||||
"Process Remittance": "Process Remittance",
|
||||
"Profile": "Profile",
|
||||
"Provisioning Criteria": "Provisioning Criteria",
|
||||
"Provisioning Entries": "Provisioning Entries",
|
||||
@@ -3482,6 +3573,7 @@
|
||||
"Scheduler Job History": "Scheduler Job History",
|
||||
"Scheduler Jobs can set batch jobs": "By using Scheduler Jobs, you can set batch jobs to be completed at regular intervals. From this page you can schedule, modify or delete jobs.",
|
||||
"Search": "Search",
|
||||
"Search and assign this beneficiary to an existing Mifos client account": "Search and assign this beneficiary to an existing Mifos client account",
|
||||
"Search by Text": "Search by Text",
|
||||
"SearchByName": "Search by Name",
|
||||
"SearchByClient": "Search by client name, external Id, mobile",
|
||||
@@ -3661,7 +3753,21 @@
|
||||
"“Maker-Checker” principle requires every tasks": "The “Maker-Checker” principle requires every tasks to be completed by two people to reduce the chance of errors and misuse. One person initiates the process and the second completes it.",
|
||||
"Buy Down Fees": "Buy Down Fees",
|
||||
"Undo Write-off Description": "This action will reverse the write-off transaction and restore the loan to its previous active status. Please provide a note explaining the reason for this action.",
|
||||
"UploadDocumentHint": "Upload a PDF or image to generate a preview."
|
||||
"UploadDocumentHint": "Upload a PDF or image to generate a preview.",
|
||||
"This remittance has a terminal status and cannot be processed further.": "This remittance has a terminal status and cannot be processed further.",
|
||||
"This remittance has already been paid successfully.": "This remittance has already been paid successfully.",
|
||||
"Review the transaction and recipient details below, then assign the payout.": "Review the transaction and recipient details below, then assign the payout.",
|
||||
"Please review the details carefully before confirming the payment.": "Please review the details carefully before confirming the payment.",
|
||||
"Register the transactional profile for the beneficiary. Add entries for individuals or legal entities.": "Register the transactional profile for the beneficiary. Add entries for individuals or legal entities.",
|
||||
"No transactional profile entries yet. Add one below.": "No transactional profile entries yet. Add one below.",
|
||||
"Click to remove": "Click to remove",
|
||||
"Recipient is not valid for this transaction. Please check the details.": "Recipient is not valid for this transaction. Please check the details.",
|
||||
"Recipient validation failed. Please check the document details and try again.": "Recipient validation failed. Please check the document details and try again.",
|
||||
"Missing required information. Please try again.": "Missing required information. Please try again.",
|
||||
"Failed to confirm payment. Please try again or contact support.": "Failed to confirm payment. Please try again or contact support.",
|
||||
"Failed to assign payout. Please try again.": "Failed to assign payout. Please try again.",
|
||||
"Remittance not found. Please check the reference number and try again.": "Remittance not found. Please check the reference number and try again.",
|
||||
"Unable to load vendors. Please check your connection and try again.": "Unable to load vendors. Please check your connection and try again."
|
||||
},
|
||||
"auditTrail": {
|
||||
"actions": {
|
||||
@@ -3840,6 +3946,7 @@
|
||||
"Indicates whether to enforce a minimum balance": "Indicates whether to enforce a minimum balance",
|
||||
"Indicates whether the dividends can be processed even share account of this share product is inactive": "Indicates whether the dividends can be processed even share account of this share product is inactive",
|
||||
"Individual Collection Sheet": "Individual Collection Sheet",
|
||||
"Process Remittance": "Process Remittance",
|
||||
"Input to calculating the repayment schedule for a loan account": "These fields are input to calculating the repayment schedule for a loan account and are used to determine when payments are due.",
|
||||
"Interest effective from date": "Interest effective from date",
|
||||
"Interest rate is posted or credited to a saving account": "The period at which interest rate is posted or credited to a saving account",
|
||||
|
||||
@@ -560,7 +560,15 @@
|
||||
"Is Self Service": "Es autoservicio",
|
||||
"Advanced Search": "Búsqueda avanzada",
|
||||
"Show advanced search": "Mostrar búsqueda avanzada",
|
||||
"Hide advanced search": "Ocultar búsqueda avanzada"
|
||||
"Hide advanced search": "Ocultar búsqueda avanzada",
|
||||
"Validate Recipient": "Validar Beneficiario",
|
||||
"Assign Payout": "Asignar Pago",
|
||||
"Confirm Payment": "Confirmar Pago",
|
||||
"Print Receipt": "Imprimir Recibo",
|
||||
"New Remittance": "Nueva Remesa",
|
||||
"View Receipt": "Ver Recibo",
|
||||
"Add Entry": "Agregar Entrada",
|
||||
"Retry": "Reintentar"
|
||||
},
|
||||
"catalogs": {
|
||||
"Interest payment waiver": "Exención de pago de intereses",
|
||||
@@ -801,6 +809,9 @@
|
||||
"Passport": "Pasaporte",
|
||||
"Id": "Identificación",
|
||||
"Drivers License": "Licencia de conducir",
|
||||
"Driver's License": "Licencia de Conducir",
|
||||
"National ID": "Identificación Nacional",
|
||||
"Voter ID": "Credencial de Elector",
|
||||
"Any Other Id Type": "Cualquier otro tipo de identificación",
|
||||
"Check loan repayment overdue": "Verificar pagos vencidos de créditos",
|
||||
"Update loan arrears aging": "Actualizar antigüedad de atrasos en los créditos",
|
||||
@@ -1296,7 +1307,30 @@
|
||||
"Contact Information": "Información de Contacto",
|
||||
"Classification": "Clasificación",
|
||||
"Status": "Estado",
|
||||
"Pending Tasks": "Tareas pendientes"
|
||||
"Pending Tasks": "Tareas pendientes",
|
||||
"Process Remittance": "Procesar Remesa",
|
||||
"Search Remittance": "Buscar Remesa",
|
||||
"Remittance Details": "Detalles de la Remesa",
|
||||
"Search Beneficiary": "Buscar Beneficiario",
|
||||
"Beneficiary Details": "Detalles del Beneficiario",
|
||||
"Transactional Profile": "Perfil Transaccional",
|
||||
"Confirm Payout": "Confirmar Pago",
|
||||
"Confirm Payment": "Confirmar Pago",
|
||||
"Payment Receipt": "Recibo de Pago",
|
||||
"Transaction": "Transacción",
|
||||
"Recipient": "Beneficiario",
|
||||
"Payout Assignment": "Asignación de Pago",
|
||||
"Assign Payout": "Asignar Pago",
|
||||
"Validated Recipient Information": "Información del Beneficiario Validada",
|
||||
"Identification": "Identificación",
|
||||
"Professional Information": "Información Profesional",
|
||||
"Assign to Mifos Client": "Asignar a Cliente Mifos",
|
||||
"Add Profile Entry": "Agregar Entrada de Perfil",
|
||||
"Recipient Details": "Detalles del Beneficiario",
|
||||
"Confirmation Details": "Detalles de Confirmación",
|
||||
"Payment Successful": "Pago Exitoso",
|
||||
"Transaction Status": "Estado de la Transacción",
|
||||
"Transaction Summary": "Resumen de la Transacción"
|
||||
},
|
||||
"inputs": {
|
||||
"Floating rate": "Tasa flotante",
|
||||
@@ -1986,6 +2020,7 @@
|
||||
"Liability Account": "Cuenta de responsabilidad",
|
||||
"Liability Account (CR)": "Cuenta de Pasivo (CR)",
|
||||
"Lifted On": "levantado",
|
||||
"Loading vendors...": "Cargando proveedores...",
|
||||
"Link savings": "Ahorro de enlaces",
|
||||
"Linked Savings Account": "Cuenta de Ahorros vinculada",
|
||||
"Linked Savings Account(Dividend Posting)": "Cuenta de Ahorros vinculada (publicación de dividendos)",
|
||||
@@ -2462,6 +2497,7 @@
|
||||
"Start Time": "Hora de inicio",
|
||||
"Started On": "Iniciado en",
|
||||
"Starting date": "Fecha de inicio",
|
||||
"State": "Estado",
|
||||
"State / Province": "Estado o Provincia",
|
||||
"Status": "Estado",
|
||||
"Step Name": "Nombre del paso",
|
||||
@@ -2821,11 +2857,50 @@
|
||||
"Redeem Shares": "Canjear acciones",
|
||||
"Approve Additional Shares": "Aprobar acciones adicionales",
|
||||
"Reject Additional Shares": "Rechazar acciones adicionales",
|
||||
"Tasks": "Tareas"
|
||||
"Tasks": "Tareas",
|
||||
"Vendor": "Proveedor",
|
||||
"Loading vendors...": "Cargando proveedores...",
|
||||
"Failed to load vendors": "Error al cargar proveedores",
|
||||
"External Reference ID": "ID de Referencia Externa",
|
||||
"Reference ID is required": "Se requiere ID de referencia",
|
||||
"Minimum 3 characters": "Mínimo 3 caracteres",
|
||||
"Sent By": "Enviado Por",
|
||||
"Deposited At": "Depositado En",
|
||||
"Sender Name": "Nombre del Remitente",
|
||||
"Payout Amount": "Monto del Pago",
|
||||
"Recipient Name": "Nombre del Beneficiario",
|
||||
"Individual": "Individual",
|
||||
"Legal Entity": "Persona Moral",
|
||||
"# Remittance": "# Remesa",
|
||||
"Avg Amount/Transaction": "Monto Promedio/Transacción",
|
||||
"# Senders": "# Remitentes",
|
||||
"Sender Registry": "Registro de Remitentes",
|
||||
"Sender": "Remitente",
|
||||
"Document": "Documento",
|
||||
"Document Number": "Número de Documento",
|
||||
"First name is required": "Se requiere nombre",
|
||||
"Last name is required": "Se requiere apellido",
|
||||
"Document number is required": "Se requiere número de documento",
|
||||
"National Identity Card Number": "Número de Cédula de Identidad Nacional",
|
||||
"Mothers Maiden Name": "Apellido de Soltera de la Madre",
|
||||
"Municipality": "Municipio",
|
||||
"Locality": "Localidad",
|
||||
"New Locality": "Nueva Localidad",
|
||||
"Occupation": "Ocupación",
|
||||
"Economic Activity": "Actividad Económica",
|
||||
"Entity Member": "Miembro de Entidad",
|
||||
"Search Client": "Buscar Cliente",
|
||||
"Type client name or account number": "Escriba nombre del cliente o número de cuenta"
|
||||
},
|
||||
"placeholders": {
|
||||
"# Remittance": "# Remesa",
|
||||
"Add new server": "Agregar nuevo servidor",
|
||||
"Enter document number": "Ingrese número de documento",
|
||||
"Enter first name (include middle name)": "Ingrese nombre (incluya segundo nombre)",
|
||||
"Enter last name": "Ingrese apellido",
|
||||
"Enter remittance reference number": "Ingrese número de referencia de remesa",
|
||||
"Search Activity": "Actividad de búsqueda",
|
||||
"Sender name": "Nombre del remitente",
|
||||
"Enter your username": "Ingrese su nombre de usuario",
|
||||
"Enter your password": "Ingrese su contraseña"
|
||||
},
|
||||
@@ -3852,7 +3927,23 @@
|
||||
"Create Interest Refund": "Crear reembolso de intereses",
|
||||
"ACTIVE": "ACTIVO",
|
||||
"PENDING": "PENDIENTE",
|
||||
"INACTIVE": "INACTIVO"
|
||||
"INACTIVE": "INACTIVO",
|
||||
"Unable to load vendors. Please check your connection and try again.": "No se pudieron cargar los proveedores. Verifique su conexión e intente de nuevo.",
|
||||
"This remittance has a terminal status and cannot be processed further.": "Esta remesa tiene un estado terminal y no puede ser procesada.",
|
||||
"This remittance has already been paid successfully.": "Esta remesa ya ha sido pagada exitosamente.",
|
||||
"Register the transactional profile for the beneficiary. Add entries for individuals or legal entities.": "Registre el perfil transaccional del beneficiario. Agregue entradas para individuos o personas morales.",
|
||||
"Click to remove": "Clic para eliminar",
|
||||
"No transactional profile entries yet. Add one below.": "Aún no hay entradas en el perfil transaccional. Agregue una a continuación.",
|
||||
"Please review the details carefully before confirming the payment.": "Revise los detalles cuidadosamente antes de confirmar el pago.",
|
||||
"Search and assign this beneficiary to an existing Mifos client account": "Buscar y asignar este beneficiario a una cuenta de cliente Mifos existente",
|
||||
"Failed to search clients. Please try again.": "Error al buscar clientes. Intente de nuevo.",
|
||||
"Recipient is not valid for this transaction. Please check the details.": "El beneficiario no es válido para esta transacción. Verifique los detalles.",
|
||||
"Recipient validation failed. Please check the document details and try again.": "La validación del beneficiario falló. Verifique los detalles del documento e intente de nuevo.",
|
||||
"Missing required information. Please try again.": "Falta información requerida. Intente de nuevo.",
|
||||
"Failed to confirm payment. Please try again or contact support.": "Error al confirmar el pago. Intente de nuevo o contacte al soporte.",
|
||||
"Failed to assign payout. Please try again.": "Error al asignar el pago. Intente de nuevo.",
|
||||
"Remittance not found. Please check the reference number and try again.": "Remesa no encontrada. Verifique el número de referencia e intente de nuevo.",
|
||||
"Review the transaction and recipient details below, then assign the payout.": "Revise los detalles de la transacción y del beneficiario a continuación, luego asigne el pago."
|
||||
},
|
||||
"countries": {
|
||||
"AF": "Afganistán",
|
||||
|
||||
@@ -85,6 +85,15 @@ export const environment = {
|
||||
mifosInterbankTransfersApiVersion: loadedEnv['mifosInterbankTransfersApiVersion'] || '/v1.0',
|
||||
mifosInterbankTransfersEnabled: loadedEnv['mifosInterbankTransfersEnabled'] ?? true,
|
||||
|
||||
/** Remittance Module Integration */
|
||||
mifosRemittanceApiUrl: loadedEnv['mifosRemittanceApiUrl'] || '',
|
||||
mifosRemittanceApiProvider: loadedEnv['mifosRemittanceApiProvider'] || '/remittance-api',
|
||||
mifosRemittanceApiVersion: loadedEnv['mifosRemittanceApiVersion'] || '/v1',
|
||||
mifosRemittanceEnabled:
|
||||
loadedEnv['mifosRemittanceEnabled'] === true || loadedEnv['mifosRemittanceEnabled'] === 'true',
|
||||
mifosRemittanceApiHeader: loadedEnv['mifosRemittanceApiHeader'] || 'X-Gravitee-Api-Key',
|
||||
mifosRemittanceApiKey: loadedEnv['mifosRemittanceApiKey'] || '',
|
||||
|
||||
minPasswordLength: loadedEnv['minPasswordLength'] || 12,
|
||||
|
||||
/** External National ID System integration */
|
||||
|
||||
@@ -87,6 +87,14 @@ export const environment = {
|
||||
mifosInterbankTransfersApiVersion: window.env?.mifosInterbankTransfersApiVersion || '/v1.0',
|
||||
mifosInterbankTransfersEnabled: window.env?.mifosInterbankTransfersEnabled ?? true,
|
||||
|
||||
/** Remittance Module Integration */
|
||||
mifosRemittanceApiUrl: window.env?.mifosRemittanceApiUrl || '',
|
||||
mifosRemittanceApiProvider: window.env?.mifosRemittanceApiProvider || '/remittance-api',
|
||||
mifosRemittanceApiVersion: window.env?.mifosRemittanceApiVersion || '/v1',
|
||||
mifosRemittanceEnabled: window.env?.mifosRemittanceEnabled === true || window.env?.mifosRemittanceEnabled === 'true',
|
||||
mifosRemittanceApiHeader: window.env?.mifosRemittanceApiHeader || 'X-Gravitee-Api-Key',
|
||||
mifosRemittanceApiKey: window.env?.mifosRemittanceApiKey || '',
|
||||
|
||||
/**
|
||||
* External National ID System Integration
|
||||
* When enabled, client creation/editing will lookup external National ID
|
||||
|
||||
@@ -719,6 +719,24 @@ mifosx-notifications-page td {
|
||||
color: inherit !important;
|
||||
}
|
||||
|
||||
// ===== Global Button Icon Alignment =====
|
||||
// Handles the span + mat-icon pattern used throughout the application
|
||||
.mat-mdc-button,
|
||||
.mat-mdc-raised-button,
|
||||
.mat-mdc-outlined-button {
|
||||
span {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
|
||||
mat-icon {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.full-width-flex {
|
||||
display: flex;
|
||||
justify-content: flex-start;
|
||||
@@ -854,3 +872,31 @@ mat-card {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/* Print styles — hide app shell, show only receipt content */
|
||||
@media print {
|
||||
mat-sidenav,
|
||||
mifosx-toolbar,
|
||||
mifosx-breadcrumb,
|
||||
mifosx-footer,
|
||||
.sidebar-panel,
|
||||
.mat-mdc-tab-header,
|
||||
.mat-stepper-vertical-line,
|
||||
.mat-step-header {
|
||||
display: none !important;
|
||||
}
|
||||
|
||||
mat-sidenav-content.sidenav {
|
||||
margin-left: 0 !important;
|
||||
}
|
||||
|
||||
body,
|
||||
html {
|
||||
background: #fff !important;
|
||||
}
|
||||
|
||||
@page {
|
||||
/* stylelint-disable-next-line unit-allowed-list */
|
||||
margin: 1.5cm;
|
||||
}
|
||||
}
|
||||
|
||||
Vendored
+5
@@ -24,6 +24,11 @@ interface Window {
|
||||
mifosInterbankTransfersApiProvider?: string;
|
||||
mifosInterbankTransfersApiVersion?: string;
|
||||
mifosInterbankTransfersEnabled?: boolean;
|
||||
mifosRemittanceApiUrl?: string;
|
||||
mifosRemittanceApiProvider?: string;
|
||||
mifosRemittanceApiVersion?: string;
|
||||
mifosRemittanceEnabled?: boolean;
|
||||
mifosRemittanceApiKey?: string;
|
||||
minPasswordLength?: number;
|
||||
};
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user