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:
co-authored by
Claude Sonnet 4.6
parent
a915259ba1
commit
4666a476fd
@@ -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;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user