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:
2026-09-16 17:17:22 +05:30
co-authored by Claude Opus 5
parent d18bd9cf37
commit b71619c402
15 changed files with 2382 additions and 614 deletions
+289 -214
View File
@@ -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">
&larr; 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>
+577 -95
View File
@@ -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;
}
}
`
]