diff --git a/src/app/election/components/election-audit-log/election-audit-log.component.html b/src/app/election/components/election-audit-log/election-audit-log.component.html index e73d81c8d..0deebc967 100644 --- a/src/app/election/components/election-audit-log/election-audit-log.component.html +++ b/src/app/election/components/election-audit-log/election-audit-log.component.html @@ -104,9 +104,76 @@ } - @if (expandedId === log.id && parsedDetails(log.details)) { -
-
{{ parsedDetails(log.details) }}
+ @if (expandedId === log.id && log.details) { +
+ Details + + @if (isArrayDetails(log.details)) { + @if (isWinnerArray(log.details)) { + + + + + + + + + + + @for (row of voteEntries(log.details); track $index) { + + + + + + } + +
PostWinnerVotes
+ {{ row.post }} + {{ row.winner }}{{ row.votes }}
+ } @else { + + + + + + + + @if (hasVoteEdits(log.details)) { + + } + + + + @for (row of voteEntries(log.details); track $index) { + + + + + @if (hasVoteEdits(log.details)) { + + } + + } + +
CandidatePostVotesPrevious
{{ row.candidate }} + {{ row.post }} + {{ row.votes }}{{ row.previousVotes ?? '—' }}
+ } + } @else if (detailsEntries(log.details).length > 0) { + +
+ @for (entry of detailsEntries(log.details); track entry.key) { +
+ {{ entry.label }} + {{ formatValue(entry) }} +
+ } +
+ } @else { + +
{{ parsedDetails(log.details) }}
+ }
}
diff --git a/src/app/election/components/election-audit-log/election-audit-log.component.scss b/src/app/election/components/election-audit-log/election-audit-log.component.scss index fdc534e99..4ceb242e6 100644 --- a/src/app/election/components/election-audit-log/election-audit-log.component.scss +++ b/src/app/election/components/election-audit-log/election-audit-log.component.scss @@ -309,9 +309,136 @@ background: #f8fafc; border: 1px solid #e2e8f0; border-radius: 8px; - padding: 10px 12px; + padding: 12px 14px; - pre { + .details-label { + display: block; + font-size: 10px; + font-weight: 700; + text-transform: uppercase; + letter-spacing: 1px; + color: #94a3b8; + margin-bottom: 10px; + } + + .detail-rows { + display: flex; + flex-direction: column; + } + + .detail-row { + display: flex; + align-items: baseline; + gap: 12px; + padding: 5px 0; + border-bottom: 1px solid #f1f5f9; + + &:last-child { + border-bottom: none; + } + } + + .detail-key { + min-width: 160px; + max-width: 160px; + font-size: 12px; + color: #64748b; + font-weight: 500; + flex-shrink: 0; + } + + .detail-val { + font-size: 13px; + color: #1e293b; + word-break: break-word; + + &.badge-pass { + display: inline-block; + color: #16a34a; + font-weight: 600; + background: #f0fdf4; + padding: 1px 10px; + border-radius: 20px; + border: 1px solid #bbf7d0; + font-size: 12px; + } + + &.badge-fail { + display: inline-block; + color: #dc2626; + font-weight: 600; + background: #fef2f2; + padding: 1px 10px; + border-radius: 20px; + border: 1px solid #fecaca; + font-size: 12px; + } + + &.badge-warn { + display: inline-block; + color: #92400e; + font-weight: 600; + background: #fffbeb; + padding: 1px 10px; + border-radius: 20px; + border: 1px solid #fde68a; + font-size: 12px; + } + } + + .vote-table { + width: 100%; + border-collapse: collapse; + font-size: 13px; + + th { + text-align: left; + padding: 6px 10px; + border-bottom: 2px solid #e2e8f0; + color: #64748b; + font-weight: 600; + font-size: 11px; + text-transform: uppercase; + letter-spacing: 0.5px; + } + + td { + padding: 7px 10px; + border-bottom: 1px solid #f1f5f9; + color: #1e293b; + } + + tr:last-child td { + border-bottom: none; + } + + .vote-count { + font-weight: 700; + color: #2563eb; + font-size: 15px; + } + + .vote-prev { + color: #94a3b8; + text-decoration: line-through; + } + + .winner-name { + font-weight: 600; + color: #16a34a; + } + + .post-chip { + background: #eff6ff; + color: #1d4ed8; + border-radius: 4px; + padding: 1px 7px; + font-size: 11px; + font-weight: 600; + } + } + + .detail-raw { margin: 0; font-size: 12px; color: #2d3748; diff --git a/src/app/election/components/election-audit-log/election-audit-log.component.ts b/src/app/election/components/election-audit-log/election-audit-log.component.ts index e68f3801a..1e42f2e63 100644 --- a/src/app/election/components/election-audit-log/election-audit-log.component.ts +++ b/src/app/election/components/election-audit-log/election-audit-log.component.ts @@ -15,7 +15,76 @@ import { Subject } from 'rxjs'; import { debounceTime, takeUntil } from 'rxjs/operators'; import { ElectionService } from '../../election.service'; +const DETAIL_LABELS: Record = { + // 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 = { + // 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 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 = { + 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; }