feat(cashier, sponsor-dd): GL settlement and DD funding entry points, DD registry redesign
Transaction Entry (clerk): - Route a two-row Fund Transfer of Dr SHR / Cr GL to the share-withdrawal request with settlementMode GL, so a clerk can settle a share withdrawal into any GL account instead of the member's linked savings account. - Offer GL as an account type for Demand Draft funding, and submit it as a GL debit (no client or savings/current account is involved). My Work: - Add a voucher detail dialog so a clerk can open a raised voucher from the request list and see its GL lines. Closing Cash Report: - Fix the total row so it reflects the corrected opening balance. DD Leaflet Registry and DD Inventory: - Redesign both pages onto the shared usr-* visual language: gradient header badge, status filter chips with counts, card-wrapped sticky-header table, and one primary action per row with the rest behind an overflow menu. Filtering, pagination and every action condition are unchanged. - Register faHandPaper, needed by the new Stop Payment menu item. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -5,124 +5,244 @@
|
||||
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="inventory-container">
|
||||
<div class="header-row">
|
||||
<div class="title-area">
|
||||
<h2>Sponsor Bank DD Inventory</h2>
|
||||
<p>Manage allocated ranges of physical Demand Draft leaflets received from Sponsor Banks.</p>
|
||||
<!--
|
||||
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="inv-container">
|
||||
<!-- Hero header -->
|
||||
<div class="inv-hero">
|
||||
<div class="inv-hero__left">
|
||||
<div class="inv-hero__icon">
|
||||
<fa-icon icon="university"></fa-icon>
|
||||
</div>
|
||||
<div>
|
||||
<h1 class="inv-hero__title">Sponsor Bank DD Inventory</h1>
|
||||
<p class="inv-hero__sub">
|
||||
Manage allocated ranges of physical Demand Draft leaflets received from Sponsor Banks.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<button mat-flat-button color="primary" (click)="openAddInventoryDialog()">Register DD Series</button>
|
||||
<button class="inv-btn primary" (click)="openAddInventoryDialog()">
|
||||
<fa-icon icon="plus"></fa-icon>
|
||||
Register DD Series
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<!-- Says why the clerk is here when they arrived from a dividend claim rather than from the menu.
|
||||
Without it, a page listing every DD series gives no clue which member is waiting at the
|
||||
counter or how much the draft has to be drawn for. -->
|
||||
<!-- Dividend Context Banner -->
|
||||
@if (dividendContext) {
|
||||
<div class="dividend-banner">
|
||||
<div class="dividend-banner__text">
|
||||
<strong>Issuing a dividend Demand Draft</strong>
|
||||
<div class="inv-dividend-banner">
|
||||
<div class="inv-dividend-banner__ic">
|
||||
<fa-icon icon="hand-holding-usd"></fa-icon>
|
||||
</div>
|
||||
<div class="inv-dividend-banner__text">
|
||||
<strong>Issuing Dividend Demand Draft</strong>
|
||||
<span>
|
||||
{{ dividendContext.beneficiary }} ·
|
||||
{{ dividendContext.amount | number: '1.2-2' }}
|
||||
Beneficiary: <strong>{{ dividendContext.beneficiary }}</strong> · Amount:
|
||||
<strong>₹{{ dividendContext.amount | number: '1.2-2' }}</strong>
|
||||
@if (dividendContext.financialYear) {
|
||||
· FY {{ dividendContext.financialYear }}
|
||||
}
|
||||
</span>
|
||||
<span class="dividend-banner__hint">Open a series below and choose an available leaflet.</span>
|
||||
<span class="inv-dividend-banner__hint">Select a series below to pick an available leaflet.</span>
|
||||
</div>
|
||||
<button mat-button class="action-btn" (click)="cancelDividendClaim()">Cancel</button>
|
||||
<button class="inv-btn ghost small" (click)="cancelDividendClaim()">
|
||||
<fa-icon icon="times-circle"></fa-icon>
|
||||
Cancel Claim
|
||||
</button>
|
||||
</div>
|
||||
}
|
||||
|
||||
<mat-card>
|
||||
<table mat-table [dataSource]="dataSource">
|
||||
<!-- ID Column -->
|
||||
<ng-container matColumnDef="id">
|
||||
<th mat-header-cell *matHeaderCellDef>Batch ID</th>
|
||||
<td mat-cell *matCellDef="let element">{{ element.id }}</td>
|
||||
</ng-container>
|
||||
|
||||
<!-- Sponsor Bank Column -->
|
||||
<ng-container matColumnDef="sponsorBankName">
|
||||
<th mat-header-cell *matHeaderCellDef>Sponsor Bank</th>
|
||||
<td mat-cell *matCellDef="let element">{{ element.sponsorBankName }}</td>
|
||||
</ng-container>
|
||||
|
||||
<!-- Branch Column -->
|
||||
<ng-container matColumnDef="branchName">
|
||||
<th mat-header-cell *matHeaderCellDef>Branch</th>
|
||||
<td mat-cell *matCellDef="let element">{{ element.branchName }}</td>
|
||||
</ng-container>
|
||||
|
||||
<!-- Series Start Column -->
|
||||
<ng-container matColumnDef="seriesStart">
|
||||
<th mat-header-cell *matHeaderCellDef>Start No.</th>
|
||||
<td mat-cell *matCellDef="let element">{{ element.seriesStart }}</td>
|
||||
</ng-container>
|
||||
|
||||
<!-- Series End Column -->
|
||||
<ng-container matColumnDef="seriesEnd">
|
||||
<th mat-header-cell *matHeaderCellDef>End No.</th>
|
||||
<td mat-cell *matCellDef="let element">{{ element.seriesEnd }}</td>
|
||||
</ng-container>
|
||||
|
||||
<!-- Total Leaves Column -->
|
||||
<ng-container matColumnDef="totalLeaves">
|
||||
<th mat-header-cell *matHeaderCellDef>Total Leaves</th>
|
||||
<td mat-cell *matCellDef="let element">{{ element.totalLeaves }}</td>
|
||||
</ng-container>
|
||||
|
||||
<!-- Available Leaves Column -->
|
||||
<ng-container matColumnDef="availableLeaves">
|
||||
<th mat-header-cell *matHeaderCellDef>Available Leaves</th>
|
||||
<td mat-cell *matCellDef="let element">
|
||||
<span [class.leaves-low]="element.availableLeaves === 0">{{ element.availableLeaves }}</span>
|
||||
</td>
|
||||
</ng-container>
|
||||
|
||||
<!-- Received Date Column -->
|
||||
<ng-container matColumnDef="receivedDate">
|
||||
<th mat-header-cell *matHeaderCellDef>Received Date</th>
|
||||
<td mat-cell *matCellDef="let element">{{ element.receivedDate }}</td>
|
||||
</ng-container>
|
||||
|
||||
<!-- Status Column -->
|
||||
<ng-container matColumnDef="status">
|
||||
<th mat-header-cell *matHeaderCellDef>Status</th>
|
||||
<td mat-cell *matCellDef="let element">
|
||||
<span class="status-badge" [ngClass]="element.status.toLowerCase()">
|
||||
{{ element.status }}
|
||||
</span>
|
||||
</td>
|
||||
</ng-container>
|
||||
|
||||
<!-- Actions Column -->
|
||||
<ng-container matColumnDef="actions">
|
||||
<th mat-header-cell *matHeaderCellDef>Actions</th>
|
||||
<td mat-cell *matCellDef="let element">
|
||||
<!-- The dividend context travels on, so the leaflets page knows this is a dividend
|
||||
draft and not an ordinary counter sale. -->
|
||||
<button
|
||||
mat-button
|
||||
class="action-btn"
|
||||
[routerLink]="['/sponsor-dd/leaflets', element.id]"
|
||||
[queryParams]="dividendQueryParams"
|
||||
>
|
||||
{{ dividendContext ? 'Choose Leaflet' : 'View Leaflets' }}
|
||||
</button>
|
||||
</td>
|
||||
</ng-container>
|
||||
|
||||
<tr mat-header-row *matHeaderRowDef="displayedColumns"></tr>
|
||||
<tr mat-row *matRowDef="let row; columns: displayedColumns"></tr>
|
||||
</table>
|
||||
|
||||
<div
|
||||
*ngIf="dataSource.length === 0"
|
||||
class="no-records-row"
|
||||
style="text-align: center; padding: 32px; color: #64748b"
|
||||
>
|
||||
No DD series registered yet. Click "Register DD Series" to add stock.
|
||||
<!-- KPI Metrics Bar -->
|
||||
<div class="inv-kpi-grid">
|
||||
<div class="inv-kpi-card">
|
||||
<div class="inv-kpi-card__icon blue">
|
||||
<fa-icon icon="layer-group"></fa-icon>
|
||||
</div>
|
||||
<div class="inv-kpi-card__info">
|
||||
<span class="inv-kpi-card__label">Total Series Batches</span>
|
||||
<span class="inv-kpi-card__val">{{ totalBatches }}</span>
|
||||
</div>
|
||||
</div>
|
||||
</mat-card>
|
||||
|
||||
<div class="inv-kpi-card">
|
||||
<div class="inv-kpi-card__icon green">
|
||||
<fa-icon icon="check-circle"></fa-icon>
|
||||
</div>
|
||||
<div class="inv-kpi-card__info">
|
||||
<span class="inv-kpi-card__label">Active Stock Series</span>
|
||||
<span class="inv-kpi-card__val">{{ activeBatchesCount }}</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="inv-kpi-card">
|
||||
<div class="inv-kpi-card__icon purple">
|
||||
<fa-icon icon="money-check"></fa-icon>
|
||||
</div>
|
||||
<div class="inv-kpi-card__info">
|
||||
<span class="inv-kpi-card__label">Total Allocated Leaves</span>
|
||||
<span class="inv-kpi-card__val">{{ totalLeavesCount | number }}</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="inv-kpi-card">
|
||||
<div class="inv-kpi-card__icon emerald">
|
||||
<fa-icon icon="list-ul"></fa-icon>
|
||||
</div>
|
||||
<div class="inv-kpi-card__info">
|
||||
<span class="inv-kpi-card__label">Available Leaves</span>
|
||||
<span class="inv-kpi-card__val">{{ totalAvailableLeaves | number }}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Search & Filter Controls -->
|
||||
<div class="inv-toolbar">
|
||||
<div class="inv-search">
|
||||
<fa-icon icon="search" class="inv-search__ic"></fa-icon>
|
||||
<input
|
||||
type="text"
|
||||
[(ngModel)]="searchText"
|
||||
placeholder="Search by Sponsor Bank, Branch, Batch ID, or Series No..."
|
||||
/>
|
||||
<button class="inv-search__clear" *ngIf="searchText" (click)="searchText = ''" matTooltip="Clear">
|
||||
<fa-icon icon="times"></fa-icon>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div class="inv-filter-chips">
|
||||
<button class="inv-chip" [class.active]="selectedStatusFilter === 'ALL'" (click)="selectedStatusFilter = 'ALL'">
|
||||
All Batches
|
||||
<span class="inv-chip__count">{{ dataSource.length }}</span>
|
||||
</button>
|
||||
<button
|
||||
class="inv-chip active-chip"
|
||||
[class.active]="selectedStatusFilter === 'ACTIVE'"
|
||||
(click)="selectedStatusFilter = 'ACTIVE'"
|
||||
>
|
||||
<span class="inv-chip__dot green"></span>
|
||||
Active
|
||||
<span class="inv-chip__count">{{ activeBatchesCount }}</span>
|
||||
</button>
|
||||
<button
|
||||
class="inv-chip exhausted-chip"
|
||||
[class.active]="selectedStatusFilter === 'EXHAUSTED'"
|
||||
(click)="selectedStatusFilter = 'EXHAUSTED'"
|
||||
>
|
||||
<span class="inv-chip__dot grey"></span>
|
||||
Exhausted
|
||||
<span class="inv-chip__count">{{ dataSource.length - activeBatchesCount }}</span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Main Card & Data Table -->
|
||||
<div class="inv-card">
|
||||
<div class="inv-card__head">
|
||||
<div class="inv-card__title">
|
||||
<fa-icon icon="table" class="inv-card__title-ic"></fa-icon>
|
||||
Registered Series
|
||||
<span class="inv-card__count">{{ filteredData.length }} of {{ dataSource.length }}</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="inv-table-wrap">
|
||||
<table mat-table [dataSource]="filteredData" class="inv-table">
|
||||
<!-- ID Column -->
|
||||
<ng-container matColumnDef="id">
|
||||
<th mat-header-cell *matHeaderCellDef>Batch ID</th>
|
||||
<td mat-cell *matCellDef="let element" class="inv-mono inv-strong">#{{ element.id }}</td>
|
||||
</ng-container>
|
||||
|
||||
<!-- Sponsor Bank Column -->
|
||||
<ng-container matColumnDef="sponsorBankName">
|
||||
<th mat-header-cell *matHeaderCellDef>Sponsor Bank</th>
|
||||
<td mat-cell *matCellDef="let element" class="inv-cell-bank">
|
||||
<fa-icon icon="building" class="bank-icon"></fa-icon>
|
||||
<span class="inv-strong">{{ element.sponsorBankName }}</span>
|
||||
</td>
|
||||
</ng-container>
|
||||
|
||||
<!-- Branch Column -->
|
||||
<ng-container matColumnDef="branchName">
|
||||
<th mat-header-cell *matHeaderCellDef>Branch</th>
|
||||
<td mat-cell *matCellDef="let element">{{ element.branchName }}</td>
|
||||
</ng-container>
|
||||
|
||||
<!-- Series Start Column -->
|
||||
<ng-container matColumnDef="seriesStart">
|
||||
<th mat-header-cell *matHeaderCellDef>Start No.</th>
|
||||
<td mat-cell *matCellDef="let element" class="inv-mono">{{ element.seriesStart }}</td>
|
||||
</ng-container>
|
||||
|
||||
<!-- Series End Column -->
|
||||
<ng-container matColumnDef="seriesEnd">
|
||||
<th mat-header-cell *matHeaderCellDef>End No.</th>
|
||||
<td mat-cell *matCellDef="let element" class="inv-mono">{{ element.seriesEnd }}</td>
|
||||
</ng-container>
|
||||
|
||||
<!-- Total Leaves Column -->
|
||||
<ng-container matColumnDef="totalLeaves">
|
||||
<th mat-header-cell *matHeaderCellDef class="inv-num">Total Leaves</th>
|
||||
<td mat-cell *matCellDef="let element" class="inv-num inv-mono">{{ element.totalLeaves }}</td>
|
||||
</ng-container>
|
||||
|
||||
<!-- Available Leaves Column -->
|
||||
<ng-container matColumnDef="availableLeaves">
|
||||
<th mat-header-cell *matHeaderCellDef class="inv-num">Available Leaves</th>
|
||||
<td mat-cell *matCellDef="let element" class="inv-num">
|
||||
<span class="inv-leaves-badge" [class.low]="element.availableLeaves === 0">
|
||||
{{ element.availableLeaves }}
|
||||
</span>
|
||||
</td>
|
||||
</ng-container>
|
||||
|
||||
<!-- Received Date Column -->
|
||||
<ng-container matColumnDef="receivedDate">
|
||||
<th mat-header-cell *matHeaderCellDef>Received Date</th>
|
||||
<td mat-cell *matCellDef="let element" class="inv-muted inv-mono">{{ element.receivedDate }}</td>
|
||||
</ng-container>
|
||||
|
||||
<!-- Status Column -->
|
||||
<ng-container matColumnDef="status">
|
||||
<th mat-header-cell *matHeaderCellDef>Status</th>
|
||||
<td mat-cell *matCellDef="let element">
|
||||
<span class="inv-badge" [ngClass]="element.status.toLowerCase()">
|
||||
<span class="inv-badge__dot"></span>
|
||||
{{ element.status }}
|
||||
</span>
|
||||
</td>
|
||||
</ng-container>
|
||||
|
||||
<!-- Actions Column -->
|
||||
<ng-container matColumnDef="actions">
|
||||
<th mat-header-cell *matHeaderCellDef class="inv-actions-col">Actions</th>
|
||||
<td mat-cell *matCellDef="let element" class="inv-actions-col">
|
||||
<button
|
||||
class="inv-btn primary small"
|
||||
[routerLink]="['/sponsor-dd/leaflets', element.id]"
|
||||
[queryParams]="dividendQueryParams"
|
||||
>
|
||||
<fa-icon [icon]="dividendContext ? 'hand-holding-usd' : 'eye'"></fa-icon>
|
||||
{{ dividendContext ? 'Choose Leaflet' : 'View Leaflets' }}
|
||||
</button>
|
||||
</td>
|
||||
</ng-container>
|
||||
|
||||
<tr mat-header-row *matHeaderRowDef="displayedColumns"></tr>
|
||||
<tr mat-row *matRowDef="let row; columns: displayedColumns"></tr>
|
||||
</table>
|
||||
|
||||
@if (filteredData.length === 0) {
|
||||
<div class="inv-empty">
|
||||
<fa-icon icon="inbox" class="inv-empty__ic"></fa-icon>
|
||||
<p class="inv-empty__title">No DD Series Found</p>
|
||||
<p class="inv-empty__sub">No registered DD stock series match your filter criteria.</p>
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -22,6 +22,8 @@ import { MatNativeDateModule } from '@angular/material/core';
|
||||
import { MatSnackBar, MatSnackBarModule } from '@angular/material/snack-bar';
|
||||
import { FaIconComponent } from '@fortawesome/angular-fontawesome';
|
||||
import { HttpClient } from '@angular/common/http';
|
||||
import { MatIconModule } from '@angular/material/icon';
|
||||
import { MatTooltipModule } from '@angular/material/tooltip';
|
||||
import { SponsorDDService } from '../sponsor-dd.service';
|
||||
|
||||
@Component({
|
||||
@@ -30,106 +32,482 @@ import { SponsorDDService } from '../sponsor-dd.service';
|
||||
imports: [
|
||||
CommonModule,
|
||||
RouterModule,
|
||||
FormsModule,
|
||||
ReactiveFormsModule,
|
||||
MatCardModule,
|
||||
MatTableModule,
|
||||
MatButtonModule,
|
||||
MatDialogModule,
|
||||
MatSnackBarModule
|
||||
MatSnackBarModule,
|
||||
MatFormFieldModule,
|
||||
MatInputModule,
|
||||
MatIconModule,
|
||||
MatTooltipModule,
|
||||
FaIconComponent
|
||||
],
|
||||
templateUrl: './dd-inventory-list.component.html',
|
||||
styles: [
|
||||
`
|
||||
.inventory-container {
|
||||
.inv-container {
|
||||
padding: 24px;
|
||||
max-width: 1200px;
|
||||
max-width: 1360px;
|
||||
margin: 0 auto;
|
||||
}
|
||||
.header-row {
|
||||
|
||||
/* Hero Header */
|
||||
.inv-hero {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
margin-bottom: 24px;
|
||||
gap: 16px;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
.title-area h2 {
|
||||
margin: 0 0 4px 0;
|
||||
font-size: 24px;
|
||||
font-weight: 600;
|
||||
color: #1e293b;
|
||||
}
|
||||
.title-area p {
|
||||
margin: 0;
|
||||
color: #64748b;
|
||||
font-size: 14px;
|
||||
}
|
||||
.mat-mdc-card {
|
||||
border-radius: 12px;
|
||||
border: 1px solid #e2e8f0;
|
||||
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.05);
|
||||
overflow: hidden;
|
||||
}
|
||||
table {
|
||||
width: 100%;
|
||||
}
|
||||
th {
|
||||
font-weight: 600;
|
||||
color: #475569;
|
||||
background-color: #f8fafc;
|
||||
}
|
||||
td {
|
||||
color: #334155;
|
||||
}
|
||||
.status-badge {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
padding: 4px 8px;
|
||||
border-radius: 9999px;
|
||||
font-size: 12px;
|
||||
font-weight: 500;
|
||||
}
|
||||
.status-badge.active {
|
||||
background-color: #dcfce7;
|
||||
color: #15803d;
|
||||
}
|
||||
.status-badge.exhausted {
|
||||
background-color: #f1f5f9;
|
||||
color: #475569;
|
||||
}
|
||||
.action-btn {
|
||||
color: #7c3aed;
|
||||
font-weight: 500;
|
||||
}
|
||||
.leaves-low {
|
||||
color: #b91c1c;
|
||||
font-weight: 600;
|
||||
}
|
||||
.dividend-banner {
|
||||
.inv-hero__left {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 16px;
|
||||
margin-bottom: 20px;
|
||||
padding: 12px 16px;
|
||||
border: 1px solid #c7d2fe;
|
||||
border-left: 4px solid #4f46e5;
|
||||
border-radius: 8px;
|
||||
background-color: #eef2ff;
|
||||
}
|
||||
.dividend-banner__text {
|
||||
.inv-hero__icon {
|
||||
width: 52px;
|
||||
height: 52px;
|
||||
border-radius: 14px;
|
||||
background: linear-gradient(135deg, #2563eb 0%, #4f46e5 100%);
|
||||
color: #ffffff;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
font-size: 22px;
|
||||
box-shadow: 0 4px 14px rgba(37, 99, 235, 0.25);
|
||||
}
|
||||
.inv-hero__title {
|
||||
margin: 0;
|
||||
font-size: 22px;
|
||||
font-weight: 800;
|
||||
color: #0f172a;
|
||||
letter-spacing: -0.02em;
|
||||
}
|
||||
.inv-hero__sub {
|
||||
margin: 2px 0 0 0;
|
||||
font-size: 13.5px;
|
||||
color: #64748b;
|
||||
}
|
||||
|
||||
/* Dividend Banner */
|
||||
.inv-dividend-banner {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 16px;
|
||||
margin-bottom: 24px;
|
||||
padding: 16px 20px;
|
||||
border-radius: 12px;
|
||||
border: 1px solid #c7d2fe;
|
||||
background: linear-gradient(135deg, #eef2ff 0%, #f5f3ff 100%);
|
||||
box-shadow: 0 2px 8px rgba(79, 70, 229, 0.08);
|
||||
}
|
||||
.inv-dividend-banner__ic {
|
||||
width: 40px;
|
||||
height: 40px;
|
||||
border-radius: 10px;
|
||||
background: #4f46e5;
|
||||
color: #fff;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
font-size: 16px;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
.inv-dividend-banner__text {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 2px;
|
||||
font-size: 13px;
|
||||
font-size: 13.5px;
|
||||
color: #312e81;
|
||||
}
|
||||
.dividend-banner__text strong {
|
||||
font-size: 13.5px;
|
||||
}
|
||||
.dividend-banner__hint {
|
||||
color: #4338ca;
|
||||
.inv-dividend-banner__hint {
|
||||
font-size: 12px;
|
||||
color: #4338ca;
|
||||
}
|
||||
.dividend-banner button {
|
||||
.inv-dividend-banner button {
|
||||
margin-left: auto;
|
||||
}
|
||||
|
||||
/* KPI Cards */
|
||||
.inv-kpi-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fit, minmax(240px, 1fr));
|
||||
gap: 16px;
|
||||
margin-bottom: 24px;
|
||||
}
|
||||
.inv-kpi-card {
|
||||
background: #ffffff;
|
||||
border-radius: 12px;
|
||||
padding: 18px 20px;
|
||||
border: 1px solid #e2e8f0;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 16px;
|
||||
box-shadow: 0 1px 3px rgba(15, 23, 42, 0.04);
|
||||
transition:
|
||||
transform 0.15s ease,
|
||||
box-shadow 0.15s ease;
|
||||
}
|
||||
.inv-kpi-card:hover {
|
||||
transform: translateY(-2px);
|
||||
box-shadow: 0 6px 16px rgba(15, 23, 42, 0.08);
|
||||
}
|
||||
.inv-kpi-card__icon {
|
||||
width: 46px;
|
||||
height: 46px;
|
||||
border-radius: 12px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
font-size: 18px;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
.inv-kpi-card__icon.blue {
|
||||
background: #eff6ff;
|
||||
color: #2563eb;
|
||||
}
|
||||
.inv-kpi-card__icon.green {
|
||||
background: #f0fdf4;
|
||||
color: #16a34a;
|
||||
}
|
||||
.inv-kpi-card__icon.purple {
|
||||
background: #faf5ff;
|
||||
color: #9333ea;
|
||||
}
|
||||
.inv-kpi-card__icon.emerald {
|
||||
background: #ecfdf5;
|
||||
color: #059669;
|
||||
}
|
||||
.inv-kpi-card__info {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
.inv-kpi-card__label {
|
||||
font-size: 12px;
|
||||
font-weight: 600;
|
||||
color: #64748b;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.04em;
|
||||
}
|
||||
.inv-kpi-card__val {
|
||||
font-size: 24px;
|
||||
font-weight: 800;
|
||||
color: #0f172a;
|
||||
margin-top: 2px;
|
||||
line-height: 1;
|
||||
}
|
||||
|
||||
/* Toolbar */
|
||||
.inv-toolbar {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
gap: 16px;
|
||||
margin-bottom: 16px;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
.inv-search {
|
||||
position: relative;
|
||||
flex: 1;
|
||||
min-width: 280px;
|
||||
max-width: 460px;
|
||||
}
|
||||
.inv-search input {
|
||||
width: 100%;
|
||||
padding: 10px 14px 10px 38px;
|
||||
border: 1px solid #cbd5e1;
|
||||
border-radius: 10px;
|
||||
font-size: 13.5px;
|
||||
background: #ffffff;
|
||||
color: #0f172a;
|
||||
outline: none;
|
||||
box-sizing: border-box;
|
||||
transition:
|
||||
border-color 0.15s,
|
||||
box-shadow 0.15s;
|
||||
}
|
||||
.inv-search input:focus {
|
||||
border-color: #2563eb;
|
||||
box-shadow: 0 0 0 3px rgba(37, 99, 235, 0.12);
|
||||
}
|
||||
.inv-search__ic {
|
||||
position: absolute;
|
||||
left: 13px;
|
||||
top: 50%;
|
||||
transform: translateY(-50%);
|
||||
color: #94a3b8;
|
||||
font-size: 13px;
|
||||
pointer-events: none;
|
||||
}
|
||||
.inv-search__clear {
|
||||
position: absolute;
|
||||
right: 10px;
|
||||
top: 50%;
|
||||
transform: translateY(-50%);
|
||||
border: none;
|
||||
background: transparent;
|
||||
color: #94a3b8;
|
||||
cursor: pointer;
|
||||
padding: 4px;
|
||||
}
|
||||
.inv-search__clear:hover {
|
||||
color: #ef4444;
|
||||
}
|
||||
|
||||
.inv-filter-chips {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
}
|
||||
.inv-chip {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
padding: 8px 14px;
|
||||
border-radius: 999px;
|
||||
border: 1px solid #e2e8f0;
|
||||
background: #ffffff;
|
||||
color: #475569;
|
||||
font-size: 13px;
|
||||
font-weight: 600;
|
||||
cursor: pointer;
|
||||
transition: all 0.15s ease;
|
||||
}
|
||||
.inv-chip:hover {
|
||||
background: #f8fafc;
|
||||
border-color: #cbd5e1;
|
||||
}
|
||||
.inv-chip.active {
|
||||
background: #1e40af;
|
||||
border-color: #1e40af;
|
||||
color: #ffffff !important;
|
||||
}
|
||||
.inv-chip.active-chip.active {
|
||||
background: #16a34a;
|
||||
border-color: #16a34a;
|
||||
color: #ffffff !important;
|
||||
}
|
||||
.inv-chip.exhausted-chip.active {
|
||||
background: #475569;
|
||||
border-color: #475569;
|
||||
color: #ffffff !important;
|
||||
}
|
||||
.inv-chip__dot {
|
||||
width: 7px;
|
||||
height: 7px;
|
||||
border-radius: 50%;
|
||||
}
|
||||
.inv-chip__dot.green {
|
||||
background: #16a34a;
|
||||
}
|
||||
.inv-chip__dot.grey {
|
||||
background: #64748b;
|
||||
}
|
||||
.inv-chip.active .inv-chip__dot {
|
||||
background: #ffffff;
|
||||
}
|
||||
.inv-chip__count {
|
||||
padding: 2px 7px;
|
||||
border-radius: 999px;
|
||||
background: rgba(0, 0, 0, 0.07);
|
||||
font-size: 11px;
|
||||
font-weight: 700;
|
||||
}
|
||||
.inv-chip.active .inv-chip__count {
|
||||
background: rgba(255, 255, 255, 0.25);
|
||||
color: #ffffff;
|
||||
}
|
||||
|
||||
/* Card & Table */
|
||||
.inv-card {
|
||||
background: #ffffff;
|
||||
border-radius: 14px;
|
||||
border: 1px solid #e2e8f0;
|
||||
box-shadow: 0 4px 12px rgba(15, 23, 42, 0.05);
|
||||
overflow: hidden;
|
||||
}
|
||||
.inv-card__head {
|
||||
padding: 16px 20px;
|
||||
border-bottom: 1px solid #e2e8f0;
|
||||
background: linear-gradient(180deg, #f8fafc 0%, #ffffff 100%);
|
||||
}
|
||||
.inv-card__title {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
font-size: 15px;
|
||||
font-weight: 700;
|
||||
color: #0f172a;
|
||||
}
|
||||
.inv-card__title-ic {
|
||||
color: #2563eb;
|
||||
}
|
||||
.inv-card__count {
|
||||
margin-left: 6px;
|
||||
padding: 2px 10px;
|
||||
border-radius: 999px;
|
||||
background: #eff6ff;
|
||||
color: #1e40af;
|
||||
font-size: 11px;
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
.inv-table-wrap {
|
||||
overflow-x: auto;
|
||||
}
|
||||
.inv-table {
|
||||
width: 100%;
|
||||
border-collapse: collapse;
|
||||
}
|
||||
.inv-table th {
|
||||
background: #f1f5f9;
|
||||
color: #475569;
|
||||
font-size: 11px;
|
||||
font-weight: 700;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.05em;
|
||||
padding: 12px 16px;
|
||||
border-bottom: 1px solid #e2e8f0;
|
||||
white-space: nowrap;
|
||||
}
|
||||
.inv-table td {
|
||||
padding: 12px 16px;
|
||||
color: #334155;
|
||||
border-bottom: 1px solid #f1f5f9;
|
||||
font-size: 13.5px;
|
||||
vertical-align: middle;
|
||||
}
|
||||
.inv-table tr:hover {
|
||||
background: #f8fafc;
|
||||
}
|
||||
|
||||
.inv-cell-bank {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
}
|
||||
.bank-icon {
|
||||
color: #2563eb;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.inv-mono {
|
||||
font-family: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace;
|
||||
}
|
||||
.inv-strong {
|
||||
font-weight: 700;
|
||||
color: #0f172a;
|
||||
}
|
||||
.inv-muted {
|
||||
color: #64748b;
|
||||
}
|
||||
.inv-num {
|
||||
text-align: right;
|
||||
}
|
||||
|
||||
.inv-leaves-badge {
|
||||
display: inline-block;
|
||||
padding: 4px 10px;
|
||||
border-radius: 6px;
|
||||
background: #f0fdf4;
|
||||
color: #16a34a;
|
||||
font-weight: 700;
|
||||
font-family: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace;
|
||||
}
|
||||
.inv-leaves-badge.low {
|
||||
background: #fef2f2;
|
||||
color: #dc2626;
|
||||
}
|
||||
|
||||
.inv-badge {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
padding: 4px 10px;
|
||||
border-radius: 999px;
|
||||
font-size: 12px;
|
||||
font-weight: 600;
|
||||
}
|
||||
.inv-badge__dot {
|
||||
width: 6px;
|
||||
height: 6px;
|
||||
border-radius: 50%;
|
||||
background: currentColor;
|
||||
}
|
||||
.inv-badge.active {
|
||||
background: #dcfce7;
|
||||
color: #15803d;
|
||||
}
|
||||
.inv-badge.exhausted {
|
||||
background: #f1f5f9;
|
||||
color: #64748b;
|
||||
}
|
||||
|
||||
.inv-actions-col {
|
||||
text-align: right;
|
||||
}
|
||||
|
||||
/* Buttons */
|
||||
.inv-btn {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
padding: 9px 18px;
|
||||
border-radius: 10px;
|
||||
font-size: 13.5px;
|
||||
font-weight: 600;
|
||||
cursor: pointer;
|
||||
border: 1px solid transparent;
|
||||
transition: all 0.15s ease;
|
||||
}
|
||||
.inv-btn.primary {
|
||||
background: linear-gradient(135deg, #2563eb 0%, #1d4ed8 100%);
|
||||
color: #ffffff;
|
||||
box-shadow: 0 2px 8px rgba(37, 99, 235, 0.3);
|
||||
}
|
||||
.inv-btn.primary:hover {
|
||||
transform: translateY(-1px);
|
||||
box-shadow: 0 4px 14px rgba(37, 99, 235, 0.4);
|
||||
}
|
||||
.inv-btn.ghost {
|
||||
background: #ffffff;
|
||||
border-color: #cbd5e1;
|
||||
color: #475569;
|
||||
}
|
||||
.inv-btn.ghost:hover {
|
||||
background: #f8fafc;
|
||||
color: #0f172a;
|
||||
}
|
||||
.inv-btn.small {
|
||||
padding: 6px 12px;
|
||||
font-size: 12px;
|
||||
border-radius: 8px;
|
||||
}
|
||||
|
||||
.inv-empty {
|
||||
text-align: center;
|
||||
padding: 48px 20px;
|
||||
color: #64748b;
|
||||
}
|
||||
.inv-empty__ic {
|
||||
font-size: 36px;
|
||||
color: #cbd5e1;
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
.inv-empty__title {
|
||||
font-size: 16px;
|
||||
font-weight: 700;
|
||||
color: #334155;
|
||||
margin: 0 0 4px 0;
|
||||
}
|
||||
.inv-empty__sub {
|
||||
font-size: 13px;
|
||||
margin: 0;
|
||||
}
|
||||
`
|
||||
]
|
||||
})
|
||||
@@ -140,14 +518,9 @@ export class DdInventoryListComponent implements OnInit {
|
||||
private route = inject(ActivatedRoute);
|
||||
private router = inject(Router);
|
||||
|
||||
/**
|
||||
* The dividend this visit is drawing a draft for, when the clerk arrived from a dividend claim
|
||||
* rather than from the menu.
|
||||
*
|
||||
* Null on an ordinary visit, which is what keeps this page's normal behaviour untouched — the
|
||||
* banner, the relabelled action and the forwarded query params all hang off this being set.
|
||||
*/
|
||||
dividendContext: { lockId: number; amount: number; beneficiary: string; financialYear: string } | null = null;
|
||||
searchText = '';
|
||||
selectedStatusFilter = 'ALL';
|
||||
|
||||
displayedColumns: string[] = [
|
||||
'id',
|
||||
@@ -168,13 +541,6 @@ export class DdInventoryListComponent implements OnInit {
|
||||
this.loadInventories();
|
||||
}
|
||||
|
||||
/**
|
||||
* Picks up the dividend a claim sent us here to pay out.
|
||||
*
|
||||
* Read from the URL rather than handed over in a service so a refreshed or bookmarked page still
|
||||
* knows which lock it is drawing against — losing it silently would issue an ordinary counter
|
||||
* draft against the member instead of settling their dividend.
|
||||
*/
|
||||
private readDividendContext(): void {
|
||||
const params = this.route.snapshot.queryParams;
|
||||
const lockId = Number(params['dividendLockId']);
|
||||
@@ -189,7 +555,6 @@ export class DdInventoryListComponent implements OnInit {
|
||||
};
|
||||
}
|
||||
|
||||
/** The dividend context to carry onto the leaflets page; empty on an ordinary visit. */
|
||||
get dividendQueryParams(): Record<string, string | number> {
|
||||
return this.dividendContext
|
||||
? {
|
||||
@@ -201,7 +566,6 @@ export class DdInventoryListComponent implements OnInit {
|
||||
: {};
|
||||
}
|
||||
|
||||
/** Abandons the claim and returns to the dividend dashboard, leaving the lock untouched. */
|
||||
cancelDividendClaim(): void {
|
||||
this.router.navigate([
|
||||
'/clerk',
|
||||
@@ -212,7 +576,7 @@ export class DdInventoryListComponent implements OnInit {
|
||||
loadInventories(): void {
|
||||
this.sponsorDDService.getInventories().subscribe({
|
||||
next: (data) => {
|
||||
this.dataSource = data;
|
||||
this.dataSource = data || [];
|
||||
},
|
||||
error: (err) => {
|
||||
this.snackBar.open('Error loading inventories: ' + (err.error?.message || err.message), 'Close', {
|
||||
@@ -222,9 +586,44 @@ export class DdInventoryListComponent implements OnInit {
|
||||
});
|
||||
}
|
||||
|
||||
get totalBatches(): number {
|
||||
return this.dataSource.length;
|
||||
}
|
||||
|
||||
get activeBatchesCount(): number {
|
||||
return this.dataSource.filter((x) => (x.status || '').toUpperCase() === 'ACTIVE').length;
|
||||
}
|
||||
|
||||
get totalLeavesCount(): number {
|
||||
return this.dataSource.reduce((acc, curr) => acc + (Number(curr.totalLeaves) || 0), 0);
|
||||
}
|
||||
|
||||
get totalAvailableLeaves(): number {
|
||||
return this.dataSource.reduce((acc, curr) => acc + (Number(curr.availableLeaves) || 0), 0);
|
||||
}
|
||||
|
||||
get filteredData(): any[] {
|
||||
let list = this.dataSource;
|
||||
if (this.selectedStatusFilter !== 'ALL') {
|
||||
list = list.filter((item) => (item.status || '').toUpperCase() === this.selectedStatusFilter);
|
||||
}
|
||||
if (this.searchText.trim()) {
|
||||
const q = this.searchText.trim().toLowerCase();
|
||||
list = list.filter(
|
||||
(item) =>
|
||||
String(item.id).toLowerCase().includes(q) ||
|
||||
(item.sponsorBankName || '').toLowerCase().includes(q) ||
|
||||
(item.branchName || '').toLowerCase().includes(q) ||
|
||||
String(item.seriesStart).includes(q) ||
|
||||
String(item.seriesEnd).includes(q)
|
||||
);
|
||||
}
|
||||
return list;
|
||||
}
|
||||
|
||||
openAddInventoryDialog(): void {
|
||||
const dialogRef = this.dialog.open(AddInventoryDialogComponent, {
|
||||
width: '500px',
|
||||
width: '520px',
|
||||
disableClose: true
|
||||
});
|
||||
|
||||
|
||||
@@ -5,251 +5,326 @@
|
||||
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="leaflet-container">
|
||||
<div class="header-row">
|
||||
<div class="title-area">
|
||||
<button mat-button routerLink="/sponsor-dd/inventory" style="margin-bottom: 8px; color: #64748b; padding-left: 0">
|
||||
← Back to Inventories
|
||||
<div class="ddreg-container">
|
||||
<!-- Hero header -->
|
||||
<div class="ddreg-header">
|
||||
<div class="ddreg-header__left">
|
||||
<button class="ddreg-back" routerLink="/sponsor-dd/inventory" matTooltip="Back to Inventories">
|
||||
<fa-icon icon="arrow-left"></fa-icon>
|
||||
</button>
|
||||
<h2>DD Leaflet Registry</h2>
|
||||
<p *ngIf="inventoryDetails">
|
||||
Sponsor Bank: <strong>{{ inventoryDetails.sponsorBankName }}</strong> | Series Range:
|
||||
<strong>{{ inventoryDetails.seriesStart }} - {{ inventoryDetails.seriesEnd }}</strong>
|
||||
</p>
|
||||
<fa-icon icon="money-check-alt" class="ddreg-header__icon"></fa-icon>
|
||||
<div>
|
||||
<h2 class="ddreg-header__title">DD Leaflet Registry</h2>
|
||||
<div class="ddreg-header__sub" *ngIf="inventoryDetails">
|
||||
Sponsor Bank <strong>{{ inventoryDetails.sponsorBankName }}</strong> · Series
|
||||
<strong>{{ inventoryDetails.seriesStart }}–{{ inventoryDetails.seriesEnd }}</strong>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- KPI Metrics Row -->
|
||||
<div class="ddreg-kpi-grid">
|
||||
<div class="ddreg-kpi-card">
|
||||
<div class="ddreg-kpi-card__icon blue">
|
||||
<fa-icon icon="money-check-alt"></fa-icon>
|
||||
</div>
|
||||
<div class="ddreg-kpi-card__info">
|
||||
<span class="ddreg-kpi-card__label">Total Stock</span>
|
||||
<span class="ddreg-kpi-card__val">{{ dataSource.data.length }}</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="ddreg-kpi-card">
|
||||
<div class="ddreg-kpi-card__icon green">
|
||||
<fa-icon icon="check-circle"></fa-icon>
|
||||
</div>
|
||||
<div class="ddreg-kpi-card__info">
|
||||
<span class="ddreg-kpi-card__label">Available</span>
|
||||
<span class="ddreg-kpi-card__val">{{ statusCount('AVAILABLE') }}</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="ddreg-kpi-card">
|
||||
<div class="ddreg-kpi-card__icon indigo">
|
||||
<fa-icon icon="file-alt"></fa-icon>
|
||||
</div>
|
||||
<div class="ddreg-kpi-card__info">
|
||||
<span class="ddreg-kpi-card__label">Issued</span>
|
||||
<span class="ddreg-kpi-card__val">{{ statusCount('ISSUED') }}</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="ddreg-kpi-card">
|
||||
<div class="ddreg-kpi-card__icon amber">
|
||||
<fa-icon icon="ban"></fa-icon>
|
||||
</div>
|
||||
<div class="ddreg-kpi-card__info">
|
||||
<span class="ddreg-kpi-card__label">Cancelled / Stopped</span>
|
||||
<span class="ddreg-kpi-card__val">{{ statusCount('CANCELLED') + statusCount('STOPPED') }}</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="ddreg-kpi-card">
|
||||
<div class="ddreg-kpi-card__icon slate">
|
||||
<fa-icon icon="times-circle"></fa-icon>
|
||||
</div>
|
||||
<div class="ddreg-kpi-card__info">
|
||||
<span class="ddreg-kpi-card__label">Revoked / Spoiled</span>
|
||||
<span class="ddreg-kpi-card__val">{{ statusCount('REVOKED') }}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Says why the clerk is here when they arrived from a dividend claim, and which member is
|
||||
waiting. Without it a registry of DD numbers gives no clue what the next click commits to. -->
|
||||
<div class="dividend-banner" *ngIf="dividendContext">
|
||||
<div class="dividend-banner__text">
|
||||
<div class="ddreg-dividend" *ngIf="dividendContext">
|
||||
<fa-icon icon="hand-holding-usd" class="ddreg-dividend__ic"></fa-icon>
|
||||
<div class="ddreg-dividend__text">
|
||||
<strong>Issuing a dividend Demand Draft</strong>
|
||||
<span>
|
||||
{{ dividendContext.beneficiary }} · {{ dividendContext.amount | number: '1.2-2' }}
|
||||
{{ dividendContext.beneficiary }} · ₹{{ dividendContext.amount | number: '1.2-2' }}
|
||||
<ng-container *ngIf="dividendContext.financialYear"> · FY {{ dividendContext.financialYear }}</ng-container>
|
||||
</span>
|
||||
<span class="dividend-banner__hint">Pick an available leaflet below and press Issue.</span>
|
||||
<span class="ddreg-dividend__hint">Pick an available leaflet below and press "Draw for Dividend".</span>
|
||||
</div>
|
||||
<button mat-button class="action-btn" (click)="cancelDividendClaim()">Cancel</button>
|
||||
<button class="ddreg-btn ghost small" (click)="cancelDividendClaim()">Cancel</button>
|
||||
</div>
|
||||
|
||||
<mat-form-field appearance="outline" class="search-field">
|
||||
<mat-label>Search by DD Leaflet No.</mat-label>
|
||||
<input matInput [value]="searchText" (input)="onSearchChange($event.target.value)" placeholder="e.g. 3005" />
|
||||
</mat-form-field>
|
||||
|
||||
<div class="status-filter-row">
|
||||
<button mat-stroked-button color="accent" [class.filter-active]="todayOnly" (click)="toggleTodayOnly()">
|
||||
<!-- Toolbar: search + date scope -->
|
||||
<div class="ddreg-toolbar">
|
||||
<div class="ddreg-toolbar__search">
|
||||
<fa-icon icon="search" class="ddreg-toolbar__search-ic"></fa-icon>
|
||||
<input
|
||||
type="text"
|
||||
[value]="searchText"
|
||||
(input)="onSearchChange($event.target.value)"
|
||||
placeholder="Search by DD Leaflet No., e.g. 3005"
|
||||
/>
|
||||
<button class="ddreg-toolbar__clear" *ngIf="searchText" (click)="onSearchChange('')" matTooltip="Clear">
|
||||
<fa-icon icon="times"></fa-icon>
|
||||
</button>
|
||||
</div>
|
||||
<button class="ddreg-btn ghost" [class.active]="todayOnly" (click)="toggleTodayOnly()">
|
||||
<fa-icon icon="calendar-day"></fa-icon>
|
||||
{{ todayOnly ? "Today's Issued DDs Only" : 'Showing All Dates' }}
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<!-- Status filter chips — doubles as an at-a-glance count of every leaflet state -->
|
||||
<div class="ddreg-chips">
|
||||
<button
|
||||
mat-stroked-button
|
||||
*ngFor="let status of statusOptions"
|
||||
[color]="selectedStatus === status ? 'primary' : undefined"
|
||||
[class.filter-active]="selectedStatus === status"
|
||||
class="ddreg-chip"
|
||||
[ngClass]="'s-' + status.toLowerCase()"
|
||||
[class.active]="selectedStatus === status"
|
||||
(click)="filterByStatus(status)"
|
||||
>
|
||||
{{ status === 'ALL' ? 'All' : status }}
|
||||
<span class="filter-count">({{ statusCount(status) }})</span>
|
||||
<span class="ddreg-chip__dot"></span>
|
||||
{{ status === 'ALL' ? 'All' : (status | titlecase) }}
|
||||
<span class="ddreg-chip__count">{{ statusCount(status) }}</span>
|
||||
</button>
|
||||
</div>
|
||||
<p class="hint-text" *ngIf="todayOnly">
|
||||
Only DDs actually ISSUED today are shown - AVAILABLE, CANCELLED, STOPPED and CLEARED are hidden. Click "Showing All
|
||||
<p class="ddreg-hint" *ngIf="todayOnly">
|
||||
<fa-icon icon="info-circle"></fa-icon>
|
||||
Only DDs actually ISSUED today are shown — Available, Cancelled, Stopped and Cleared are hidden. Click "Showing All
|
||||
Dates" above to see everything.
|
||||
</p>
|
||||
|
||||
<mat-card>
|
||||
<table mat-table [dataSource]="dataSource">
|
||||
<!-- DD Number Column -->
|
||||
<ng-container matColumnDef="ddNumber">
|
||||
<th mat-header-cell *matHeaderCellDef>DD Leaflet No.</th>
|
||||
<td mat-cell *matCellDef="let element">
|
||||
<strong>{{ element.ddNumber }}</strong>
|
||||
</td>
|
||||
</ng-container>
|
||||
<!-- Leaflets table -->
|
||||
<div class="ddreg-card">
|
||||
<div class="ddreg-card__head">
|
||||
<div class="ddreg-card__title">
|
||||
<fa-icon icon="list" class="ddreg-card__title-ic"></fa-icon>
|
||||
Leaflets
|
||||
<span class="ddreg-card__count">{{ dataSource.filteredData.length }} of {{ dataSource.data.length }}</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="ddreg-table-wrap">
|
||||
<table mat-table [dataSource]="dataSource" class="ddreg-table">
|
||||
<!-- DD Number Column -->
|
||||
<ng-container matColumnDef="ddNumber">
|
||||
<th mat-header-cell *matHeaderCellDef>DD Leaflet No.</th>
|
||||
<td mat-cell *matCellDef="let element" class="ddreg-mono ddreg-strong">{{ element.ddNumber }}</td>
|
||||
</ng-container>
|
||||
|
||||
<!-- Status Column -->
|
||||
<ng-container matColumnDef="status">
|
||||
<th mat-header-cell *matHeaderCellDef>Status</th>
|
||||
<td mat-cell *matCellDef="let element">
|
||||
<span class="status-badge" [ngClass]="element.status.toLowerCase()">
|
||||
{{ element.status }}
|
||||
</span>
|
||||
</td>
|
||||
</ng-container>
|
||||
|
||||
<!-- Face Value Column -->
|
||||
<ng-container matColumnDef="faceValue">
|
||||
<th mat-header-cell *matHeaderCellDef>Face Value</th>
|
||||
<td mat-cell *matCellDef="let element">
|
||||
{{ element.faceValue > 0 ? (element.faceValue | number: '1.2-2') : '-' }}
|
||||
</td>
|
||||
</ng-container>
|
||||
|
||||
<!-- Client Name Column -->
|
||||
<ng-container matColumnDef="issuedToClientName">
|
||||
<th mat-header-cell *matHeaderCellDef>Purchaser / Client</th>
|
||||
<!-- A dividend payout has no purchasing client: the society raised the draft out of its own
|
||||
Dividend Payable to pay the member. Falling back to the walk-in purchaser name shows
|
||||
that, instead of repeating the beneficiary and reading as a counter sale. -->
|
||||
<td mat-cell *matCellDef="let element">
|
||||
{{ element.issuedToClientName || element.walkinPurchaserName || '-' }}
|
||||
</td>
|
||||
</ng-container>
|
||||
|
||||
<!-- Beneficiary Column -->
|
||||
<ng-container matColumnDef="beneficiaryName">
|
||||
<th mat-header-cell *matHeaderCellDef>Beneficiary</th>
|
||||
<td mat-cell *matCellDef="let element">{{ element.beneficiaryName || '-' }}</td>
|
||||
</ng-container>
|
||||
|
||||
<!-- Issued Date Column -->
|
||||
<ng-container matColumnDef="issuedDate">
|
||||
<th mat-header-cell *matHeaderCellDef>Issued Date</th>
|
||||
<td mat-cell *matCellDef="let element">{{ element.issuedDate || '-' }}</td>
|
||||
</ng-container>
|
||||
|
||||
<!-- Advice Status Column -->
|
||||
<ng-container matColumnDef="adviceStatus">
|
||||
<th mat-header-cell *matHeaderCellDef>Advice Status</th>
|
||||
<td mat-cell *matCellDef="let element">
|
||||
{{ element.status === 'ISSUED' ? element.adviceStatus : '-' }}
|
||||
</td>
|
||||
</ng-container>
|
||||
|
||||
<!-- Actions Column -->
|
||||
<ng-container matColumnDef="actions">
|
||||
<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="dividendContext && element.status === 'AVAILABLE' && !element.pendingIssueRequestId"
|
||||
(click)="openIssueDialog(element)"
|
||||
>
|
||||
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"
|
||||
size="small"
|
||||
*ngIf="element.status === 'ISSUED'"
|
||||
(click)="openCancelDialog(element)"
|
||||
>
|
||||
Cancel Draft
|
||||
</button>
|
||||
<button
|
||||
mat-button
|
||||
color="warn"
|
||||
size="small"
|
||||
*ngIf="element.status === 'ISSUED'"
|
||||
(click)="onStopPayment(element)"
|
||||
>
|
||||
Stop Payment
|
||||
</button>
|
||||
<button
|
||||
mat-stroked-button
|
||||
size="small"
|
||||
*ngIf="element.status === 'ISSUED' && !element.printCount"
|
||||
(click)="openPrintDialog(element)"
|
||||
>
|
||||
Print
|
||||
</button>
|
||||
<button
|
||||
mat-stroked-button
|
||||
color="primary"
|
||||
size="small"
|
||||
*ngIf="
|
||||
element.status === 'ISSUED' && element.printCount > 0 && element.approvedUnconsumedReprintRequestId
|
||||
"
|
||||
(click)="openPrintDialog(element)"
|
||||
>
|
||||
Print (Reprint Approved)
|
||||
</button>
|
||||
<button
|
||||
mat-stroked-button
|
||||
size="small"
|
||||
*ngIf="
|
||||
element.status === 'ISSUED' &&
|
||||
element.printCount > 0 &&
|
||||
!element.approvedUnconsumedReprintRequestId &&
|
||||
!element.pendingReprintRequestId
|
||||
"
|
||||
(click)="openRequestReprintDialog(element)"
|
||||
>
|
||||
Request Reprint
|
||||
</button>
|
||||
<span class="pending-badge" *ngIf="element.status === 'ISSUED' && element.pendingReprintRequestId">
|
||||
Reprint Pending Approval (Req #{{ element.pendingReprintRequestId }})
|
||||
<!-- Status Column -->
|
||||
<ng-container matColumnDef="status">
|
||||
<th mat-header-cell *matHeaderCellDef>Status</th>
|
||||
<td mat-cell *matCellDef="let element">
|
||||
<span class="ddreg-badge" [ngClass]="element.status.toLowerCase()">
|
||||
<span class="ddreg-badge__dot"></span>
|
||||
{{ element.status | titlecase }}
|
||||
</span>
|
||||
</td>
|
||||
</ng-container>
|
||||
|
||||
<!-- The leaflet is ruined and cannot be handed over. Offered beside Request Reprint
|
||||
because the two look alike from the counter: reprint when the paper survived, revoke
|
||||
when it did not. -->
|
||||
<button
|
||||
mat-stroked-button
|
||||
color="warn"
|
||||
size="small"
|
||||
*ngIf="element.status === 'ISSUED' && !element.pendingRevokeRequestId"
|
||||
(click)="openRevokeDialog(element)"
|
||||
>
|
||||
Revoke
|
||||
</button>
|
||||
<span class="pending-badge" *ngIf="element.status === 'ISSUED' && element.pendingRevokeRequestId">
|
||||
Revoke Pending Approval (Req #{{ element.pendingRevokeRequestId }})
|
||||
</span>
|
||||
<!-- Face Value Column -->
|
||||
<ng-container matColumnDef="faceValue">
|
||||
<th mat-header-cell *matHeaderCellDef class="ddreg-num">Face Value</th>
|
||||
<td mat-cell *matCellDef="let element" class="ddreg-num ddreg-mono">
|
||||
{{ element.faceValue > 0 ? '₹' + (element.faceValue | number: '1.2-2') : '—' }}
|
||||
</td>
|
||||
</ng-container>
|
||||
|
||||
<!-- Both ends of a replacement, so the pad reconciles: a revoked leaflet says where its
|
||||
draft went, and the blank that took it says what it stands in for. -->
|
||||
<span class="pending-badge" *ngIf="element.revokedReplacedByDdNumber">
|
||||
Revoked → replaced by DD {{ element.revokedReplacedByDdNumber }}
|
||||
</span>
|
||||
<span class="pending-badge" *ngIf="element.revokedReplacementOfDdNumber">
|
||||
Replaces spoiled DD {{ element.revokedReplacementOfDdNumber }}
|
||||
</span>
|
||||
</div>
|
||||
</td>
|
||||
</ng-container>
|
||||
<!-- Client Name Column -->
|
||||
<ng-container matColumnDef="issuedToClientName">
|
||||
<th mat-header-cell *matHeaderCellDef>Purchaser / Client</th>
|
||||
<!-- A dividend payout has no purchasing client: the society raised the draft out of its own
|
||||
Dividend Payable to pay the member. Falling back to the walk-in purchaser name shows
|
||||
that, instead of repeating the beneficiary and reading as a counter sale. -->
|
||||
<td mat-cell *matCellDef="let element">
|
||||
{{ element.issuedToClientName || element.walkinPurchaserName || '—' }}
|
||||
</td>
|
||||
</ng-container>
|
||||
|
||||
<tr mat-header-row *matHeaderRowDef="displayedColumns"></tr>
|
||||
<tr mat-row *matRowDef="let row; columns: displayedColumns"></tr>
|
||||
</table>
|
||||
<!-- Beneficiary Column -->
|
||||
<ng-container matColumnDef="beneficiaryName">
|
||||
<th mat-header-cell *matHeaderCellDef>Beneficiary</th>
|
||||
<td mat-cell *matCellDef="let element">{{ element.beneficiaryName || '—' }}</td>
|
||||
</ng-container>
|
||||
|
||||
<div
|
||||
*ngIf="dataSource.filteredData.length === 0"
|
||||
class="no-records-row"
|
||||
style="text-align: center; padding: 32px; color: #64748b"
|
||||
>
|
||||
{{ dataSource.data.length === 0 ? 'Loading leaflets...' : 'No leaflets match the selected filter.' }}
|
||||
<!-- Issued Date Column -->
|
||||
<ng-container matColumnDef="issuedDate">
|
||||
<th mat-header-cell *matHeaderCellDef>Issued Date</th>
|
||||
<td mat-cell *matCellDef="let element" class="ddreg-mono">{{ element.issuedDate || '—' }}</td>
|
||||
</ng-container>
|
||||
|
||||
<!-- Advice Status Column -->
|
||||
<ng-container matColumnDef="adviceStatus">
|
||||
<th mat-header-cell *matHeaderCellDef>Advice Status</th>
|
||||
<td mat-cell *matCellDef="let element" class="ddreg-muted">
|
||||
{{ element.status === 'ISSUED' ? (element.adviceStatus | titlecase) : '—' }}
|
||||
</td>
|
||||
</ng-container>
|
||||
|
||||
<!-- Actions Column -->
|
||||
<ng-container matColumnDef="actions">
|
||||
<th mat-header-cell *matHeaderCellDef class="ddreg-actions-col">Actions</th>
|
||||
<td mat-cell *matCellDef="let element" class="ddreg-actions-col">
|
||||
<div class="ddreg-actions">
|
||||
<!-- 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
|
||||
class="ddreg-btn primary small"
|
||||
*ngIf="dividendContext && element.status === 'AVAILABLE' && !element.pendingIssueRequestId"
|
||||
(click)="openIssueDialog(element)"
|
||||
>
|
||||
<fa-icon icon="hand-holding-usd"></fa-icon>
|
||||
Draw for Dividend
|
||||
</button>
|
||||
<span class="ddreg-pending" *ngIf="element.status === 'AVAILABLE' && element.pendingIssueRequestId">
|
||||
<fa-icon icon="clock"></fa-icon>
|
||||
Pending Approval (Req #{{ element.pendingIssueRequestId }})
|
||||
</span>
|
||||
|
||||
<!-- Print is the action a clerk reaches for almost every time an issued draft is
|
||||
opened, so it stays a visible button; everything rarer (edit, reprint, revoke,
|
||||
stop, cancel) is one tap away behind the ⋮ menu instead of competing for space. -->
|
||||
<button
|
||||
class="ddreg-btn primary small"
|
||||
*ngIf="element.status === 'ISSUED' && !element.printCount"
|
||||
(click)="openPrintDialog(element)"
|
||||
>
|
||||
<fa-icon icon="print"></fa-icon>
|
||||
Print
|
||||
</button>
|
||||
<button
|
||||
class="ddreg-btn primary small"
|
||||
*ngIf="
|
||||
element.status === 'ISSUED' && element.printCount > 0 && element.approvedUnconsumedReprintRequestId
|
||||
"
|
||||
(click)="openPrintDialog(element)"
|
||||
>
|
||||
<fa-icon icon="print"></fa-icon>
|
||||
Print (Reprint Approved)
|
||||
</button>
|
||||
|
||||
<ng-container *ngIf="element.status === 'ISSUED'">
|
||||
<button class="ddreg-act more" [matMenuTriggerFor]="rowMenu" matTooltip="More actions">
|
||||
<fa-icon icon="ellipsis-v"></fa-icon>
|
||||
</button>
|
||||
<mat-menu #rowMenu="matMenu" xPosition="before" class="ddreg-menu">
|
||||
<!-- 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-menu-item *ngIf="!element.pendingAmendmentRequestId" (click)="openAmendDialog(element)">
|
||||
<fa-icon icon="pencil-alt" class="ddreg-menu__ic"></fa-icon>
|
||||
Edit
|
||||
</button>
|
||||
<button
|
||||
mat-menu-item
|
||||
*ngIf="
|
||||
element.printCount > 0 &&
|
||||
!element.approvedUnconsumedReprintRequestId &&
|
||||
!element.pendingReprintRequestId
|
||||
"
|
||||
(click)="openRequestReprintDialog(element)"
|
||||
>
|
||||
<fa-icon icon="redo" class="ddreg-menu__ic"></fa-icon>
|
||||
Request Reprint
|
||||
</button>
|
||||
<!-- The leaflet is ruined and cannot be handed over. Grouped beside Request Reprint
|
||||
because the two look alike from the counter: reprint when the paper survived,
|
||||
revoke when it did not. -->
|
||||
<button mat-menu-item *ngIf="!element.pendingRevokeRequestId" (click)="openRevokeDialog(element)">
|
||||
<fa-icon icon="ban" class="ddreg-menu__ic warn"></fa-icon>
|
||||
Revoke
|
||||
</button>
|
||||
<button mat-menu-item (click)="onStopPayment(element)">
|
||||
<fa-icon icon="hand-paper" class="ddreg-menu__ic warn"></fa-icon>
|
||||
Stop Payment
|
||||
</button>
|
||||
<button mat-menu-item (click)="openCancelDialog(element)">
|
||||
<fa-icon icon="times-circle" class="ddreg-menu__ic warn"></fa-icon>
|
||||
Cancel Draft
|
||||
</button>
|
||||
</mat-menu>
|
||||
</ng-container>
|
||||
|
||||
<span class="ddreg-pending" *ngIf="element.status === 'ISSUED' && element.pendingAmendmentRequestId">
|
||||
<fa-icon icon="clock"></fa-icon>
|
||||
Amendment Pending (Req #{{ element.pendingAmendmentRequestId }})
|
||||
</span>
|
||||
<span class="ddreg-pending" *ngIf="element.status === 'ISSUED' && element.pendingReprintRequestId">
|
||||
<fa-icon icon="clock"></fa-icon>
|
||||
Reprint Pending (Req #{{ element.pendingReprintRequestId }})
|
||||
</span>
|
||||
<span class="ddreg-pending" *ngIf="element.status === 'ISSUED' && element.pendingRevokeRequestId">
|
||||
<fa-icon icon="clock"></fa-icon>
|
||||
Revoke Pending (Req #{{ element.pendingRevokeRequestId }})
|
||||
</span>
|
||||
|
||||
<!-- Both ends of a replacement, so the pad reconciles: a revoked leaflet says where
|
||||
its draft went, and the blank that took it says what it stands in for. -->
|
||||
<span class="ddreg-info" *ngIf="element.revokedReplacedByDdNumber">
|
||||
<fa-icon icon="arrow-right"></fa-icon>
|
||||
Replaced by DD {{ element.revokedReplacedByDdNumber }}
|
||||
</span>
|
||||
<span class="ddreg-info" *ngIf="element.revokedReplacementOfDdNumber">
|
||||
<fa-icon icon="undo"></fa-icon>
|
||||
Replaces spoiled DD {{ element.revokedReplacementOfDdNumber }}
|
||||
</span>
|
||||
</div>
|
||||
</td>
|
||||
</ng-container>
|
||||
|
||||
<tr mat-header-row *matHeaderRowDef="displayedColumns"></tr>
|
||||
<tr mat-row *matRowDef="let row; columns: displayedColumns"></tr>
|
||||
</table>
|
||||
|
||||
<div class="ddreg-empty" *ngIf="dataSource.filteredData.length === 0">
|
||||
<fa-icon [icon]="dataSource.data.length === 0 ? 'spinner' : 'inbox'"></fa-icon>
|
||||
{{ dataSource.data.length === 0 ? 'Loading leaflets…' : 'No leaflets match the selected filter.' }}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<mat-paginator [pageSizeOptions]="[25, 50, 100, 250]" [pageSize]="25" showFirstLastButtons></mat-paginator>
|
||||
</mat-card>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -21,6 +21,9 @@ import { MatSelectModule } from '@angular/material/select';
|
||||
import { MatDatepickerModule } from '@angular/material/datepicker';
|
||||
import { MatNativeDateModule } from '@angular/material/core';
|
||||
import { MatSnackBar, MatSnackBarModule } from '@angular/material/snack-bar';
|
||||
import { MatMenuModule } from '@angular/material/menu';
|
||||
import { MatIconModule } from '@angular/material/icon';
|
||||
import { MatTooltipModule } from '@angular/material/tooltip';
|
||||
import { FaIconComponent } from '@fortawesome/angular-fontawesome';
|
||||
import { HttpClient } from '@angular/common/http';
|
||||
import { debounceTime } from 'rxjs/operators';
|
||||
@@ -40,145 +43,624 @@ import { DividendClaimRequestService } from '../../clerk/dividend-claim-request.
|
||||
MatDialogModule,
|
||||
MatSnackBarModule,
|
||||
MatFormFieldModule,
|
||||
MatInputModule
|
||||
MatInputModule,
|
||||
MatMenuModule,
|
||||
MatIconModule,
|
||||
MatTooltipModule,
|
||||
FaIconComponent
|
||||
],
|
||||
templateUrl: './dd-list.component.html',
|
||||
styles: [
|
||||
`
|
||||
.leaflet-container {
|
||||
padding: 24px;
|
||||
max-width: 1200px;
|
||||
/* Same visual language as users.component.scss (gradient header icon, KPI-style chips, card
|
||||
table with sticky head + row-hover, icon-button/overflow-menu actions) so this registry
|
||||
reads as part of the same app rather than a one-off style. */
|
||||
.ddreg-container {
|
||||
padding: 20px 24px;
|
||||
max-width: 1320px;
|
||||
margin: 0 auto;
|
||||
}
|
||||
.dividend-banner {
|
||||
|
||||
/* ─── Header ─── */
|
||||
.ddreg-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 16px;
|
||||
margin-bottom: 20px;
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
.ddreg-header__left {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 12px;
|
||||
}
|
||||
.ddreg-back {
|
||||
width: 36px;
|
||||
height: 36px;
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
border-radius: 9px;
|
||||
border: 1px solid #d0d5dd;
|
||||
background: #fff;
|
||||
color: #475569;
|
||||
cursor: pointer;
|
||||
transition: all 0.15s;
|
||||
}
|
||||
.ddreg-back:hover {
|
||||
background: #f8fafc;
|
||||
border-color: #94a3b8;
|
||||
color: #1e293b;
|
||||
}
|
||||
.ddreg-header__icon {
|
||||
width: 44px;
|
||||
height: 44px;
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
border-radius: 12px;
|
||||
background: linear-gradient(135deg, #1e88e5 0%, #7c3aed 100%);
|
||||
color: #fff;
|
||||
font-size: 18px;
|
||||
box-shadow: 0 2px 8px rgb(29 78 216 / 25%);
|
||||
}
|
||||
.ddreg-header__title {
|
||||
margin: 0;
|
||||
font-size: 20px;
|
||||
font-weight: 800;
|
||||
color: #0d1b2a;
|
||||
line-height: 1.1;
|
||||
}
|
||||
.ddreg-header__sub {
|
||||
margin-top: 2px;
|
||||
font-size: 12.5px;
|
||||
color: #64748b;
|
||||
}
|
||||
|
||||
/* ─── Dividend context banner ─── */
|
||||
.ddreg-dividend {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 14px;
|
||||
margin-bottom: 16px;
|
||||
padding: 12px 16px;
|
||||
border: 1px solid #c7d2fe;
|
||||
border-left: 4px solid #4f46e5;
|
||||
border-radius: 8px;
|
||||
background-color: #eef2ff;
|
||||
border-radius: 10px;
|
||||
background: linear-gradient(180deg, #eef2ff 0%, #f5f3ff 100%);
|
||||
}
|
||||
.dividend-banner__text {
|
||||
.ddreg-dividend__ic {
|
||||
width: 34px;
|
||||
height: 34px;
|
||||
flex-shrink: 0;
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
border-radius: 9px;
|
||||
background: #4f46e5;
|
||||
color: #fff;
|
||||
font-size: 14px;
|
||||
}
|
||||
.ddreg-dividend__text {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 2px;
|
||||
font-size: 13px;
|
||||
color: #312e81;
|
||||
}
|
||||
.dividend-banner__hint {
|
||||
.ddreg-dividend__hint {
|
||||
color: #4338ca;
|
||||
font-size: 12px;
|
||||
}
|
||||
.dividend-banner button {
|
||||
.ddreg-dividend .ddreg-btn {
|
||||
margin-left: auto;
|
||||
}
|
||||
.header-row {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
margin-bottom: 24px;
|
||||
}
|
||||
.title-area h2 {
|
||||
margin: 0 0 4px 0;
|
||||
font-size: 24px;
|
||||
font-weight: 600;
|
||||
color: #1e293b;
|
||||
}
|
||||
.title-area p {
|
||||
margin: 0;
|
||||
color: #64748b;
|
||||
font-size: 14px;
|
||||
}
|
||||
.search-field {
|
||||
width: 320px;
|
||||
display: block;
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
.mat-mdc-card {
|
||||
border-radius: 12px;
|
||||
border: 1px solid #e2e8f0;
|
||||
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.05);
|
||||
overflow: hidden;
|
||||
}
|
||||
table {
|
||||
width: 100%;
|
||||
}
|
||||
th {
|
||||
font-weight: 600;
|
||||
color: #475569;
|
||||
background-color: #f8fafc;
|
||||
}
|
||||
td {
|
||||
color: #334155;
|
||||
}
|
||||
.status-badge {
|
||||
|
||||
/* ─── Buttons ─── */
|
||||
.ddreg-btn {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
padding: 4px 8px;
|
||||
border-radius: 9999px;
|
||||
gap: 8px;
|
||||
padding: 8px 16px;
|
||||
border-radius: 9px;
|
||||
font-size: 13px;
|
||||
font-weight: 600;
|
||||
cursor: pointer;
|
||||
transition: all 0.15s;
|
||||
white-space: nowrap;
|
||||
border: 1px solid transparent;
|
||||
background: #fff;
|
||||
}
|
||||
.ddreg-btn fa-icon {
|
||||
font-size: 12px;
|
||||
font-weight: 500;
|
||||
}
|
||||
.status-badge.available {
|
||||
background-color: #dcfce7;
|
||||
color: #15803d;
|
||||
}
|
||||
.status-badge.issued {
|
||||
background-color: #dbeafe;
|
||||
color: #1d4ed8;
|
||||
}
|
||||
.status-badge.cleared {
|
||||
background-color: #f1f5f9;
|
||||
.ddreg-btn.ghost {
|
||||
border-color: #d0d5dd;
|
||||
color: #475569;
|
||||
}
|
||||
.status-badge.cancelled {
|
||||
background-color: #fee2e2;
|
||||
.ddreg-btn.ghost:hover {
|
||||
background: #f8fafc;
|
||||
border-color: #94a3b8;
|
||||
color: #0d1b2a;
|
||||
}
|
||||
.ddreg-btn.ghost.active {
|
||||
background: #ede9fe;
|
||||
border-color: #c4b5fd;
|
||||
color: #6d28d9;
|
||||
}
|
||||
.ddreg-btn.primary {
|
||||
background: linear-gradient(135deg, #1e88e5 0%, #1e40af 100%);
|
||||
border-color: #1e40af;
|
||||
color: #fff;
|
||||
box-shadow: 0 2px 6px -2px rgb(30 64 175 / 35%);
|
||||
}
|
||||
.ddreg-btn.primary:hover {
|
||||
box-shadow: 0 4px 12px -3px rgb(30 64 175 / 45%);
|
||||
transform: translateY(-1px);
|
||||
}
|
||||
.ddreg-btn.small {
|
||||
padding: 6px 12px;
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
/* ─── Toolbar ─── */
|
||||
.ddreg-toolbar {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 12px;
|
||||
flex-wrap: wrap;
|
||||
margin-bottom: 12px;
|
||||
}
|
||||
.ddreg-toolbar__search {
|
||||
position: relative;
|
||||
flex: 1;
|
||||
min-width: 260px;
|
||||
max-width: 420px;
|
||||
}
|
||||
.ddreg-toolbar__search input {
|
||||
width: 100%;
|
||||
padding: 9px 34px;
|
||||
border: 1px solid #d0d5dd;
|
||||
border-radius: 9px;
|
||||
font-size: 13px;
|
||||
background: #fff;
|
||||
color: #0d1b2a;
|
||||
transition: all 0.15s;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
.ddreg-toolbar__search input:focus {
|
||||
outline: none;
|
||||
border-color: #1e88e5;
|
||||
box-shadow: 0 0 0 3px rgb(30 136 229 / 12%);
|
||||
}
|
||||
.ddreg-toolbar__search-ic {
|
||||
position: absolute;
|
||||
left: 12px;
|
||||
top: 50%;
|
||||
transform: translateY(-50%);
|
||||
color: #94a3b8;
|
||||
font-size: 12px;
|
||||
pointer-events: none;
|
||||
}
|
||||
.ddreg-toolbar__clear {
|
||||
position: absolute;
|
||||
right: 8px;
|
||||
top: 50%;
|
||||
transform: translateY(-50%);
|
||||
width: 22px;
|
||||
height: 22px;
|
||||
border-radius: 50%;
|
||||
border: 1px solid #e2e8f0;
|
||||
background: #fff;
|
||||
color: #94a3b8;
|
||||
font-size: 10px;
|
||||
cursor: pointer;
|
||||
}
|
||||
.ddreg-toolbar__clear:hover {
|
||||
background: #fef2f2;
|
||||
color: #ef4444;
|
||||
border-color: #fecaca;
|
||||
}
|
||||
|
||||
/* ─── KPI Cards ─── */
|
||||
.ddreg-kpi-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
|
||||
gap: 14px;
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
.ddreg-kpi-card {
|
||||
background: #ffffff;
|
||||
border-radius: 12px;
|
||||
padding: 16px 18px;
|
||||
border: 1px solid #e2e8f0;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 14px;
|
||||
box-shadow: 0 1px 3px rgba(15, 23, 42, 0.04);
|
||||
transition:
|
||||
transform 0.15s ease,
|
||||
box-shadow 0.15s ease;
|
||||
}
|
||||
.ddreg-kpi-card:hover {
|
||||
transform: translateY(-2px);
|
||||
box-shadow: 0 6px 16px rgba(15, 23, 42, 0.08);
|
||||
}
|
||||
.ddreg-kpi-card__icon {
|
||||
width: 42px;
|
||||
height: 42px;
|
||||
border-radius: 10px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
font-size: 16px;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
.ddreg-kpi-card__icon.blue {
|
||||
background: #eff6ff;
|
||||
color: #2563eb;
|
||||
}
|
||||
.ddreg-kpi-card__icon.green {
|
||||
background: #f0fdf4;
|
||||
color: #16a34a;
|
||||
}
|
||||
.ddreg-kpi-card__icon.indigo {
|
||||
background: #eef2ff;
|
||||
color: #4f46e5;
|
||||
}
|
||||
.ddreg-kpi-card__icon.amber {
|
||||
background: #fffbeb;
|
||||
color: #d97706;
|
||||
}
|
||||
.ddreg-kpi-card__icon.slate {
|
||||
background: #f8fafc;
|
||||
color: #64748b;
|
||||
}
|
||||
|
||||
.ddreg-kpi-card__info {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
.ddreg-kpi-card__label {
|
||||
font-size: 11.5px;
|
||||
font-weight: 600;
|
||||
color: #64748b;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.04em;
|
||||
}
|
||||
.ddreg-kpi-card__val {
|
||||
font-size: 22px;
|
||||
font-weight: 800;
|
||||
color: #0f172a;
|
||||
margin-top: 1px;
|
||||
line-height: 1;
|
||||
}
|
||||
|
||||
/* ─── Status filter chips (doubles as the count-at-a-glance) ─── */
|
||||
.ddreg-chips {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 8px;
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
.ddreg-chip {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 7px;
|
||||
padding: 7px 12px;
|
||||
border-radius: 999px;
|
||||
border: 1px solid #e2e8f0;
|
||||
background: #fff;
|
||||
color: #475569;
|
||||
font-size: 12.5px;
|
||||
font-weight: 600;
|
||||
cursor: pointer;
|
||||
transition: all 0.15s;
|
||||
}
|
||||
.ddreg-chip:hover {
|
||||
border-color: #cbd5e1;
|
||||
background: #f8fafc;
|
||||
}
|
||||
.ddreg-chip__dot {
|
||||
width: 7px;
|
||||
height: 7px;
|
||||
border-radius: 50%;
|
||||
background: currentColor;
|
||||
opacity: 0.6;
|
||||
}
|
||||
.ddreg-chip__count {
|
||||
padding: 1px 7px;
|
||||
border-radius: 999px;
|
||||
background: rgb(0 0 0 / 6%);
|
||||
font-size: 11px;
|
||||
font-weight: 700;
|
||||
}
|
||||
.ddreg-chip.s-available {
|
||||
color: #15803d;
|
||||
}
|
||||
.ddreg-chip.s-issued {
|
||||
color: #1d4ed8;
|
||||
}
|
||||
.ddreg-chip.s-cancelled {
|
||||
color: #b91c1c;
|
||||
}
|
||||
.status-badge.stopped {
|
||||
background-color: #fef3c7;
|
||||
.ddreg-chip.s-stopped {
|
||||
color: #b45309;
|
||||
}
|
||||
.ddreg-chip.s-cleared,
|
||||
.ddreg-chip.s-revoked {
|
||||
color: #475569;
|
||||
}
|
||||
.ddreg-chip.s-all {
|
||||
color: #4338ca;
|
||||
}
|
||||
.ddreg-chip.active {
|
||||
background: #1e40af;
|
||||
border-color: #1e40af;
|
||||
color: #ffffff !important;
|
||||
}
|
||||
.ddreg-chip.active.s-available {
|
||||
background: #16a34a;
|
||||
border-color: #16a34a;
|
||||
}
|
||||
.ddreg-chip.active.s-issued {
|
||||
background: #2563eb;
|
||||
border-color: #2563eb;
|
||||
}
|
||||
.ddreg-chip.active.s-cancelled {
|
||||
background: #dc2626;
|
||||
border-color: #dc2626;
|
||||
}
|
||||
.ddreg-chip.active.s-stopped {
|
||||
background: #d97706;
|
||||
border-color: #d97706;
|
||||
}
|
||||
.ddreg-chip.active.s-cleared,
|
||||
.ddreg-chip.active.s-revoked {
|
||||
background: #475569;
|
||||
border-color: #475569;
|
||||
}
|
||||
.ddreg-chip.active.s-all {
|
||||
background: #4f46e5;
|
||||
border-color: #4f46e5;
|
||||
}
|
||||
.ddreg-chip.active .ddreg-chip__dot {
|
||||
background: #fff;
|
||||
opacity: 0.9;
|
||||
}
|
||||
.ddreg-chip.active .ddreg-chip__count {
|
||||
background: rgb(255 255 255 / 25%);
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
.ddreg-hint {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
color: #64748b;
|
||||
font-size: 12.5px;
|
||||
margin: 0 0 16px;
|
||||
}
|
||||
|
||||
/* ─── Card wrapper ─── */
|
||||
.ddreg-card {
|
||||
background: #fff;
|
||||
border: 1px solid #e2e8f0;
|
||||
border-radius: 12px;
|
||||
box-shadow: 0 1px 3px rgb(15 23 42 / 6%);
|
||||
overflow: hidden;
|
||||
}
|
||||
.ddreg-card__head {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
padding: 14px 18px;
|
||||
border-bottom: 1px solid #e2e8f0;
|
||||
background: linear-gradient(180deg, #f8fafc 0%, #fff 100%);
|
||||
}
|
||||
.ddreg-card__title {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
font-size: 15px;
|
||||
font-weight: 700;
|
||||
color: #0d1b2a;
|
||||
}
|
||||
.ddreg-card__title-ic {
|
||||
color: #1e88e5;
|
||||
}
|
||||
.ddreg-card__count {
|
||||
margin-left: 8px;
|
||||
padding: 2px 10px;
|
||||
background: #eff6ff;
|
||||
color: #1e40af;
|
||||
border-radius: 999px;
|
||||
font-size: 11px;
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
/* ─── Table ─── */
|
||||
.ddreg-table-wrap {
|
||||
overflow: auto;
|
||||
}
|
||||
.ddreg-table {
|
||||
width: 100%;
|
||||
}
|
||||
.ddreg-table th {
|
||||
position: sticky;
|
||||
top: 0;
|
||||
z-index: 1;
|
||||
background: #f1f5f9;
|
||||
color: #475569;
|
||||
text-align: left;
|
||||
padding: 11px 14px !important;
|
||||
font-size: 10.5px;
|
||||
font-weight: 700;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.05em;
|
||||
border-bottom: 1px solid #e2e8f0;
|
||||
}
|
||||
.ddreg-table td {
|
||||
padding: 10px 14px !important;
|
||||
color: #334155;
|
||||
border-bottom: 1px solid #f1f5f9;
|
||||
}
|
||||
.ddreg-table tr.mat-mdc-row:hover {
|
||||
background: #f8fafc;
|
||||
}
|
||||
.ddreg-num {
|
||||
text-align: right;
|
||||
}
|
||||
.ddreg-mono {
|
||||
font-family: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace;
|
||||
font-size: 12.5px;
|
||||
}
|
||||
.ddreg-strong {
|
||||
font-weight: 700;
|
||||
color: #0d1b2a;
|
||||
}
|
||||
.ddreg-muted {
|
||||
color: #94a3b8;
|
||||
}
|
||||
|
||||
/* ─── Status badge (per-row, same palette as the filter chips) ─── */
|
||||
.ddreg-badge {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
padding: 4px 10px;
|
||||
border-radius: 9999px;
|
||||
font-size: 11.5px;
|
||||
font-weight: 600;
|
||||
}
|
||||
.ddreg-badge__dot {
|
||||
width: 6px;
|
||||
height: 6px;
|
||||
border-radius: 50%;
|
||||
background: currentColor;
|
||||
}
|
||||
.ddreg-badge.available {
|
||||
background: #dcfce7;
|
||||
color: #15803d;
|
||||
}
|
||||
.ddreg-badge.issued {
|
||||
background: #dbeafe;
|
||||
color: #1d4ed8;
|
||||
}
|
||||
.ddreg-badge.cleared {
|
||||
background: #f1f5f9;
|
||||
color: #475569;
|
||||
}
|
||||
.ddreg-badge.cancelled {
|
||||
background: #fee2e2;
|
||||
color: #b91c1c;
|
||||
}
|
||||
.ddreg-badge.stopped {
|
||||
background: #fef3c7;
|
||||
color: #b45309;
|
||||
}
|
||||
/* Spoiled stock. Grey rather than red: nothing went wrong with the customer's draft, only
|
||||
with the paper it was printed on, and the draft itself is alive on another leaflet. */
|
||||
.status-badge.revoked {
|
||||
background-color: #e2e8f0;
|
||||
.ddreg-badge.revoked {
|
||||
background: #e2e8f0;
|
||||
color: #475569;
|
||||
}
|
||||
.action-btn-group {
|
||||
display: flex;
|
||||
gap: 8px;
|
||||
|
||||
/* ─── Actions ─── */
|
||||
.ddreg-actions-col {
|
||||
min-width: 190px;
|
||||
}
|
||||
.pending-badge {
|
||||
.ddreg-actions {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
flex-wrap: wrap;
|
||||
gap: 6px;
|
||||
}
|
||||
.ddreg-act {
|
||||
width: 30px;
|
||||
height: 30px;
|
||||
border: 1px solid #e2e8f0;
|
||||
background: #fff;
|
||||
border-radius: 7px;
|
||||
cursor: pointer;
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
padding: 4px 10px;
|
||||
border-radius: 9999px;
|
||||
font-size: 12px;
|
||||
font-weight: 500;
|
||||
background-color: #fef3c7;
|
||||
color: #b45309;
|
||||
}
|
||||
.status-filter-row {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 8px;
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
.status-filter-row button.filter-active {
|
||||
background-color: #ede9fe;
|
||||
}
|
||||
.filter-count {
|
||||
justify-content: center;
|
||||
color: #64748b;
|
||||
margin-left: 4px;
|
||||
font-size: 12px;
|
||||
transition: all 0.12s;
|
||||
}
|
||||
.hint-text {
|
||||
.ddreg-act:hover {
|
||||
transform: translateY(-1px);
|
||||
}
|
||||
.ddreg-act.more:hover {
|
||||
background: #f5f3ff;
|
||||
border-color: #ddd6fe;
|
||||
color: #7c3aed;
|
||||
}
|
||||
.ddreg-menu__ic {
|
||||
width: 18px;
|
||||
margin-right: 10px;
|
||||
color: #64748b;
|
||||
font-size: 13px;
|
||||
margin: -8px 0 16px 0;
|
||||
text-align: center;
|
||||
}
|
||||
.ddreg-menu__ic.warn {
|
||||
color: #ef4444;
|
||||
}
|
||||
|
||||
.ddreg-pending {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 5px;
|
||||
padding: 4px 10px;
|
||||
border-radius: 9999px;
|
||||
font-size: 11px;
|
||||
font-weight: 600;
|
||||
background: #fef3c7;
|
||||
color: #b45309;
|
||||
}
|
||||
.ddreg-pending fa-icon {
|
||||
font-size: 10px;
|
||||
}
|
||||
.ddreg-info {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 5px;
|
||||
font-size: 11.5px;
|
||||
color: #64748b;
|
||||
}
|
||||
.ddreg-info fa-icon {
|
||||
font-size: 10px;
|
||||
}
|
||||
|
||||
.ddreg-empty {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
padding: 48px 16px;
|
||||
color: #94a3b8;
|
||||
font-size: 13px;
|
||||
}
|
||||
.ddreg-empty fa-icon {
|
||||
font-size: 22px;
|
||||
opacity: 0.6;
|
||||
}
|
||||
|
||||
/* ─── Responsive ─── */
|
||||
@media (max-width: 900px) {
|
||||
.ddreg-toolbar {
|
||||
flex-direction: column;
|
||||
align-items: stretch;
|
||||
}
|
||||
.ddreg-toolbar__search {
|
||||
max-width: none;
|
||||
}
|
||||
}
|
||||
@media (max-width: 600px) {
|
||||
.ddreg-table th,
|
||||
.ddreg-table td {
|
||||
padding: 8px 10px !important;
|
||||
font-size: 12px;
|
||||
}
|
||||
}
|
||||
`
|
||||
]
|
||||
|
||||
@@ -108,13 +108,14 @@ export class SponsorDDService {
|
||||
chargesAmount?: number;
|
||||
issuingBranchId: number;
|
||||
issuedDate: string; // yyyy-MM-dd
|
||||
paymentMethod: 'ACCOUNT_DEBIT' | 'CHEQUE' | 'CASH';
|
||||
paymentMethod: 'ACCOUNT_DEBIT' | 'CHEQUE' | 'CASH' | 'GL';
|
||||
savingsAccountId?: number;
|
||||
currentAccountId?: number;
|
||||
chequeNumber?: string;
|
||||
chequeBankName?: string;
|
||||
cashierDepositRequestId?: number;
|
||||
denominationData?: string;
|
||||
glAccountId?: number;
|
||||
}
|
||||
): Observable<any> {
|
||||
return this.http.post<any>(`/sponsordd/leaflets/${leafletId}/issue-request`, data);
|
||||
|
||||
Reference in New Issue
Block a user