feat: cash-only registers, day book sections and DD amendments
This commit is contained in:
@@ -122,18 +122,40 @@
|
||||
<th mat-header-cell *matHeaderCellDef>Actions</th>
|
||||
<td mat-cell *matCellDef="let element">
|
||||
<div class="action-btn-group">
|
||||
<!-- An ordinary draft is raised from the counter, not from this registry: Transaction
|
||||
Entry carries the purchaser, the funding and the denomination entry a DD needs,
|
||||
while this screen is the leaflet register — what each leaflet is and what has since
|
||||
happened to it.
|
||||
The button stays for a dividend claim, which arrives here from the dividend
|
||||
dashboard with the beneficiary and amount already settled and only needs a leaflet
|
||||
picked; there is no payment to collect, so there is nothing for the counter to do. -->
|
||||
<button
|
||||
mat-flat-button
|
||||
color="primary"
|
||||
size="small"
|
||||
*ngIf="element.status === 'AVAILABLE' && !element.pendingIssueRequestId"
|
||||
*ngIf="dividendContext && element.status === 'AVAILABLE' && !element.pendingIssueRequestId"
|
||||
(click)="openIssueDialog(element)"
|
||||
>
|
||||
Issue Draft
|
||||
Draw for Dividend
|
||||
</button>
|
||||
<span class="pending-badge" *ngIf="element.status === 'AVAILABLE' && element.pendingIssueRequestId">
|
||||
Pending Approval (Req #{{ element.pendingIssueRequestId }})
|
||||
</span>
|
||||
<!-- Corrects the beneficiary, or the branch and city the draft is payable at. The
|
||||
draft itself is unchanged until a manager approves, so a second request is not
|
||||
offered while one is still pending. -->
|
||||
<button
|
||||
mat-stroked-button
|
||||
color="primary"
|
||||
size="small"
|
||||
*ngIf="element.status === 'ISSUED' && !element.pendingAmendmentRequestId"
|
||||
(click)="openAmendDialog(element)"
|
||||
>
|
||||
Edit
|
||||
</button>
|
||||
<span class="pending-badge" *ngIf="element.status === 'ISSUED' && element.pendingAmendmentRequestId">
|
||||
Amendment Pending (Req #{{ element.pendingAmendmentRequestId }})
|
||||
</span>
|
||||
<button
|
||||
mat-stroked-button
|
||||
color="warn"
|
||||
|
||||
@@ -380,6 +380,26 @@ export class DdListComponent implements OnInit, AfterViewInit {
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Corrects an issued draft's beneficiary, or the branch and city it is payable at.
|
||||
*
|
||||
* The draft is unchanged until a manager approves — it is already printed and with the purchaser,
|
||||
* so the list is reloaded to show the pending request rather than a new set of details.
|
||||
*/
|
||||
openAmendDialog(leaflet: any): void {
|
||||
const dialogRef = this.dialog.open(AmendDdDialogComponent, {
|
||||
width: '560px',
|
||||
data: { leaflet },
|
||||
disableClose: true
|
||||
});
|
||||
|
||||
dialogRef.afterClosed().subscribe((result) => {
|
||||
if (result) {
|
||||
this.loadLeaflets();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
onStopPayment(leaflet: any): void {
|
||||
if (confirm(`Are you sure you want to place a Stop Payment on DD No. ${leaflet.ddNumber}?`)) {
|
||||
this.sponsorDDService
|
||||
@@ -446,8 +466,7 @@ export class DdListComponent implements OnInit, AfterViewInit {
|
||||
providers: [DatePipe],
|
||||
template: `
|
||||
<h2 mat-dialog-title>
|
||||
{{ dividend ? 'Pay Dividend by Demand Draft' : 'Issue Demand Draft' }} (DD No:
|
||||
{{ data.leaflet.ddNumber }})
|
||||
{{ dividend ? 'Pay Dividend by Demand Draft' : 'Issue Demand Draft' }} (DD No: {{ data.leaflet.ddNumber }})
|
||||
</h2>
|
||||
<mat-dialog-content>
|
||||
<!-- A dividend draft is not a counter sale: the society already owes the member the money, so
|
||||
@@ -461,8 +480,8 @@ export class DdListComponent implements OnInit, AfterViewInit {
|
||||
<ng-container *ngIf="dividend.financialYear"> · FY {{ dividend.financialYear }}</ng-container>
|
||||
</span>
|
||||
<span class="dividend-callout__hint">
|
||||
Funded from Dividend Payable — no charge, no GST, nothing collected from the member. The draft is drawn
|
||||
once a manager approves the claim.
|
||||
Funded from Dividend Payable — no charge, no GST, nothing collected from the member. The draft is drawn once
|
||||
a manager approves the claim.
|
||||
</span>
|
||||
</div>
|
||||
</ng-container>
|
||||
@@ -661,8 +680,9 @@ export class DdListComponent implements OnInit, AfterViewInit {
|
||||
|
||||
<ng-container
|
||||
*ngIf="
|
||||
!dividend && (issueForm.get('paymentMethod')?.value === 'ACCOUNT_DEBIT' ||
|
||||
issueForm.get('paymentMethod')?.value === 'CHEQUE')
|
||||
!dividend &&
|
||||
(issueForm.get('paymentMethod')?.value === 'ACCOUNT_DEBIT' ||
|
||||
issueForm.get('paymentMethod')?.value === 'CHEQUE')
|
||||
"
|
||||
>
|
||||
<p class="hint-text" *ngIf="additionalLinesArray.length > 0">
|
||||
@@ -1004,6 +1024,11 @@ export class IssueDraftDialogComponent implements OnInit {
|
||||
Validators.required,
|
||||
Validators.pattern(/^\d{12}$/)
|
||||
]);
|
||||
// This screen has no denomination entry of its own, so the notes can only be counted by the
|
||||
// cashier's deposit request -- which is why the hint above tells the user to have one raised
|
||||
// first. Without this validator the field could be left blank, and the draft was then
|
||||
// recorded as cash with no breakdown behind it: real money in the drawer that never reached
|
||||
// the closing report. Requiring it makes the screen behave the way it already describes.
|
||||
cashierDepositRequestId?.setValidators([Validators.required]);
|
||||
clientId?.disable();
|
||||
this.loadAvailableCashierDeposits();
|
||||
@@ -1984,3 +2009,164 @@ export class PrintDraftDialogComponent implements OnInit {
|
||||
return parts.join(' ') + ' Rupees';
|
||||
}
|
||||
}
|
||||
|
||||
// ── AMEND ISSUED DD DIALOG COMPONENT ──
|
||||
//
|
||||
// Corrects the three details of a draft that can be wrong without the money being wrong: who may
|
||||
// present it, and the branch and city it is payable at. Face value, purchaser and funding are not
|
||||
// offered — the cash for those has already moved, and editing them would leave the ledger
|
||||
// disagreeing with the printed instrument. A draft wrong in that way is cancelled and re-issued.
|
||||
//
|
||||
// The draft itself is untouched until a manager approves: it is already in the purchaser's hands,
|
||||
// so a clerk cannot quietly rewrite what it says.
|
||||
@Component({
|
||||
selector: 'mifosx-amend-dd-dialog',
|
||||
standalone: true,
|
||||
imports: [
|
||||
CommonModule,
|
||||
FormsModule,
|
||||
ReactiveFormsModule,
|
||||
MatDialogModule,
|
||||
MatFormFieldModule,
|
||||
MatInputModule,
|
||||
MatButtonModule
|
||||
],
|
||||
template: `
|
||||
<h2 mat-dialog-title>Amend Demand Draft (DD No: {{ data.leaflet.ddNumber }})</h2>
|
||||
<mat-dialog-content>
|
||||
<form [formGroup]="amendForm" class="dialog-form">
|
||||
<p class="hint-text">
|
||||
Only the beneficiary and where the draft is payable can be corrected. Face value (<strong
|
||||
>₹{{ data.leaflet.faceValue | number: '1.2-2' }}</strong
|
||||
>) and the purchaser cannot — the money for those has already moved, so a draft wrong there is cancelled and
|
||||
re-issued. The draft stays as printed until a manager approves this.
|
||||
</p>
|
||||
|
||||
<mat-form-field appearance="outline" class="full-width">
|
||||
<mat-label>Beneficiary Name</mat-label>
|
||||
<input matInput formControlName="beneficiaryName" style="text-transform: uppercase;" />
|
||||
<mat-hint>Currently: {{ data.leaflet.beneficiaryName || '—' }}</mat-hint>
|
||||
</mat-form-field>
|
||||
|
||||
<div class="row">
|
||||
<mat-form-field appearance="outline" class="half-width">
|
||||
<mat-label>Payable At Branch</mat-label>
|
||||
<input matInput formControlName="payableAtBranch" />
|
||||
<mat-hint>Currently: {{ data.leaflet.payableAtBranch || '—' }}</mat-hint>
|
||||
</mat-form-field>
|
||||
|
||||
<mat-form-field appearance="outline" class="half-width">
|
||||
<mat-label>Payable At City</mat-label>
|
||||
<input matInput formControlName="payableAtCity" />
|
||||
<mat-hint>Currently: {{ data.leaflet.payableAtCity || '—' }}</mat-hint>
|
||||
</mat-form-field>
|
||||
</div>
|
||||
|
||||
<mat-form-field appearance="outline" class="full-width">
|
||||
<mat-label>Reason for Amendment</mat-label>
|
||||
<textarea matInput formControlName="reason" rows="3"></textarea>
|
||||
<mat-error *ngIf="amendForm.get('reason')?.hasError('required')">A reason is required</mat-error>
|
||||
</mat-form-field>
|
||||
</form>
|
||||
</mat-dialog-content>
|
||||
<mat-dialog-actions align="end">
|
||||
<button mat-button (click)="onCancel()">Cancel</button>
|
||||
<button mat-flat-button color="primary" [disabled]="amendForm.invalid || loading" (click)="onSubmit()">
|
||||
Submit for Approval
|
||||
</button>
|
||||
</mat-dialog-actions>
|
||||
`,
|
||||
styles: [
|
||||
`
|
||||
.dialog-form {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 12px;
|
||||
padding-top: 8px;
|
||||
}
|
||||
.full-width {
|
||||
width: 100%;
|
||||
}
|
||||
.row {
|
||||
display: flex;
|
||||
gap: 16px;
|
||||
}
|
||||
.half-width {
|
||||
flex: 1;
|
||||
}
|
||||
.hint-text {
|
||||
margin: 0 0 4px;
|
||||
font-size: 12px;
|
||||
line-height: 1.5;
|
||||
color: #64748b;
|
||||
}
|
||||
`
|
||||
]
|
||||
})
|
||||
export class AmendDdDialogComponent implements OnInit {
|
||||
private fb = inject(FormBuilder);
|
||||
private sponsorDDService = inject(SponsorDDService);
|
||||
private dialogRef = inject(MatDialogRef<AmendDdDialogComponent>);
|
||||
private snackBar = inject(MatSnackBar);
|
||||
protected data = inject(MAT_DIALOG_DATA);
|
||||
|
||||
amendForm!: FormGroup;
|
||||
loading = false;
|
||||
|
||||
ngOnInit(): void {
|
||||
// Prefilled with what the draft says, so the clerk edits the wrong word rather than retyping
|
||||
// all three fields and risking a second mistake.
|
||||
this.amendForm = this.fb.group({
|
||||
beneficiaryName: [this.data.leaflet.beneficiaryName ?? ''],
|
||||
payableAtBranch: [this.data.leaflet.payableAtBranch ?? ''],
|
||||
payableAtCity: [this.data.leaflet.payableAtCity ?? ''],
|
||||
reason: [
|
||||
'',
|
||||
Validators.required
|
||||
]
|
||||
});
|
||||
}
|
||||
|
||||
onCancel(): void {
|
||||
this.dialogRef.close(false);
|
||||
}
|
||||
|
||||
onSubmit(): void {
|
||||
if (this.amendForm.invalid) return;
|
||||
|
||||
const formVal = this.amendForm.value;
|
||||
const beneficiaryName = (formVal.beneficiaryName ?? '').trim();
|
||||
const payableAtBranch = (formVal.payableAtBranch ?? '').trim();
|
||||
const payableAtCity = (formVal.payableAtCity ?? '').trim();
|
||||
|
||||
// Refused here as well as on the server: an amendment that changes nothing still costs a
|
||||
// manager's attention, and saying so now is quicker than a round trip.
|
||||
const unchanged =
|
||||
beneficiaryName === (this.data.leaflet.beneficiaryName ?? '').trim() &&
|
||||
payableAtBranch === (this.data.leaflet.payableAtBranch ?? '').trim() &&
|
||||
payableAtCity === (this.data.leaflet.payableAtCity ?? '').trim();
|
||||
if (unchanged) {
|
||||
this.snackBar.open('Nothing has been changed on this draft.', 'Close', { duration: 4000 });
|
||||
return;
|
||||
}
|
||||
|
||||
this.loading = true;
|
||||
this.sponsorDDService
|
||||
.requestDdAmendment(this.data.leaflet.id, {
|
||||
beneficiaryName: beneficiaryName || undefined,
|
||||
payableAtBranch: payableAtBranch || undefined,
|
||||
payableAtCity: payableAtCity || undefined,
|
||||
reason: formVal.reason.trim()
|
||||
})
|
||||
.subscribe({
|
||||
next: () => {
|
||||
this.snackBar.open('Amendment submitted for manager approval', 'Close', { duration: 3000 });
|
||||
this.dialogRef.close(true);
|
||||
},
|
||||
error: (err: any) => {
|
||||
this.loading = false;
|
||||
this.snackBar.open('Error: ' + (err.error?.message || err.message), 'Close', { duration: 5000 });
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -9,9 +9,11 @@
|
||||
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';
|
||||
@@ -26,67 +28,98 @@ import { SponsorDDService } from '../sponsor-dd.service';
|
||||
MatDialogModule,
|
||||
MatFormFieldModule,
|
||||
MatInputModule,
|
||||
MatSelectModule,
|
||||
MatButtonModule,
|
||||
MatSnackBarModule
|
||||
],
|
||||
template: `
|
||||
<h2 mat-dialog-title>GST / GL Settings</h2>
|
||||
<h2 mat-dialog-title>Sponsor DD GST & GL Mapping Configuration</h2>
|
||||
<mat-dialog-content>
|
||||
<p class="hint-text">
|
||||
GST is charged on the DD issuance commission/charges only, never on the DD face value. The rate below applies to
|
||||
every DD submitted from now on - it's locked in per-request at submission time, so changing it here never
|
||||
affects requests already in flight.
|
||||
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>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')">
|
||||
Must be between 0 and 100
|
||||
</mat-error>
|
||||
<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 Limit Amount</mat-label>
|
||||
<input matInput type="number" formControlName="ddLimitAmount" step="0.01" />
|
||||
<mat-hint>
|
||||
Maximum face value for a single DD, and the maximum combined face value of all DDs issued to the same
|
||||
beneficiary on the same day. Leave blank for no limit.
|
||||
</mat-hint>
|
||||
<mat-error *ngIf="form.get('ddLimitAmount')?.hasError('min')">Cannot be negative</mat-error>
|
||||
<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>
|
||||
|
||||
<div class="readonly-accounts">
|
||||
<div class="account-row">
|
||||
<span>DD Payable</span><strong>{{ mapping?.ddPayableGlAccountName || '-' }}</strong>
|
||||
</div>
|
||||
<div class="account-row">
|
||||
<span>Cheque Clearing</span><strong>{{ mapping?.chequeClearingGlAccountName || '-' }}</strong>
|
||||
</div>
|
||||
<div class="account-row">
|
||||
<span>Cash</span><strong>{{ mapping?.cashGlAccountName || '-' }}</strong>
|
||||
</div>
|
||||
<div class="account-row">
|
||||
<span>Cash (Pending Approval)</span><strong>{{ mapping?.cashSuspenseGlAccountName || '-' }}</strong>
|
||||
</div>
|
||||
<div class="account-row">
|
||||
<span>Charges Income</span><strong>{{ mapping?.chargesIncomeGlAccountName || '-' }}</strong>
|
||||
</div>
|
||||
<div class="account-row">
|
||||
<span>GST Payable</span><strong>{{ mapping?.gstPayableGlAccountName || '-' }}</strong>
|
||||
</div>
|
||||
<!-- 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>
|
||||
<p class="hint-text">
|
||||
GL account assignments above are set up once by an administrator and shown here read-only.
|
||||
</p>
|
||||
</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()">
|
||||
Save
|
||||
{{ saving ? 'Saving...' : 'Save Mapping' }}
|
||||
</button>
|
||||
</mat-dialog-actions>
|
||||
`,
|
||||
@@ -101,25 +134,22 @@ import { SponsorDDService } from '../sponsor-dd.service';
|
||||
.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 12px 0;
|
||||
margin: 0 0 16px 0;
|
||||
line-height: 1.4;
|
||||
}
|
||||
.readonly-accounts {
|
||||
border: 1px solid #e2e8f0;
|
||||
border-radius: 8px;
|
||||
padding: 12px 16px;
|
||||
background: #f8fafc;
|
||||
}
|
||||
.account-row {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
font-size: 13px;
|
||||
padding: 4px 0;
|
||||
color: #334155;
|
||||
}
|
||||
.account-row span {
|
||||
.loading-state {
|
||||
padding: 24px;
|
||||
text-align: center;
|
||||
color: #64748b;
|
||||
}
|
||||
`
|
||||
@@ -127,6 +157,7 @@ import { SponsorDDService } from '../sponsor-dd.service';
|
||||
})
|
||||
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);
|
||||
@@ -135,9 +166,27 @@ export class GlSettingsDialogComponent implements OnInit {
|
||||
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,
|
||||
[
|
||||
@@ -151,20 +200,42 @@ export class GlSettingsDialogComponent implements OnInit {
|
||||
[Validators.min(0)]
|
||||
]
|
||||
});
|
||||
this.sponsorDDService.getGlMappings().subscribe({
|
||||
next: (mappings) => {
|
||||
this.mapping = mappings && mappings.length ? mappings[0] : null;
|
||||
if (this.mapping) {
|
||||
this.form.patchValue({
|
||||
gstPercentage: this.mapping.gstPercentage ?? 18,
|
||||
ddLimitAmount: this.mapping.ddLimitAmount ?? null
|
||||
});
|
||||
}
|
||||
this.loading = false;
|
||||
|
||||
this.loadGlAccountsAndMapping();
|
||||
}
|
||||
|
||||
loadGlAccountsAndMapping(): void {
|
||||
this.loading = true;
|
||||
this.http.get<any[]>('/glaccounts?manualEntriesAllowed=true&disabled=false').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 settings: ' + (err.error?.message || err.message), 'Close', {
|
||||
this.snackBar.open('Error loading GL accounts: ' + (err.error?.message || err.message), 'Close', {
|
||||
duration: 5000
|
||||
});
|
||||
}
|
||||
@@ -172,29 +243,32 @@ export class GlSettingsDialogComponent implements OnInit {
|
||||
}
|
||||
|
||||
onSave(): void {
|
||||
if (this.form.invalid || !this.mapping) return;
|
||||
if (this.form.invalid) return;
|
||||
|
||||
this.saving = true;
|
||||
const val = this.form.value;
|
||||
this.sponsorDDService
|
||||
.upsertGlMapping({
|
||||
sponsorBankId: 0,
|
||||
ddPayableGlAccountId: this.mapping.ddPayableGlAccountId,
|
||||
chequeClearingGlAccountId: this.mapping.chequeClearingGlAccountId,
|
||||
cashGlAccountId: this.mapping.cashGlAccountId,
|
||||
chargesIncomeGlAccountId: this.mapping.chargesIncomeGlAccountId,
|
||||
cashSuspenseGlAccountId: this.mapping.cashSuspenseGlAccountId,
|
||||
gstPercentage: this.form.value.gstPercentage,
|
||||
gstPayableGlAccountId: this.mapping.gstPayableGlAccountId,
|
||||
ddLimitAmount: this.form.value.ddLimitAmount
|
||||
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('GST percentage updated', 'Close', { duration: 3000 });
|
||||
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: ' + (err.error?.message || err.message), 'Close', { duration: 5000 });
|
||||
this.snackBar.open('Error saving GL settings: ' + (err.error?.message || err.message), 'Close', {
|
||||
duration: 5000
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@@ -49,6 +49,18 @@ export class SponsorDDService {
|
||||
return this.http.get<any>(`/sponsordd/leaflets/by-number/${ddNumber}`);
|
||||
}
|
||||
|
||||
getNextAvailableLeaflet(branchId?: number, sponsorBankId?: number, inventoryId?: number): Observable<any> {
|
||||
const params: any = {};
|
||||
if (branchId) params.branchId = branchId;
|
||||
if (sponsorBankId) params.sponsorBankId = sponsorBankId;
|
||||
if (inventoryId) params.inventoryId = inventoryId;
|
||||
return this.http.get<any>('/sponsordd/leaflets/next-available', { params });
|
||||
}
|
||||
|
||||
getSponsorBanks(): Observable<any[]> {
|
||||
return this.http.get<any[]>('/sponsorbanks');
|
||||
}
|
||||
|
||||
// ── Actions ──
|
||||
issueDemandDraft(
|
||||
leafletId: number,
|
||||
@@ -102,6 +114,7 @@ export class SponsorDDService {
|
||||
chequeNumber?: string;
|
||||
chequeBankName?: string;
|
||||
cashierDepositRequestId?: number;
|
||||
denominationData?: string;
|
||||
}
|
||||
): Observable<any> {
|
||||
return this.http.post<any>(`/sponsordd/leaflets/${leafletId}/issue-request`, data);
|
||||
@@ -205,6 +218,35 @@ export class SponsorDDService {
|
||||
return this.http.post<any>(`/sponsordd/reprint-requests/${id}/reject`, { rejectionReason });
|
||||
}
|
||||
|
||||
// ── DD amendment requests (maker-checker) ──
|
||||
// Correcting an issued draft's beneficiary, or the branch and city it is payable at. Nothing else
|
||||
// is amendable: the money behind the face value has already moved, so a draft wrong in that way
|
||||
// is cancelled and re-issued rather than edited.
|
||||
|
||||
/**
|
||||
* Maker step. A field left blank keeps what the draft already carries, so correcting only the
|
||||
* city does not mean retyping the beneficiary and risking a second mistake.
|
||||
*/
|
||||
requestDdAmendment(
|
||||
leafletId: number,
|
||||
changes: { beneficiaryName?: string; payableAtBranch?: string; payableAtCity?: string; reason: string }
|
||||
): Observable<any> {
|
||||
return this.http.post<any>(`/sponsordd/leaflets/${leafletId}/amendment-request`, changes);
|
||||
}
|
||||
|
||||
getAllAmendmentRequests(status?: string): Observable<any[]> {
|
||||
const params = status ? { status } : {};
|
||||
return this.http.get<any[]>('/sponsordd/amendment-requests', { params });
|
||||
}
|
||||
|
||||
approveAmendmentRequest(id: number): Observable<any> {
|
||||
return this.http.post<any>(`/sponsordd/amendment-requests/${id}/approve`, {});
|
||||
}
|
||||
|
||||
rejectAmendmentRequest(id: number, rejectionReason: string): Observable<any> {
|
||||
return this.http.post<any>(`/sponsordd/amendment-requests/${id}/reject`, { rejectionReason });
|
||||
}
|
||||
|
||||
// ── Approver signature (stamped onto the printed DD) ──
|
||||
// Reuses Fineract's generic document-attachment API (entityType is a free-form string with
|
||||
// no per-type backend wiring required) rather than building dedicated signature storage.
|
||||
|
||||
Reference in New Issue
Block a user