Files
cbsnexora/src/app/sponsor-dd/gl-settings-dialog/gl-settings-dialog.component.ts
T
niteshandClaude Sonnet 5 7b77166b19 fix: tenant-config fixes, cashier/share/savings UI bugs, and menu-access hardcoding
- Add Balance Sheet report and disabled-features accounting config
- Point menu-access tenant header and env.js/environment.ts tenant identifier
  at env config instead of hardcoded 'default'/'lincpay' literals
- Surface the real backend error (not a generic "no reference id") when an
  OTP send fails in contact-otp-block
- Fix branch-day "₹null" display bug (null vs undefined) and add a guard
  blocking a branch's first-ever day open before cash denomination migration
- Dynamically size the denomination count input and block re-opening an
  already-open business day from Migrate Cash Denomination
- Lock the denomination-managed cash GL row in Migrate Opening Balances
  (read-only, with a link back to Migrate Cash Denomination)
- Remove duplicate Cancel/Submit action bars on the Share Product and
  Savings Account creation preview steps; align Previous into the same row
- Hide "Apply Collection" on a share account once its advance collection
  is already settled (further purchases go through Apply Additional Shares)

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-13 15:19:21 +05:30

282 lines
11 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 { 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,
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 GL Account -->
<mat-form-field appearance="outline" class="full-width">
<mat-label>Sponsor DD Payable GL Account *</mat-label>
<mat-select formControlName="ddPayableGlAccountId" required placeholder="Select DD Payable Liability GL">
<mat-option *ngFor="let acc of glAccounts" [value]="acc.id"> {{ acc.glCode }} · {{ acc.name }} </mat-option>
</mat-select>
<mat-hint>Credited for the DD Face Value</mat-hint>
<mat-error *ngIf="form.get('ddPayableGlAccountId')?.hasError('required')">Required</mat-error>
</mat-form-field>
<!-- 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;
}
.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({
ddPayableGlAccountId: [
null,
[Validators.required]
],
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,
ddPayableGlAccountId: val.ddPayableGlAccountId,
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: () => {
this.snackBar.open('Sponsor DD GL Mapping & GST Settings saved successfully!', 'Close', { duration: 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();
}
}