shield dashboard

This commit is contained in:
string 2025-10-25 16:15:16 +05:30
parent aade12d6ff
commit f24138cfbd
31 changed files with 2223 additions and 4 deletions

View File

@ -11,6 +11,9 @@
<h3>{{ 'Dashboard_builder' | translate }}</h3>
</div>
<div class="clr-col-4" style="text-align: right;">
<button class="btn btn-success" [routerLink]="['/cns-portal/shield-dashboard']">
<clr-icon shape="shield"></clr-icon>Shield Dashboard
</button>
<button id="add" class="btn btn-primary" (click)="gotorunner()">
<clr-icon shape="grid-view"></clr-icon>{{ 'Dashboard_runner' | translate }}
</button>
@ -140,6 +143,3 @@
</div>
</div>
</clr-modal>

View File

@ -0,0 +1,125 @@
# Shield Dashboard
A professional analytics dashboard UI similar to the "Shield Overall Dashboard" design.
## Components
### Layout
- **Left Sidebar**: Contains application name, KPI metrics, and filter controls
- **Main Dashboard Area**: Grid-based responsive layout with various data visualization components
### Components
1. **Sidebar Filters**
- Application header with title
- KPI metrics display (Total Leads, Total Deals)
- Filter controls (Sales Rep, Partner, Traction Channel, Sub Product Line)
- Reset filters button
2. **Bar Chart**
- Title: "Deal Stage Wise Progress"
- Visualizes deal progress across different stages
- Responsive design with loading shimmer effect
3. **Donut Charts**
- End Customer Stage Split
- Segment Penetration
- Interactive with tooltips
4. **Map Chart**
- Dealer locations with colored markers
- Interactive hover tooltips showing dealer status
- Legend for status colors
5. **Data Table**
- Cross/Up Selling Scope
- Scrollable with sticky header
- Alternating row colors
- Sortable columns
- Probability bars with color coding
6. **Deal Details Card**
- Company Name, Stage, Description, Amount, Stage Days
- Color-coded deal stages
- Responsive grid layout
7. **Quarterwise Flow**
- Placeholder for chart configuration
- "Chart configuration incomplete" message
## Features
### Responsiveness
- Grid-based layout adapts to different screen sizes
- Mobile-friendly design with stacked layout on small screens
- Flexible components that resize appropriately
### Filter Functionality
- Global filters that affect all dashboard components
- Shared state management using BehaviorSubject
- Real-time updates when filters change
- Reset filters functionality
### Visual Design
- Dark Navy Blue for headers and bars
- Bright Orange for highlights and KPI boxes
- White background for charts and tables
- Modern color scheme with consistent styling
### Interactive Elements
- Chart tooltips on hover
- Interactive map with dealer status information
- Sortable data table
- Loading shimmer effects during data updates
## Technical Implementation
### Technologies Used
- Angular 16
- Chart.js with ng2-charts
- Clarity Design System
- SCSS for styling
### State Management
- Shared service using BehaviorSubject for filter state
- Reactive components that update based on filter changes
- Simulated data updates (no backend integration)
### Responsive Design
- CSS Grid and Flexbox layouts
- Media queries for different screen sizes
- Relative units for scalable components
## Component Structure
```
shield-dashboard/
├── shield-dashboard.component.ts|.html|.scss
├── services/
│ └── dashboard-filter.service.ts
├── components/
│ ├── sidebar-filters/
│ │ ├── sidebar-filters.component.ts|.html|.scss
│ ├── bar-chart/
│ │ ├── bar-chart.component.ts|.html|.scss
│ ├── donut-chart/
│ │ ├── donut-chart.component.ts|.html|.scss
│ ├── map-chart/
│ │ ├── map-chart.component.ts|.html|.scss
│ ├── data-table/
│ │ ├── data-table.component.ts|.html|.scss
│ ├── deal-details-card/
│ │ ├── deal-details-card.component.ts|.html|.scss
│ ├── quarterwise-flow/
│ │ ├── quarterwise-flow.component.ts|.html|.scss
│ └── loading-shimmer/
│ ├── loading-shimmer.component.ts|.html|.scss
└── shield-dashboard-routing.module.ts
```
## Usage
To navigate to the Shield Dashboard, visit:
`/cns-portal/shield-dashboard`
The dashboard is fully responsive and will adapt to different screen sizes. All components are interconnected through the shared filter service, so changing any filter will update all visualizations in real-time.

View File

@ -0,0 +1,20 @@
<div class="chart-container">
<div class="chart-header">
<h3>Deal Stage Wise Progress</h3>
</div>
<div class="chart-wrapper">
<div class="chart-content" [class.loading]="isLoading">
<canvas
baseChart
[data]="barChartData"
[options]="barChartOptions"
[type]="barChartType"
(chartClick)="chartClicked($event)"
(chartHover)="chartHovered($event)">
</canvas>
<div class="loading-overlay" *ngIf="isLoading">
<div class="shimmer-bar"></div>
</div>
</div>
</div>
</div>

View File

@ -0,0 +1,70 @@
.chart-container {
height: 100%;
display: flex;
flex-direction: column;
.chart-header {
margin-bottom: 20px;
h3 {
font-size: 18px;
font-weight: 600;
color: #0a192f;
margin: 0;
}
}
.chart-wrapper {
flex: 1;
position: relative;
.chart-content {
position: relative;
height: 100%;
&.loading {
opacity: 0.7;
canvas {
filter: blur(2px);
}
}
canvas {
max-width: 100%;
max-height: 100%;
transition: filter 0.3s ease;
}
.loading-overlay {
position: absolute;
top: 0;
left: 0;
right: 0;
bottom: 0;
display: flex;
align-items: center;
justify-content: center;
background: rgba(255, 255, 255, 0.8);
.shimmer-bar {
width: 80%;
height: 20px;
background: linear-gradient(90deg, #f0f0f0 25%, #e0e0e0 50%, #f0f0f0 75%);
background-size: 200% 100%;
animation: shimmer 1.5s infinite;
border-radius: 4px;
}
}
}
}
}
@keyframes shimmer {
0% {
background-position: -200% 0;
}
100% {
background-position: 200% 0;
}
}

View File

@ -0,0 +1,140 @@
import { Component, OnInit, ViewChild, AfterViewInit, OnDestroy } from '@angular/core';
import { BaseChartDirective } from 'ng2-charts';
import { ChartConfiguration, ChartData, ChartEvent, ChartType } from 'chart.js';
import { DashboardFilterService } from '../../services/dashboard-filter.service';
import { Subscription } from 'rxjs';
@Component({
selector: 'app-shield-bar-chart',
templateUrl: './bar-chart.component.html',
styleUrls: ['./bar-chart.component.scss']
})
export class BarChartComponent implements OnInit, AfterViewInit, OnDestroy {
@ViewChild(BaseChartDirective) chart?: BaseChartDirective;
private filterSubscription: Subscription = new Subscription();
// Loading state
isLoading: boolean = false;
public barChartOptions: ChartConfiguration['options'] = {
responsive: true,
maintainAspectRatio: false,
scales: {
x: {
grid: {
color: 'rgba(0, 0, 0, 0.05)'
},
ticks: {
color: '#64748b'
}
},
y: {
beginAtZero: true,
grid: {
color: 'rgba(0, 0, 0, 0.05)'
},
ticks: {
color: '#64748b'
}
}
},
plugins: {
legend: {
display: false
},
tooltip: {
backgroundColor: 'rgba(10, 25, 47, 0.9)',
titleColor: '#ff6b35',
bodyColor: '#ffffff',
borderColor: 'rgba(255, 107, 53, 0.3)',
borderWidth: 1,
cornerRadius: 6
}
}
};
public barChartType: ChartType = 'bar';
public barChartPlugins = [];
public barChartData: ChartData<'bar'> = {
labels: ['Prospecting', 'Qualification', 'Needs Analysis', 'Value Proposition', 'Decision Making', 'Negotiation'],
datasets: [
{
data: [65, 59, 80, 81, 56, 55],
label: 'Deal Progress',
backgroundColor: '#0a192f',
borderColor: '#0a192f',
borderWidth: 1,
borderRadius: 4
}
]
};
constructor(private filterService: DashboardFilterService) { }
ngOnInit(): void {
// Subscribe to filter changes
this.filterSubscription.add(
this.filterService.filterState$.subscribe(filters => {
this.updateChartData(filters);
})
);
}
ngAfterViewInit(): void {
// Initial chart render
this.updateChart();
}
ngOnDestroy(): void {
// Unsubscribe from all subscriptions
this.filterSubscription.unsubscribe();
}
// Update chart data based on filters
updateChartData(filters: any): void {
// Show loading state
this.isLoading = true;
// Simulate data change based on filters
// In a real implementation, this would fetch new data from an API
const baseData = [65, 59, 80, 81, 56, 55];
// Apply filter effects (simplified logic)
let multiplier = 1;
if (filters.salesRep) multiplier *= 0.9;
if (filters.partner) multiplier *= 0.85;
if (filters.tractionChannel) multiplier *= 0.95;
if (filters.subProductLine) multiplier *= 0.8;
// Add a small delay to simulate loading
setTimeout(() => {
this.barChartData.datasets[0].data = baseData.map(value =>
Math.floor(value * multiplier)
);
// Update chart
this.updateChart();
// Hide loading state
this.isLoading = false;
}, 300);
}
// Update chart with new data
updateChart(): void {
if (this.chart) {
this.chart.update();
}
}
// events
public chartClicked({ event, active }: { event?: ChartEvent, active?: {}[] }): void {
console.log(event, active);
}
public chartHovered({ event, active }: { event?: ChartEvent, active?: {}[] }): void {
console.log(event, active);
}
}

View File

@ -0,0 +1,78 @@
<div class="table-container">
<div class="table-header">
<h3>Cross/Up Selling Scope</h3>
</div>
<div class="table-wrapper">
<table class="data-table">
<thead>
<tr>
<th (click)="sortTable('companyName')" class="sortable">
Company Name
<span class="sort-indicator" *ngIf="sortColumn === 'companyName'">
<clr-icon *ngIf="sortDirection === 'asc'" shape="arrow up"></clr-icon>
<clr-icon *ngIf="sortDirection === 'desc'" shape="arrow down"></clr-icon>
</span>
</th>
<th (click)="sortTable('contactPerson')" class="sortable">
Contact Person
<span class="sort-indicator" *ngIf="sortColumn === 'contactPerson'">
<clr-icon *ngIf="sortDirection === 'asc'" shape="arrow up"></clr-icon>
<clr-icon *ngIf="sortDirection === 'desc'" shape="arrow down"></clr-icon>
</span>
</th>
<th (click)="sortTable('product')" class="sortable">
Product
<span class="sort-indicator" *ngIf="sortColumn === 'product'">
<clr-icon *ngIf="sortDirection === 'asc'" shape="arrow up"></clr-icon>
<clr-icon *ngIf="sortDirection === 'desc'" shape="arrow down"></clr-icon>
</span>
</th>
<th (click)="sortTable('potentialValue')" class="sortable">
Potential Value
<span class="sort-indicator" *ngIf="sortColumn === 'potentialValue'">
<clr-icon *ngIf="sortDirection === 'asc'" shape="arrow up"></clr-icon>
<clr-icon *ngIf="sortDirection === 'desc'" shape="arrow down"></clr-icon>
</span>
</th>
<th (click)="sortTable('probability')" class="sortable">
Probability
<span class="sort-indicator" *ngIf="sortColumn === 'probability'">
<clr-icon *ngIf="sortDirection === 'asc'" shape="arrow up"></clr-icon>
<clr-icon *ngIf="sortDirection === 'desc'" shape="arrow down"></clr-icon>
</span>
</th>
<th (click)="sortTable('nextAction')" class="sortable">
Next Action
<span class="sort-indicator" *ngIf="sortColumn === 'nextAction'">
<clr-icon *ngIf="sortDirection === 'asc'" shape="arrow up"></clr-icon>
<clr-icon *ngIf="sortDirection === 'desc'" shape="arrow down"></clr-icon>
</span>
</th>
<th (click)="sortTable('actionDate')" class="sortable">
Action Date
<span class="sort-indicator" *ngIf="sortColumn === 'actionDate'">
<clr-icon *ngIf="sortDirection === 'asc'" shape="arrow up"></clr-icon>
<clr-icon *ngIf="sortDirection === 'desc'" shape="arrow down"></clr-icon>
</span>
</th>
</tr>
</thead>
<tbody>
<tr *ngFor="let item of crossSellingData; let i = index" [class.even]="i % 2 === 0">
<td>{{ item.companyName }}</td>
<td>{{ item.contactPerson }}</td>
<td>{{ item.product }}</td>
<td>{{ formatCurrency(item.potentialValue) }}</td>
<td>
<div class="probability-bar">
<div class="probability-fill" [style.width.%]="item.probability" [style.background-color]="item.probability > 70 ? '#0a192f' : item.probability > 50 ? '#ff6b35' : '#64748b'"></div>
<span class="probability-text">{{ formatProbability(item.probability) }}</span>
</div>
</td>
<td>{{ item.nextAction }}</td>
<td>{{ item.actionDate }}</td>
</tr>
</tbody>
</table>
</div>
</div>

View File

@ -0,0 +1,107 @@
.table-container {
height: 100%;
display: flex;
flex-direction: column;
.table-header {
margin-bottom: 20px;
h3 {
font-size: 18px;
font-weight: 600;
color: #0a192f;
margin: 0;
}
}
.table-wrapper {
flex: 1;
overflow: auto;
.data-table {
width: 100%;
border-collapse: collapse;
background: white;
thead {
background: #f1f5f9;
position: sticky;
top: 0;
z-index: 5;
th {
padding: 12px 15px;
text-align: left;
font-weight: 600;
color: #0a192f;
border-bottom: 2px solid #e2e8f0;
cursor: pointer;
user-select: none;
position: relative;
&:hover {
background: #e2e8f0;
}
&.sortable {
padding-right: 30px;
}
.sort-indicator {
position: absolute;
right: 10px;
top: 50%;
transform: translateY(-50%);
color: #ff6b35;
}
}
}
tbody {
tr {
border-bottom: 1px solid #e2e8f0;
&.even {
background: #f8fafc;
}
&:hover {
background: #f1f5f9;
}
td {
padding: 12px 15px;
color: #334155;
font-size: 14px;
.probability-bar {
position: relative;
width: 100%;
height: 20px;
background: #e2e8f0;
border-radius: 10px;
overflow: hidden;
.probability-fill {
height: 100%;
border-radius: 10px;
transition: width 0.3s ease;
}
.probability-text {
position: absolute;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
font-size: 12px;
font-weight: 500;
color: white;
text-shadow: 0 1px 1px rgba(0, 0, 0, 0.5);
}
}
}
}
}
}
}
}

View File

@ -0,0 +1,117 @@
import { Component, OnInit, OnDestroy } from '@angular/core';
import { DashboardFilterService } from '../../services/dashboard-filter.service';
import { Subscription } from 'rxjs';
interface CrossSellingItem {
id: number;
companyName: string;
contactPerson: string;
product: string;
potentialValue: number;
probability: number;
nextAction: string;
actionDate: string;
}
@Component({
selector: 'app-shield-data-table',
templateUrl: './data-table.component.html',
styleUrls: ['./data-table.component.scss']
})
export class DataTableComponent implements OnInit, OnDestroy {
private filterSubscription: Subscription = new Subscription();
// Mock data for cross/up selling scope
originalCrossSellingData: CrossSellingItem[] = [
{ id: 1, companyName: 'Tech Solutions Inc', contactPerson: 'John Smith', product: 'Product A', potentialValue: 50000, probability: 75, nextAction: 'Follow-up call', actionDate: '2023-06-15' },
{ id: 2, companyName: 'Global Enterprises', contactPerson: 'Sarah Johnson', product: 'Product B', potentialValue: 75000, probability: 60, nextAction: 'Send proposal', actionDate: '2023-06-18' },
{ id: 3, companyName: 'Innovative Systems', contactPerson: 'Mike Brown', product: 'Product C', potentialValue: 30000, probability: 85, nextAction: 'Demo scheduled', actionDate: '2023-06-20' },
{ id: 4, companyName: 'Future Tech Ltd', contactPerson: 'Emily Davis', product: 'Product A', potentialValue: 45000, probability: 70, nextAction: 'Send quote', actionDate: '2023-06-22' },
{ id: 5, companyName: 'Digital Dynamics', contactPerson: 'Robert Wilson', product: 'Product B', potentialValue: 60000, probability: 55, nextAction: 'Meeting scheduled', actionDate: '2023-06-25' },
{ id: 6, companyName: 'Alpha Solutions', contactPerson: 'Lisa Miller', product: 'Product C', potentialValue: 40000, probability: 80, nextAction: 'Follow-up email', actionDate: '2023-06-28' },
{ id: 7, companyName: 'Beta Innovations', contactPerson: 'David Taylor', product: 'Product A', potentialValue: 55000, probability: 65, nextAction: 'Product demo', actionDate: '2023-06-28' },
{ id: 8, companyName: 'Gamma Technologies', contactPerson: 'Jennifer Anderson', product: 'Product B', potentialValue: 70000, probability: 50, nextAction: 'Proposal review', actionDate: '2023-07-05' }
];
crossSellingData: CrossSellingItem[] = [...this.originalCrossSellingData];
// Sorting properties
sortColumn: keyof CrossSellingItem = 'companyName';
sortDirection: 'asc' | 'desc' = 'asc';
constructor(private filterService: DashboardFilterService) { }
ngOnInit(): void {
// Subscribe to filter changes
this.filterSubscription.add(
this.filterService.filterState$.subscribe(filters => {
this.updateTableData(filters);
})
);
}
ngOnDestroy(): void {
// Unsubscribe from all subscriptions
this.filterSubscription.unsubscribe();
}
// Update table data based on filters
updateTableData(filters: any): void {
// Simulate data change based on filters
// In a real implementation, this would fetch new data from an API
if (filters.salesRep || filters.partner || filters.tractionChannel || filters.subProductLine) {
// Apply filter effects (simplified logic)
const filteredData = this.originalCrossSellingData.filter(item => {
// Simple filtering logic - in a real app, this would be more sophisticated
return Math.random() > 0.3; // Randomly filter out some items
});
this.crossSellingData = filteredData;
} else {
// No filters applied, show all data
this.crossSellingData = [...this.originalCrossSellingData];
}
// Re-apply current sorting
this.sortTable(this.sortColumn);
}
// Sort table by column
sortTable(column: keyof CrossSellingItem): void {
if (this.sortColumn === column) {
this.sortDirection = this.sortDirection === 'asc' ? 'desc' : 'asc';
} else {
this.sortColumn = column;
this.sortDirection = 'asc';
}
this.crossSellingData.sort((a, b) => {
const aValue = a[column];
const bValue = b[column];
if (aValue < bValue) {
return this.sortDirection === 'asc' ? -1 : 1;
}
if (aValue > bValue) {
return this.sortDirection === 'asc' ? 1 : -1;
}
return 0;
});
}
// Format currency
formatCurrency(value: number): string {
return new Intl.NumberFormat('en-US', {
style: 'currency',
currency: 'USD',
minimumFractionDigits: 0,
maximumFractionDigits: 0
}).format(value);
}
// Format probability as percentage
formatProbability(probability: number): string {
return `${probability}%`;
}
}

View File

@ -0,0 +1,36 @@
<div class="deal-details-container">
<div class="deal-details-header">
<h3>Deal Details</h3>
</div>
<div class="deal-cards">
<div class="deal-card" *ngFor="let deal of dealDetails">
<div class="deal-header">
<div class="company-name">{{ deal.companyName }}</div>
<div class="deal-stage" [style.background-color]="getStageColor(deal.stage)">
{{ deal.stage }}
</div>
</div>
<div class="deal-description">
{{ deal.description }}
</div>
<div class="deal-info">
<div class="info-item">
<div class="info-label">Amount</div>
<div class="info-value">{{ formatCurrency(deal.amount) }}</div>
</div>
<div class="info-item">
<div class="info-label">Stage Days</div>
<div class="info-value">{{ deal.stageDays }} days</div>
</div>
<div class="info-item">
<div class="info-label">Contact</div>
<div class="info-value">{{ deal.contactPerson }}</div>
</div>
<div class="info-item">
<div class="info-label">Last Contact</div>
<div class="info-value">{{ deal.lastContact }}</div>
</div>
</div>
</div>
</div>
</div>

View File

@ -0,0 +1,102 @@
.deal-details-container {
height: 100%;
display: flex;
flex-direction: column;
.deal-details-header {
margin-bottom: 20px;
h3 {
font-size: 18px;
font-weight: 600;
color: #0a192f;
margin: 0;
}
}
.deal-cards {
flex: 1;
display: flex;
flex-direction: column;
gap: 15px;
overflow-y: auto;
.deal-card {
background: white;
border-radius: 10px;
padding: 20px;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.05);
border: 1px solid #e2e8f0;
transition: transform 0.2s ease, box-shadow 0.2s ease;
&:hover {
transform: translateY(-2px);
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1);
}
.deal-header {
display: flex;
justify-content: space-between;
align-items: flex-start;
margin-bottom: 15px;
.company-name {
font-size: 18px;
font-weight: 600;
color: #0a192f;
flex: 1;
}
.deal-stage {
padding: 6px 12px;
border-radius: 20px;
font-size: 12px;
font-weight: 500;
color: white;
text-align: center;
min-width: 120px;
}
}
.deal-description {
color: #64748b;
font-size: 14px;
line-height: 1.5;
margin-bottom: 20px;
}
.deal-info {
display: grid;
grid-template-columns: repeat(2, 1fr);
gap: 15px;
.info-item {
.info-label {
font-size: 12px;
color: #94a3b8;
margin-bottom: 4px;
}
.info-value {
font-size: 14px;
font-weight: 500;
color: #0a192f;
}
}
}
}
}
}
// Responsive design
@media (max-width: 768px) {
.deal-details-container {
.deal-cards {
.deal-card {
.deal-info {
grid-template-columns: 1fr;
}
}
}
}
}

View File

@ -0,0 +1,119 @@
import { Component, OnInit, OnDestroy } from '@angular/core';
import { DashboardFilterService } from '../../services/dashboard-filter.service';
import { Subscription } from 'rxjs';
interface DealDetail {
id: number;
companyName: string;
stage: string;
description: string;
amount: number;
stageDays: number;
contactPerson: string;
lastContact: string;
}
@Component({
selector: 'app-shield-deal-details-card',
templateUrl: './deal-details-card.component.html',
styleUrls: ['./deal-details-card.component.scss']
})
export class DealDetailsCardComponent implements OnInit, OnDestroy {
private filterSubscription: Subscription = new Subscription();
// Mock deal details data
originalDealDetails: DealDetail[] = [
{
id: 1,
companyName: 'Tech Solutions Inc',
stage: 'Negotiation',
description: 'Enterprise software solution for HR management',
amount: 125000,
stageDays: 15,
contactPerson: 'John Smith',
lastContact: '2023-06-10'
},
{
id: 2,
companyName: 'Global Enterprises',
stage: 'Decision Making',
description: 'Cloud infrastructure services migration',
amount: 85000,
stageDays: 8,
contactPerson: 'Sarah Johnson',
lastContact: '2023-06-12'
},
{
id: 3,
companyName: 'Innovative Systems',
stage: 'Value Proposition',
description: 'Custom AI implementation for logistics',
amount: 210000,
stageDays: 22,
contactPerson: 'Mike Brown',
lastContact: '2023-06-05'
}
];
dealDetails: DealDetail[] = [...this.originalDealDetails];
// Stage colors
stageColors: { [key: string]: string } = {
'Prospecting': '#93c5fd',
'Qualification': '#60a5fa',
'Needs Analysis': '#3b82f6',
'Value Proposition': '#1d4ed8',
'Decision Making': '#0a192f',
'Negotiation': '#ff6b35'
};
constructor(private filterService: DashboardFilterService) { }
ngOnInit(): void {
// Subscribe to filter changes
this.filterSubscription.add(
this.filterService.filterState$.subscribe(filters => {
this.updateDealData(filters);
})
);
}
ngOnDestroy(): void {
// Unsubscribe from all subscriptions
this.filterSubscription.unsubscribe();
}
// Update deal data based on filters
updateDealData(filters: any): void {
// Simulate data change based on filters
// In a real implementation, this would fetch new data from an API
if (filters.salesRep || filters.partner || filters.tractionChannel || filters.subProductLine) {
// Apply filter effects (simplified logic)
const filteredData = this.originalDealDetails.filter(item => {
// Simple filtering logic - in a real app, this would be more sophisticated
return Math.random() > 0.2; // Randomly filter out some items
});
this.dealDetails = filteredData;
} else {
// No filters applied, show all data
this.dealDetails = [...this.originalDealDetails];
}
}
// Get stage color
getStageColor(stage: string): string {
return this.stageColors[stage] || '#64748b';
}
// Format currency
formatCurrency(value: number): string {
return new Intl.NumberFormat('en-US', {
style: 'currency',
currency: 'USD',
minimumFractionDigits: 0,
maximumFractionDigits: 0
}).format(value);
}
}

View File

@ -0,0 +1,21 @@
<div class="chart-container">
<div class="chart-header">
<h3>{{ chartTitle }}</h3>
</div>
<div class="chart-wrapper">
<div class="chart-content" [class.loading]="isLoading">
<canvas
baseChart
[datasets]="chartData.datasets"
[labels]="chartData.labels"
[options]="donutChartOptions"
[type]="donutChartType"
(chartClick)="chartClicked($event)"
(chartHover)="chartHovered($event)">
</canvas>
<div class="loading-overlay" *ngIf="isLoading">
<div class="shimmer-donut"></div>
</div>
</div>
</div>
</div>

View File

@ -0,0 +1,70 @@
.chart-container {
height: 100%;
display: flex;
flex-direction: column;
.chart-header {
margin-bottom: 20px;
h3 {
font-size: 18px;
font-weight: 600;
color: #0a192f;
margin: 0;
}
}
.chart-wrapper {
flex: 1;
position: relative;
.chart-content {
position: relative;
height: 100%;
&.loading {
opacity: 0.7;
canvas {
filter: blur(2px);
}
}
canvas {
max-width: 100%;
max-height: 100%;
transition: filter 0.3s ease;
}
.loading-overlay {
position: absolute;
top: 0;
left: 0;
right: 0;
bottom: 0;
display: flex;
align-items: center;
justify-content: center;
background: rgba(255, 255, 255, 0.8);
.shimmer-donut {
width: 120px;
height: 120px;
border-radius: 50%;
background: linear-gradient(90deg, #f0f0f0 25%, #e0e0e0 50%, #f0f0f0 75%);
background-size: 200% 100%;
animation: shimmer 1.5s infinite;
}
}
}
}
}
@keyframes shimmer {
0% {
background-position: -200% 0;
}
100% {
background-position: 200% 0;
}
}

View File

@ -0,0 +1,195 @@
import { Component, Input, OnInit, ViewChild, OnDestroy } from '@angular/core';
import { BaseChartDirective } from 'ng2-charts';
import { ChartConfiguration, ChartData, ChartEvent, ChartType } from 'chart.js';
import { DashboardFilterService } from '../../services/dashboard-filter.service';
import { Subscription } from 'rxjs';
@Component({
selector: 'app-shield-donut-chart',
templateUrl: './donut-chart.component.html',
styleUrls: ['./donut-chart.component.scss']
})
export class DonutChartComponent implements OnInit, OnDestroy {
@Input() chartType: 'endCustomer' | 'segmentPenetration' = 'endCustomer';
@ViewChild(BaseChartDirective) chart?: BaseChartDirective;
private filterSubscription: Subscription = new Subscription();
// Loading state
isLoading: boolean = false;
public donutChartOptions: ChartConfiguration['options'] = {
responsive: true,
maintainAspectRatio: false,
plugins: {
legend: {
display: true,
position: 'bottom',
labels: {
color: '#64748b',
font: {
size: 12
},
padding: 20
}
},
tooltip: {
backgroundColor: 'rgba(10, 25, 47, 0.9)',
titleColor: '#ff6b35',
bodyColor: '#ffffff',
borderColor: 'rgba(255, 107, 53, 0.3)',
borderWidth: 1,
cornerRadius: 6,
callbacks: {
label: function(context) {
const label = context.label || '';
const value = context.parsed;
return `${label}: ${value}`;
}
}
}
}
};
public donutChartType: ChartType = 'doughnut';
public donutChartPlugins = [];
// Data for End Customer Stage Split
public endCustomerData: ChartData<'doughnut'> = {
labels: ['Prospecting', 'Qualification', 'Needs Analysis', 'Value Proposition', 'Decision Making'],
datasets: [
{
data: [30, 25, 20, 15, 10],
backgroundColor: [
'#0a192f',
'#1e3a8a',
'#3b82f6',
'#60a5fa',
'#93c5fd'
],
hoverBackgroundColor: [
'#1e3a8a',
'#3b82f6',
'#60a5fa',
'#93c5fd',
'#bfdbfe'
],
borderWidth: 0
}
]
};
// Data for Segment Penetration
public segmentPenetrationData: ChartData<'doughnut'> = {
labels: ['Enterprise', 'Mid-Market', 'SMB', 'Startup'],
datasets: [
{
data: [40, 30, 20, 10],
backgroundColor: [
'#ff6b35',
'#ff8c66',
'#ffb099',
'#ffd6cc'
],
hoverBackgroundColor: [
'#ff8c66',
'#ffb099',
'#ffd6cc',
'#ffffff'
],
borderWidth: 0
}
]
};
constructor(private filterService: DashboardFilterService) { }
ngOnInit(): void {
// Subscribe to filter changes
this.filterSubscription.add(
this.filterService.filterState$.subscribe(filters => {
this.updateChartData(filters);
})
);
}
ngOnDestroy(): void {
// Unsubscribe from all subscriptions
this.filterSubscription.unsubscribe();
}
// Get chart data based on chart type
get chartData(): ChartData<'doughnut'> {
return this.chartType === 'endCustomer' ? this.endCustomerData : this.segmentPenetrationData;
}
// Get chart title based on chart type
get chartTitle(): string {
return this.chartType === 'endCustomer'
? 'End Customer Stage Split'
: 'Segment Penetration';
}
// Update chart data based on filters
updateChartData(filters: any): void {
// Show loading state
this.isLoading = true;
// Simulate data change based on filters
// In a real implementation, this would fetch new data from an API
// Add a small delay to simulate loading
setTimeout(() => {
if (this.chartType === 'endCustomer') {
const baseData = [30, 25, 20, 15, 10];
// Apply filter effects (simplified logic)
let multiplier = 1;
if (filters.salesRep) multiplier *= 0.9;
if (filters.partner) multiplier *= 0.85;
if (filters.tractionChannel) multiplier *= 0.95;
if (filters.subProductLine) multiplier *= 0.8;
this.endCustomerData.datasets[0].data = baseData.map(value =>
Math.floor(value * multiplier)
);
} else {
const baseData = [40, 30, 20, 10];
// Apply filter effects (simplified logic)
let multiplier = 1;
if (filters.salesRep) multiplier *= 0.85;
if (filters.partner) multiplier *= 0.9;
if (filters.tractionChannel) multiplier *= 0.95;
if (filters.subProductLine) multiplier *= 0.75;
this.segmentPenetrationData.datasets[0].data = baseData.map(value =>
Math.floor(value * multiplier)
);
}
// Update chart
this.updateChart();
// Hide loading state
this.isLoading = false;
}, 300);
}
// Update chart with new data
updateChart(): void {
if (this.chart) {
this.chart.update();
}
}
// events
public chartClicked({ event, active }: { event?: ChartEvent, active?: {}[] }): void {
console.log(event, active);
}
public chartHovered({ event, active }: { event?: ChartEvent, active?: {}[] }): void {
console.log(event, active);
}
}

View File

@ -0,0 +1,45 @@
.shimmer-container {
position: relative;
overflow: hidden;
.shimmer-content {
transition: opacity 0.3s ease;
}
.shimmer-overlay {
position: absolute;
top: 0;
left: 0;
right: 0;
bottom: 0;
background: rgba(255, 255, 255, 0.7);
display: flex;
align-items: center;
justify-content: center;
z-index: 100;
.shimmer-animation {
width: 100%;
height: 100%;
background: linear-gradient(90deg, #f0f0f0 25%, #e0e0e0 50%, #f0f0f0 75%);
background-size: 200% 100%;
animation: shimmer 1.5s infinite;
border-radius: 8px;
}
}
&.active {
.shimmer-content {
opacity: 0.5;
}
}
}
@keyframes shimmer {
0% {
background-position: -200% 0;
}
100% {
background-position: 200% 0;
}
}

View File

@ -0,0 +1,19 @@
import { Component, Input } from '@angular/core';
@Component({
selector: 'app-shield-loading-shimmer',
template: `
<div class="shimmer-container" [ngClass]="{ 'active': loading }">
<div class="shimmer-content">
<ng-content></ng-content>
</div>
<div class="shimmer-overlay" *ngIf="loading">
<div class="shimmer-animation"></div>
</div>
</div>
`,
styleUrls: ['./loading-shimmer.component.scss']
})
export class LoadingShimmerComponent {
@Input() loading: boolean = false;
}

View File

@ -0,0 +1,43 @@
<div class="map-container">
<div class="map-header">
<h3>Dealer Locations</h3>
</div>
<div class="map-wrapper">
<!-- Simple map representation with dealer markers -->
<div class="map-placeholder">
<div class="india-map">
<!-- Simplified India map outline -->
<div class="map-outline"></div>
<!-- Dealer markers -->
<div
*ngFor="let dealer of dealerLocations"
class="dealer-marker"
[style.left]="dealer.lng * 0.8 + 10 + '%'"
[style.top]="100 - (dealer.lat * 1.2) + '%'"
[style.background-color]="getStatusColor(dealer.status)"
(mouseenter)="onDealerHover(dealer)"
(mouseleave)="onDealerLeave()"
[attr.data-tooltip]="dealer.name + ' - ' + dealer.city + ' (' + getStatusLabel(dealer.status) + ')'">
</div>
</div>
<!-- Legend -->
<div class="map-legend">
<div class="legend-item" *ngFor="let status of ['active', 'inactive', 'training', 'onboarding']">
<div class="legend-color" [style.background-color]="getStatusColor(status)"></div>
<div class="legend-label">{{ getStatusLabel(status) }}</div>
</div>
</div>
<!-- Dealer info tooltip -->
<div class="dealer-tooltip" *ngIf="hoveredDealer" [style.left]="hoveredDealer.lng * 0.8 + 12 + '%'" [style.top]="100 - (hoveredDealer.lat * 1.2) - 5 + '%'">
<div class="tooltip-content">
<div class="dealer-name">{{ hoveredDealer.name }}</div>
<div class="dealer-location">{{ hoveredDealer.city }}, {{ hoveredDealer.state }}</div>
<div class="dealer-status" [style.color]="getStatusColor(hoveredDealer.status)">{{ getStatusLabel(hoveredDealer.status) }}</div>
</div>
</div>
</div>
</div>
</div>

View File

@ -0,0 +1,135 @@
.map-container {
height: 100%;
display: flex;
flex-direction: column;
.map-header {
margin-bottom: 20px;
h3 {
font-size: 18px;
font-weight: 600;
color: #0a192f;
margin: 0;
}
}
.map-wrapper {
flex: 1;
position: relative;
.map-placeholder {
height: 100%;
background: #f8fafc;
border-radius: 8px;
position: relative;
overflow: hidden;
.india-map {
width: 100%;
height: 100%;
position: relative;
background: #e2e8f0;
.map-outline {
position: absolute;
top: 10%;
left: 10%;
right: 10%;
bottom: 10%;
background: #cbd5e1;
border-radius: 50% 40% 45% 50% / 40% 50% 40% 45%;
box-shadow: inset 0 0 20px rgba(0, 0, 0, 0.1);
}
.dealer-marker {
position: absolute;
width: 16px;
height: 16px;
border-radius: 50%;
cursor: pointer;
transform: translate(-50%, -50%);
box-shadow: 0 2px 6px rgba(0, 0, 0, 0.3);
transition: transform 0.2s ease, box-shadow 0.2s ease;
&:hover {
transform: translate(-50%, -50%) scale(1.3);
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.4);
z-index: 10;
}
}
}
.map-legend {
position: absolute;
bottom: 20px;
left: 20px;
background: rgba(255, 255, 255, 0.9);
border-radius: 8px;
padding: 12px;
box-shadow: 0 2px 10px rgba(0, 0, 0, 0.1);
display: flex;
gap: 15px;
.legend-item {
display: flex;
align-items: center;
gap: 6px;
.legend-color {
width: 12px;
height: 12px;
border-radius: 50%;
}
.legend-label {
font-size: 12px;
color: #64748b;
}
}
}
.dealer-tooltip {
position: absolute;
background: rgba(10, 25, 47, 0.95);
color: white;
border-radius: 6px;
padding: 10px 15px;
transform: translate(-50%, -100%);
margin-top: -10px;
z-index: 100;
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.2);
min-width: 180px;
&::after {
content: '';
position: absolute;
top: 100%;
left: 50%;
margin-left: -5px;
border-width: 5px;
border-style: solid;
border-color: rgba(10, 25, 47, 0.95) transparent transparent transparent;
}
.tooltip-content {
.dealer-name {
font-weight: 600;
margin-bottom: 4px;
}
.dealer-location {
font-size: 13px;
color: #cbd5e1;
margin-bottom: 4px;
}
.dealer-status {
font-size: 12px;
font-weight: 500;
}
}
}
}
}
}

View File

@ -0,0 +1,109 @@
import { Component, OnInit, OnDestroy } from '@angular/core';
import { DashboardFilterService } from '../../services/dashboard-filter.service';
import { Subscription } from 'rxjs';
interface DealerLocation {
id: number;
name: string;
lat: number;
lng: number;
status: 'active' | 'inactive' | 'training' | 'onboarding';
city: string;
state: string;
}
@Component({
selector: 'app-shield-map-chart',
templateUrl: './map-chart.component.html',
styleUrls: ['./map-chart.component.scss']
})
export class MapChartComponent implements OnInit, OnDestroy {
private filterSubscription: Subscription = new Subscription();
// Mock dealer location data
originalDealerLocations: DealerLocation[] = [
{ id: 1, name: 'ABC Motors', lat: 28.6139, lng: 77.2090, status: 'active', city: 'New Delhi', state: 'Delhi' },
{ id: 2, name: 'XYZ Auto', lat: 19.0760, lng: 72.8777, status: 'active', city: 'Mumbai', state: 'Maharashtra' },
{ id: 3, name: 'PQR Dealers', lat: 13.0827, lng: 80.2707, status: 'training', city: 'Chennai', state: 'Tamil Nadu' },
{ id: 4, name: 'LMN Enterprises', lat: 12.9716, lng: 77.5946, status: 'inactive', city: 'Bangalore', state: 'Karnataka' },
{ id: 5, name: 'DEF Solutions', lat: 22.5726, lng: 88.3639, status: 'active', city: 'Kolkata', state: 'West Bengal' },
{ id: 6, name: 'GHI Services', lat: 25.3176, lng: 82.9739, status: 'onboarding', city: 'Varanasi', state: 'Uttar Pradesh' },
{ id: 7, name: 'JKL Group', lat: 23.0225, lng: 72.5714, status: 'active', city: 'Ahmedabad', state: 'Gujarat' },
{ id: 8, name: 'MNO Corp', lat: 18.5204, lng: 73.8567, status: 'training', city: 'Pune', state: 'Maharashtra' }
];
dealerLocations: DealerLocation[] = [...this.originalDealerLocations];
// Status colors
statusColors = {
active: '#0a192f',
inactive: '#64748b',
training: '#ff6b35',
onboarding: '#f59e0b'
};
// Status labels
statusLabels = {
active: 'Active',
inactive: 'Inactive',
training: 'Training',
onboarding: 'Onboarding'
};
hoveredDealer: DealerLocation | null = null;
constructor(private filterService: DashboardFilterService) { }
ngOnInit(): void {
// Subscribe to filter changes
this.filterSubscription.add(
this.filterService.filterState$.subscribe(filters => {
this.updateMapData(filters);
})
);
}
ngOnDestroy(): void {
// Unsubscribe from all subscriptions
this.filterSubscription.unsubscribe();
}
// Update map data based on filters
updateMapData(filters: any): void {
// Simulate data change based on filters
// In a real implementation, this would fetch new data from an API
if (filters.salesRep || filters.partner || filters.tractionChannel || filters.subProductLine) {
// Apply filter effects (simplified logic)
const filteredData = this.originalDealerLocations.filter(location => {
// Simple filtering logic - in a real app, this would be more sophisticated
return Math.random() > 0.25; // Randomly filter out some locations
});
this.dealerLocations = filteredData;
} else {
// No filters applied, show all data
this.dealerLocations = [...this.originalDealerLocations];
}
}
// Get status color based on dealer status
getStatusColor(status: string): string {
return (this.statusColors as any)[status] || '#64748b';
}
// Get status label based on dealer status
getStatusLabel(status: string): string {
return (this.statusLabels as any)[status] || status;
}
// Handle dealer hover
onDealerHover(dealer: DealerLocation): void {
this.hoveredDealer = dealer;
}
// Handle dealer leave
onDealerLeave(): void {
this.hoveredDealer = null;
}
}

View File

@ -0,0 +1,20 @@
<div class="quarterwise-flow-container">
<div class="quarterwise-flow-header">
<h3>Quarterwise Flow</h3>
</div>
<div class="quarterwise-flow-content">
<div class="placeholder-message">
<div class="message-icon">
<i class="info-icon"></i>
</div>
<div class="message-text">
<h4>Chart Configuration Incomplete</h4>
<p>Please configure the quarterwise flow chart settings to visualize the data.</p>
</div>
<button class="configure-button">
<i class="config-icon">⚙️</i>
Configure Chart
</button>
</div>
</div>
</div>

View File

@ -0,0 +1,70 @@
.quarterwise-flow-container {
height: 100%;
display: flex;
flex-direction: column;
.quarterwise-flow-header {
margin-bottom: 20px;
h3 {
font-size: 18px;
font-weight: 600;
color: #0a192f;
margin: 0;
}
}
.quarterwise-flow-content {
flex: 1;
display: flex;
align-items: center;
justify-content: center;
.placeholder-message {
text-align: center;
padding: 30px;
background: #f8fafc;
border-radius: 12px;
border: 1px dashed #cbd5e1;
.message-icon {
color: #94a3b8;
margin-bottom: 20px;
}
.message-text {
h4 {
color: #0a192f;
margin: 0 0 10px 0;
font-weight: 600;
}
p {
color: #64748b;
margin: 0 0 20px 0;
font-size: 14px;
line-height: 1.5;
}
}
.configure-button {
padding: 10px 20px;
background: #0a192f;
color: white;
border: none;
border-radius: 6px;
font-size: 14px;
font-weight: 500;
cursor: pointer;
display: inline-flex;
align-items: center;
gap: 8px;
transition: background-color 0.2s ease;
&:hover {
background: #1e3a8a;
}
}
}
}
}

View File

@ -0,0 +1,15 @@
import { Component, OnInit } from '@angular/core';
@Component({
selector: 'app-shield-quarterwise-flow',
templateUrl: './quarterwise-flow.component.html',
styleUrls: ['./quarterwise-flow.component.scss']
})
export class QuarterwiseFlowComponent implements OnInit {
constructor() { }
ngOnInit(): void {
}
}

View File

@ -0,0 +1,76 @@
<div class="sidebar-filters">
<!-- Application Header -->
<div class="app-header">
<h1 class="app-name">Shield</h1>
<h2 class="dashboard-title">Overall Dashboard</h2>
</div>
<!-- KPI Metrics -->
<div class="kpi-section">
<div class="kpi-card total-leads">
<div class="kpi-title">Total Leads</div>
<div class="kpi-value">{{ totalLeads }}</div>
</div>
<div class="kpi-card total-deals">
<div class="kpi-title">Total Deals</div>
<div class="kpi-value">{{ totalDeals }}</div>
</div>
</div>
<!-- Filters Section -->
<div class="filters-section">
<h3 class="filters-title">Filters</h3>
<div class="filter-group">
<label for="salesRep">Sales Rep</label>
<select
id="salesRep"
[(ngModel)]="selectedSalesRep"
(ngModelChange)="updateFilter('salesRep', $event)"
class="filter-select">
<option value="">All Sales Reps</option>
<option *ngFor="let rep of salesReps" [value]="rep">{{ rep }}</option>
</select>
</div>
<div class="filter-group">
<label for="partner">Partner</label>
<select
id="partner"
[(ngModel)]="selectedPartner"
(ngModelChange)="updateFilter('partner', $event)"
class="filter-select">
<option value="">All Partners</option>
<option *ngFor="let partner of partners" [value]="partner">{{ partner }}</option>
</select>
</div>
<div class="filter-group">
<label for="tractionChannel">Traction Channel</label>
<select
id="tractionChannel"
[(ngModel)]="selectedTractionChannel"
(ngModelChange)="updateFilter('tractionChannel', $event)"
class="filter-select">
<option value="">All Channels</option>
<option *ngFor="let channel of tractionChannels" [value]="channel">{{ channel }}</option>
</select>
</div>
<div class="filter-group">
<label for="subProductLine">Sub Product Line</label>
<select
id="subProductLine"
[(ngModel)]="selectedSubProductLine"
(ngModelChange)="updateFilter('subProductLine', $event)"
class="filter-select">
<option value="">All Lines</option>
<option *ngFor="let line of subProductLines" [value]="line">{{ line }}</option>
</select>
</div>
<button class="reset-button" (click)="resetFilters()">
Reset Filters
</button>
</div>
</div>

View File

@ -0,0 +1,119 @@
.sidebar-filters {
height: 100%;
display: flex;
flex-direction: column;
.app-header {
margin-bottom: 30px;
.app-name {
font-size: 24px;
font-weight: 700;
margin: 0 0 5px 0;
color: #ff6b35;
}
.dashboard-title {
font-size: 18px;
font-weight: 500;
margin: 0;
color: #cbd5e1;
}
}
.kpi-section {
display: flex;
flex-direction: column;
gap: 15px;
margin-bottom: 30px;
.kpi-card {
background: rgba(255, 255, 255, 0.1);
border-radius: 10px;
padding: 15px;
text-align: center;
.kpi-title {
font-size: 14px;
color: #94a3b8;
margin-bottom: 8px;
}
.kpi-value {
font-size: 24px;
font-weight: 700;
color: white;
}
}
.total-leads {
border-left: 3px solid #ff6b35;
}
.total-deals {
border-left: 3px solid #0a192f;
}
}
.filters-section {
flex: 1;
.filters-title {
font-size: 18px;
color: white;
margin: 0 0 20px 0;
padding-bottom: 10px;
border-bottom: 1px solid rgba(255, 255, 255, 0.1);
}
.filter-group {
margin-bottom: 20px;
label {
display: block;
font-size: 14px;
color: #cbd5e1;
margin-bottom: 8px;
}
.filter-select {
width: 100%;
padding: 10px 15px;
background: rgba(255, 255, 255, 0.1);
border: 1px solid rgba(255, 255, 255, 0.2);
border-radius: 8px;
color: white;
font-size: 14px;
&:focus {
outline: none;
border-color: #ff6b35;
box-shadow: 0 0 0 2px rgba(255, 107, 53, 0.2);
}
option {
background: #0a192f;
color: white;
}
}
}
.reset-button {
width: 100%;
padding: 12px;
background: rgba(255, 255, 255, 0.1);
color: white;
border: 1px solid rgba(255, 255, 255, 0.2);
border-radius: 8px;
font-size: 16px;
font-weight: 500;
cursor: pointer;
transition: all 0.2s ease;
&:hover {
background: rgba(255, 107, 53, 0.2);
border-color: #ff6b35;
}
}
}
}

View File

@ -0,0 +1,51 @@
import { Component, OnInit } from '@angular/core';
import { DashboardFilterService, FilterState } from '../../services/dashboard-filter.service';
import { Observable } from 'rxjs';
@Component({
selector: 'app-shield-sidebar-filters',
templateUrl: './sidebar-filters.component.html',
styleUrls: ['./sidebar-filters.component.scss']
})
export class SidebarFiltersComponent implements OnInit {
// Filter options
salesReps = ['All Sales Reps', 'John Smith', 'Jane Doe', 'Mike Johnson', 'Sarah Wilson'];
partners = ['All Partners', 'Partner A', 'Partner B', 'Partner C', 'Partner D'];
tractionChannels = ['All Channels', 'Direct', 'Indirect', 'Online', 'Referral'];
subProductLines = ['All Lines', 'Product Line 1', 'Product Line 2', 'Product Line 3'];
// Current filter values
selectedSalesRep = '';
selectedPartner = '';
selectedTractionChannel = '';
selectedSubProductLine = '';
// KPI data
totalLeads = 1248;
totalDeals = 842;
constructor(private filterService: DashboardFilterService) { }
ngOnInit(): void {
// Subscribe to KPI data changes
this.filterService.kpiData$.subscribe(kpiData => {
this.totalLeads = kpiData.totalLeads;
this.totalDeals = kpiData.totalDeals;
});
}
// Update filter state when any filter changes
updateFilter(filterType: keyof FilterState, value: string): void {
this.filterService.updateFilter(filterType, value);
}
// Reset all filters to default values
resetFilters(): void {
this.selectedSalesRep = '';
this.selectedPartner = '';
this.selectedTractionChannel = '';
this.selectedSubProductLine = '';
this.filterService.resetFilters();
}
}

View File

@ -0,0 +1,10 @@
export * from './shield-dashboard.component';
// Export all components
export * from './components/sidebar-filters/sidebar-filters.component';
export * from './components/bar-chart/bar-chart.component';
export * from './components/donut-chart/donut-chart.component';
export * from './components/map-chart/map-chart.component';
export * from './components/data-table/data-table.component';
export * from './components/deal-details-card/deal-details-card.component';
export * from './components/quarterwise-flow/quarterwise-flow.component';

View File

@ -0,0 +1,84 @@
import { Injectable } from '@angular/core';
import { BehaviorSubject, Observable } from 'rxjs';
// Define the filter state interface
export interface FilterState {
salesRep: string;
partner: string;
tractionChannel: string;
subProductLine: string;
}
@Injectable({
providedIn: 'root'
})
export class DashboardFilterService {
// Shared filter state using BehaviorSubject
private filterStateSubject = new BehaviorSubject<FilterState>({
salesRep: '',
partner: '',
tractionChannel: '',
subProductLine: ''
});
public filterState$ = this.filterStateSubject.asObservable();
// KPI data
private kpiDataSubject = new BehaviorSubject<{ totalLeads: number; totalDeals: number }>({
totalLeads: 1248,
totalDeals: 842
});
public kpiData$ = this.kpiDataSubject.asObservable();
constructor() { }
// Update filter state
updateFilter(filterType: keyof FilterState, value: string): void {
const currentState = this.filterStateSubject.value;
const newState = { ...currentState, [filterType]: value };
this.filterStateSubject.next(newState);
// Simulate KPI data change based on filters
this.updateKpiData(newState);
}
// Reset all filters to default values
resetFilters(): void {
this.filterStateSubject.next({
salesRep: '',
partner: '',
tractionChannel: '',
subProductLine: ''
});
// Reset KPI data to default values
this.kpiDataSubject.next({
totalLeads: 1248,
totalDeals: 842
});
}
// Update KPI data based on filters (simulated)
private updateKpiData(filters: FilterState): void {
// This is a simplified simulation - in a real app, this would come from an API
let totalLeads = 1248;
let totalDeals = 842;
// Apply filter effects (simplified logic)
if (filters.salesRep) totalLeads = Math.floor(totalLeads * 0.8);
if (filters.partner) totalDeals = Math.floor(totalDeals * 0.9);
if (filters.tractionChannel) totalLeads = Math.floor(totalLeads * 0.85);
if (filters.subProductLine) totalDeals = Math.floor(totalDeals * 0.95);
this.kpiDataSubject.next({
totalLeads,
totalDeals
});
}
// Get current filter values
getCurrentFilters(): FilterState {
return this.filterStateSubject.value;
}
}

View File

@ -0,0 +1,16 @@
import { NgModule } from '@angular/core';
import { RouterModule, Routes } from '@angular/router';
import { ShieldDashboardComponent } from './shield-dashboard.component';
const routes: Routes = [
{
path: '',
component: ShieldDashboardComponent
}
];
@NgModule({
imports: [RouterModule.forChild(routes)],
exports: [RouterModule]
})
export class ShieldDashboardRoutingModule { }

View File

@ -0,0 +1,58 @@
<div class="shield-dashboard">
<div class="dashboard-container">
<!-- Sidebar Filters -->
<app-shield-sidebar-filters class="sidebar"></app-shield-sidebar-filters>
<!-- Main Dashboard Content -->
<div class="main-content">
<!-- KPI Metrics -->
<div class="kpi-section">
<div class="kpi-card total-leads">
<div class="kpi-title">Total Leads</div>
<div class="kpi-value">1,248</div>
</div>
<div class="kpi-card total-deals">
<div class="kpi-title">Total Deals</div>
<div class="kpi-value">842</div>
</div>
</div>
<!-- Charts and Components Grid -->
<div class="dashboard-grid">
<!-- Bar Chart -->
<div class="grid-item">
<app-shield-bar-chart></app-shield-bar-chart>
</div>
<!-- Donut Charts -->
<div class="grid-item">
<app-shield-donut-chart chartType="endCustomer"></app-shield-donut-chart>
</div>
<div class="grid-item">
<app-shield-donut-chart chartType="segmentPenetration"></app-shield-donut-chart>
</div>
<!-- Map Section -->
<div class="grid-item map-container">
<app-shield-map-chart></app-shield-map-chart>
</div>
<!-- Data Table -->
<div class="grid-item table-container">
<app-shield-data-table></app-shield-data-table>
</div>
<!-- Deal Details Card -->
<div class="grid-item">
<app-shield-deal-details-card></app-shield-deal-details-card>
</div>
<!-- Quarterwise Flow -->
<div class="grid-item">
<app-shield-quarterwise-flow></app-shield-quarterwise-flow>
</div>
</div>
</div>
</div>
</div>

View File

@ -0,0 +1,134 @@
.shield-dashboard {
height: 100%;
width: 100%;
background-color: #f5f7fa;
font-family: 'Inter', sans-serif;
.dashboard-container {
display: flex;
height: 100%;
width: 100%;
.sidebar {
width: 300px;
background: linear-gradient(135deg, #0a192f 0%, #172a45 100%);
color: white;
padding: 20px;
box-shadow: 3px 0 10px rgba(0, 0, 0, 0.1);
z-index: 10;
flex-shrink: 0;
}
.main-content {
flex: 1;
padding: 20px;
overflow-y: auto;
.kpi-section {
display: flex;
gap: 20px;
margin-bottom: 30px;
.kpi-card {
flex: 1;
background: white;
border-radius: 12px;
padding: 20px;
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.05);
text-align: center;
.kpi-title {
font-size: 16px;
color: #64748b;
margin-bottom: 10px;
}
.kpi-value {
font-size: 32px;
font-weight: 700;
}
}
.total-leads {
border-top: 4px solid #ff6b35;
}
.total-deals {
border-top: 4px solid #0a192f;
}
}
.dashboard-grid {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(400px, 1fr));
gap: 20px;
.grid-item {
background: white;
border-radius: 12px;
padding: 20px;
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.05);
&.map-container {
grid-column: span 2;
min-height: 400px;
}
&.table-container {
grid-column: span 2;
min-height: 300px;
}
}
}
}
}
}
// Responsive design
@media (max-width: 1200px) {
.shield-dashboard {
.dashboard-container {
.sidebar {
width: 250px;
}
.main-content {
.dashboard-grid {
grid-template-columns: repeat(auto-fit, minmax(350px, 1fr));
}
}
}
}
}
@media (max-width: 768px) {
.shield-dashboard {
.dashboard-container {
flex-direction: column;
.sidebar {
width: 100%;
padding: 15px;
}
.main-content {
padding: 15px;
.kpi-section {
flex-direction: column;
gap: 15px;
}
.dashboard-grid {
grid-template-columns: 1fr;
.grid-item {
&.map-container, &.table-container {
grid-column: span 1;
}
}
}
}
}
}
}

View File

@ -0,0 +1,15 @@
import { Component, OnInit } from '@angular/core';
@Component({
selector: 'app-shield-dashboard',
templateUrl: './shield-dashboard.component.html',
styleUrls: ['./shield-dashboard.component.scss']
})
export class ShieldDashboardComponent implements OnInit {
constructor() { }
ngOnInit(): void {
}
}