fix(tasks): lay the checker inbox popup out properly
Tests / testRigor (push) Canceled after 0s
Playwright E2E Tests / Run Playwright Tests (push) Canceled after 0s
build-run / Run Lint, Build and Deploy (push) Canceled after 0s
Publish Image in Docker Hub / build (push) Canceled after 0s

The dialog rendered but read badly: the heading appeared twice, and every field
ran together onto one line ("ID: 125Status: User: mifosAction: DELETE").

Cause was the global flex-40/flex-60/flex-fill utilities the template used --
they are sized for a full-width page, so inside a 680px dialog the label and
value columns collapsed and wrapped into each other. Replaced them with a
self-contained label-above-value grid that reflows to one column when narrow,
so the same markup reads correctly as a page and as a popup.

Also: moved the heading into the shared header so it is rendered once in both
modes, gave the actions real buttons instead of Material raised buttons styled
by two global overrides, put DELETE in a red tag so a destructive entry is
visible at a glance, and dropped MatDivider/MatDialogModule from imports now
that the markup no longer uses them.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-09-01 18:40:59 +05:30
co-authored by Claude Opus 5
parent e8097bc2cb
commit 63134f172a
3 changed files with 302 additions and 158 deletions
@@ -6,125 +6,121 @@
file, You can obtain one at http://mozilla.org/MPL/2.0/. file, You can obtain one at http://mozilla.org/MPL/2.0/.
--> -->
<div [class.vci--dialog]="isDialog"> <div class="vci" [class.vci--dialog]="isDialog">
<!-- The heading lives here in both modes, so a dialog does not end up showing it twice. -->
<div class="vci-head">
<span class="vci-head__title">{{ 'labels.heading.View Checker Inbox' | translate }}</span>
@if (isDialog) { @if (isDialog) {
<div class="vci-dialog-bar"> <button type="button" class="vci-head__close" (click)="close()">
<span class="vci-dialog-bar__title">{{ 'labels.heading.View Checker Inbox' | translate }}</span> <fa-icon icon="times"></fa-icon>
<span class="vci-dialog-bar__close" (click)="close()">✕ {{ 'labels.buttons.Close' | translate }}</span> <span>{{ 'labels.buttons.Close' | translate }}</span>
</div> </button>
} }
</div>
<!-- As a dialog the detail is fetched rather than resolved, so nothing below can be rendered <!-- As a dialog the detail is fetched rather than resolved, so nothing below can render until it
until it arrives — the template reads checkerInboxDetail.* unguarded. --> arrives — every field reads checkerInboxDetail.* directly. -->
@if (loading) { @if (loading) {
<div class="vci-loading">{{ 'labels.heading.Loading' | translate }}…</div> <div class="vci-loading">
<fa-icon icon="spinner" class="vci-loading__ic"></fa-icon>
<span>{{ 'labels.heading.Loading' | translate }}…</span>
</div>
} }
@if (checkerInboxDetail) { @if (checkerInboxDetail) {
<div class="vci-body">
@if (isProductEditEntry) { @if (isProductEditEntry) {
<div class="container m-b-20"> <div class="vci-notice">
<div class="mat-elevation-z1" style="padding: 12px 16px; border-left: 4px solid #f59e0b; background: #fffbeb">
This is a product edit awaiting approval. Approve or reject it from This is a product edit awaiting approval. Approve or reject it from
<strong>Approvals &gt; Pending Product Edits</strong> instead — actioning it here would not release its lock. <strong>Approvals &gt; Pending Product Edits</strong> instead — actioning it here would not release its lock.
</div> </div>
</div>
} @else { } @else {
<div class="layout-row align-end gap-1percent layout-lt-md-column container m-b-20"> <div class="vci-actions">
<button mat-raised-button color="success" (click)="approveChecker()"> <button type="button" class="vci-btn vci-btn--approve" (click)="approveChecker()">
<fa-icon icon="check" class="m-r-10"></fa-icon>{{ 'labels.buttons.Approve' | translate }} <fa-icon icon="check"></fa-icon><span>{{ 'labels.buttons.Approve' | translate }}</span>
</button> </button>
<button mat-raised-button color="warn" (click)="deleteChecker()"> <button type="button" class="vci-btn vci-btn--delete" (click)="deleteChecker()">
<fa-icon icon="trash" class="m-r-10"></fa-icon>{{ 'labels.buttons.Delete' | translate }} <fa-icon icon="trash"></fa-icon><span>{{ 'labels.buttons.Delete' | translate }}</span>
</button> </button>
<button mat-raised-button color="reject" (click)="rejectChecker()"> <button type="button" class="vci-btn vci-btn--reject" (click)="rejectChecker()">
<fa-icon icon="times" class="m-r-10"></fa-icon>{{ 'labels.buttons.Reject' | translate }} <fa-icon icon="times"></fa-icon><span>{{ 'labels.buttons.Reject' | translate }}</span>
</button> </button>
</div> </div>
} }
<div class="container"> <div class="vci-section">
<mat-card> <div class="vci-section__title">{{ 'labels.heading.Details' | translate }}</div>
<mat-card-content> <!-- A real label/value grid rather than the global flex-40/flex-60 utilities, which are sized
<div class="layout-row-wrap responsive-column"> for a full-width page and collapse into one run-together line inside a dialog. -->
<h2 class="mat-h2 flex-fill">{{ 'labels.heading.View Checker Inbox' | translate }}</h2> <dl class="vci-grid">
<div class="vci-kv">
<mat-divider [inset]="true"></mat-divider> <dt>{{ 'labels.inputs.ID' | translate }}</dt>
<dd>{{ checkerInboxDetail.id }}</dd>
<h3 class="mat-h3 flex-fill">{{ 'labels.heading.Details' | translate }}</h3>
<mat-divider [inset]="true"></mat-divider>
<div class="flex-fill">
<span class="flex-40">{{ 'labels.inputs.ID' | translate }}:</span>
<span class="flex-60">{{ checkerInboxDetail.id }}</span>
</div> </div>
<div class="vci-kv">
<div class="flex-fill"> <dt>{{ 'labels.inputs.Status' | translate }}</dt>
<span class="flex-40">{{ 'labels.inputs.Status' | translate }}:</span> <dd>{{ checkerInboxDetail.processingResult || '—' }}</dd>
<span class="flex-60">{{ checkerInboxDetail.processingResult }}</span>
</div> </div>
<div class="vci-kv">
<div class="flex-fill"> <dt>{{ 'labels.inputs.User' | translate }}</dt>
<span class="flex-40">{{ 'labels.inputs.User' | translate }}:</span> <dd>{{ checkerInboxDetail.maker || '—' }}</dd>
<span class="flex-60">{{ checkerInboxDetail.maker }}</span>
</div> </div>
<div class="vci-kv">
<div class="flex-fill"> <dt>{{ 'labels.inputs.Action' | translate }}</dt>
<span class="flex-40">{{ 'labels.inputs.Action' | translate }}:</span> <dd>
<span class="flex-60">{{ checkerInboxDetail.actionName }}</span> <span class="vci-tag" [class.vci-tag--danger]="checkerInboxDetail.actionName === 'DELETE'">{{
checkerInboxDetail.actionName
}}</span>
</dd>
</div> </div>
<div class="vci-kv">
<div class="flex-fill"> <dt>{{ 'labels.inputs.Entity' | translate }}</dt>
<span class="flex-40">{{ 'labels.inputs.Entity' | translate }}:</span> <dd>{{ checkerInboxDetail.entityName }}</dd>
<span class="flex-60">{{ checkerInboxDetail.entityName }}</span>
</div> </div>
@if (checkerInboxDetail.resourceId) { @if (checkerInboxDetail.resourceId) {
<div class="flex-fill"> <div class="vci-kv">
<span class="flex-40">{{ 'labels.inputs.Resource ID' | translate }}:</span> <dt>{{ 'labels.inputs.Resource ID' | translate }}</dt>
<span class="flex-60">{{ checkerInboxDetail.resourceId }}</span> <dd>{{ checkerInboxDetail.resourceId }}</dd>
</div> </div>
} }
<div class="vci-kv">
<div class="flex-fill"> <dt>{{ 'labels.inputs.Date' | translate }}</dt>
<span class="flex-40">{{ 'labels.inputs.Date' | translate }}:</span> <dd>{{ checkerInboxDetail.madeOnDate | dateFormat }}</dd>
<span class="flex-60">{{ checkerInboxDetail.madeOnDate | dateFormat }}</span>
</div> </div>
@if (checkerInboxDetail.officeName) { @if (checkerInboxDetail.officeName) {
<div class="flex-fill"> <div class="vci-kv">
<span class="flex-40">{{ 'labels.inputs.Office' | translate }}:</span> <dt>{{ 'labels.inputs.Office' | translate }}</dt>
<span class="flex-60">{{ checkerInboxDetail.officeName }}</span> <dd>{{ checkerInboxDetail.officeName }}</dd>
</div> </div>
} }
@if (checkerInboxDetail.savingsAccountNo) { @if (checkerInboxDetail.savingsAccountNo) {
<div class="flex-fill"> <div class="vci-kv">
<span class="flex-40">{{ 'labels.inputs.Savings Account Number' | translate }}:</span> <dt>{{ 'labels.inputs.Savings Account Number' | translate }}</dt>
<span class="flex-60">{{ checkerInboxDetail.savingsAccountNo }}</span> <dd>{{ checkerInboxDetail.savingsAccountNo }}</dd>
</div> </div>
} }
@if (checkerInboxDetail.groupLevelName) { @if (checkerInboxDetail.groupLevelName) {
<div class="flex-fill"> <div class="vci-kv">
<span class="flex-40"> {{ checkerInboxDetail.groupLevelName }}</span> <dt>{{ checkerInboxDetail.groupLevelName }}</dt>
<span class="flex-60">{{ checkerInboxDetail.groupName }}</span> <dd>{{ checkerInboxDetail.groupName }}</dd>
</div> </div>
} }
</dl>
</div>
@if (displayJSONData) { @if (displayJSONData) {
<h3 class="mat-h3 flex-fill">{{ 'labels.heading.Command' | translate }}</h3> <div class="vci-section">
<mat-divider [inset]="true"></mat-divider> <div class="vci-section__title">{{ 'labels.heading.Command' | translate }}</div>
@for (json of jsondata | keyvalue; track json) { <dl class="vci-grid">
<div class="flex-fill"> @for (json of jsondata | keyvalue; track json.key) {
<span class="flex-40">{{ json.key }}:</span> <div class="vci-kv">
<span class="flex-60">{{ json.value }}</span> <dt>{{ json.key }}</dt>
<dd>{{ json.value }}</dd>
</div> </div>
} }
} </dl>
</div> </div>
</mat-card-content> }
</mat-card>
</div> </div>
} }
</div> </div>
@@ -6,45 +6,70 @@
* file, You can obtain one at http://mozilla.org/MPL/2.0/. * file, You can obtain one at http://mozilla.org/MPL/2.0/.
*/ */
.mat-elevation-z1 { $success: #16a34a;
margin: 1em 0 1.5em; $danger: #dc2626;
$warning: #d97706;
$text-dark: #0d1b2a;
$text-mid: #475569;
$text-soft: #94a3b8;
$border: #e2e8f0;
$bg-page: #f8fafc;
:host {
display: block;
} }
/* Opened as a popup rather than a full page — sizes to its own content (capped so a long command .vci {
payload still scrolls inside the dialog) instead of stretching to the viewport. */ background: #fff;
.vci--dialog {
width: min(720px, 92vw); /* Opened as a popup rather than a full page: sized to its own content, capped so a long command
payload scrolls inside the dialog instead of pushing it off screen. As a route it stays a
normal page-width card. */
&--dialog {
width: min(680px, 92vw);
max-height: 85vh; max-height: 85vh;
overflow-y: auto; overflow-y: auto;
border-radius: 10px;
}
.container { &:not(&--dialog) {
max-width: none; max-width: 900px;
padding: 0; margin: 20px auto;
border: 1px solid $border;
border-radius: 12px;
overflow: hidden;
} }
} }
.vci-dialog-bar { // ── Header ───────────────────────────────────────────────────────────────────
.vci-head {
display: flex; display: flex;
align-items: center; align-items: center;
gap: 8px; gap: 12px;
padding: 10px 16px; padding: 14px 20px;
border-bottom: 1px solid $border;
background: #fff; background: #fff;
border-bottom: 1px solid #e2e8f0;
position: sticky; position: sticky;
top: 0; top: 0;
z-index: 1; z-index: 1;
&__title { &__title {
font-size: 14px; font-size: 15px;
font-weight: 700; font-weight: 700;
color: #475569; color: $text-dark;
} }
&__close { &__close {
margin-left: auto; margin-left: auto;
font-size: 12px; display: inline-flex;
font-weight: 500; align-items: center;
color: #94a3b8; gap: 6px;
border: none;
background: none;
font-family: inherit;
font-size: 12.5px;
font-weight: 600;
color: $text-soft;
cursor: pointer; cursor: pointer;
padding: 6px 12px; padding: 6px 12px;
border-radius: 6px; border-radius: 6px;
@@ -52,43 +77,169 @@
&:hover { &:hover {
background: #f1f5f9; background: #f1f5f9;
color: #475569; color: $text-mid;
} }
} }
} }
.vci-loading { .vci-loading {
display: flex;
align-items: center;
justify-content: center;
gap: 10px;
padding: 48px 24px; padding: 48px 24px;
text-align: center;
font-size: 14px; font-size: 14px;
color: #94a3b8; color: $text-soft;
&__ic {
animation: vci-spin 1s linear infinite;
}
} }
h2, @keyframes vci-spin {
h3, to {
h4 { transform: rotate(360deg);
}
}
.vci-body {
padding: 16px 20px 20px;
display: flex;
flex-direction: column;
gap: 16px;
}
// ── Actions ──────────────────────────────────────────────────────────────────
.vci-actions {
display: flex;
gap: 10px;
flex-wrap: wrap;
}
.vci-btn {
display: inline-flex;
align-items: center;
gap: 7px;
height: 36px;
padding: 0 18px;
border-radius: 8px;
font-family: inherit;
font-size: 13.5px;
font-weight: 600;
cursor: pointer;
border: 1.5px solid transparent;
transition: all 0.15s;
&--approve {
background: #f0fdf4;
color: $success;
border-color: #86efac;
&:hover {
background: $success;
color: #fff;
border-color: $success;
}
}
&--delete {
background: $danger;
color: #fff;
border-color: $danger;
&:hover {
filter: brightness(1.08);
}
}
&--reject {
background: #fff;
color: $text-mid;
border-color: $border;
&:hover {
background: #f1f5f9;
color: $text-dark;
}
}
}
.vci-notice {
padding: 12px 16px;
border-left: 4px solid $warning;
background: #fffbeb;
border-radius: 6px;
font-size: 13px;
line-height: 1.5;
color: #92400e;
}
// ── Detail sections ──────────────────────────────────────────────────────────
.vci-section {
border: 1px solid $border;
border-radius: 10px;
overflow: hidden;
&__title {
padding: 10px 16px;
background: $bg-page;
border-bottom: 1px solid $border;
font-size: 11px;
font-weight: 800;
text-transform: uppercase;
letter-spacing: 0.07em;
color: $text-mid;
}
}
/* One label above its value, two pairs per row where there is room. This replaces the global
flex-40/flex-60 utilities, which are sized for a full-width page and ran every field together
onto a single line inside the dialog. */
.vci-grid {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(220px, 1fr));
gap: 14px 24px;
margin: 0; margin: 0;
padding: 16px;
}
.vci-kv {
display: flex;
flex-direction: column;
gap: 3px;
min-width: 0;
dt {
font-size: 10.5px;
font-weight: 700;
text-transform: uppercase;
letter-spacing: 0.06em;
color: $text-soft;
}
dd {
margin: 0;
font-size: 14px;
font-weight: 500; font-weight: 500;
color: $text-dark;
line-height: 1.35;
overflow-wrap: anywhere;
}
} }
span { .vci-tag {
margin: 0.5em 0; display: inline-flex;
} align-items: center;
padding: 2px 10px;
border-radius: 20px;
background: #e0f2fe;
color: #0369a1;
font-size: 11.5px;
font-weight: 700;
letter-spacing: 0.04em;
.margin-t { &--danger {
margin-top: 1em; background: #fee2e2;
} color: #991b1b;
}
mat-divider {
margin: 0 0 0.5em;
}
.mat-raised-button.mat-success {
color: #fff;
background-color: #008000ab;
}
.mat-raised-button.mat-reject {
color: #fff;
background-color: #ffca00;
} }
@@ -10,7 +10,7 @@
import { Component, inject } from '@angular/core'; import { Component, inject } from '@angular/core';
import { ActivatedRoute, Router } from '@angular/router'; import { ActivatedRoute, Router } from '@angular/router';
import * as _ from 'lodash'; import * as _ from 'lodash';
import { MatDialog, MatDialogRef, MAT_DIALOG_DATA, MatDialogModule } from '@angular/material/dialog'; import { MatDialog, MatDialogRef, MAT_DIALOG_DATA } from '@angular/material/dialog';
/** Custom Services */ /** Custom Services */
import { TasksService } from '../tasks.service'; import { TasksService } from '../tasks.service';
@@ -19,7 +19,6 @@ import { TasksService } from '../tasks.service';
import { ConfirmationDialogComponent } from 'app/shared/confirmation-dialog/confirmation-dialog.component'; import { ConfirmationDialogComponent } from 'app/shared/confirmation-dialog/confirmation-dialog.component';
import { TranslateService } from '@ngx-translate/core'; import { TranslateService } from '@ngx-translate/core';
import { FaIconComponent } from '@fortawesome/angular-fontawesome'; import { FaIconComponent } from '@fortawesome/angular-fontawesome';
import { MatDivider } from '@angular/material/divider';
import { KeyValuePipe } from '@angular/common'; import { KeyValuePipe } from '@angular/common';
import { DateFormatPipe } from '../../pipes/date-format.pipe'; import { DateFormatPipe } from '../../pipes/date-format.pipe';
import { STANDALONE_SHARED_IMPORTS } from 'app/standalone-shared.module'; import { STANDALONE_SHARED_IMPORTS } from 'app/standalone-shared.module';
@@ -31,10 +30,8 @@ import { STANDALONE_SHARED_IMPORTS } from 'app/standalone-shared.module';
imports: [ imports: [
...STANDALONE_SHARED_IMPORTS, ...STANDALONE_SHARED_IMPORTS,
FaIconComponent, FaIconComponent,
MatDivider,
KeyValuePipe, KeyValuePipe,
DateFormatPipe, DateFormatPipe
MatDialogModule
] ]
}) })
export class ViewCheckerInboxComponent { export class ViewCheckerInboxComponent {