feat: cash-only registers, day book sections and DD amendments
build-run / Run Lint, Build and Deploy (push) Canceled after 0s
Publish Image in Docker Hub / build (push) Canceled after 0s
Playwright E2E Tests / Run Playwright Tests (push) Canceled after 0s
Tests / testRigor (push) Canceled after 0s

This commit is contained in:
2026-09-03 18:26:22 +05:30
parent 6442562df0
commit fbce73fd17
111 changed files with 9076 additions and 3507 deletions
@@ -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"
+192 -6
View File
@@ -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 });
}
});
}
}