feat(election): structured audit log details UI

Replaced raw JSON pre dump with readable key-value rows.
PASS/FAIL/WON values show as green badge, FAIL/REJECT/CANCELLED as red.
Manual vote counts shown as table with Candidate/Post/Votes/Previous columns.
Winner results shown as separate table with Post/Winner/Votes.
Added DETAIL_LABELS for 50+ field keys covering all audit action types.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
Jaanu423
2026-07-01 15:01:48 +05:30
co-authored by Claude Sonnet 4.6
parent a915259ba1
commit 4666a476fd
3 changed files with 409 additions and 11 deletions
@@ -15,7 +15,76 @@ import { Subject } from 'rxjs';
import { debounceTime, takeUntil } from 'rxjs/operators';
import { ElectionService } from '../../election.service';
const DETAIL_LABELS: Record<string, string> = {
// Election state transitions
electionTitle: 'Election',
newStatus: 'New Status',
reason: 'Reason',
// Election config fields
title: 'Title',
description: 'Description',
electionType: 'Election Type',
electionDate: 'Election Date',
resultDate: 'Result Date',
nominationStartDate: 'Nomination Start',
nominationEndDate: 'Nomination End',
votingStartDate: 'Voting Start',
votingEndDate: 'Voting End',
// Nomination submission
nominationId: 'Nomination ID',
applicantName: 'Applicant Name',
memberId: 'Member ID',
postId: 'Post ID',
gender: 'Gender',
caste: 'Caste',
// Eligibility check
overallResult: 'Overall Result',
age: 'Age',
shareHolding: 'Share Holding',
area: 'Area',
ageRemarks: 'Age Note',
genderRemarks: 'Gender Note',
casteRemarks: 'Caste Note',
shareRemarks: 'Share Note',
areaRemarks: 'Area Note',
// CEO Pre-check
directorLoan: 'Director Loan',
directorLoanOutstanding: 'Loan Outstanding (₹)',
directorLoanRemarks: 'Director Loan Note',
relationAccount: 'Relation Account',
relationRemarks: 'Relation Account Note',
kyc: 'KYC',
kycRemarks: 'KYC Note',
familyKyc: 'Family KYC',
familyKycRemarks: 'Family KYC Note',
guarantor: 'Guarantor',
guarantorRemarks: 'Guarantor Note',
aml: 'AML',
amlRemarks: 'AML Note',
familyLoan: 'Family Loan',
familyLoanRemarks: 'Family Loan Note',
// Approval action
action: 'Action',
stepName: 'Approval Step',
stepNumber: 'Step Number',
remarks: 'Remarks',
rejectionReason: 'Rejection Reason',
// Candidate
symbolUpdated: 'Symbol Updated',
profileUpdated: 'Profile Updated',
briefProfile: 'Brief Profile',
filename: 'File Name',
mimeType: 'MIME Type',
// Winner results (array items)
post: 'Post',
winner: 'Winner',
votes: 'Votes',
candidate: 'Candidate',
previousVotes: 'Previous Votes'
};
const ACTION_META: Record<string, { label: string; icon: string; color: string }> = {
// Election Config
CREATE_ELECTION: { label: 'Election Created', icon: 'add_circle', color: '#16a34a' },
UPDATE_ELECTION: { label: 'Election Updated', icon: 'edit', color: '#2563eb' },
ACTIVATE_ELECTION: { label: 'Activated', icon: 'play_circle', color: '#16a34a' },
@@ -25,18 +94,41 @@ const ACTION_META: Record<string, { label: string; icon: string; color: string }
CANCEL_ELECTION: { label: 'Election Cancelled', icon: 'cancel', color: '#dc2626' },
CREATE_POST: { label: 'Post Created', icon: 'work', color: '#7c3aed' },
UPDATE_POST: { label: 'Post Updated', icon: 'work_outline', color: '#2563eb' },
// Election Config — Failed
ACTIVATE_ELECTION_FAILED: { label: 'Activate Failed', icon: 'error_outline', color: '#dc2626' },
CLOSE_NOMINATIONS_FAILED: { label: 'Close Nominations Failed', icon: 'error_outline', color: '#dc2626' },
OPEN_VOTING_FAILED: { label: 'Open Voting Failed', icon: 'error_outline', color: '#dc2626' },
CLOSE_VOTING_FAILED: { label: 'Close Voting Failed', icon: 'error_outline', color: '#dc2626' },
CANCEL_ELECTION_FAILED: { label: 'Cancel Failed', icon: 'error_outline', color: '#dc2626' },
// Nomination
SUBMIT_NOMINATION: { label: 'Nomination Submitted', icon: 'person_add', color: '#2563eb' },
WITHDRAW_NOMINATION: { label: 'Nomination Withdrawn', icon: 'person_remove', color: '#dc2626' },
RESUBMIT_NOMINATION: { label: 'Nomination Resubmitted', icon: 'refresh', color: '#2563eb' },
ELIGIBILITY_CHECK: { label: 'Eligibility Checked', icon: 'fact_check', color: '#7c3aed' },
CEO_PRECHECK: { label: 'CEO Pre-check', icon: 'manage_accounts', color: '#7c3aed' },
APPROVAL_ACTION: { label: 'Approval Action', icon: 'approval', color: '#d97706' },
GENERATE_NO_DUE_CERTIFICATE: { label: 'No Due Certificate', icon: 'receipt_long', color: '#0891b2' },
// Nomination — Failed
SUBMIT_NOMINATION_FAILED: { label: 'Nomination Submit Failed', icon: 'error_outline', color: '#dc2626' },
WITHDRAW_NOMINATION_FAILED: { label: 'Withdraw Failed', icon: 'error_outline', color: '#dc2626' },
ELIGIBILITY_CHECK_FAILED: { label: 'Eligibility Check Failed', icon: 'error_outline', color: '#dc2626' },
CEO_PRECHECK_FAILED: { label: 'CEO Pre-check Failed', icon: 'error_outline', color: '#dc2626' },
APPROVAL_ACTION_FAILED: { label: 'Approval Failed', icon: 'error_outline', color: '#dc2626' },
// Candidate
APPROVE_CANDIDATE: { label: 'Candidate Approved', icon: 'verified', color: '#16a34a' },
REMOVE_CANDIDATE: { label: 'Candidate Removed', icon: 'remove_circle', color: '#dc2626' },
UPLOAD_SYMBOL: { label: 'Symbol Uploaded', icon: 'image', color: '#2563eb' },
UPDATE_CANDIDATE: { label: 'Candidate Updated', icon: 'edit_note', color: '#2563eb' },
// Candidate — Failed
APPROVE_CANDIDATE_FAILED: { label: 'Approve Candidate Failed', icon: 'error_outline', color: '#dc2626' },
REMOVE_CANDIDATE_FAILED: { label: 'Remove Candidate Failed', icon: 'error_outline', color: '#dc2626' },
// Voting
CAST_VOTE: { label: 'Vote Cast', icon: 'ballot', color: '#16a34a' },
DECLARE_RESULTS: { label: 'Results Declared', icon: 'gavel', color: '#7c3aed' }
MANUAL_VOTE_ENTRY: { label: 'Manual Vote Entry', icon: 'edit_note', color: '#0891b2' },
MANUAL_VOTE_ENTRY_FAILED: { label: 'Vote Entry Failed', icon: 'error_outline', color: '#dc2626' },
// Results
DECLARE_RESULTS: { label: 'Results Declared', icon: 'gavel', color: '#7c3aed' },
DECLARE_RESULTS_FAILED: { label: 'Declare Results Failed', icon: 'error_outline', color: '#dc2626' }
};
@Component({
@@ -96,7 +188,12 @@ export class ElectionAuditLogComponent implements OnInit, OnDestroy {
'CLOSE_VOTING',
'CANCEL_ELECTION',
'CREATE_POST',
'UPDATE_POST'
'UPDATE_POST',
'ACTIVATE_ELECTION_FAILED',
'CLOSE_NOMINATIONS_FAILED',
'OPEN_VOTING_FAILED',
'CLOSE_VOTING_FAILED',
'CANCEL_ELECTION_FAILED'
],
NOMINATION: [
'SUBMIT_NOMINATION',
@@ -104,16 +201,31 @@ export class ElectionAuditLogComponent implements OnInit, OnDestroy {
'RESUBMIT_NOMINATION',
'ELIGIBILITY_CHECK',
'CEO_PRECHECK',
'APPROVAL_ACTION'
'APPROVAL_ACTION',
'GENERATE_NO_DUE_CERTIFICATE',
'SUBMIT_NOMINATION_FAILED',
'WITHDRAW_NOMINATION_FAILED',
'ELIGIBILITY_CHECK_FAILED',
'CEO_PRECHECK_FAILED',
'APPROVAL_ACTION_FAILED'
],
CANDIDATE: [
'APPROVE_CANDIDATE',
'REMOVE_CANDIDATE',
'UPLOAD_SYMBOL',
'UPDATE_CANDIDATE'
'UPDATE_CANDIDATE',
'APPROVE_CANDIDATE_FAILED',
'REMOVE_CANDIDATE_FAILED'
],
VOTE: ['CAST_VOTE'],
RESULT: ['DECLARE_RESULTS']
VOTE: [
'CAST_VOTE',
'MANUAL_VOTE_ENTRY',
'MANUAL_VOTE_ENTRY_FAILED'
],
RESULT: [
'DECLARE_RESULTS',
'DECLARE_RESULTS_FAILED'
]
};
get availableActions(): string[] {
@@ -226,6 +338,98 @@ export class ElectionAuditLogComponent implements OnInit, OnDestroy {
}
}
isArrayDetails(details: string | null): boolean {
if (!details) return false;
try {
return Array.isArray(JSON.parse(details));
} catch {
return false;
}
}
voteEntries(details: string | null): any[] {
if (!details) return [];
try {
const arr = JSON.parse(details);
return Array.isArray(arr) ? arr : [];
} catch {
return [];
}
}
hasVoteEdits(details: string | null): boolean {
return this.voteEntries(details).some((r) => r.previousVotes !== undefined);
}
isWinnerArray(details: string | null): boolean {
const arr = this.voteEntries(details);
return arr.length > 0 && arr[0].winner !== undefined;
}
detailsEntries(details: string | null): { key: string; label: string; value: string }[] {
if (!details) return [];
try {
const obj = JSON.parse(details);
if (Array.isArray(obj)) return [];
return Object.entries(obj)
.filter(
([
,
v
]) => v !== null && v !== undefined && v !== ''
)
.map(
([
k,
v
]) => ({
key: k,
label: DETAIL_LABELS[k] ?? k.replace(/([A-Z])/g, ' $1').replace(/^./, (s) => s.toUpperCase()),
value: typeof v === 'boolean' ? (v ? 'Yes' : 'No') : String(v)
})
);
} catch {
return [];
}
}
valueBadge(value: string): string {
const v = value?.toUpperCase();
if ([
'PASS',
'YES',
'APPROVE',
'WON',
'ACTIVE',
'VOTING_OPEN'
].includes(v)) return 'badge-pass';
if ([
'FAIL',
'NO',
'REJECT',
'CANCELLED'
].includes(v)) return 'badge-fail';
if ([
'NOMINATION_CLOSED',
'VOTING_CLOSED'
].includes(v)) return 'badge-warn';
return '';
}
formatValue(entry: { key: string; value: string }): string {
const STATUS_LABELS: Record<string, string> = {
ACTIVE: 'Active (Nominations Open)',
NOMINATION_CLOSED: 'Nominations Closed',
VOTING_OPEN: 'Voting Open',
VOTING_CLOSED: 'Voting Closed',
CANCELLED: 'Cancelled'
};
if (entry.key === 'newStatus' && STATUS_LABELS[entry.value]) {
return STATUS_LABELS[entry.value];
}
return entry.value;
}
toggleExpand(id: number): void {
this.expandedId = this.expandedId === id ? null : id;
}