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:
co-authored by
Claude Opus 4.7
parent
325228c514
commit
83ea57ccf0
@@ -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) {
|
||||
|
||||
Reference in New Issue
Block a user