From dac169ae15116819f043c966a2b1bd73b7769321 Mon Sep 17 00:00:00 2001 From: Jaanu423 Date: Tue, 30 Jun 2026 11:53:28 +0530 Subject: [PATCH] =?UTF-8?q?feat(election):=20candidate=20voter=20analysis?= =?UTF-8?q?=20=E2=80=94=20Chart.js=20charts,=20cross-dimension=20insights?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - New CandidateAnalysisComponent with Chart.js doughnut charts for all demographic/financial/social dimensions (gender, age, area, caste, sub-caste, religion, occupation, share-holding, tenure, loan, education, marital status, monthly income, savings account depth) - Section 4: Multi-Dimension Voter Alignment card showing core base (caste + area cross-match), caste %, area %, and savings engagement - Candidate board: added Analyse button linking to analysis page - Hero header, KPI strip, 4 dashboard sections with hover charts Co-Authored-By: Claude Sonnet 4.6 --- .../candidate-analysis.component.html | 779 +++++++++++ .../candidate-analysis.component.scss | 1223 +++++++++++++++++ .../candidate-analysis.component.ts | 403 ++++++ .../candidate-board.component.html | 3 + .../candidate-board.component.scss | 25 + src/app/election/election-routing.module.ts | 8 + src/app/election/election.service.ts | 6 + src/environments/.env.ts | 4 +- 8 files changed, 2449 insertions(+), 2 deletions(-) create mode 100644 src/app/election/components/candidate-analysis/candidate-analysis.component.html create mode 100644 src/app/election/components/candidate-analysis/candidate-analysis.component.scss create mode 100644 src/app/election/components/candidate-analysis/candidate-analysis.component.ts diff --git a/src/app/election/components/candidate-analysis/candidate-analysis.component.html b/src/app/election/components/candidate-analysis/candidate-analysis.component.html new file mode 100644 index 000000000..24016c4aa --- /dev/null +++ b/src/app/election/components/candidate-analysis/candidate-analysis.component.html @@ -0,0 +1,779 @@ + +
+
+
+
+
+
+ + +
+
+ + chevron_left Back to Candidates + + @if (data) { +
+
+
{{ data.candidateName?.charAt(0) }}
+
+
+
+
+ {{ data.postName }} + Voter Influence Analysis +
+

{{ data.candidateName }}

+ + @if (hasCriteria(data)) { +
+ Target Criteria: + @if (data.postGenderType && data.postGenderType !== 'ANY') { + Gender: {{ data.postGenderType }} + } + @if (data.postMinAge && data.postMaxAge) { + Age: {{ data.postMinAge }}–{{ data.postMaxAge }} yrs + } @else if (data.postMinAge) { + Min Age: {{ data.postMinAge }} yrs + } @else if (data.postMaxAge) { + Max Age: {{ data.postMaxAge }} yrs + } + @if (data.postMinShareHolding) { + Min Shares: {{ data.postMinShareHolding }} + } +
+ } +
+
+ } +
+ + @if (loading) { +
+
+

Building voter analysis...

+
+ } @else if (error || !data) { +
+ error_outline +

{{ errorMessage || 'Analysis not available.' }}

+ Return to Candidates +
+ } @else { + +
+ +
+
+ group +
+
+
{{ data.totalActiveShareholders }}
+
Total Shareholders
+
Active members
+
+
+
+
+ cake +
+
+
{{ data.eligibleByAge }}
+
+ @if (!data.postMinAge && !data.postMaxAge) { + Age-Eligible + } @else { + Age-Eligible ({{ data.postMinAge }}–{{ data.postMaxAge }}) + } +
+
Fits age threshold
+
+
+
+
+ diversity_3 +
+
+
{{ data.casteMatchCount }}
+
Same Social Category
+
Matches candidate
+
+
+
+
+ account_balance +
+
+
{{ data.activeLoanHolders }}
+
Active Loan Holders
+
With active accounts
+
+
+
+
+ family_restroom +
+
+
{{ data.shareholdersWithFamily }}
+
With Family Records
+
Household influence
+
+
+
+ + +
+ +
+
+ people_outline +

Demographic & Professional Profile

+ 6 Analysis Dimensions +
+ +
+ +
+
+
people
+ Gender Distribution + @if (data.candidateGender) { + Candidate: {{ data.candidateGender }} + } +
+
+
+
+ +
+
+ {{ data.totalActiveShareholders }} + total +
+
+
+ @for (item of data.genderBreakdown; track item.label; let i = $index) { +
+ + + {{ item.label }} + @if (item.match) { + ★ Match + } + + {{ item.percent }}% +
+ } +
+
+
+ + +
+
+
cake
+ Age Group Distribution + @if (data.candidateAge) { + Age: {{ data.candidateAge }} yrs + } +
+
+
+
+ +
+
+ {{ data.totalActiveShareholders }} + total +
+
+
+ @for (item of data.ageBreakdown; track item.label; let i = $index) { +
+ + + {{ item.label }} Group + @if (item.match) { + ★ Match + } + + {{ item.percent }}% +
+ } +
+
+
+ + +
+
+
temple_hindu
+ Religion Profile +
+
+ @if (data.religionBreakdown?.length > 0) { +
+
+ +
+
+ {{ data.totalActiveShareholders }} + total +
+
+
+ @for (item of data.religionBreakdown; track item.label; let i = $index) { +
+ + {{ item.label }} + {{ item.percent }}% +
+ } +
+ } @else { +

No religion data recorded yet.

+ } +
+
+ + +
+
+
work
+ Occupation Profile +
+
+ @if (data.occupationBreakdown?.length > 0) { +
+
+ +
+
+ {{ data.totalActiveShareholders }} + total +
+
+
+ @for (item of data.occupationBreakdown; track item.label; let i = $index) { +
+ + {{ item.label }} + {{ item.percent }}% +
+ } +
+ } @else { +

No occupation data recorded yet.

+ } +
+
+ + +
+
+
school
+ Education Level +
+
+ @if (data.educationBreakdown?.length > 0) { +
+
+ +
+
+ {{ data.totalActiveShareholders }} + total +
+
+
+ @for (item of data.educationBreakdown; track item.label; let i = $index) { +
+ + {{ item.label }} + {{ item.percent }}% +
+ } +
+ } @else { +

No education data recorded yet.

+ } +
+
+ + +
+
+
favorite
+ Marital Status Profile +
+
+ @if (data.maritalBreakdown?.length > 0) { +
+
+ +
+
+ {{ data.totalActiveShareholders }} + total +
+
+
+ @for (item of data.maritalBreakdown; track item.label; let i = $index) { +
+ + {{ item.label }} + {{ item.percent }}% +
+ } +
+ } @else { +

No marital status data recorded yet.

+ } +
+
+
+
+ + +
+
+ public +

Social & Geographic Influence

+ 3 Analysis Dimensions +
+ +
+ +
+
+
location_on
+ Area / City Distribution + @if (data.candidateArea) { + Candidate: {{ data.candidateArea }} + } +
+
+
+
+ +
+
+ {{ data.totalActiveShareholders }} + total +
+
+
+ @for (item of data.areaBreakdown; track item.label; let i = $index) { +
+ + + {{ item.label }} + @if (item.match) { + ★ Match + } + + {{ item.percent }}% +
+ } +
+
+
+ + +
+
+
diversity_3
+ Social Category (Caste) + @if (data.candidateCaste) { + Candidate: {{ data.candidateCaste }} + } +
+
+
+
+ +
+
+ {{ data.casteMatchCount }} + match +
+
+
+ @for (item of data.casteBreakdown; track item.label; let i = $index) { +
+ + + {{ item.label }} + @if (item.match) { + ★ Match + } + + {{ item.percent }}% +
+ } +
+
+
+ + +
+
+
account_tree
+ Sub-Caste Breakdown + @if (data.candidateSubCaste) { + Candidate: {{ data.candidateSubCaste }} + } +
+ + @if (data.subCasteBreakdown?.length > 0) { +
+
+
+
+ +
+
+ {{ data.totalActiveShareholders }} + total +
+
+
+
+
+ @for (item of data.subCasteBreakdown; track item.label; let i = $index) { +
+
+ {{ item.label }} + @if (item.match) { + ★ Match + } +
+ +
+ } +
+
+
+ } @else { +

No sub-caste data recorded for shareholders yet.

+ } +
+
+
+ + +
+
+ savings +

Financial & Family Engagement

+ 6 Analysis Dimensions +
+ +
+ +
+
+
account_balance_wallet
+ Share Holding Distribution +
+
+
+
+ +
+
+ {{ data.totalActiveShareholders }} + total +
+
+
+ @for (item of data.shareHoldingBreakdown; track item.label; let i = $index) { +
+ + {{ item.label }} + {{ item.percent }}% +
+ } +
+
+
+ + +
+
+
history
+ Member Tenure +
+
+
+
+ +
+
+ {{ data.totalActiveShareholders }} + total +
+
+
+ @for (item of data.tenureBreakdown; track item.label; let i = $index) { +
+ + {{ item.label }} + {{ item.percent }}% +
+ } +
+
+
+ + +
+
+
currency_rupee
+ Monthly Income Distribution +
+
+ @if (data.monthlyIncomeBreakdown?.length > 0) { +
+
+ +
+
+ {{ data.totalActiveShareholders }} + total +
+
+
+ @for (item of data.monthlyIncomeBreakdown; track item.label; let i = $index) { +
+ + {{ item.label }} + {{ item.percent }}% +
+ } +
+ } @else { +

No monthly income data recorded yet.

+ } +
+
+ + +
+
+
savings
+ Savings Account Depth +
+
+ @if (data.savingsBreakdown?.length > 0) { +
+
+ +
+
+ {{ data.activeSavingsHolders }} + active savings +
+
+
+ @for (item of data.savingsBreakdown; track item.label) { +
+ + {{ item.label }} + {{ item.percent }}% +
+ } +
+ } @else { +

No savings data available.

+ } +
+
+ + +
+
+
account_balance
+ Financial Engagement — Active Loans +
+
+
+
+
+ +
+
+ {{ activeLoanPct(data) }}% + active +
+
+
+
+
+ @for (item of data.loanHolderBreakdown; track item.label) { +
+
+ + {{ item.label }} +
+
{{ item.count }}
+
{{ item.percent }}% of shareholders
+
+ } +
+
+ info_outline + Members with active loans have an ongoing financial relationship, indicating higher engagement and + interest in institutional decisions. +
+
+
+
+ + +
+
+
family_restroom
+ Family Member Network Influence +
+
+
+
+
+ group +
+
+
{{ data.shareholdersWithFamily }}
+
Shareholders with Family Records
+
+
+
+
+
+ diversity_1 +
+
+
{{ data.totalFamilyMembers }}
+
Total Family Members Registered
+
+
+
+
+
+ calculate +
+
+
+ {{ + data.shareholdersWithFamily > 0 + ? (data.totalFamilyMembers / data.shareholdersWithFamily | number: '1.1-1') + : '—' + }} +
+
Avg Registered Members / Household
+
+
+
+
+ insights + Family records indicate broader community networks. A single voting shareholder with multiple family + records acts as a proxy shapeholder, often scaling voting decisions across their entire + household. +
+
+
+
+
+ + +
+
+ auto_graph +

Voter Influence Insights

+ Multi-Dimension Analysis +
+ +
+ +
+
+
hub
+ Multi-Dimension Voter Alignment + Candidate's core voter base overlap +
+
+
+
+ hub +
+
{{ data.crossMatchCount }}
+
{{ crossMatchPct(data) }}%
+
Core Base — Same Caste & Same Area
+
+
+
+ diversity_3 +
+
{{ data.casteMatchCount }}
+
{{ casteMatchPct(data) }}%
+
Same Social Category (Caste Alignment)
+
+
+
+ location_on +
+
{{ data.areaMatchCount }}
+
{{ areaMatchPct(data) }}%
+
Same Area / City (Geographic Proximity)
+
+
+
+ savings +
+
{{ data.activeSavingsHolders }}
+
{{ activeSavingsPct(data) }}%
+
Active Savings Holders (Financially Engaged)
+
+
+
+ lightbulb + The Core Base — voters matching both the candidate's social category AND geographic + area — represents the strongest aligned voter segment. Caste, area, and financial engagement are the + three strongest predictors of voter support in cooperative elections. +
+
+
+
+
+ } +
diff --git a/src/app/election/components/candidate-analysis/candidate-analysis.component.scss b/src/app/election/components/candidate-analysis/candidate-analysis.component.scss new file mode 100644 index 000000000..8f003d561 --- /dev/null +++ b/src/app/election/components/candidate-analysis/candidate-analysis.component.scss @@ -0,0 +1,1223 @@ +/** + * Copyright since 2025 Mifos Initiative + * + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. + */ + +// ─── Design Tokens ──────────────────────────────────────────────────────────── +$radius: 16px; +$radius-sm: 8px; +$shadow-sm: + 0 4px 6px -1px rgb(0 0 0 / 2%), + 0 2px 4px -1px rgb(0 0 0 / 1%); +$shadow-md: + 0 10px 25px -5px rgb(15 23 42 / 8%), + 0 8px 16px -6px rgb(15 23 42 / 4%); +$shadow-lg: + 0 20px 40px -10px rgb(15 23 42 / 12%), + 0 10px 20px -8px rgb(15 23 42 / 6%); +$font: + 'Outfit', + 'Inter', + -apple-system, + blinkmacsystemfont, + 'Segoe UI', + roboto, + sans-serif; + +// Colors +$bg-main: #f8fafc; +$border-color: rgb(226 232 240 / 80%); +$text-main: #0f172a; +$text-sub: #64748b; +$primary: #4f46e5; +$accent-green: #10b981; + +// ─── Page Shell ─────────────────────────────────────────────────────────────── +.va-page { + background: $bg-main; + min-height: 100%; + font-family: $font; + color: $text-main; + padding-bottom: 40px; + position: relative; + overflow-x: hidden; +} + +.backdrop-blobs { + position: absolute; + inset: 0; + z-index: 0; + overflow: hidden; + pointer-events: none; + + .blob { + position: absolute; + border-radius: 50%; + filter: blur(140px); + opacity: 0.07; + mix-blend-mode: multiply; + } + + .blob-1 { + top: 8%; + left: 10%; + width: 450px; + height: 450px; + background: #6366f1; + } + + .blob-2 { + top: 40%; + right: 5%; + width: 550px; + height: 550px; + background: #06b6d4; + } + + .blob-3 { + bottom: 12%; + left: 15%; + width: 500px; + height: 500px; + background: #ec4899; + } +} + +// ─── Hero Header (Redesigned - Light Mode Infographic Style) ────────────────── +.hero { + background: linear-gradient(135deg, #eef2ff 0%, #e0f2fe 50%, #f0fdf4 100%); + padding: 24px 32px 36px; + position: relative; + overflow: hidden; + border-bottom: 1px solid #cbd5e1; + z-index: 2; + + &::before { + content: ''; + position: absolute; + top: -100px; + right: -100px; + width: 400px; + height: 400px; + background: radial-gradient(circle, rgb(99 102 241 / 10%) 0%, transparent 70%); + pointer-events: none; + } + + &::after { + content: ''; + position: absolute; + bottom: -80px; + left: 20%; + width: 300px; + height: 300px; + background: radial-gradient(circle, rgb(16 185 129 / 5%) 0%, transparent 70%); + pointer-events: none; + } +} + +.hero-overlay { + position: absolute; + inset: 0; + background: url("data:image/svg+xml,%3Csvg width='80' height='80' viewBox='0 0 80 80' xmlns='http://www.w3.org/2000/svg'%3E%3Cg fill='%236366f1' fill-opacity='0.015' fill-rule='evenodd'%3E%3Cpath d='M0 0h40v40H0V0zm40 40h40v40H40V40zm0-40h2v2h-2V0zm0 4h2v2h-2V4zM4 40h2v2H4v-2zm0 4h2v2H4v-2z'/%3E%3C/g%3E%3C/svg%3E"); + opacity: 0.8; + pointer-events: none; +} + +.hero-back { + display: inline-flex; + align-items: center; + gap: 4px; + color: #475569; + text-decoration: none; + font-size: 13px; + font-weight: 600; + margin-bottom: 24px; + transition: all 0.25s cubic-bezier(0.4, 0, 0.2, 1); + position: relative; + z-index: 2; + padding: 6px 12px; + background: rgb(15 23 42 / 4%); + border: 1px solid rgb(15 23 42 / 8%); + border-radius: 99px; + + mat-icon { + font-size: 18px; + height: 18px; + width: 18px; + } + + &:hover { + color: #0f172a; + background: rgb(15 23 42 / 8%); + border-color: rgb(15 23 42 / 15%); + transform: translateX(-2px); + } +} + +.hero-body { + display: flex; + align-items: center; + gap: 28px; + position: relative; + z-index: 2; + + @media (width <= 768px) { + flex-direction: column; + align-items: flex-start; + gap: 20px; + } +} + +.hero-avatar-wrapper { + position: relative; + flex-shrink: 0; +} + +.hero-avatar { + width: 80px; + height: 80px; + border-radius: 50%; + background: linear-gradient(135deg, #6366f1 0%, #3b82f6 100%); + border: 2px solid #fff; + color: #fff; + font-size: 38px; + font-weight: 800; + display: flex; + align-items: center; + justify-content: center; + box-shadow: 0 8px 24px rgb(99 102 241 / 18%); + letter-spacing: -1px; + text-transform: uppercase; +} + +.hero-avatar-glow { + position: absolute; + inset: -4px; + border-radius: 50%; + background: linear-gradient(135deg, #6366f1, #10b981); + z-index: -1; + opacity: 0.3; + filter: blur(8px); + animation: pulse-avatar 3s ease-in-out infinite; +} + +@keyframes pulse-avatar { + 0%, + 100% { + transform: scale(1); + opacity: 0.2; + } + + 50% { + transform: scale(1.05); + opacity: 0.4; + } +} + +.hero-info { + flex: 1; + min-width: 0; +} + +.hero-badge-row { + display: flex; + align-items: center; + gap: 12px; + flex-wrap: wrap; + margin-bottom: 8px; +} + +.post-badge { + background: linear-gradient(135deg, rgb(79 70 229 / 10%) 0%, rgb(124 58 237 / 10%) 100%); + color: #4f46e5; + border: 1px solid rgb(79 70 229 / 18%); + border-radius: 20px; + padding: 3px 14px; + font-size: 11px; + font-weight: 700; + letter-spacing: 0.5px; + text-transform: uppercase; +} + +.analysis-tag { + color: #475569; + font-size: 13px; + font-weight: 650; +} + +.hero-name { + font-size: 32px; + font-weight: 800; + color: #0f172a; + margin: 0 0 12px; + letter-spacing: -0.8px; + line-height: 1.15; +} + +.hero-criteria { + display: flex; + align-items: center; + gap: 8px; + flex-wrap: wrap; + margin-top: 14px; + padding-top: 12px; + border-top: 1px solid #cbd5e1; +} + +.criteria-title { + color: #475569; + font-size: 11px; + font-weight: 700; + text-transform: uppercase; + letter-spacing: 0.5px; + margin-right: 4px; +} + +.cp { + background: rgb(217 119 6 / 10%); + color: #b45309; + border: 1px solid rgb(217 119 6 / 20%); + border-radius: 20px; + padding: 2px 12px; + font-size: 11px; + font-weight: 600; + display: inline-flex; + align-items: center; +} + +// ─── State Boxes ───────────────────────────────────────────────────────────── +.state-box { + padding: 100px 24px; + text-align: center; + color: $text-sub; + display: flex; + flex-direction: column; + align-items: center; + justify-content: center; + + mat-icon { + font-size: 64px; + height: 64px; + width: 64px; + display: block; + margin-bottom: 20px; + color: #cbd5e1; + } + + p { + font-size: 15px; + font-weight: 500; + margin: 0 0 20px; + color: #64748b; + } +} + +.err-icon { + color: #ef4444 !important; + animation: bounce-err 2s infinite; +} + +@keyframes bounce-err { + 0%, + 100% { + transform: translateY(0); + } + + 50% { + transform: translateY(-6px); + } +} + +.err-msg { + max-width: 400px; + line-height: 1.5; +} + +.err-btn { + display: inline-flex; + align-items: center; + padding: 10px 20px; + background: #3b82f6; + color: white; + border-radius: 8px; + font-weight: 600; + text-decoration: none; + font-size: 13px; + box-shadow: 0 4px 12px rgb(59 130 246 / 20%); + transition: all 0.2s; + + &:hover { + background: #2563eb; + box-shadow: 0 6px 16px rgb(59 130 246 / 30%); + transform: translateY(-1px); + } +} + +.ring-spinner { + width: 48px; + height: 48px; + border: 4px solid #e2e8f0; + border-top-color: $primary; + border-radius: 50%; + animation: spin 0.8s linear infinite; + margin-bottom: 20px; +} + +@keyframes spin { + to { + transform: rotate(360deg); + } +} + +// ─── KPI Strip ──────────────────────────────────────────────────────────────── +.kpi-row { + display: grid; + grid-template-columns: 1.4fr repeat(5, 1fr); + gap: 16px; + padding: 24px 32px; + position: relative; + z-index: 2; + + @media (width <= 1200px) { + grid-template-columns: repeat(3, 1fr); + } + + @media (width <= 768px) { + grid-template-columns: repeat(2, 1fr); + padding: 16px 20px; + gap: 12px; + } + + @media (width <= 480px) { + grid-template-columns: 1fr; + } +} + +.kpi { + background: white; + border-radius: $radius; + padding: 18px 20px; + display: flex; + align-items: center; + gap: 16px; + box-shadow: $shadow-sm; + border: 1px solid $border-color; + transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1); + + &:hover { + box-shadow: $shadow-md; + transform: translateY(-2px); + border-color: rgb(79 70 229 / 25%); + } +} + +.kpi-featured { + background: linear-gradient(135deg, #fff 0%, #eff6ff 100%); + border: 1px solid #bfdbfe; + box-shadow: 0 4px 14px rgb(37 99 235 / 5%); + + &:hover { + border-color: #3b82f6; + } +} + +.kpi-icon { + width: 48px; + height: 48px; + border-radius: 12px; + display: flex; + align-items: center; + justify-content: center; + flex-shrink: 0; + + mat-icon { + font-size: 24px; + height: 24px; + width: 24px; + } +} + +.kpi-body { + min-width: 0; +} + +.kpi-val { + font-size: 28px; + font-weight: 800; + color: $text-main; + line-height: 1; + margin-bottom: 4px; + letter-spacing: -0.8px; +} + +.featured-val { + color: #2563eb; + font-size: 32px; +} + +.kpi-lbl { + font-size: 11px; + font-weight: 700; + color: $text-sub; + line-height: 1.35; + text-transform: uppercase; + letter-spacing: 0.3px; +} + +.kpi-sub { + font-size: 11px; + font-weight: 700; + color: #2563eb; + margin-top: 3px; +} + +.kpi-sub-text { + font-size: 10px; + font-weight: 500; + color: $text-sub; + margin-top: 2px; +} + +// ─── Dashboard Sections Layout ─────────────────────────────────────────────── +.dashboard-sections { + padding: 0 32px; + display: flex; + flex-direction: column; + gap: 36px; + position: relative; + z-index: 2; + + @media (width <= 768px) { + padding: 0 20px; + gap: 28px; + } +} + +.dashboard-section { + animation: section-slide-in 0.6s cubic-bezier(0.16, 1, 0.3, 1) both; + + @for $i from 1 through 5 { + &:nth-child(#{$i}) { + animation-delay: $i * 0.15s; + } + } +} + +@keyframes section-slide-in { + from { + opacity: 0; + transform: translateY(20px); + } + + to { + opacity: 1; + transform: translateY(0); + } +} + +.section-header { + display: flex; + align-items: center; + gap: 12px; + margin-bottom: 20px; + padding-bottom: 8px; + border-bottom: 2px solid #e2e8f0; + + mat-icon { + color: $primary; + font-size: 26px; + height: 26px; + width: 26px; + } + + h2 { + font-size: 18px; + font-weight: 800; + color: $text-main; + margin: 0; + letter-spacing: -0.3px; + } + + .section-badge { + margin-left: auto; + font-size: 11px; + font-weight: 700; + color: $primary; + background: rgb(79 70 229 / 8%); + padding: 3px 10px; + border-radius: 99px; + text-transform: uppercase; + letter-spacing: 0.3px; + } +} + +// ─── Analysis Grid ──────────────────────────────────────────────────────────── +.a-grid { + display: grid; + grid-template-columns: repeat(2, 1fr); + gap: 20px; + + @media (width <= 1024px) { + grid-template-columns: 1fr; + } +} + +.a-card { + background: white; + border-radius: $radius; + padding: 24px; + box-shadow: + 0 4px 20px -2px rgb(0 0 0 / 3%), + 0 1px 3px rgb(0 0 0 / 2%); + border: 1px solid rgb(226 232 240 / 80%); + border-top: 4px solid var(--ac, #e2e8f0); + transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1); + display: flex; + flex-direction: column; + justify-content: space-between; + + &:hover { + box-shadow: + 0 20px 35px -10px rgb(99 102 241 / 6%), + 0 10px 20px -8px rgb(99 102 241 / 3%); + transform: translateY(-4px); + border-color: rgb(99 102 241 / 15%); + } +} + +.a-full { + grid-column: 1 / -1; +} + +// ─── Card Header ───────────────────────────────────────────────────────────── +.ac-head { + display: flex; + align-items: center; + gap: 12px; + margin-bottom: 20px; +} + +.ac-icon { + width: 40px; + height: 40px; + border-radius: 10px; + background: var(--ac-bg, #f1f5f9); + display: flex; + align-items: center; + justify-content: center; + flex-shrink: 0; + + mat-icon { + font-size: 20px; + height: 20px; + width: 20px; + color: var(--ac, #64748b); + } +} + +.ac-title { + font-size: 15px; + font-weight: 700; + color: $text-main; + flex: 1; +} + +.ac-tag { + background: var(--ac-bg, #f1f5f9); + color: var(--ac, #64748b); + border: 1px solid rgb(0 0 0 / 5%); + border-radius: 20px; + padding: 3px 12px; + font-size: 11px; + font-weight: 700; + white-space: nowrap; + flex-shrink: 0; +} + +// ─── Flex helpers ───────────────────────────────────────────────────────────── +.flex-col { + display: flex; + flex-direction: column; + align-items: center; + width: 100%; +} + +// ─── Chart Canvas Wrappers ─────────────────────────────────────────────────── +.chart-canvas-wrap { + position: relative; + width: 100%; + height: 100%; +} + +.bar-chart-wrap { + position: relative; + width: 100%; + margin-bottom: 16px; + filter: drop-shadow(0 6px 10px rgb(0 0 0 / 3%)); +} + +.vertical-bar-wrap { + height: 160px; +} + +.horizontal-bar-wrap { + height: 140px; +} + +.horizontal-bar-wrap-large { + height: 200px; +} + +// ─── Donut Center Overlay ──────────────────────────────────────────────────── +.donut-wrap { + position: relative; + width: 150px; + height: 150px; + flex-shrink: 0; + margin-bottom: 20px; + filter: drop-shadow(0 6px 12px rgb(0 0 0 / 4%)); +} + +.donut-center { + position: absolute; + inset: 22%; + background: white; + border-radius: 50%; + display: flex; + flex-direction: column; + align-items: center; + justify-content: center; + box-shadow: 0 4px 10px rgb(0 0 0 / 6%); + z-index: 10; + text-align: center; + padding: 8px; + box-sizing: border-box; +} + +.dc-val { + font-size: 24px; + font-weight: 800; + color: $text-main; + line-height: 1; + letter-spacing: -0.5px; +} + +.dc-sub { + font-size: 10px; + color: $text-sub; + font-weight: 700; + text-transform: uppercase; + letter-spacing: 0.5px; + margin-top: 2px; +} + +// ─── Interactive HTML Legends ──────────────────────────────────────────────── +.chart-legend { + width: 100%; + display: flex; + flex-direction: column; + gap: 8px; +} + +.cl-row { + display: flex; + align-items: center; + gap: 10px; + padding: 8px 12px; + background: #f8fafc; + border-radius: 10px; + border: 1px solid #f1f5f9; + transition: all 0.2s ease; + + &.cl-match { + background: var(--ac-bg, #eff6ff); + border-color: rgb(79 70 229 / 15%); + + .cl-label { + font-weight: 700; + color: $text-main; + } + + .cl-pct-badge { + background: var(--ac, #3b82f6); + color: white; + } + } + + &:hover { + background: #f1f5f9; + transform: translateX(2px); + } +} + +.cl-dot { + width: 10px; + height: 10px; + border-radius: 50%; + flex-shrink: 0; +} + +.cl-label { + font-size: 12.5px; + color: #475569; + font-weight: 500; + flex: 1; + display: flex; + align-items: center; + gap: 6px; +} + +.mbadge { + background: var(--ac, #2563eb); + color: white; + border-radius: 99px; + padding: 2px 8px; + font-size: 9.5px; + font-weight: 700; + letter-spacing: 0.2px; + text-transform: uppercase; +} + +.cl-pct-badge { + font-size: 12px; + font-weight: 700; + color: $text-main; + background: #e2e8f0; + padding: 2px 8px; + border-radius: 6px; + min-width: 38px; + text-align: center; +} + +// ─── Sub-Caste Layout ──────────────────────────────────────────────────────── +.full-card-split { + display: grid; + grid-template-columns: 1.2fr 1fr; + gap: 32px; + align-items: center; + margin-top: 8px; + + @media (width <= 850px) { + grid-template-columns: 1fr; + gap: 20px; + } +} + +.split-left { + width: 100%; +} + +.split-right { + width: 100%; +} + +.caste-details-grid { + display: grid; + grid-template-columns: 1fr 1fr; + gap: 10px; + + @media (width <= 480px) { + grid-template-columns: 1fr; + } +} + +.caste-detail-item { + background: #f8fafc; + border: 1px solid #e2e8f0; + border-radius: 12px; + padding: 12px 14px; + transition: all 0.2s ease; + + &.caste-match { + background: rgb(217 70 239 / 5%); + border-color: rgb(217 70 239 / 25%); + + .cdi-label { + color: #c026d3; + font-weight: 700; + } + } + + &:hover { + transform: translateY(-2px); + box-shadow: $shadow-sm; + } +} + +.cdi-header { + display: flex; + align-items: center; + justify-content: space-between; + margin-bottom: 6px; +} + +.cdi-label { + font-size: 13px; + font-weight: 600; + color: $text-main; +} + +.cdi-footer { + display: flex; + align-items: center; + justify-content: space-between; +} + +.cdi-count { + font-size: 11px; + color: $text-sub; +} + +.cdi-percent { + font-size: 12px; + font-weight: 700; + color: $text-main; +} + +// ─── Loan Layout (Redesigned) ──────────────────────────────────────────────── +.loan-layout { + display: grid; + grid-template-columns: auto 1fr; + gap: 32px; + align-items: center; + margin-top: 8px; + + @media (width <= 800px) { + grid-template-columns: 1fr; + gap: 20px; + justify-items: center; + } +} + +.loan-chart-section { + flex-shrink: 0; +} + +.loan-donut-wrap { + position: relative; + width: 160px; + height: 95px; + overflow: hidden; +} + +.gauge-center { + position: absolute; + bottom: 0; + left: 50%; + transform: translate(-50%, 0); + background: white; + border-radius: 50% 50% 0 0; + width: 110px; + height: 55px; + display: flex; + flex-direction: column; + align-items: center; + justify-content: flex-end; + padding-bottom: 8px; + box-shadow: 0 -4px 10px rgb(0 0 0 / 5%); + text-align: center; + z-index: 10; + box-sizing: border-box; +} + +.ldc-val { + font-size: 24px; + font-weight: 800; + color: #b45309; + line-height: 1; + letter-spacing: -0.5px; +} + +.ldc-sub { + font-size: 9px; + color: $text-sub; + font-weight: 700; + text-transform: uppercase; + letter-spacing: 0.5px; + margin-top: 2px; +} + +.loan-stats-section { + width: 100%; +} + +.loan-stats { + display: flex; + gap: 16px; + margin-bottom: 16px; + flex-wrap: wrap; +} + +.loan-stat { + flex: 1; + min-width: 180px; + background: #f8fafc; + border: 1px solid #e2e8f0; + border-radius: 12px; + padding: 16px; + transition: all 0.2s ease; + + &.loan-stat-active { + background: #fffbeb; + border-color: #fde68a; + + .ls-num { + color: #b45309; + } + } + + &:hover { + transform: translateY(-2px); + box-shadow: $shadow-sm; + } +} + +.ls-header { + display: flex; + align-items: center; + gap: 8px; + margin-bottom: 8px; +} + +.ls-dot { + width: 8px; + height: 8px; + border-radius: 50%; +} + +.ls-label { + font-size: 11px; + font-weight: 700; + color: $text-sub; + text-transform: uppercase; + letter-spacing: 0.3px; +} + +.ls-num { + font-size: 34px; + font-weight: 800; + color: $text-main; + line-height: 1; + margin-bottom: 4px; + letter-spacing: -1px; +} + +.ls-pct { + font-size: 11px; + color: $text-sub; +} + +.loan-note { + display: flex; + align-items: flex-start; + gap: 10px; + background: #f1f5f9; + border-radius: 10px; + padding: 12px 16px; + font-size: 12.5px; + color: #475569; + line-height: 1.5; + + mat-icon { + font-size: 18px; + height: 18px; + width: 18px; + flex-shrink: 0; + color: #475569; + margin-top: 1px; + } +} + +// ─── Family Network (Redesigned) ───────────────────────────────────────────── +.fam-metrics-layout { + display: flex; + flex-direction: column; + gap: 16px; + margin-top: 8px; +} + +.fam-metrics { + display: grid; + grid-template-columns: 1fr auto 1.1fr auto 1fr; + align-items: center; + background: #f8fafc; + border: 1px solid #e2e8f0; + border-radius: $radius; + overflow: hidden; + + @media (width <= 850px) { + grid-template-columns: 1fr; + gap: 12px; + padding: 16px; + } +} + +.fam-metric { + padding: 24px; + display: flex; + align-items: center; + gap: 16px; + + @media (width <= 850px) { + padding: 10px 0; + } +} + +.fm-icon-badge { + width: 44px; + height: 44px; + border-radius: 12px; + display: flex; + align-items: center; + justify-content: center; + flex-shrink: 0; + + mat-icon { + font-size: 22px; + height: 22px; + width: 22px; + } +} + +.fm-details { + min-width: 0; +} + +.fm-val { + font-size: 32px; + font-weight: 800; + line-height: 1; + margin-bottom: 4px; + letter-spacing: -1px; +} + +.fm-lbl { + font-size: 12px; + color: $text-sub; + font-weight: 500; + line-height: 1.35; +} + +.fam-sep { + width: 1px; + height: 50px; + background: #e2e8f0; + + @media (width <= 850px) { + display: none; + } +} + +.insight-note { + display: flex; + align-items: flex-start; + gap: 10px; + background: #eff6ff; + border: 1px solid #dbeafe; + border-radius: 10px; + padding: 12px 16px; + font-size: 12.5px; + color: #1e3a8a; + line-height: 1.5; + + mat-icon { + font-size: 18px; + height: 18px; + width: 18px; + flex-shrink: 0; + color: #1d4ed8; + margin-top: 1px; + } +} + +// ─── Voter Influence Insight Card ───────────────────────────────────────────── +.influence-insight { + display: grid; + grid-template-columns: repeat(4, 1fr); + gap: 16px; + margin-top: 8px; + + @media (width <= 1100px) { + grid-template-columns: repeat(2, 1fr); + } + + @media (width <= 600px) { + grid-template-columns: 1fr; + } +} + +.influence-metric { + display: flex; + flex-direction: column; + align-items: center; + text-align: center; + padding: 20px 16px; + background: #f8fafc; + border-radius: 14px; + border: 1px solid #e2e8f0; + gap: 6px; + transition: all 0.25s cubic-bezier(0.4, 0, 0.2, 1); + + &.im-highlight { + background: linear-gradient(135deg, rgb(139 92 246 / 7%), rgb(59 130 246 / 4%)); + border-color: rgb(139 92 246 / 25%); + box-shadow: 0 4px 16px rgb(139 92 246 / 8%); + } + + &:hover { + transform: translateY(-3px); + box-shadow: $shadow-md; + border-color: rgb(99 102 241 / 20%); + } +} + +.im-icon { + width: 44px; + height: 44px; + border-radius: 12px; + display: flex; + align-items: center; + justify-content: center; + margin-bottom: 6px; + flex-shrink: 0; + + mat-icon { + font-size: 22px; + height: 22px; + width: 22px; + } +} + +.im-val { + font-size: 2.4rem; + font-weight: 800; + line-height: 1; + color: $text-main; + letter-spacing: -1.5px; +} + +.im-pct { + font-size: 1rem; + font-weight: 700; +} + +.im-label { + font-size: 11px; + color: $text-sub; + font-weight: 500; + line-height: 1.4; + max-width: 160px; +} + +// ─── Helpers ────────────────────────────────────────────────────────────────── +.no-data { + font-size: 13.5px; + color: $text-sub; + text-align: center; + padding: 32px 0 16px; + margin: 0; + font-style: italic; +} diff --git a/src/app/election/components/candidate-analysis/candidate-analysis.component.ts b/src/app/election/components/candidate-analysis/candidate-analysis.component.ts new file mode 100644 index 000000000..fd5cb152d --- /dev/null +++ b/src/app/election/components/candidate-analysis/candidate-analysis.component.ts @@ -0,0 +1,403 @@ +/** + * Copyright since 2025 Mifos Initiative + * + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. + */ +import { Component, OnInit, OnDestroy, inject } from '@angular/core'; +import { CommonModule } from '@angular/common'; +import { RouterModule, ActivatedRoute } from '@angular/router'; +import { MatIconModule } from '@angular/material/icon'; +import { ElectionService } from '../../election.service'; +import { Chart, registerables } from 'chart.js'; + +Chart.register(...registerables); + +@Component({ + selector: 'mifosx-candidate-analysis', + standalone: true, + imports: [ + CommonModule, + RouterModule, + MatIconModule + ], + templateUrl: './candidate-analysis.component.html', + styleUrls: ['./candidate-analysis.component.scss'] +}) +export class CandidateAnalysisComponent implements OnInit, OnDestroy { + private service = inject(ElectionService); + private route = inject(ActivatedRoute); + + electionId!: number; + candidateId!: number; + data: any = null; + loading = true; + error = false; + errorMessage = ''; + + private chartInstances: any[] = []; + + // ── Color palettes for donut charts ─────────────────────────────────────── + readonly genderPalette = [ + '#93c5fd', + '#f472b6', + '#cbd5e1' + ]; + readonly castePalette = [ + '#c084fc', + '#67e8f9', + '#6ee7b7', + '#fca5a5', + '#cbd5e1' + ]; + readonly religionPalette = [ + '#fdb574', + '#fca5a5', + '#fde047', + '#93c5fd', + '#c084fc', + '#86efac', + '#cbd5e1' + ]; + readonly agePalette = [ + '#a5b4fc', + '#c084fc', + '#d8b4fe', + '#f9a8d4', + '#fda4af', + '#cbd5e1' + ]; + readonly loanPalette = ['#cbd5e1']; + readonly educationPalette = [ + '#818cf8', + '#a5b4fc', + '#c7d2fe', + '#e0e7ff', + '#cbd5e1' + ]; + readonly maritalPalette = [ + '#fbbf24', + '#fb923c', + '#a3e635', + '#34d399', + '#94a3b8' + ]; + readonly incomePalette = [ + '#4ade80', + '#86efac', + '#6ee7b7', + '#5eead4', + '#a5f3fc', + '#cbd5e1' + ]; + + ngOnInit(): void { + this.electionId = +this.route.snapshot.params['electionId']; + this.candidateId = +this.route.snapshot.params['candidateId']; + + this.service.getCandidateVoterAnalysis(this.electionId, this.candidateId).subscribe({ + next: (d) => { + this.data = d; + this.loading = false; + setTimeout(() => this.initCharts(), 0); + }, + error: (err) => { + this.loading = false; + this.error = true; + this.errorMessage = err?.error?.defaultUserMessage || err?.error?.message || 'Analysis not available.'; + } + }); + } + + ngOnDestroy(): void { + this.chartInstances.forEach((chart) => { + if (chart) { + chart.destroy(); + } + }); + this.chartInstances = []; + } + + initCharts() { + if (!this.data) return; + + if (this.data.genderBreakdown?.length) { + this.createDoughnutChart( + 'gender-chart', + this.data.genderBreakdown, + '#ec4899', + this.genderPalette, + this.data.totalActiveShareholders, + 'total' + ); + } + if (this.data.ageBreakdown?.length) { + this.createDoughnutChart( + 'age-chart', + this.data.ageBreakdown, + '#3b82f6', + this.agePalette, + this.data.totalActiveShareholders, + 'total' + ); + } + if (this.data.areaBreakdown?.length) { + this.createDoughnutChart( + 'area-chart', + this.data.areaBreakdown, + '#10b981', + [ + '#34d399', + '#6ee7b7', + '#a7f3d0', + '#cbd5e1' + ], + this.data.totalActiveShareholders, + 'total' + ); + } + if (this.data.casteBreakdown?.length) { + this.createDoughnutChart( + 'caste-chart', + this.data.casteBreakdown, + '#8b5cf6', + this.castePalette, + this.data.casteMatchCount, + 'match' + ); + } + if (this.data.subCasteBreakdown?.length) { + this.createDoughnutChart( + 'subcaste-chart', + this.data.subCasteBreakdown, + '#d946ef', + [ + '#e879f9', + '#f0abfc', + '#f5d0fe', + '#cbd5e1' + ], + this.data.totalActiveShareholders, + 'total' + ); + } + if (this.data.religionBreakdown?.length) { + this.createDoughnutChart( + 'religion-chart', + this.data.religionBreakdown, + '#f97316', + this.religionPalette, + this.data.totalActiveShareholders, + 'total' + ); + } + if (this.data.occupationBreakdown?.length) { + this.createDoughnutChart( + 'occupation-chart', + this.data.occupationBreakdown, + '#0891b2', + [ + '#22d3ee', + '#67e8f9', + '#a5f3fc', + '#cbd5e1' + ], + this.data.totalActiveShareholders, + 'total' + ); + } + if (this.data.shareHoldingBreakdown?.length) { + this.createDoughnutChart( + 'shareholding-chart', + this.data.shareHoldingBreakdown, + '#6366f1', + [ + '#818cf8', + '#a5b4fc', + '#c7d2fe', + '#cbd5e1' + ], + this.data.totalActiveShareholders, + 'total' + ); + } + if (this.data.tenureBreakdown?.length) { + this.createDoughnutChart( + 'tenure-chart', + this.data.tenureBreakdown, + '#06b6d4', + [ + '#22d3ee', + '#67e8f9', + '#cbd5e1' + ], + this.data.totalActiveShareholders, + 'total' + ); + } + if (this.data.loanHolderBreakdown?.length) { + this.createDoughnutChart( + 'loan-chart', + this.data.loanHolderBreakdown, + '#f59e0b', + this.loanPalette, + this.activeLoanPct(this.data), + 'active', + true + ); + } + if (this.data.educationBreakdown?.length) { + this.createDoughnutChart( + 'education-chart', + this.data.educationBreakdown, + '#6366f1', + this.educationPalette, + this.data.totalActiveShareholders, + 'total' + ); + } + if (this.data.maritalBreakdown?.length) { + this.createDoughnutChart( + 'marital-chart', + this.data.maritalBreakdown, + '#f59e0b', + this.maritalPalette, + this.data.totalActiveShareholders, + 'total' + ); + } + if (this.data.monthlyIncomeBreakdown?.length) { + this.createDoughnutChart( + 'income-chart', + this.data.monthlyIncomeBreakdown, + '#16a34a', + this.incomePalette, + this.data.totalActiveShareholders, + 'total' + ); + } + if (this.data.savingsBreakdown?.length) { + this.createDoughnutChart( + 'savings-chart', + this.data.savingsBreakdown, + '#0ea5e9', + ['#cbd5e1'], + this.data.activeSavingsHolders, + 'active savings' + ); + } + } + + private createDoughnutChart( + canvasId: string, + breakdown: any[], + accent: string, + palette: string[], + defaultVal: any, + defaultSub: string, + gauge: boolean = false + ) { + const el = document.getElementById(canvasId) as HTMLCanvasElement; + if (!el) return; + + const labels = breakdown.map((item) => item.label); + const data = breakdown.map((item) => item.percent); + const colors = breakdown.map((item, idx) => (item.match ? accent : palette[idx % palette.length])); + + const chart = new Chart(el, { + type: 'doughnut', + data: { + labels: labels, + datasets: [ + { + data: data, + backgroundColor: colors, + borderWidth: 3, + borderColor: '#ffffff', + borderRadius: 5, // Rounded corners on doughnut segments for clean graphic style + hoverOffset: 6 + } + ] + }, + options: { + responsive: true, + maintainAspectRatio: false, + cutout: '70%', + circumference: gauge ? 180 : 360, + rotation: gauge ? -90 : 0, + plugins: { + legend: { display: false }, + tooltip: { enabled: false } // Disable native tooltip to prevent center overlay clipping + }, + onHover: (_event: any, activeElements: any[]) => { + const valEl = document.getElementById(canvasId + '-val'); + const subEl = document.getElementById(canvasId + '-sub'); + if (!valEl || !subEl) return; + + if (activeElements && activeElements.length > 0) { + const index = activeElements[0].index; + const label = labels[index]; + const val = data[index]; + valEl.innerText = val + '%'; + subEl.innerText = label; + subEl.style.color = colors[index]; + subEl.style.fontWeight = '700'; + } else { + valEl.innerText = typeof defaultVal === 'number' ? defaultVal + (gauge ? '%' : '') : String(defaultVal); + subEl.innerText = defaultSub; + subEl.style.color = ''; + subEl.style.fontWeight = ''; + } + } + } + }); + this.chartInstances.push(chart); + } + + barWidth(pct: number): string { + return Math.min(pct, 100) + '%'; + } + barHeight(pct: number): string { + return Math.min(pct, 100) + '%'; + } + + hasCriteria(d: any): boolean { + return (d.postGenderType && d.postGenderType !== 'ANY') || d.postMinAge || d.postMaxAge || d.postMinShareHolding; + } + + eligiblePct(d: any): number { + if (!d?.totalActiveShareholders) return 0; + return Math.round((d.actualEligibleVoters / d.totalActiveShareholders) * 100); + } + + activeLoanPct(d: any): number { + if (!d?.totalActiveShareholders) return 0; + return Math.round((d.activeLoanHolders / d.totalActiveShareholders) * 100); + } + + activeSavingsPct(d: any): number { + if (!d?.totalActiveShareholders) return 0; + return Math.round((d.activeSavingsHolders / d.totalActiveShareholders) * 100); + } + + crossMatchPct(d: any): number { + if (!d?.totalActiveShareholders) return 0; + return Math.round((d.crossMatchCount / d.totalActiveShareholders) * 100); + } + + casteMatchPct(d: any): number { + if (!d?.totalActiveShareholders) return 0; + return Math.round((d.casteMatchCount / d.totalActiveShareholders) * 100); + } + + areaMatchPct(d: any): number { + if (!d?.totalActiveShareholders) return 0; + return Math.round((d.areaMatchCount / d.totalActiveShareholders) * 100); + } + + itemColor(item: any, idx: number, accent: string, palette: string[]): string { + if (item.match) return accent; + return palette[idx % palette.length]; + } +} diff --git a/src/app/election/components/candidate-board/candidate-board.component.html b/src/app/election/components/candidate-board/candidate-board.component.html index cd44c0d00..dc3894679 100644 --- a/src/app/election/components/candidate-board/candidate-board.component.html +++ b/src/app/election/components/candidate-board/candidate-board.component.html @@ -111,6 +111,9 @@
+ + analytics Analyse + @if (!candidate.isActive) { Removed } @else { diff --git a/src/app/election/components/candidate-board/candidate-board.component.scss b/src/app/election/components/candidate-board/candidate-board.component.scss index 1cdd972ce..9d74b694b 100644 --- a/src/app/election/components/candidate-board/candidate-board.component.scss +++ b/src/app/election/components/candidate-board/candidate-board.component.scss @@ -303,8 +303,33 @@ .candidate-actions { display: flex; justify-content: center; + gap: 6px; margin-top: 6px; + .btn-analyse { + display: inline-flex; + align-items: center; + gap: 3px; + background: #eff6ff; + color: #2563eb; + border: 1px solid #bfdbfe; + padding: 4px 10px; + border-radius: 6px; + font-size: 11px; + text-decoration: none; + cursor: pointer; + + mat-icon { + font-size: 13px; + height: 13px; + width: 13px; + } + + &:hover { + background: #dbeafe; + } + } + .btn-remove { display: inline-flex; align-items: center; diff --git a/src/app/election/election-routing.module.ts b/src/app/election/election-routing.module.ts index ae6e65e19..af5faf3a5 100644 --- a/src/app/election/election-routing.module.ts +++ b/src/app/election/election-routing.module.ts @@ -81,6 +81,14 @@ const routes: Routes = [ (m) => m.ElectionAuditLogComponent ), data: { title: 'Election Audit Log' } + }, + { + path: ':electionId/candidates/:candidateId/analysis', + loadComponent: () => + import('./components/candidate-analysis/candidate-analysis.component').then( + (m) => m.CandidateAnalysisComponent + ), + data: { title: 'Candidate Voter Analysis' } } ] } diff --git a/src/app/election/election.service.ts b/src/app/election/election.service.ts index 061a69537..fa15b3da7 100644 --- a/src/app/election/election.service.ts +++ b/src/app/election/election.service.ts @@ -157,6 +157,12 @@ export class ElectionService { return this.http.get(`/election/${electionId}/vote-summary`).pipe(catchError(() => of(null))); } + getCandidateVoterAnalysis(electionId: number, candidateId: number): Observable { + return this.http + .get(`/election/${electionId}/candidates/${candidateId}/voter-analysis`) + .pipe(catchError(() => of(null))); + } + getAuditLog( electionId: number, params?: { entityType?: string; action?: string; search?: string; page?: number; size?: number } diff --git a/src/environments/.env.ts b/src/environments/.env.ts index 4a9053046..4591d6cef 100644 --- a/src/environments/.env.ts +++ b/src/environments/.env.ts @@ -9,8 +9,8 @@ /* tslint:disable */ export default { 'mifos_x': { - 'version': '260623', - 'hash': '3d9b0480' + 'version': '260626', + 'hash': '9e33392b' }, 'allow_switching_backend_instance': true };