shield dashboard
This commit is contained in:
		
							parent
							
								
									aade12d6ff
								
							
						
					
					
						commit
						f24138cfbd
					
				| @ -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> | ||||
|     | ||||
|    | ||||
|    | ||||
| @ -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. | ||||
| @ -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> | ||||
| @ -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; | ||||
|   } | ||||
| } | ||||
| @ -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); | ||||
|   } | ||||
| } | ||||
| @ -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> | ||||
| @ -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); | ||||
|               } | ||||
|             } | ||||
|           } | ||||
|         } | ||||
|       } | ||||
|     } | ||||
|   } | ||||
| } | ||||
| @ -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}%`; | ||||
|   } | ||||
| } | ||||
| @ -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> | ||||
| @ -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; | ||||
|         } | ||||
|       } | ||||
|     } | ||||
|   } | ||||
| } | ||||
| @ -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); | ||||
|   } | ||||
| } | ||||
| @ -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> | ||||
| @ -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; | ||||
|   } | ||||
| } | ||||
| @ -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); | ||||
|   } | ||||
| } | ||||
| @ -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; | ||||
|   } | ||||
| } | ||||
| @ -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; | ||||
| } | ||||
| @ -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> | ||||
| @ -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; | ||||
|           } | ||||
|         } | ||||
|       } | ||||
|     } | ||||
|   } | ||||
| } | ||||
| @ -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; | ||||
|   } | ||||
| } | ||||
| @ -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> | ||||
| @ -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; | ||||
|         } | ||||
|       } | ||||
|     } | ||||
|   } | ||||
| } | ||||
| @ -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 { | ||||
|   } | ||||
| 
 | ||||
| } | ||||
| @ -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> | ||||
| @ -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; | ||||
|       } | ||||
|     } | ||||
|   } | ||||
| } | ||||
| @ -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(); | ||||
|   } | ||||
| } | ||||
| @ -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'; | ||||
| @ -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; | ||||
|   } | ||||
| } | ||||
| @ -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 { } | ||||
| @ -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> | ||||
| @ -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; | ||||
|             } | ||||
|           } | ||||
|         } | ||||
|       } | ||||
|     } | ||||
|   } | ||||
| } | ||||
| @ -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 { | ||||
|   } | ||||
| 
 | ||||
| } | ||||
		Loading…
	
	
			
			x
			
			
		
	
		Reference in New Issue
	
	Block a user