327 lines
13 KiB
TypeScript
327 lines
13 KiB
TypeScript
/**
|
|
* Copyright since 2025 Mifos Initiative
|
|
*
|
|
* This Source Code Form is subject to the terms of the Mozilla Public
|
|
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
|
* file, You can obtain one at http://mozilla.org/MPL/2.0/.
|
|
*/
|
|
|
|
import { Component, OnInit, inject } from '@angular/core';
|
|
import { CommonModule } from '@angular/common';
|
|
import { FormsModule, ReactiveFormsModule, FormBuilder, FormGroup, Validators } from '@angular/forms';
|
|
import { HttpClient } from '@angular/common/http';
|
|
import { MatDialogModule, MatDialogRef } from '@angular/material/dialog';
|
|
import { MatFormFieldModule } from '@angular/material/form-field';
|
|
import { MatInputModule } from '@angular/material/input';
|
|
import { MatSelectModule } from '@angular/material/select';
|
|
import { MatButtonModule } from '@angular/material/button';
|
|
import { MatIconModule } from '@angular/material/icon';
|
|
import { MatSnackBar, MatSnackBarModule } from '@angular/material/snack-bar';
|
|
import { SponsorDDService } from '../sponsor-dd.service';
|
|
|
|
@Component({
|
|
selector: 'mifosx-gl-settings-dialog',
|
|
standalone: true,
|
|
imports: [
|
|
CommonModule,
|
|
FormsModule,
|
|
ReactiveFormsModule,
|
|
MatDialogModule,
|
|
MatFormFieldModule,
|
|
MatInputModule,
|
|
MatSelectModule,
|
|
MatButtonModule,
|
|
MatIconModule,
|
|
MatSnackBarModule
|
|
],
|
|
template: `
|
|
<h2 mat-dialog-title>Sponsor DD GST & GL Mapping Configuration</h2>
|
|
<mat-dialog-content>
|
|
<p class="hint-text">
|
|
Configure the one-time General Ledger (GL) accounts for Demand Draft creation. When a walk-in or bank customer
|
|
creates a DD, the system will automatically route the face value, charges, and GST to these mapped accounts.
|
|
</p>
|
|
|
|
<form [formGroup]="form" class="dialog-form" *ngIf="!loading">
|
|
<!--
|
|
DD Payable is no longer set here.
|
|
|
|
It belongs to the sponsor bank: a draft drawn on SBI is owed to SBI and one on HDFC to
|
|
HDFC, and what is outstanding to either is the figure a settlement with that bank is
|
|
reconciled against — which one shared account cannot answer. Everything remaining on this
|
|
screen is the same whichever sponsor a draft is drawn on: this bank's own charges income,
|
|
its GST, its cash and clearing accounts, and its DD policy.
|
|
-->
|
|
<div class="payable-moved-note">
|
|
<mat-icon>account_balance</mat-icon>
|
|
<div>
|
|
<strong>DD Payable is now set per sponsor bank.</strong>
|
|
<div class="payable-moved-sub">
|
|
Each bank credits its own payable account, so what is owed to each can be settled separately. Set it in
|
|
<em>Organization > Sponsor Banks</em>.
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Charges Income GL Account -->
|
|
<mat-form-field appearance="outline" class="full-width">
|
|
<mat-label>DD Charges / Commission Income GL *</mat-label>
|
|
<mat-select formControlName="chargesIncomeGlAccountId" required placeholder="Select Charges Income GL">
|
|
<mat-option *ngFor="let acc of glAccounts" [value]="acc.id"> {{ acc.glCode }} · {{ acc.name }} </mat-option>
|
|
</mat-select>
|
|
<mat-hint>Credited for DD Issuance Charges/Commission</mat-hint>
|
|
<mat-error *ngIf="form.get('chargesIncomeGlAccountId')?.hasError('required')">Required</mat-error>
|
|
</mat-form-field>
|
|
|
|
<!-- GST Payable GL Account -->
|
|
<mat-form-field appearance="outline" class="full-width">
|
|
<mat-label>GST Payable on Charges GL *</mat-label>
|
|
<mat-select formControlName="gstPayableGlAccountId" required placeholder="Select GST Liability GL">
|
|
<mat-option *ngFor="let acc of glAccounts" [value]="acc.id"> {{ acc.glCode }} · {{ acc.name }} </mat-option>
|
|
</mat-select>
|
|
<mat-hint>Credited for GST collected on charges</mat-hint>
|
|
<mat-error *ngIf="form.get('gstPayableGlAccountId')?.hasError('required')">Required</mat-error>
|
|
</mat-form-field>
|
|
|
|
<!-- Cash GL Account -->
|
|
<mat-form-field appearance="outline" class="full-width">
|
|
<mat-label>Cash at Tellers / Vault GL Account *</mat-label>
|
|
<mat-select formControlName="cashGlAccountId" required placeholder="Select Cash Asset GL">
|
|
<mat-option *ngFor="let acc of glAccounts" [value]="acc.id"> {{ acc.glCode }} · {{ acc.name }} </mat-option>
|
|
</mat-select>
|
|
<mat-hint>Debited when cash is received from walk-in customer</mat-hint>
|
|
<mat-error *ngIf="form.get('cashGlAccountId')?.hasError('required')">Required</mat-error>
|
|
</mat-form-field>
|
|
|
|
<!-- Cheque Clearing GL Account -->
|
|
<mat-form-field appearance="outline" class="full-width">
|
|
<mat-label>Cheque Clearing Suspense GL Account</mat-label>
|
|
<mat-select formControlName="chequeClearingGlAccountId" placeholder="Select Cheque Clearing GL (Optional)">
|
|
<mat-option [value]="null">-- None --</mat-option>
|
|
<mat-option *ngFor="let acc of glAccounts" [value]="acc.id"> {{ acc.glCode }} · {{ acc.name }} </mat-option>
|
|
</mat-select>
|
|
<mat-hint>Debited when DD is paid via cheque</mat-hint>
|
|
</mat-form-field>
|
|
|
|
<div class="row-two-fields">
|
|
<!-- GST Percentage -->
|
|
<mat-form-field appearance="outline" class="half-width">
|
|
<mat-label>GST Percentage (%) *</mat-label>
|
|
<input matInput type="number" formControlName="gstPercentage" step="0.01" />
|
|
<mat-error *ngIf="form.get('gstPercentage')?.hasError('required')">Required</mat-error>
|
|
<mat-error *ngIf="form.get('gstPercentage')?.hasError('min') || form.get('gstPercentage')?.hasError('max')">
|
|
0 - 100%
|
|
</mat-error>
|
|
</mat-form-field>
|
|
|
|
<!-- DD Limit Amount -->
|
|
<mat-form-field appearance="outline" class="half-width">
|
|
<mat-label>DD Limit Amount (₹)</mat-label>
|
|
<input matInput type="number" formControlName="ddLimitAmount" step="0.01" />
|
|
<mat-hint>Max limit per draft/day</mat-hint>
|
|
<mat-error *ngIf="form.get('ddLimitAmount')?.hasError('min')">Cannot be negative</mat-error>
|
|
</mat-form-field>
|
|
</div>
|
|
</form>
|
|
|
|
<div *ngIf="loading" class="loading-state">
|
|
<p>Loading GL accounts and settings...</p>
|
|
</div>
|
|
</mat-dialog-content>
|
|
<mat-dialog-actions align="end">
|
|
<button mat-button (click)="onClose()">Close</button>
|
|
<button mat-flat-button color="primary" [disabled]="loading || form.invalid || saving" (click)="onSave()">
|
|
{{ saving ? 'Saving...' : 'Save Mapping' }}
|
|
</button>
|
|
</mat-dialog-actions>
|
|
`,
|
|
styles: [
|
|
`
|
|
.dialog-form {
|
|
display: flex;
|
|
flex-direction: column;
|
|
gap: 12px;
|
|
padding-top: 8px;
|
|
}
|
|
.payable-moved-note {
|
|
display: flex;
|
|
gap: 10px;
|
|
align-items: flex-start;
|
|
margin-bottom: 18px;
|
|
padding: 11px 14px;
|
|
background: #eff6ff;
|
|
border: 1px solid #bfdbfe;
|
|
border-left: 3px solid #1d4ed8;
|
|
border-radius: 6px;
|
|
font-size: 13px;
|
|
color: #334155;
|
|
}
|
|
.payable-moved-note mat-icon {
|
|
color: #1d4ed8;
|
|
flex-shrink: 0;
|
|
}
|
|
.payable-moved-sub {
|
|
margin-top: 3px;
|
|
font-size: 11.5px;
|
|
line-height: 1.5;
|
|
color: #64748b;
|
|
}
|
|
.full-width {
|
|
width: 100%;
|
|
}
|
|
.row-two-fields {
|
|
display: flex;
|
|
gap: 16px;
|
|
}
|
|
.half-width {
|
|
flex: 1;
|
|
}
|
|
.hint-text {
|
|
color: #64748b;
|
|
font-size: 13px;
|
|
margin: 0 0 16px 0;
|
|
line-height: 1.4;
|
|
}
|
|
.loading-state {
|
|
padding: 24px;
|
|
text-align: center;
|
|
color: #64748b;
|
|
}
|
|
`
|
|
]
|
|
})
|
|
export class GlSettingsDialogComponent implements OnInit {
|
|
private fb = inject(FormBuilder);
|
|
private http = inject(HttpClient);
|
|
private sponsorDDService = inject(SponsorDDService);
|
|
private dialogRef = inject(MatDialogRef<GlSettingsDialogComponent>);
|
|
private snackBar = inject(MatSnackBar);
|
|
|
|
form!: FormGroup;
|
|
loading = true;
|
|
saving = false;
|
|
mapping: any = null;
|
|
glAccounts: any[] = [];
|
|
|
|
ngOnInit(): void {
|
|
this.form = this.fb.group({
|
|
// No longer edited here — it lives on the sponsor bank. Kept in the form, unvalidated, so
|
|
// saving this screen round-trips the stored value instead of clearing it: existing banks were
|
|
// seeded from it on upgrade, and nothing should quietly erase what they were seeded from.
|
|
ddPayableGlAccountId: [null],
|
|
chargesIncomeGlAccountId: [
|
|
null,
|
|
[Validators.required]
|
|
],
|
|
gstPayableGlAccountId: [
|
|
null,
|
|
[Validators.required]
|
|
],
|
|
cashGlAccountId: [
|
|
null,
|
|
[Validators.required]
|
|
],
|
|
chequeClearingGlAccountId: [null],
|
|
gstPercentage: [
|
|
18,
|
|
[
|
|
Validators.required,
|
|
Validators.min(0),
|
|
Validators.max(100)
|
|
]
|
|
],
|
|
ddLimitAmount: [
|
|
null,
|
|
[Validators.min(0)]
|
|
]
|
|
});
|
|
|
|
this.loadGlAccountsAndMapping();
|
|
}
|
|
|
|
loadGlAccountsAndMapping(): void {
|
|
this.loading = true;
|
|
// usage=1 (Detail only): these settings map real Sponsor DD transactions onto a GL account, and
|
|
// a Header account can never receive one, so it must never be offered here.
|
|
this.http.get<any[]>('/glaccounts?manualEntriesAllowed=true&disabled=false&usage=1').subscribe({
|
|
next: (accounts) => {
|
|
this.glAccounts = accounts || [];
|
|
this.sponsorDDService.getGlMappings().subscribe({
|
|
next: (mappings) => {
|
|
this.mapping = mappings && mappings.length ? mappings[0] : null;
|
|
if (this.mapping) {
|
|
this.form.patchValue({
|
|
ddPayableGlAccountId: this.mapping.ddPayableGlAccountId ?? null,
|
|
chargesIncomeGlAccountId: this.mapping.chargesIncomeGlAccountId ?? null,
|
|
gstPayableGlAccountId: this.mapping.gstPayableGlAccountId ?? null,
|
|
cashGlAccountId: this.mapping.cashGlAccountId ?? null,
|
|
chequeClearingGlAccountId: this.mapping.chequeClearingGlAccountId ?? null,
|
|
gstPercentage: this.mapping.gstPercentage ?? 18,
|
|
ddLimitAmount: this.mapping.ddLimitAmount ?? null
|
|
});
|
|
}
|
|
this.loading = false;
|
|
},
|
|
error: (err) => {
|
|
this.loading = false;
|
|
this.snackBar.open('Error loading GL mapping: ' + (err.error?.message || err.message), 'Close', {
|
|
duration: 5000
|
|
});
|
|
}
|
|
});
|
|
},
|
|
error: (err) => {
|
|
this.loading = false;
|
|
this.snackBar.open('Error loading GL accounts: ' + (err.error?.message || err.message), 'Close', {
|
|
duration: 5000
|
|
});
|
|
}
|
|
});
|
|
}
|
|
|
|
onSave(): void {
|
|
if (this.form.invalid) return;
|
|
|
|
this.saving = true;
|
|
const val = this.form.value;
|
|
this.sponsorDDService
|
|
.upsertGlMapping({
|
|
sponsorBankId: 0,
|
|
// DD Payable is set per sponsor bank (Organization > Sponsor Banks) and is the only source
|
|
// DD posting reads, so this screen no longer owns it. We still round-trip whatever the row
|
|
// already had -- so an existing value is preserved -- but a tenant that never set it (NULL)
|
|
// now saves fine instead of hitting "Sponsor DD Payable GL account is required".
|
|
ddPayableGlAccountId: this.mapping?.ddPayableGlAccountId ?? null,
|
|
chequeClearingGlAccountId: val.chequeClearingGlAccountId,
|
|
cashGlAccountId: val.cashGlAccountId,
|
|
chargesIncomeGlAccountId: val.chargesIncomeGlAccountId,
|
|
cashSuspenseGlAccountId: this.mapping?.cashSuspenseGlAccountId || val.cashGlAccountId,
|
|
gstPercentage: val.gstPercentage,
|
|
gstPayableGlAccountId: val.gstPayableGlAccountId,
|
|
ddLimitAmount: val.ddLimitAmount
|
|
})
|
|
.subscribe({
|
|
next: (res: any) => {
|
|
// A save submitted for approval comes back HTTP 200 with rollbackTransaction: true - the
|
|
// mapping has not actually been applied yet, so this must not claim success the same way an
|
|
// immediate save does.
|
|
const message = res?.rollbackTransaction
|
|
? 'Sponsor DD GL Mapping & GST Settings submitted for approval'
|
|
: 'Sponsor DD GL Mapping & GST Settings saved successfully!';
|
|
this.snackBar.open(message, 'Close', { duration: res?.rollbackTransaction ? 4000 : 3000 });
|
|
this.dialogRef.close(true);
|
|
},
|
|
error: (err) => {
|
|
this.saving = false;
|
|
this.snackBar.open('Error saving GL settings: ' + (err.error?.message || err.message), 'Close', {
|
|
duration: 5000
|
|
});
|
|
}
|
|
});
|
|
}
|
|
|
|
onClose(): void {
|
|
this.dialogRef.close();
|
|
}
|
|
}
|