Client-stepper: pre-fill existing KYC/address proofs in edit, fix identifier saves

Edit-mode now shows the previously selected KYC document type + uploaded file thumbnail
(loads from /clients/{id}/identifiers + /client_identifiers/{id}/documents). Same for
address proof on the India address step. Closes the gap where editing a client showed
an empty file picker even when proof had been uploaded on create.

Identifier-save hardened across all three callsites (create-client, edit-client,
identities-tab.createIdentifierWithDocs) - the KYC chip picker proofId is a CODE
string ('AADHAAR'), but backend ClientIdentifierRequest.documentTypeId is Long, which
Spring rejects with HttpMessageNotReadableException -> 400. resolveDocumentTypeId
helper looks up the code against /kyc/proof-types?type=id and substitutes the numeric
CodeValue id before posting. Skips silently for constitution-specific non-individual
docs that have no CodeValue mapping.

KYC payload normalises panNumber / enterprisePan / gstinNumber / cinNumber via
toUpperCase + whitespace strip so user-typed lowercase or trailing spaces don't trip
the backend regex (validation.msg.CLIENTKYC.panNumber.does.not.match.regexp).

State name match in client-address save now normalises both sides (lowercase, collapse
underscores + spaces) so legacy MADHYA_PRADESH resolves to 'Madhya Pradesh' returned
by api.postalpincode.in. Backed by tenant migration 0297.

Identities-tab resolver flattened - was returning identifiers before the per-row
forkJoin(getClientIdentificationDocuments) completed, so the view rendered empty
documents arrays. Now uses switchMap + forkJoin so the resolver awaits completion.

Also bundles concurrent work-in-progress on letter-mgmt, organization, users,
bank-calendar, shell, nominee/notes/identifier dialogs, and theming.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
Aditya
2026-05-20 12:40:58 +05:30
co-authored by Claude Opus 4.7
parent 325228c514
commit 83ea57ccf0
84 changed files with 10281 additions and 1194 deletions
@@ -74,6 +74,23 @@
<div class="cal-legend__item"><span class="cal-legend__dot today"></span> Today</div>
</div>
<div class="cal-legend__actions">
<button
class="cal-legend__tmpl-btn"
(click)="downloadImportTemplate()"
matTooltip="Download a pre-formatted .xlsx with sample rows + instructions"
>
<fa-icon icon="file-excel"></fa-icon> Download Template
</button>
<button
class="cal-legend__import-btn"
(click)="triggerImport()"
[disabled]="importing"
matTooltip="Bulk-create holidays from a filled-in template"
>
<fa-icon [icon]="importing ? 'spinner' : 'upload'" [class.fa-spin]="importing"></fa-icon>
{{ importing ? 'Importing…' : 'Import Holidays' }}
</button>
<input #importInput type="file" accept=".xlsx" (change)="onImportFileChosen($event)" hidden />
<button class="cal-legend__print-btn" (click)="printHolidayList()">
<fa-icon icon="print"></fa-icon> Print Holiday List
</button>
@@ -83,6 +100,200 @@
</div>
</div>
<!-- Import result banner -->
@if (importResult) {
<div
class="cal-import-result"
[class.cal-import-result--ok]="importResult.imported > 0 && importResult.skipped.length === 0"
[class.cal-import-result--mixed]="importResult.imported > 0 && importResult.skipped.length > 0"
[class.cal-import-result--fail]="importResult.imported === 0"
>
<div class="cal-import-result__head">
<fa-icon
[icon]="
importResult.imported > 0 && importResult.skipped.length === 0
? 'check-circle'
: importResult.imported === 0
? 'times-circle'
: 'exclamation-triangle'
"
class="cal-import-result__ic"
></fa-icon>
<div>
<div class="cal-import-result__title">
Import — {{ importResult.imported }} of {{ importResult.total }} added
</div>
<div class="cal-import-result__sub">
@if (importResult.skipped.length === 0) {
All rows were imported successfully.
} @else {
{{ importResult.skipped.length }} row{{ importResult.skipped.length === 1 ? '' : 's' }} skipped — see
below.
}
</div>
</div>
<button class="cal-import-result__close" (click)="dismissImportResult()" matTooltip="Dismiss">
<fa-icon icon="times"></fa-icon>
</button>
</div>
@if (importResult.skipped.length > 0) {
<ul class="cal-import-result__list">
@for (s of importResult.skipped; track s.row) {
<li>
<strong>Row {{ s.row }}:</strong> {{ s.reason }}
</li>
}
</ul>
}
</div>
}
<!-- Holiday List Panel (office-wise) -->
<div class="cal-list">
<div class="cal-list__header">
<div class="cal-list__title">
<fa-icon icon="list-alt" class="cal-list__title-ic"></fa-icon>
<span>Holiday List {{ currentYear }}</span>
<span class="cal-list__count">{{ filteredHolidayRows.length }} of {{ holidayRows.length }}</span>
</div>
<div class="cal-list__filters">
<div class="cal-list__search">
<fa-icon icon="search" class="cal-list__search-ic"></fa-icon>
<input type="text" placeholder="Search by name, office, description…" [(ngModel)]="filterSearch" />
</div>
<select [(ngModel)]="filterOfficeId" class="cal-list__select">
<option [ngValue]="''">All Offices</option>
@for (o of offices; track o.id) {
<option [ngValue]="o.id">{{ o.name }}</option>
}
</select>
<select [(ngModel)]="filterType" class="cal-list__select">
<option value="">All Types</option>
<option value="GAZETTED">Gazetted (RBI)</option>
<option value="CUSTOM">Custom</option>
</select>
@if (filterSearch || filterOfficeId !== '' || filterType) {
<button class="cal-list__clear" (click)="clearListFilters()"><fa-icon icon="times"></fa-icon> Clear</button>
}
</div>
</div>
<div class="cal-list__table-wrap">
<table class="cal-list__table">
<thead>
<tr>
<th class="num">#</th>
<th>Date</th>
<th>Day</th>
<th>Holiday / Occasion</th>
<th>Office</th>
<th>Type</th>
<th>Status</th>
<th class="actions">Actions</th>
</tr>
</thead>
<tbody>
@for (row of filteredHolidayRows; track trackHolidayRow($index, row); let i = $index) {
<tr>
<td class="num">{{ i + 1 }}</td>
<td class="mono">{{ formatRowDate(row.fromDate) }}</td>
<td>{{ rowDayName(row.fromDate) }}</td>
<td>
<div class="cal-list__name">{{ row.name }}</div>
@if (row.description && row.description !== row.name) {
<div class="cal-list__desc">{{ row.description }}</div>
}
</td>
<td>
@for (n of row.officeNames; track n) {
<span class="cal-list__office-chip">{{ n }}</span>
}
</td>
<td>
@if (row.isGazetted) {
<span class="cal-list__pill gazetted">Gazetted</span>
} @else {
<span class="cal-list__pill custom">Custom</span>
}
</td>
<td>
<span
class="cal-list__pill status"
[class.active]="row.status === 'Active' || row.status === 'holidayStatusType.active'"
>
{{ row.status === 'holidayStatusType.active' ? 'Active' : row.status }}
</span>
</td>
<td class="actions">
<button
class="cal-list__act edit"
(click)="editHoliday(row)"
[disabled]="row.isGazetted"
[matTooltip]="row.isGazetted ? 'Gazetted holiday — read-only' : 'Edit holiday'"
>
<fa-icon icon="pencil-alt"></fa-icon>
</button>
<button
class="cal-list__act remove"
(click)="removeHoliday(row)"
[disabled]="row.isGazetted"
[matTooltip]="row.isGazetted ? 'Gazetted holiday — cannot remove' : 'Remove holiday'"
>
<fa-icon icon="trash"></fa-icon>
</button>
<button
class="cal-list__act more"
[matMenuTriggerFor]="rowMenu"
matTooltip="More actions"
(click)="$event.stopPropagation()"
>
<fa-icon icon="ellipsis-v"></fa-icon>
</button>
<mat-menu #rowMenu="matMenu" xPosition="before" class="cal-list__menu">
<button
mat-menu-item
(click)="activateHoliday(row)"
[disabled]="row.isGazetted || isHolidayActive(row)"
>
<fa-icon icon="check-circle" class="cal-list__menu-ic ok"></fa-icon>
@if (isHolidayActive(row)) {
Already Active
} @else {
Activate
}
</button>
<button mat-menu-item (click)="editHoliday(row)" [disabled]="row.isGazetted">
<fa-icon icon="pencil-alt" class="cal-list__menu-ic"></fa-icon>
Edit Holiday
</button>
<button mat-menu-item (click)="removeHoliday(row)" [disabled]="row.isGazetted">
<fa-icon icon="trash" class="cal-list__menu-ic warn"></fa-icon>
Remove Holiday
</button>
<button mat-menu-item (click)="copyHolidayId(row)" [disabled]="row.id == null">
<fa-icon icon="copy" class="cal-list__menu-ic"></fa-icon>
Copy Holiday ID
</button>
</mat-menu>
</td>
</tr>
}
@if (filteredHolidayRows.length === 0) {
<tr>
<td colspan="8" class="cal-list__empty">
@if (holidayListLoading) {
<fa-icon icon="spinner" class="fa-spin"></fa-icon> Loading holidays…
} @else {
No holidays match the current filters.
}
</td>
</tr>
}
</tbody>
</table>
</div>
</div>
<!-- Monthly Grids -->
<div class="cal-months">
@for (month of months; track month.month) {