15 Commits

Author SHA1 Message Date
Gaurav Kumar
43df9ad37c filter 2025-10-27 17:37:25 +05:30
Gaurav Kumar
8dbeedba89 filter 2025-10-27 17:04:36 +05:30
Gaurav Kumar
f654b233f6 runner 2025-10-27 16:38:38 +05:30
Gaurav Kumar
123c4698a4 filter 2025-10-27 11:49:13 +05:30
Gaurav Kumar
35e3f411a8 filter 2025-10-27 10:59:35 +05:30
Gaurav Kumar
cdcf1e07c7 Update compact-filter.component.ts 2025-10-25 21:57:10 +05:30
Gaurav Kumar
c6ad8b5c2f filter 2025-10-25 21:46:54 +05:30
Gaurav Kumar
98e0908920 sheild 2025-10-25 20:15:06 +05:30
Gaurav Kumar
8266bfdc01 dashboard 2025-10-25 20:08:04 +05:30
Gaurav Kumar
f2b6a4d145 dashboard 2025-10-25 19:28:03 +05:30
Gaurav Kumar
02f37a1bc5 shield 2025-10-25 17:55:46 +05:30
Gaurav Kumar
f24138cfbd shield dashboard 2025-10-25 16:15:16 +05:30
Gaurav Kumar
aade12d6ff filter 2025-10-25 15:02:26 +05:30
Gaurav Kumar
6c01e71d04 dashboard 2025-10-25 14:43:42 +05:30
Gaurav Kumar
f60657ca64 filter 2025-10-25 10:33:16 +05:30
89 changed files with 9183 additions and 391 deletions

View File

@@ -23,6 +23,20 @@ export interface DashboardContentModel {
component?: any; component?: any;
name: string; name: string;
type?:string; type?:string;
// Common properties
table?: string;
connection?: string;
baseFilters?: any[];
// Common filter properties
commonFilterEnabled?: boolean;
// Drilldown properties
drilldownEnabled?: boolean;
drilldownApiUrl?: string;
drilldownXAxis?: string;
drilldownYAxis?: string;
drilldownParameter?: string;
drilldownFilters?: any[];
drilldownLayers?: any[];
} }
export interface DashboardModel { export interface DashboardModel {
@@ -67,6 +81,10 @@ export class value1{
export const WidgetsMock: WidgetModel[] = [ export const WidgetsMock: WidgetModel[] = [
{
name: 'Common Filter',
identifier: 'common_filter'
},
{ {
name: 'Radar Chart', name: 'Radar Chart',
identifier: 'radar_chart' identifier: 'radar_chart'

View File

@@ -97,6 +97,7 @@ export class LoginPageComponent implements OnInit {
.subscribe( .subscribe(
resp => { resp => {
console.log('API Response received:', resp); console.log('API Response received:', resp);
// Always reset loading state when response is received
this.isLoading = false; this.isLoading = false;
// Handle different response formats // Handle different response formats
@@ -106,7 +107,7 @@ export class LoginPageComponent implements OnInit {
return; return;
} }
// Handle different response formats // Handle different response formats
if (resp.success === 'false') { if (resp.success === 'false' || resp.success === false) {
this.isError = true; this.isError = true;
this.errMsg = resp.message || 'Login failed'; this.errMsg = resp.message || 'Login failed';
return; return;
@@ -123,6 +124,7 @@ export class LoginPageComponent implements OnInit {
}, },
(errResponse: HttpErrorResponse) => { (errResponse: HttpErrorResponse) => {
console.log('API Error received:', errResponse); console.log('API Error received:', errResponse);
// Always reset loading state when error occurs
this.isLoading = false; this.isLoading = false;
this.isError = true; this.isError = true;

View File

@@ -1,20 +0,0 @@
<!DOCTYPE html>
<html>
<head>
<title>gaurav</title>
</head>
<body>
<h1>this is h1</h1>
<h2>this is h1</h2>
<h3>this is h1</h3>
<h4>this is h1</h4>
<p>Lorem ipsum, dolor sit amet consectetur adipisicing elit. Ipsa fuga, asperiores mollitia iste vitae repellendus adipisci atque eum corrupti ad placeat unde voluptatum quia perferendis neque expedita, sequi iure quo. Ut error adipisci ex cum sint, suscipit, voluptatem repellat nemo dolorum unde dolores quasi aut. A earum quo mollitia voluptatibus!</p>
</body>
</html>

View File

@@ -11,6 +11,9 @@
<h3>{{ 'Dashboard_builder' | translate }}</h3> <h3>{{ 'Dashboard_builder' | translate }}</h3>
</div> </div>
<div class="clr-col-4" style="text-align: right;"> <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()"> <button id="add" class="btn btn-primary" (click)="gotorunner()">
<clr-icon shape="grid-view"></clr-icon>{{ 'Dashboard_runner' | translate }} <clr-icon shape="grid-view"></clr-icon>{{ 'Dashboard_runner' | translate }}
</button> </button>
@@ -140,6 +143,3 @@
</div> </div>
</div> </div>
</clr-modal> </clr-modal>

View File

@@ -0,0 +1,687 @@
# Common Filter System - Example Usage
## Dashboard Layout
```
+-----------------------------------------------------+
| Common Filter Widget (Draggable) |
| |
| [Category ▼] [Status ▼] [Date Range] [Active -toggle]|
| [Save Preset] [Preset Selector] [Reset] |
+-----------------------------------------------------+
| +-----------+ +-----------+ +-----------+ |
| | Bar Chart | | Line Chart| | Pie Chart | |
| | | | | | | |
| | | | | | | |
| +-----------+ +-----------+ +-----------+ |
| |
| +-----------+ +-----------+ +-----------+ |
| | Table | | Map | | KPI Cards | |
| | | | | | | |
| | | | | | | |
| +-----------+ +-----------+ +-----------+ |
+-----------------------------------------------------+
```
## Filter Configuration Example
### 1. Creating Filter Definitions
```typescript
// In CommonFilterComponent or dashboard initialization
const filters: Filter[] = [
{
id: 'category',
field: 'product_category',
label: 'Category',
type: 'dropdown',
options: ['Electronics', 'Clothing', 'Home & Garden', 'Books', 'Sports']
},
{
id: 'status',
field: 'order_status',
label: 'Status',
type: 'multiselect',
options: ['Pending', 'Processing', 'Shipped', 'Delivered', 'Cancelled']
},
{
id: 'date_range',
field: 'order_date',
label: 'Order Date',
type: 'date-range'
},
{
id: 'active',
field: 'is_active',
label: 'Active Orders',
type: 'toggle'
}
];
// Set filters in the service
filterService.setFilters(filters);
```
### 2. Setting Initial Filter Values
```typescript
// Set initial values
filterService.updateFilterValue('category', 'Electronics');
filterService.updateFilterValue('status', ['Processing', 'Shipped']);
filterService.updateFilterValue('date_range', {
start: '2023-01-01',
end: '2023-12-31'
});
filterService.updateFilterValue('active', true);
```
### 3. Chart Component Integration
```typescript
// In bar-chart.component.ts
export class BarChartComponent implements OnInit, OnChanges, OnDestroy {
private subscriptions: Subscription[] = [];
constructor(
private dashboardService: Dashboard3Service,
private filterService: FilterService
) { }
ngOnInit(): void {
// Subscribe to filter changes
this.subscriptions.push(
this.filterService.filterState$.subscribe(filters => {
this.refreshData();
})
);
// Load initial data
this.fetchChartData();
}
fetchChartData(): void {
// Get current filter values
const filterValues = this.filterService.getFilterValues();
// Build filter parameters for API
let filterParams = '';
if (Object.keys(filterValues).length > 0) {
const filterObj = {};
// Add base filters first
if (this.baseFilters && this.baseFilters.length > 0) {
this.baseFilters.forEach(filter => {
if (filter.field && filter.value) {
filterObj[filter.field] = filter.value;
}
});
}
// Add common filters
Object.keys(filterValues).forEach(key => {
const value = filterValues[key];
if (value !== undefined && value !== null && value !== '') {
filterObj[key] = value;
}
});
if (Object.keys(filterObj).length > 0) {
filterParams = JSON.stringify(filterObj);
}
}
// Make API call with filters
this.dashboardService.getChartData(
this.table,
'bar',
this.xAxis,
this.yAxis,
this.connection,
'',
'',
filterParams
).subscribe(data => {
// Handle response
this.processChartData(data);
});
}
refreshData(): void {
this.fetchChartData();
}
ngOnDestroy(): void {
this.subscriptions.forEach(sub => sub.unsubscribe());
}
}
```
### 4. API Endpoint Example
```javascript
// Backend API endpoint example
app.get('/chart/getdashjson/bar', (req, res) => {
const {
tableName,
xAxis,
yAxes,
sureId,
filters // JSON string of filters
} = req.query;
// Parse filters
let filterObj = {};
if (filters) {
try {
filterObj = JSON.parse(filters);
} catch (e) {
console.error('Failed to parse filters:', e);
}
}
// Build database query with filters
let query = `SELECT ${xAxis}, ${yAxes} FROM ${tableName}`;
const whereConditions = [];
// Add filter conditions
Object.keys(filterObj).forEach(field => {
const value = filterObj[field];
if (Array.isArray(value)) {
// Handle multiselect (IN clause)
const values = value.map(v => `'${v}'`).join(',');
whereConditions.push(`${field} IN (${values})`);
} else if (typeof value === 'object' && value.start && value.end) {
// Handle date range
whereConditions.push(`${field} BETWEEN '${value.start}' AND '${value.end}'`);
} else if (typeof value === 'boolean') {
// Handle boolean
whereConditions.push(`${field} = ${value}`);
} else {
// Handle text and other values
whereConditions.push(`${field} = '${value}'`);
}
});
if (whereConditions.length > 0) {
query += ` WHERE ${whereConditions.join(' AND ')}`;
}
// Execute query and return results
database.query(query, (err, results) => {
if (err) {
res.status(500).json({ error: err.message });
} else {
res.json({
chartLabels: results.map(row => row[xAxis]),
chartData: [{
data: results.map(row => row[yAxes]),
label: yAxes
}]
});
}
});
});
```
## Filter Presets Example
### Saving a Preset
```typescript
// Save current filter state as a preset
const presetName = 'Q4 2023 Sales';
filterService.savePreset(presetName);
// Preset is now available in the presets list
const presets = filterService.getPresets(); // ['Q4 2023 Sales', ...]
```
### Loading a Preset
```typescript
// Load a saved preset
filterService.loadPreset('Q4 2023 Sales');
// All charts will automatically refresh with the preset filters
```
## Cross-Filtering Example
```typescript
// In a chart component, handle click events
onBarClick(event: any): void {
const clickedCategory = event.active[0]._model.label;
// Update the category filter
this.filterService.updateFilterValue('category', clickedCategory);
// All other charts will automatically update
}
```
## URL Synchronization Example
```typescript
// Update URL when filters change
this.filterService.filterState$.subscribe(filters => {
const queryParams = new URLSearchParams();
Object.keys(filters).forEach(key => {
const value = filters[key];
if (value !== undefined && value !== null && value !== '') {
if (typeof value === 'object') {
if (value.hasOwnProperty('start') && value.hasOwnProperty('end')) {
// Date range
if (value.start) queryParams.append(`${key}_start`, value.start);
if (value.end) queryParams.append(`${key}_end`, value.end);
} else {
// Other objects as JSON
queryParams.append(key, JSON.stringify(value));
}
} else {
// Simple values
queryParams.append(key, value.toString());
}
}
});
// Update browser URL
const newUrl = `${window.location.pathname}?${queryParams.toString()}`;
window.history.replaceState({}, '', newUrl);
});
// Load filters from URL on page load
ngOnInit(): void {
const urlParams = new URLSearchParams(window.location.search);
const filterState: any = {};
// Parse URL parameters back into filter state
for (const [key, value] of urlParams.entries()) {
if (key.endsWith('_start')) {
const fieldName = key.replace('_start', '');
filterState[fieldName] = filterState[fieldName] || {};
filterState[fieldName].start = value;
} else if (key.endsWith('_end')) {
const fieldName = key.replace('_end', '');
filterState[fieldName] = filterState[fieldName] || {};
filterState[fieldName].end = value;
} else {
try {
filterState[key] = JSON.parse(value);
} catch (e) {
filterState[key] = value;
}
}
}
// Apply URL filters
Object.keys(filterState).forEach(key => {
this.filterService.updateFilterValue(key, filterState[key]);
});
}
```
## Data Flow Diagram
```mermaid
sequenceDiagram
participant User
participant CommonFilterComponent
participant FilterService
participant ChartComponent
participant API
User->>CommonFilterComponent: Configure filters
CommonFilterComponent->>FilterService: Update filter state
FilterService->>ChartComponent: Notify filter change
ChartComponent->>FilterService: Request current filters
FilterService-->>ChartComponent: Return filter values
ChartComponent->>API: Fetch data with filters
API-->>ChartComponent: Return filtered data
ChartComponent->>User: Display updated chart
```
This implementation provides a robust, scalable solution for managing common filters across multiple dashboard components with real-time updates and flexible configuration options.# Common Filter System - Example Usage
## Dashboard Layout
```
+-----------------------------------------------------+
| Common Filter Widget (Draggable) |
| |
| [Category ▼] [Status ▼] [Date Range] [Active -toggle]|
| [Save Preset] [Preset Selector] [Reset] |
+-----------------------------------------------------+
| +-----------+ +-----------+ +-----------+ |
| | Bar Chart | | Line Chart| | Pie Chart | |
| | | | | | | |
| | | | | | | |
| +-----------+ +-----------+ +-----------+ |
| |
| +-----------+ +-----------+ +-----------+ |
| | Table | | Map | | KPI Cards | |
| | | | | | | |
| | | | | | | |
| +-----------+ +-----------+ +-----------+ |
+-----------------------------------------------------+
```
## Filter Configuration Example
### 1. Creating Filter Definitions
```typescript
// In CommonFilterComponent or dashboard initialization
const filters: Filter[] = [
{
id: 'category',
field: 'product_category',
label: 'Category',
type: 'dropdown',
options: ['Electronics', 'Clothing', 'Home & Garden', 'Books', 'Sports']
},
{
id: 'status',
field: 'order_status',
label: 'Status',
type: 'multiselect',
options: ['Pending', 'Processing', 'Shipped', 'Delivered', 'Cancelled']
},
{
id: 'date_range',
field: 'order_date',
label: 'Order Date',
type: 'date-range'
},
{
id: 'active',
field: 'is_active',
label: 'Active Orders',
type: 'toggle'
}
];
// Set filters in the service
filterService.setFilters(filters);
```
### 2. Setting Initial Filter Values
```typescript
// Set initial values
filterService.updateFilterValue('category', 'Electronics');
filterService.updateFilterValue('status', ['Processing', 'Shipped']);
filterService.updateFilterValue('date_range', {
start: '2023-01-01',
end: '2023-12-31'
});
filterService.updateFilterValue('active', true);
```
### 3. Chart Component Integration
```typescript
// In bar-chart.component.ts
export class BarChartComponent implements OnInit, OnChanges, OnDestroy {
private subscriptions: Subscription[] = [];
constructor(
private dashboardService: Dashboard3Service,
private filterService: FilterService
) { }
ngOnInit(): void {
// Subscribe to filter changes
this.subscriptions.push(
this.filterService.filterState$.subscribe(filters => {
this.refreshData();
})
);
// Load initial data
this.fetchChartData();
}
fetchChartData(): void {
// Get current filter values
const filterValues = this.filterService.getFilterValues();
// Build filter parameters for API
let filterParams = '';
if (Object.keys(filterValues).length > 0) {
const filterObj = {};
// Add base filters first
if (this.baseFilters && this.baseFilters.length > 0) {
this.baseFilters.forEach(filter => {
if (filter.field && filter.value) {
filterObj[filter.field] = filter.value;
}
});
}
// Add common filters
Object.keys(filterValues).forEach(key => {
const value = filterValues[key];
if (value !== undefined && value !== null && value !== '') {
filterObj[key] = value;
}
});
if (Object.keys(filterObj).length > 0) {
filterParams = JSON.stringify(filterObj);
}
}
// Make API call with filters
this.dashboardService.getChartData(
this.table,
'bar',
this.xAxis,
this.yAxis,
this.connection,
'',
'',
filterParams
).subscribe(data => {
// Handle response
this.processChartData(data);
});
}
refreshData(): void {
this.fetchChartData();
}
ngOnDestroy(): void {
this.subscriptions.forEach(sub => sub.unsubscribe());
}
}
```
### 4. API Endpoint Example
```javascript
// Backend API endpoint example
app.get('/chart/getdashjson/bar', (req, res) => {
const {
tableName,
xAxis,
yAxes,
sureId,
filters // JSON string of filters
} = req.query;
// Parse filters
let filterObj = {};
if (filters) {
try {
filterObj = JSON.parse(filters);
} catch (e) {
console.error('Failed to parse filters:', e);
}
}
// Build database query with filters
let query = `SELECT ${xAxis}, ${yAxes} FROM ${tableName}`;
const whereConditions = [];
// Add filter conditions
Object.keys(filterObj).forEach(field => {
const value = filterObj[field];
if (Array.isArray(value)) {
// Handle multiselect (IN clause)
const values = value.map(v => `'${v}'`).join(',');
whereConditions.push(`${field} IN (${values})`);
} else if (typeof value === 'object' && value.start && value.end) {
// Handle date range
whereConditions.push(`${field} BETWEEN '${value.start}' AND '${value.end}'`);
} else if (typeof value === 'boolean') {
// Handle boolean
whereConditions.push(`${field} = ${value}`);
} else {
// Handle text and other values
whereConditions.push(`${field} = '${value}'`);
}
});
if (whereConditions.length > 0) {
query += ` WHERE ${whereConditions.join(' AND ')}`;
}
// Execute query and return results
database.query(query, (err, results) => {
if (err) {
res.status(500).json({ error: err.message });
} else {
res.json({
chartLabels: results.map(row => row[xAxis]),
chartData: [{
data: results.map(row => row[yAxes]),
label: yAxes
}]
});
}
});
});
```
## Filter Presets Example
### Saving a Preset
```typescript
// Save current filter state as a preset
const presetName = 'Q4 2023 Sales';
filterService.savePreset(presetName);
// Preset is now available in the presets list
const presets = filterService.getPresets(); // ['Q4 2023 Sales', ...]
```
### Loading a Preset
```typescript
// Load a saved preset
filterService.loadPreset('Q4 2023 Sales');
// All charts will automatically refresh with the preset filters
```
## Cross-Filtering Example
```typescript
// In a chart component, handle click events
onBarClick(event: any): void {
const clickedCategory = event.active[0]._model.label;
// Update the category filter
this.filterService.updateFilterValue('category', clickedCategory);
// All other charts will automatically update
}
```
## URL Synchronization Example
```typescript
// Update URL when filters change
this.filterService.filterState$.subscribe(filters => {
const queryParams = new URLSearchParams();
Object.keys(filters).forEach(key => {
const value = filters[key];
if (value !== undefined && value !== null && value !== '') {
if (typeof value === 'object') {
if (value.hasOwnProperty('start') && value.hasOwnProperty('end')) {
// Date range
if (value.start) queryParams.append(`${key}_start`, value.start);
if (value.end) queryParams.append(`${key}_end`, value.end);
} else {
// Other objects as JSON
queryParams.append(key, JSON.stringify(value));
}
} else {
// Simple values
queryParams.append(key, value.toString());
}
}
});
// Update browser URL
const newUrl = `${window.location.pathname}?${queryParams.toString()}`;
window.history.replaceState({}, '', newUrl);
});
// Load filters from URL on page load
ngOnInit(): void {
const urlParams = new URLSearchParams(window.location.search);
const filterState: any = {};
// Parse URL parameters back into filter state
for (const [key, value] of urlParams.entries()) {
if (key.endsWith('_start')) {
const fieldName = key.replace('_start', '');
filterState[fieldName] = filterState[fieldName] || {};
filterState[fieldName].start = value;
} else if (key.endsWith('_end')) {
const fieldName = key.replace('_end', '');
filterState[fieldName] = filterState[fieldName] || {};
filterState[fieldName].end = value;
} else {
try {
filterState[key] = JSON.parse(value);
} catch (e) {
filterState[key] = value;
}
}
}
// Apply URL filters
Object.keys(filterState).forEach(key => {
this.filterService.updateFilterValue(key, filterState[key]);
});
}
```
## Data Flow Diagram
```mermaid
sequenceDiagram
participant User
participant CommonFilterComponent
participant FilterService
participant ChartComponent
participant API
User->>CommonFilterComponent: Configure filters
CommonFilterComponent->>FilterService: Update filter state
FilterService->>ChartComponent: Notify filter change
ChartComponent->>FilterService: Request current filters
FilterService-->>ChartComponent: Return filter values
ChartComponent->>API: Fetch data with filters
API-->>ChartComponent: Return filtered data
ChartComponent->>User: Display updated chart
```
This implementation provides a robust, scalable solution for managing common filters across multiple dashboard components with real-time updates and flexible configuration options.

View File

@@ -0,0 +1,219 @@
# Common Filter Implementation Guide
## Overview
This implementation provides a centralized filter system that allows users to control multiple charts simultaneously through a draggable filter widget. The system consists of:
1. **FilterService** - Central service managing filter definitions and state
2. **CommonFilterComponent** - Draggable widget for configuring filters
3. **Chart Components** - Updated chart components that subscribe to filter changes
## Architecture
```mermaid
graph TB
A[FilterService] --> B[CommonFilterComponent]
A --> C[BarChartComponent]
A --> D[LineChartComponent]
A --> E[OtherChartComponents]
F[User Interaction] --> B
```
## Key Components
### 1. FilterService
The central service that manages:
- Filter definitions (type, options, labels)
- Current filter values
- Filter presets
- Query parameter generation for API calls
#### Methods:
- `addFilter()` - Add a new filter
- `removeFilter()` - Remove a filter
- `updateFilterValue()` - Update a filter's value
- `getFilterValues()` - Get current filter values
- `resetFilters()` - Reset all filters to default values
- `savePreset()` - Save current filter state as a preset
- `loadPreset()` - Load a saved preset
- `buildQueryParams()` - Generate query parameters for API calls
### 2. CommonFilterComponent
A draggable widget that provides the UI for:
- Adding/removing filters
- Configuring filter properties
- Setting filter values
- Managing presets
### 3. Chart Components
Updated chart components that:
- Subscribe to filter changes
- Automatically refresh when filters change
- Include filter values in API calls
## Implementation Details
### Filter Types Supported
1. **Text** - Simple text input
2. **Dropdown** - Single selection from options
3. **Multiselect** - Multiple selection from options
4. **Date Range** - Start and end date selection
5. **Toggle** - Boolean on/off switch
### Data Flow
1. User adds/configures filters in CommonFilterComponent
2. FilterService stores filter definitions and values
3. Chart components subscribe to FilterService.filterState$
4. When filters change, charts automatically refresh
5. Filter values are included in API calls as query parameters
### API Integration
Filters are passed to the backend as query parameters:
- Text filter: `name=John`
- Dropdown filter: `category=A`
- Multiselect filter: `tags=["tag1","tag2"]`
- Date range filter: `date_start=2023-01-01&date_end=2023-12-31`
- Toggle filter: `isActive=true`
## Usage Examples
### Adding the Common Filter Widget
1. Drag the "Common Filter" widget from the component palette
2. Place it at the top of the dashboard
3. Configure filters as needed
### Creating Filters
```typescript
// Add a text filter
const textFilter: Filter = {
id: 'name',
field: 'name',
label: 'Name',
type: 'text'
};
// Add a dropdown filter
const dropdownFilter: Filter = {
id: 'category',
field: 'category',
label: 'Category',
type: 'dropdown',
options: ['A', 'B', 'C']
};
```
### Consuming Filters in Charts
```typescript
// In chart component
constructor(private filterService: FilterService) {}
ngOnInit(): void {
// Subscribe to filter changes
this.subscriptions.push(
this.filterService.filterState$.subscribe(filters => {
this.fetchChartData();
})
);
}
fetchChartData(): void {
// Get current filter values
const filterValues = this.filterService.getFilterValues();
// Include in API call
const queryParams = this.filterService.buildQueryParams();
// Use queryParams in API call
}
```
## JSON Structures
### Filter Definition
```json
{
"id": "filter_123456",
"field": "category",
"label": "Category",
"type": "dropdown",
"options": ["Electronics", "Clothing", "Books"],
"value": "Electronics"
}
```
### Filter State
```json
{
"category": "Electronics",
"price_range": {
"start": 100,
"end": 500
},
"in_stock": true,
"tags": ["sale", "featured"]
}
```
### API Response Format
```json
{
"chartLabels": ["Jan", "Feb", "Mar"],
"chartData": [
{
"data": [10, 20, 30],
"label": "Sales"
}
]
}
```
## Cross-Filtering Support
Charts can also support cross-filtering where clicking on one chart updates the common filters:
1. Implement click handlers in chart components
2. Update filter values through FilterService
3. All other charts automatically refresh
Example:
```typescript
onChartClick(dataPoint: any): void {
// Update a filter based on chart click
this.filterService.updateFilterValue('category', dataPoint.category);
}
```
## Preset Management
Users can save and load filter presets:
1. Configure desired filters
2. Enter a preset name and click "Save Preset"
3. Select preset from dropdown to load
4. Delete presets as needed
## URL Synchronization
Filter states can be synchronized with URL query parameters for shareable dashboards:
1. On filter change, update URL query parameters
2. On page load, read filters from URL
3. Apply filters to charts
## Optional Enhancements
1. **Real-time Updates** - WebSocket integration for live data
2. **Advanced Filtering** - Custom filter expressions
3. **Filter Dependencies** - Conditional filters based on other filter values
4. **Analytics** - Track most used filters and combinations

View File

@@ -0,0 +1,68 @@
.chart-wrapper {
height: 100%;
width: 100%;
display: flex;
flex-direction: column;
box-sizing: border-box;
.chart-header {
padding: 10px 15px;
background: #f8f8f8;
border-bottom: 1px solid #eee;
h5 {
margin: 0;
color: #333;
font-size: 16px;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
}
.chart-container {
flex: 1;
padding: 15px;
overflow: auto;
position: relative;
// Ensure chart containers fill available space
::ng-deep canvas {
max-width: 100%;
max-height: 100%;
}
}
}
// Responsive adjustments
@media (max-width: 768px) {
.chart-wrapper {
.chart-header {
padding: 8px 12px;
h5 {
font-size: 14px;
}
}
.chart-container {
padding: 10px;
}
}
}
@media (max-width: 480px) {
.chart-wrapper {
.chart-header {
padding: 6px 10px;
h5 {
font-size: 13px;
}
}
.chart-container {
padding: 8px;
}
}
}

View File

@@ -0,0 +1,39 @@
import { ComponentFixture, TestBed } from '@angular/core/testing';
import { ChartWrapperComponent } from './chart-wrapper.component';
import { FilterService } from './filter.service';
describe('ChartWrapperComponent', () => {
let component: ChartWrapperComponent;
let fixture: ComponentFixture<ChartWrapperComponent>;
let filterService: FilterService;
beforeEach(async () => {
await TestBed.configureTestingModule({
declarations: [ChartWrapperComponent],
providers: [FilterService]
})
.compileComponents();
fixture = TestBed.createComponent(ChartWrapperComponent);
component = fixture.componentInstance;
filterService = TestBed.inject(FilterService);
fixture.detectChanges();
});
it('should create', () => {
expect(component).toBeTruthy();
});
it('should subscribe to filter changes on init', () => {
spyOn(filterService.filterState$, 'subscribe');
component.ngOnInit();
expect(filterService.filterState$.subscribe).toHaveBeenCalled();
});
it('should unsubscribe from filter changes on destroy', () => {
component.ngOnInit();
spyOn(component['filterSubscription']!, 'unsubscribe');
component.ngOnDestroy();
expect(component['filterSubscription']!.unsubscribe).toHaveBeenCalled();
});
});

View File

@@ -0,0 +1,111 @@
import { Component, Input, OnInit, OnDestroy, ComponentRef, ViewChild, ViewContainerRef, HostListener } from '@angular/core';
import { Subscription } from 'rxjs';
import { FilterService } from './filter.service';
@Component({
selector: 'app-chart-wrapper',
template: `
<div class="chart-wrapper">
<div class="chart-header" *ngIf="chartTitle">
<h5>{{ chartTitle }}</h5>
</div>
<div class="chart-container">
<ng-container #chartContainer></ng-container>
</div>
</div>
`,
styleUrls: ['./chart-wrapper.component.scss']
})
export class ChartWrapperComponent implements OnInit, OnDestroy {
@Input() chartComponent: any;
@Input() chartInputs: any = {};
@Input() chartTitle: string = '';
@ViewChild('chartContainer', { read: ViewContainerRef }) chartContainer!: ViewContainerRef;
private componentRef: ComponentRef<any> | null = null;
private filterSubscription: Subscription | null = null;
constructor(private filterService: FilterService) { }
ngOnInit(): void {
this.loadChartComponent();
this.subscribeToFilters();
}
ngOnDestroy(): void {
if (this.filterSubscription) {
this.filterSubscription.unsubscribe();
}
if (this.componentRef) {
this.componentRef.destroy();
}
}
// Handle window resize events
@HostListener('window:resize', ['$event'])
onResize(event: any) {
// Notify the chart component to resize if it has a resize method
if (this.componentRef && this.componentRef.instance) {
const chartInstance = this.componentRef.instance;
// If it's a chart component with an onResize method, call it
if (chartInstance.onResize && typeof chartInstance.onResize === 'function') {
chartInstance.onResize();
}
// If it's a chart component with a chart property (from BaseChartDirective), resize it
if (chartInstance.chart && typeof chartInstance.chart.resize === 'function') {
setTimeout(() => {
chartInstance.chart.resize();
}, 100);
}
}
}
private loadChartComponent(): void {
if (this.chartContainer && this.chartComponent) {
this.chartContainer.clear();
const factory = this.chartContainer.createComponent(this.chartComponent);
this.componentRef = factory;
// Set initial inputs
Object.keys(this.chartInputs).forEach(key => {
factory.instance[key] = this.chartInputs[key];
});
}
}
private subscribeToFilters(): void {
this.filterSubscription = this.filterService.filterState$.subscribe(filterValues => {
this.updateChartWithFilters(filterValues);
});
}
private updateChartWithFilters(filterValues: any): void {
if (this.componentRef) {
// Add filter values to chart inputs
const updatedInputs = {
...this.chartInputs,
filterValues: filterValues,
// Pass the query params string for easy API integration
filterQueryParams: this.filterService.buildQueryParams()
};
// Update chart component inputs
Object.keys(updatedInputs).forEach(key => {
this.componentRef!.instance[key] = updatedInputs[key];
});
// Trigger change detection if the component has a method for it
if (this.componentRef!.instance.ngOnChanges) {
// We can't easily trigger ngOnChanges manually, but the input update should trigger it
}
// If the chart component has a method to refresh data, call it
if (this.componentRef!.instance.refreshData) {
this.componentRef!.instance.refreshData();
}
}
}
}

View File

@@ -0,0 +1,128 @@
<div class="common-filter-container">
<!-- Filter Header -->
<div class="filter-header">
<h4>Common Filters</h4>
<button class="btn btn-sm btn-primary" (click)="addFilter()">
<clr-icon shape="plus"></clr-icon> Add Filter
</button>
</div>
<!-- Presets Section -->
<div class="presets-section" *ngIf="presets.length > 0">
<div class="preset-controls">
<select [(ngModel)]="activePreset" (change)="loadPreset(activePreset || '')" class="clr-select">
<option value="">Select Preset</option>
<option *ngFor="let preset of presets" [value]="preset">{{ preset }}</option>
</select>
<button class="btn btn-sm btn-danger" (click)="resetFilters()">
<clr-icon shape="undo"></clr-icon> Reset
</button>
</div>
</div>
<!-- Save Preset Section -->
<div class="save-preset-section">
<div class="clr-input-group">
<input type="text" [(ngModel)]="newPresetName" placeholder="Preset name" class="clr-input">
<div class="clr-input-group-btn">
<button class="btn btn-sm btn-success" (click)="savePreset()" [disabled]="!newPresetName.trim()">
<clr-icon shape="floppy"></clr-icon> Save Preset
</button>
</div>
</div>
</div>
<!-- Filters Form -->
<form [formGroup]="filterForm" class="filters-form">
<div class="filters-grid">
<div *ngFor="let filter of filters" class="filter-item">
<div class="filter-header">
<span class="filter-label">{{ filter.label }}</span>
<button class="btn btn-icon btn-danger btn-sm" (click)="removeFilter(filter.id)">
<clr-icon shape="trash"></clr-icon>
</button>
</div>
<!-- Filter Type Selector -->
<div class="clr-form-control">
<select [(ngModel)]="filter.type" (ngModelChange)="updateFilter(filter.id, 'type', $event)"
[ngModelOptions]="{standalone: true}" class="clr-select filter-type-select">
<option value="text">Text</option>
<option value="dropdown">Dropdown</option>
<option value="multiselect">Multi-Select</option>
<option value="date-range">Date Range</option>
<option value="toggle">Toggle</option>
</select>
</div>
<!-- Filter Label Input -->
<div class="clr-form-control">
<input type="text" [(ngModel)]="filter.label" (ngModelChange)="updateFilter(filter.id, 'label', $event)"
[ngModelOptions]="{standalone: true}" placeholder="Filter Label" class="clr-input">
</div>
<!-- Filter Field Input -->
<div class="clr-form-control">
<input type="text" [(ngModel)]="filter.field" (ngModelChange)="updateFilter(filter.id, 'field', $event)"
[ngModelOptions]="{standalone: true}" placeholder="Field Name" class="clr-input">
</div>
<!-- Filter Options (for dropdown and multiselect) -->
<div class="clr-form-control" *ngIf="filter.type === 'dropdown' || filter.type === 'multiselect'">
<textarea [(ngModel)]="filter.options" (ngModelChange)="updateFilter(filter.id, 'options', $event ? $event.split(',') : [])"
[ngModelOptions]="{standalone: true}" placeholder="Options (comma separated)" class="clr-textarea"></textarea>
</div>
<!-- Filter Controls Based on Type -->
<div class="filter-control" [ngSwitch]="filter.type">
<!-- Text Filter -->
<div *ngSwitchCase="'text'">
<input type="text" [formControlName]="filter.id" placeholder="Enter text" class="clr-input">
</div>
<!-- Dropdown Filter -->
<div *ngSwitchCase="'dropdown'">
<select [formControlName]="filter.id" class="clr-select">
<option value="">Select an option</option>
<option *ngFor="let option of filter.options" [value]="option">{{ option }}</option>
</select>
</div>
<!-- Multi-Select Filter -->
<div *ngSwitchCase="'multiselect'">
<select [formControlName]="filter.id" multiple class="clr-select multiselect">
<option *ngFor="let option of filter.options" [value]="option">{{ option }}</option>
</select>
</div>
<!-- Date Range Filter -->
<div *ngSwitchCase="'date-range'" class="date-range-controls">
<div class="clr-form-control">
<label>Start Date</label>
<input type="date" [ngModel]="filterForm.get(filter.id + '.start')?.value"
(ngModelChange)="onDateRangeChange(filter.id, { start: $event, end: filterForm.get(filter.id + '.end')?.value })"
[ngModelOptions]="{standalone: true}" class="clr-input">
</div>
<div class="clr-form-control">
<label>End Date</label>
<input type="date" [ngModel]="filterForm.get(filter.id + '.end')?.value"
(ngModelChange)="onDateRangeChange(filter.id, { start: filterForm.get(filter.id + '.start')?.value, end: $event })"
[ngModelOptions]="{standalone: true}" class="clr-input">
</div>
</div>
<!-- Toggle Filter -->
<div *ngSwitchCase="'toggle'" class="toggle-control">
<input type="checkbox" [formControlName]="filter.id" clrToggle class="clr-toggle">
<label>{{ filter.label }}</label>
</div>
</div>
</div>
</div>
</form>
<!-- No Filters Message -->
<div class="no-filters" *ngIf="filters.length === 0">
<p>No filters added yet. Click "Add Filter" to create your first filter.</p>
</div>
</div>

View File

@@ -0,0 +1,192 @@
.common-filter-container {
padding: 15px;
background: #f5f5f5;
border: 1px solid #ddd;
border-radius: 4px;
margin-bottom: 20px;
height: 100%;
width: 100%;
box-sizing: border-box;
.filter-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 15px;
flex-wrap: wrap;
gap: 10px;
h4 {
margin: 0;
color: #333;
flex: 1;
}
.btn {
white-space: nowrap;
}
}
.presets-section {
margin-bottom: 15px;
.preset-controls {
display: flex;
gap: 10px;
align-items: center;
flex-wrap: wrap;
select {
flex: 1;
min-width: 150px;
}
.btn {
white-space: nowrap;
}
}
}
.save-preset-section {
margin-bottom: 15px;
.clr-input-group {
display: flex;
flex-wrap: wrap;
gap: 10px;
.clr-input {
flex: 1;
min-width: 150px;
}
.clr-input-group-btn {
.btn {
white-space: nowrap;
}
}
}
}
.filters-form {
.filters-grid {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(250px, 1fr));
gap: 15px;
.filter-item {
background: white;
border: 1px solid #e0e0e0;
border-radius: 4px;
padding: 15px;
display: flex;
flex-direction: column;
.filter-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 10px;
.filter-label {
font-weight: bold;
color: #333;
flex: 1;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
}
.filter-type-select {
width: 100%;
margin-bottom: 10px;
}
.clr-form-control {
margin-bottom: 10px;
input, select, textarea {
width: 100%;
min-width: 0; // Allow flexbox to shrink items
}
}
.date-range-controls {
display: flex;
gap: 10px;
flex-direction: column;
.clr-form-control {
flex: 1;
}
@media (min-width: 480px) {
flex-direction: row;
}
}
.toggle-control {
display: flex;
align-items: center;
gap: 8px;
flex-wrap: wrap;
}
.multiselect {
height: 100px;
}
}
}
}
.no-filters {
text-align: center;
padding: 20px;
color: #666;
font-style: italic;
}
}
// Responsive design for smaller screens
@media (max-width: 768px) {
.common-filter-container {
padding: 10px;
.filters-form {
.filters-grid {
grid-template-columns: 1fr;
gap: 10px;
}
}
.filter-header {
flex-direction: column;
align-items: stretch;
}
.presets-section {
.preset-controls {
flex-direction: column;
}
}
.save-preset-section {
.clr-input-group {
flex-direction: column;
}
}
}
}
@media (max-width: 480px) {
.common-filter-container {
.date-range-controls {
flex-direction: column;
}
.filter-item {
padding: 10px;
}
}
}

View File

@@ -0,0 +1,89 @@
import { ComponentFixture, TestBed } from '@angular/core/testing';
import { FormBuilder, FormsModule, ReactiveFormsModule } from '@angular/forms';
import { CommonFilterComponent } from './common-filter.component';
import { FilterService } from './filter.service';
describe('CommonFilterComponent', () => {
let component: CommonFilterComponent;
let fixture: ComponentFixture<CommonFilterComponent>;
let filterService: FilterService;
beforeEach(async () => {
await TestBed.configureTestingModule({
imports: [FormsModule, ReactiveFormsModule],
declarations: [CommonFilterComponent],
providers: [FormBuilder, FilterService]
})
.compileComponents();
fixture = TestBed.createComponent(CommonFilterComponent);
component = fixture.componentInstance;
filterService = TestBed.inject(FilterService);
fixture.detectChanges();
});
it('should create', () => {
expect(component).toBeTruthy();
});
it('should add a new filter', () => {
const initialFilters = filterService.getFilters().length;
component.addFilter();
const updatedFilters = filterService.getFilters().length;
expect(updatedFilters).toBe(initialFilters + 1);
});
it('should remove a filter', () => {
// Add a filter first
component.addFilter();
const filterId = filterService.getFilters()[0].id;
// Then remove it
const initialFilters = filterService.getFilters().length;
component.removeFilter(filterId);
const updatedFilters = filterService.getFilters().length;
expect(updatedFilters).toBe(initialFilters - 1);
});
it('should update filter properties', () => {
// Add a filter
component.addFilter();
const filter = filterService.getFilters()[0];
// Update the filter label
component.updateFilter(filter.id, 'label', 'Updated Label');
const updatedFilters = filterService.getFilters();
expect(updatedFilters[0].label).toBe('Updated Label');
});
it('should handle filter value changes', () => {
const filterId = 'test-filter';
const testValue = 'test value';
// Mock the filter service method
spyOn(filterService, 'updateFilterValue');
component.onFilterChange(filterId, testValue);
expect(filterService.updateFilterValue).toHaveBeenCalledWith(filterId, testValue);
});
it('should reset filters', () => {
spyOn(filterService, 'resetFilters');
component.resetFilters();
expect(filterService.resetFilters).toHaveBeenCalled();
});
it('should save and load presets', () => {
spyOn(filterService, 'savePreset');
spyOn(filterService, 'loadPreset');
// Test save preset
component.newPresetName = 'Test Preset';
component.savePreset();
expect(filterService.savePreset).toHaveBeenCalledWith('Test Preset');
// Test load preset
component.loadPreset('Test Preset');
expect(filterService.loadPreset).toHaveBeenCalledWith('Test Preset');
});
});

View File

@@ -0,0 +1,197 @@
import { Component, OnInit, OnDestroy, Input, HostListener } from '@angular/core';
import { FormBuilder, FormGroup } from '@angular/forms';
import { Subscription } from 'rxjs';
import { Filter, FilterService, FilterType } from './filter.service';
@Component({
selector: 'app-common-filter',
templateUrl: './common-filter.component.html',
styleUrls: ['./common-filter.component.scss']
})
export class CommonFilterComponent implements OnInit, OnDestroy {
@Input() baseFilters: any[] = [];
@Input() drilldownFilters: any[] = [];
@Input() drilldownLayers: any[] = [];
@Input() fieldName: string;
@Input() connection: number;
filters: Filter[] = [];
filterForm: FormGroup;
presets: string[] = [];
activePreset: string | null = null;
newPresetName: string = '';
private subscriptions: Subscription[] = [];
constructor(
private filterService: FilterService,
private fb: FormBuilder
) {
this.filterForm = this.fb.group({});
}
ngOnInit(): void {
// Subscribe to filter definitions
this.subscriptions.push(
this.filterService.filters$.subscribe(filters => {
this.filters = filters;
this.buildForm();
})
);
// Subscribe to filter state changes
this.subscriptions.push(
this.filterService.filterState$.subscribe(state => {
this.updateFormValues(state);
})
);
// Subscribe to preset changes
this.subscriptions.push(
this.filterService.activePreset$.subscribe(preset => {
this.activePreset = preset;
})
);
// Get initial presets
this.presets = this.filterService.getPresets();
}
ngOnDestroy(): void {
this.subscriptions.forEach(sub => sub.unsubscribe());
}
// Handle window resize events
@HostListener('window:resize', ['$event'])
onResize(event: any) {
// Trigger change detection to reflow the layout
setTimeout(() => {
// This will cause the grid to recalculate its layout
this.filters = [...this.filters];
}, 100);
}
// Build the form based on current filters
private buildForm(): void {
// Clear existing form controls
Object.keys(this.filterForm.controls).forEach(key => {
this.filterForm.removeControl(key);
});
// Add controls for each filter
this.filters.forEach(filter => {
let initialValue: any;
switch (filter.type) {
case 'multiselect':
initialValue = filter.value || [];
break;
case 'date-range':
initialValue = filter.value || { start: null, end: null };
break;
case 'toggle':
initialValue = filter.value || false;
break;
default:
initialValue = filter.value || '';
}
const control = this.fb.control(initialValue);
// Subscribe to value changes for this control
control.valueChanges.subscribe(value => {
this.onFilterChange(filter.id, value);
});
this.filterForm.addControl(filter.id, control);
});
}
// Update form values based on filter state
private updateFormValues(state: any): void {
Object.keys(state).forEach(key => {
if (this.filterForm.contains(key)) {
this.filterForm.get(key)?.setValue(state[key], { emitEvent: false });
}
});
}
// Handle filter value changes
onFilterChange(filterId: string, value: any): void {
console.log('=== COMMON FILTER DEBUG INFO ===');
console.log('Filter value changed for ID:', filterId);
console.log('New value:', value);
const filterDef = this.filters.find(f => f.id === filterId);
console.log('Filter definition:', filterDef);
this.filterService.updateFilterValue(filterId, value);
console.log('=== END COMMON FILTER DEBUG ===');
}
// Handle multiselect changes
onMultiselectChange(filterId: string, selectedValues: string[]): void {
this.filterService.updateFilterValue(filterId, selectedValues);
}
// Handle date range changes
onDateRangeChange(filterId: string, dateRange: { start: string | null, end: string | null }): void {
this.filterService.updateFilterValue(filterId, dateRange);
}
// Handle toggle changes
onToggleChange(filterId: string, checked: boolean): void {
this.filterService.updateFilterValue(filterId, checked);
}
// Reset all filters
resetFilters(): void {
this.filterService.resetFilters();
}
// Save current filter state as preset
savePreset(): void {
if (this.newPresetName.trim()) {
this.filterService.savePreset(this.newPresetName.trim());
this.presets = this.filterService.getPresets();
this.newPresetName = '';
}
}
// Load a preset
loadPreset(presetName: string): void {
this.filterService.loadPreset(presetName);
}
// Delete a preset
deletePreset(presetName: string): void {
this.filterService.deletePreset(presetName);
this.presets = this.filterService.getPresets();
}
// Add a new filter
addFilter(): void {
const newFilter: Filter = {
id: `filter_${Date.now()}`,
field: '',
label: 'New Filter',
type: 'text'
};
this.filterService.addFilter(newFilter);
}
// Remove a filter
removeFilter(filterId: string): void {
this.filterService.removeFilter(filterId);
}
// Update filter properties
updateFilter(filterId: string, property: string, value: any): void {
const filterIndex = this.filters.findIndex(f => f.id === filterId);
if (filterIndex !== -1) {
const updatedFilters = [...this.filters];
(updatedFilters[filterIndex] as any)[property] = value;
this.filterService.setFilters(updatedFilters);
}
}
}

View File

@@ -0,0 +1,113 @@
<!-- Configuration Mode -->
<div class="compact-filter-config" *ngIf="isConfigMode">
<div class="config-header">
<h5>Compact Filter Configuration</h5>
<button class="btn btn-sm btn-link" (click)="cancelConfiguration()">
<clr-icon shape="close"></clr-icon>
</button>
</div>
<div class="config-form">
<div class="clr-form-control">
<label class="clr-control-label">API URL</label>
<input type="text" [(ngModel)]="configApiUrl" (ngModelChange)="onApiUrlChange($event)" placeholder="Enter API URL" class="clr-input">
</div>
<div class="clr-form-control">
<label class="clr-control-label">Filter Key</label>
<select [(ngModel)]="configFilterKey" (ngModelChange)="onFilterKeyChange($event)" class="clr-select">
<option value="">Select a key</option>
<option *ngFor="let key of availableKeys" [value]="key">{{ key }}</option>
</select>
</div>
<div class="clr-form-control">
<label class="clr-control-label">Filter Type</label>
<select [(ngModel)]="configFilterType" (ngModelChange)="onFilterTypeChange($event)" class="clr-select">
<option value="text">Text</option>
<option value="dropdown">Dropdown</option>
<option value="multiselect">Multi-Select</option>
<option value="date-range">Date Range</option>
<option value="toggle">Toggle</option>
</select>
</div>
<!-- Options will be automatically populated for dropdown/multiselect based on API data -->
<div class="clr-form-control" *ngIf="configFilterType === 'dropdown' || configFilterType === 'multiselect'">
<label class="clr-control-label">Available Values (comma separated)</label>
<div class="available-values">
{{ availableValues.join(', ') }}
</div>
</div>
<div class="config-actions">
<button class="btn btn-sm btn-outline" (click)="cancelConfiguration()">Cancel</button>
<button class="btn btn-sm btn-primary" (click)="saveConfiguration()">Save</button>
</div>
</div>
</div>
<!-- Display Mode -->
<div class="compact-filter" *ngIf="!isConfigMode">
<div class="filter-header">
<span class="filter-label" *ngIf="filterLabel">{{ filterLabel }}</span>
<span class="filter-key" *ngIf="!filterLabel && filterKey">{{ filterKey }}</span>
<span class="filter-type">({{ filterType }})</span>
<button class="btn btn-icon btn-sm" (click)="toggleConfigMode()">
<clr-icon shape="cog"></clr-icon>
</button>
</div>
<!-- Text Filter -->
<div class="filter-control" *ngIf="filterType === 'text'">
<input type="text"
[(ngModel)]="filterValue"
(ngModelChange)="onFilterValueChange($event)"
[placeholder]="filterLabel || filterKey"
class="clr-input compact-input">
</div>
<!-- Dropdown Filter -->
<div class="filter-control" *ngIf="filterType === 'dropdown'">
<select [(ngModel)]="filterValue"
(ngModelChange)="onFilterValueChange($event)"
class="clr-select compact-select">
<option value="">{{ filterLabel || filterKey }}</option>
<option *ngFor="let option of filterOptions" [value]="option">{{ option }}</option>
</select>
</div>
<!-- Multi-Select Filter -->
<div class="filter-control" *ngIf="filterType === 'multiselect'">
<select [(ngModel)]="filterValue"
(ngModelChange)="onFilterValueChange($event)"
multiple
class="clr-select compact-multiselect">
<option *ngFor="let option of filterOptions" [value]="option">{{ option }}</option>
</select>
</div>
<!-- Date Range Filter -->
<div class="filter-control date-range" *ngIf="filterType === 'date-range'">
<input type="date"
[(ngModel)]="filterValue.start"
(ngModelChange)="onDateRangeChange({ start: $event, end: filterValue.end })"
placeholder="Start Date"
class="clr-input compact-date">
<input type="date"
[(ngModel)]="filterValue.end"
(ngModelChange)="onDateRangeChange({ start: filterValue.start, end: $event })"
placeholder="End Date"
class="clr-input compact-date">
</div>
<!-- Toggle Filter -->
<div class="filter-control toggle" *ngIf="filterType === 'toggle'">
<input type="checkbox"
[(ngModel)]="filterValue"
(ngModelChange)="onToggleChange($event)"
clrToggle
class="clr-toggle">
<label class="toggle-label">{{ filterLabel || filterKey }}</label>
</div>
</div>

View File

@@ -0,0 +1,224 @@
.compact-filter {
display: inline-block;
min-width: 120px;
max-width: 200px;
margin: 5px;
padding: 5px;
background: #f8f9fa;
border: 1px solid #e9ecef;
border-radius: 4px;
font-size: 12px;
.filter-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 5px;
.filter-label, .filter-key {
font-weight: bold;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
flex-grow: 1;
}
.filter-type {
font-size: 10px;
color: #6c757d;
margin-left: 5px;
}
.btn-icon {
padding: 2px;
min-width: auto;
}
}
.filter-control {
display: flex;
align-items: center;
gap: 5px;
&.date-range {
flex-direction: column;
}
&.toggle {
justify-content: center;
}
}
.compact-input,
.compact-select,
.compact-multiselect,
.compact-date {
width: 100%;
padding: 4px 6px;
font-size: 12px;
border-radius: 3px;
min-height: 24px;
}
.compact-select,
.compact-multiselect {
height: 24px;
}
.compact-multiselect {
height: auto;
min-height: 24px;
}
.multiselect-container {
max-height: 150px;
overflow-y: auto;
border: 1px solid #ddd;
border-radius: 4px;
padding: 5px;
background: white;
}
.checkbox-group {
display: flex;
flex-direction: column;
gap: 3px;
}
.checkbox-item {
display: flex;
align-items: center;
gap: 5px;
}
.checkbox-label {
font-size: 12px;
margin: 0;
cursor: pointer;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
.clr-checkbox {
margin: 0;
cursor: pointer;
}
.toggle-label {
margin: 0;
font-size: 12px;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
max-width: 100px;
}
.clr-toggle {
margin: 0 5px 0 0;
}
/* Responsive design */
@media (max-width: 768px) {
min-width: 100px;
max-width: 150px;
.compact-input,
.compact-select,
.compact-multiselect,
.compact-date {
font-size: 11px;
padding: 3px 5px;
}
.toggle-label {
font-size: 11px;
max-width: 80px;
}
}
@media (max-width: 480px) {
min-width: 80px;
max-width: 120px;
.compact-input,
.compact-select,
.compact-multiselect,
.compact-date {
font-size: 10px;
padding: 2px 4px;
}
.toggle-label {
font-size: 10px;
max-width: 60px;
}
}
}
.compact-filter-config {
min-width: 200px;
max-width: 300px;
padding: 10px;
background: #f8f9fa;
border: 1px solid #e9ecef;
border-radius: 4px;
.config-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 10px;
h5 {
margin: 0;
}
.btn-link {
padding: 2px;
min-width: auto;
}
}
.config-form {
.clr-form-control {
margin-bottom: 8px;
.clr-control-label {
font-size: 12px;
}
.clr-input,
.clr-select,
.clr-textarea {
font-size: 12px;
padding: 4px 6px;
}
.clr-textarea {
min-height: 60px;
}
.available-values {
background: #e9ecef;
border-radius: 3px;
padding: 6px;
font-size: 11px;
margin-top: 4px;
word-break: break-all;
}
}
.config-actions {
display: flex;
justify-content: flex-end;
gap: 5px;
margin-top: 10px;
.btn {
font-size: 12px;
padding: 4px 8px;
}
}
}
}

View File

@@ -0,0 +1,334 @@
import { Component, OnInit, Input, Output, EventEmitter, OnChanges, SimpleChanges } from '@angular/core';
import { FilterService, Filter } from './filter.service';
import { AlertsService } from 'src/app/services/fnd/alerts.service';
@Component({
selector: 'app-compact-filter',
templateUrl: './compact-filter.component.html',
styleUrls: ['./compact-filter.component.scss']
})
export class CompactFilterComponent implements OnInit, OnChanges {
@Input() filterKey: string = '';
@Input() filterType: string = 'text';
@Input() filterOptions: string[] = [];
@Input() filterLabel: string = '';
@Input() apiUrl: string = '';
@Input() connectionId: number | undefined;
@Output() filterChange = new EventEmitter<any>();
@Output() configChange = new EventEmitter<any>();
selectedFilter: Filter | null = null;
filterValue: any = '';
availableFilters: Filter[] = [];
availableKeys: string[] = [];
availableValues: string[] = [];
// Configuration properties
isConfigMode: boolean = false;
configFilterKey: string = '';
configFilterType: string = 'text';
configFilterOptions: string = '';
configFilterLabel: string = '';
configApiUrl: string = '';
configConnectionId: number | undefined;
constructor(
private filterService: FilterService,
private alertService: AlertsService
) { }
ngOnInit(): void {
// Initialize configuration from inputs
this.configFilterKey = this.filterKey;
this.configFilterType = this.filterType;
this.configFilterLabel = this.filterLabel;
this.configFilterOptions = this.filterOptions.join(',');
this.configApiUrl = this.apiUrl;
this.configConnectionId = this.connectionId;
// Load available keys and values if API URL and filter key are provided
if (this.apiUrl) {
this.loadAvailableKeys();
// Load available values for the current filter key if it's a dropdown or multiselect
if ((this.filterType === 'dropdown' || this.filterType === 'multiselect') && this.filterKey) {
this.loadAvailableValues(this.filterKey);
}
}
// Register this filter with the filter service
this.registerFilter();
// Subscribe to filter definitions to get available filters
this.filterService.filters$.subscribe(filters => {
this.availableFilters = filters;
this.updateSelectedFilter();
});
// Subscribe to filter state changes
this.filterService.filterState$.subscribe(state => {
if (this.selectedFilter && state.hasOwnProperty(this.selectedFilter.id)) {
this.filterValue = state[this.selectedFilter.id];
}
});
}
ngOnChanges(changes: SimpleChanges): void {
// If filterKey or filterType changes, re-register the filter
if (changes.filterKey || changes.filterType) {
// Load available values for the current filter key if it's a dropdown or multiselect
if ((this.filterType === 'dropdown' || this.filterType === 'multiselect') && this.filterKey) {
this.loadAvailableValues(this.filterKey);
}
this.registerFilter();
}
// Handle API URL changes
if (changes.apiUrl && !changes.apiUrl.firstChange) {
if (this.apiUrl) {
this.loadAvailableKeys();
}
}
}
// Register this filter with the filter service
registerFilter(): void {
if (this.filterKey) {
// Get current filter values from the service
const currentFilterValues = this.filterService.getFilterValues();
// Create a filter definition for this compact filter
const filterDef: Filter = {
id: `${this.filterKey}`,
field: this.filterKey,
label: this.filterLabel || this.filterKey,
type: this.filterType as any,
options: this.filterOptions,
value: this.filterValue // Use the current filter value
};
// Get current filters
const currentFilters = this.filterService.getFilters();
// Check if this filter is already registered
const existingFilterIndex = currentFilters.findIndex(f => f.id === filterDef.id);
if (existingFilterIndex >= 0) {
// Preserve the existing filter configuration
const existingFilter = currentFilters[existingFilterIndex];
// Preserve the existing filter value if it exists in the service
if (currentFilterValues.hasOwnProperty(existingFilter.id)) {
filterDef.value = currentFilterValues[existingFilter.id];
this.filterValue = filterDef.value; // Update local value
} else if (existingFilter.value !== undefined) {
// Fallback to existing filter's value if no service value
filterDef.value = existingFilter.value;
this.filterValue = filterDef.value;
}
// Preserve other configuration properties
filterDef.label = existingFilter.label;
filterDef.options = existingFilter.options || this.filterOptions;
// Update existing filter
currentFilters[existingFilterIndex] = filterDef;
} else {
// For new filters, check if there's already a value in the service
if (currentFilterValues.hasOwnProperty(filterDef.id)) {
filterDef.value = currentFilterValues[filterDef.id];
this.filterValue = filterDef.value; // Update local value
}
// Add new filter
currentFilters.push(filterDef);
}
// Update the filter service with the new filter list
this.filterService.setFilters(currentFilters);
// Update the selected filter reference
this.selectedFilter = filterDef;
}
}
updateSelectedFilter(): void {
if (this.filterKey && this.availableFilters.length > 0) {
this.selectedFilter = this.availableFilters.find(f => f.field === this.filterKey) || null;
if (this.selectedFilter) {
// Get current value for this filter from the service
const currentState = this.filterService.getFilterValues();
const filterValue = currentState[this.selectedFilter.id];
if (filterValue !== undefined) {
this.filterValue = filterValue;
} else if (this.selectedFilter.value !== undefined) {
// Use the filter's default value if no service value
this.filterValue = this.selectedFilter.value;
} else {
// Use the current filter value as fallback
this.filterValue = this.filterValue || '';
}
// Also update configuration properties from the selected filter
this.configFilterKey = this.selectedFilter.field;
this.configFilterType = this.selectedFilter.type;
this.configFilterLabel = this.selectedFilter.label;
this.configFilterOptions = (this.selectedFilter.options || []).join(',');
}
}
}
onFilterValueChange(value: any): void {
if (this.selectedFilter) {
this.filterValue = value;
this.filterService.updateFilterValue(this.selectedFilter.id, value);
this.filterChange.emit({ filterId: this.selectedFilter.id, value: value });
// Update the filter definition in the service to reflect the new value
const currentFilters = this.filterService.getFilters();
const filterIndex = currentFilters.findIndex(f => f.id === this.selectedFilter.id);
if (filterIndex >= 0) {
currentFilters[filterIndex].value = value;
this.filterService.setFilters(currentFilters);
}
}
}
onToggleChange(checked: boolean): void {
this.onFilterValueChange(checked);
}
onDateRangeChange(dateRange: { start: string | null, end: string | null }): void {
this.onFilterValueChange(dateRange);
}
// Load available keys from API
loadAvailableKeys(): void {
if (this.apiUrl) {
this.alertService.getColumnfromurl(this.apiUrl, this.connectionId).subscribe(
(keys: string[]) => {
this.availableKeys = keys;
},
(error) => {
console.error('Error loading available keys:', error);
this.availableKeys = [];
}
);
}
}
// Load available values for a specific key
loadAvailableValues(key: string): void {
if (this.apiUrl && key) {
this.alertService.getValuesFromUrl(this.apiUrl, this.connectionId, key).subscribe(
(values: string[]) => {
this.availableValues = values;
// Update filter options if this is a dropdown or multiselect
if (this.filterType === 'dropdown' || this.filterType === 'multiselect') {
this.filterOptions = values;
}
},
(error) => {
console.error('Error loading available values:', error);
this.availableValues = [];
}
);
}
}
// Configuration methods
toggleConfigMode(): void {
this.isConfigMode = !this.isConfigMode;
if (this.isConfigMode) {
// Initialize config values from current filter if available
if (this.selectedFilter) {
this.configFilterKey = this.selectedFilter.field;
this.configFilterType = this.selectedFilter.type;
this.configFilterLabel = this.selectedFilter.label;
this.configFilterOptions = (this.selectedFilter.options || []).join(',');
} else {
// Fallback to current properties
this.configFilterKey = this.filterKey;
this.configFilterType = this.filterType;
this.configFilterLabel = this.filterLabel;
this.configFilterOptions = this.filterOptions.join(',');
}
this.configApiUrl = this.apiUrl;
this.configConnectionId = this.connectionId;
}
}
saveConfiguration(): void {
const config = {
filterKey: this.configFilterKey,
filterType: this.configFilterType,
filterLabel: this.configFilterLabel,
filterOptions: this.configFilterOptions.split(',').map(opt => opt.trim()).filter(opt => opt),
apiUrl: this.configApiUrl,
connectionId: this.configConnectionId
};
// Emit configuration change
this.configChange.emit(config);
// Update local properties
this.filterKey = config.filterKey;
this.filterType = config.filterType;
this.filterLabel = config.filterLabel;
this.filterOptions = config.filterOptions;
this.apiUrl = config.apiUrl;
this.connectionId = config.connectionId;
// Load available keys if API URL is provided
if (this.apiUrl) {
this.loadAvailableKeys();
}
// Load available values for the selected key if it's a dropdown or multiselect
if ((this.configFilterType === 'dropdown' || this.configFilterType === 'multiselect') && this.configFilterKey) {
this.loadAvailableValues(this.configFilterKey);
}
// Register the updated filter with the filter service
this.registerFilter();
// Update selected filter
this.updateSelectedFilter();
// Exit config mode
this.isConfigMode = false;
}
cancelConfiguration(): void {
this.isConfigMode = false;
}
// Handle filter key change in configuration
onFilterKeyChange(key: string): void {
this.configFilterKey = key;
// Load available values for the selected key if it's a dropdown or multiselect
if ((this.configFilterType === 'dropdown' || this.configFilterType === 'multiselect') && key) {
this.loadAvailableValues(key);
}
}
// Handle API URL change in configuration
onApiUrlChange(url: string): void {
this.configApiUrl = url;
// Load available keys when API URL changes
if (url) {
this.loadAvailableKeys();
// Also clear available values since the API has changed
this.availableValues = [];
this.filterOptions = [];
}
}
// Handle filter type change in configuration
onFilterTypeChange(type: string): void {
this.configFilterType = type;
// If changing to dropdown or multiselect and we have a key selected, load values
if ((type === 'dropdown' || type === 'multiselect') && this.configFilterKey) {
this.loadAvailableValues(this.configFilterKey);
}
}
}

View File

@@ -0,0 +1,129 @@
import { TestBed } from '@angular/core/testing';
import { FilterService, Filter } from './filter.service';
describe('FilterService', () => {
let service: FilterService;
beforeEach(() => {
TestBed.configureTestingModule({});
service = TestBed.inject(FilterService);
});
it('should be created', () => {
expect(service).toBeTruthy();
});
it('should add and remove filters', () => {
const filter: Filter = {
id: 'test-filter',
field: 'testField',
label: 'Test Field',
type: 'text'
};
// Add filter
service.addFilter(filter);
const filters = service.getFilters();
expect(filters.length).toBe(1);
expect(filters[0]).toEqual(filter);
// Remove filter
service.removeFilter('test-filter');
const updatedFilters = service.getFilters();
expect(updatedFilters.length).toBe(0);
});
it('should update filter values', () => {
const filter: Filter = {
id: 'name-filter',
field: 'name',
label: 'Name',
type: 'text'
};
service.addFilter(filter);
service.updateFilterValue('name-filter', 'John Doe');
const filterValues = service.getFilterValues();
expect(filterValues['name-filter']).toBe('John Doe');
});
it('should reset filters', () => {
const textFilter: Filter = {
id: 'name',
field: 'name',
label: 'Name',
type: 'text',
value: 'John'
};
const toggleFilter: Filter = {
id: 'active',
field: 'isActive',
label: 'Active',
type: 'toggle',
value: true
};
service.setFilters([textFilter, toggleFilter]);
service.resetFilters();
const filterValues = service.getFilterValues();
expect(filterValues['name']).toBe('');
expect(filterValues['active']).toBe(false);
});
it('should manage presets', () => {
const filter: Filter = {
id: 'category',
field: 'category',
label: 'Category',
type: 'dropdown',
value: 'Electronics'
};
service.addFilter(filter);
// Save preset
service.savePreset('Electronics View');
const presets = service.getPresets();
expect(presets).toContain('Electronics View');
// Update filter and load preset
service.updateFilterValue('category', 'Clothing');
service.loadPreset('Electronics View');
const filterValues = service.getFilterValues();
expect(filterValues['category']).toBe('Electronics');
// Delete preset
service.deletePreset('Electronics View');
const updatedPresets = service.getPresets();
expect(updatedPresets).not.toContain('Electronics View');
});
it('should build query parameters', () => {
const textFilter: Filter = {
id: 'name',
field: 'name',
label: 'Name',
type: 'text'
};
const dateFilter: Filter = {
id: 'dateRange',
field: 'date',
label: 'Date Range',
type: 'date-range'
};
service.setFilters([textFilter, dateFilter]);
service.updateFilterValue('name', 'John Doe');
service.updateFilterValue('dateRange', { start: '2023-01-01', end: '2023-12-31' });
const queryParams = service.buildQueryParams();
expect(queryParams).toContain('name=John%20Doe');
expect(queryParams).toContain('dateRange_start=2023-01-01');
expect(queryParams).toContain('dateRange_end=2023-12-31');
});
});

View File

@@ -0,0 +1,199 @@
import { Injectable } from '@angular/core';
import { BehaviorSubject, Observable } from 'rxjs';
// Define the filter types
export type FilterType = 'dropdown' | 'multiselect' | 'date-range' | 'text' | 'toggle';
// Define the filter interface
export interface Filter {
id: string;
field: string;
label: string;
type: FilterType;
options?: string[]; // For dropdown and multiselect
value?: any; // Current value
placeholder?: string;
}
// Define the filter state
export interface FilterState {
[key: string]: any;
}
@Injectable({
providedIn: 'root'
})
export class FilterService {
// Store the filter definitions
private filtersSubject = new BehaviorSubject<Filter[]>([]);
public filters$ = this.filtersSubject.asObservable();
// Store the current filter values
private filterStateSubject = new BehaviorSubject<FilterState>({});
public filterState$ = this.filterStateSubject.asObservable();
// Store the active filter presets
private activePresetSubject = new BehaviorSubject<string | null>(null);
public activePreset$ = this.activePresetSubject.asObservable();
// Store filter presets
private presets: { [key: string]: FilterState } = {};
constructor() { }
// Add a new filter
addFilter(filter: Filter): void {
const currentFilters = this.filtersSubject.value;
this.filtersSubject.next([...currentFilters, filter]);
}
// Remove a filter
removeFilter(filterId: string): void {
const currentFilters = this.filtersSubject.value;
const updatedFilters = currentFilters.filter(f => f.id !== filterId);
this.filtersSubject.next(updatedFilters);
// Also remove the filter value from state
const currentState = this.filterStateSubject.value;
const newState = { ...currentState };
delete newState[filterId];
this.filterStateSubject.next(newState);
}
// Update filter value
updateFilterValue(filterId: string, value: any): void {
console.log('=== FILTER SERVICE DEBUG INFO ===');
console.log('Updating filter value for ID:', filterId);
console.log('New value:', value);
const currentState = this.filterStateSubject.value;
const newState = {
...currentState,
[filterId]: value
};
console.log('New filter state:', newState);
this.filterStateSubject.next(newState);
console.log('=== END FILTER SERVICE DEBUG ===');
}
// Get current filter values
getFilterValues(): FilterState {
return this.filterStateSubject.value;
}
// Reset all filters
resetFilters(): void {
const currentFilters = this.filtersSubject.value;
const resetState: FilterState = {};
// Initialize all filters with empty/default values
currentFilters.forEach(filter => {
switch (filter.type) {
case 'multiselect':
resetState[filter.id] = [];
break;
case 'date-range':
resetState[filter.id] = { start: null, end: null };
break;
case 'toggle':
resetState[filter.id] = false;
break;
default:
resetState[filter.id] = '';
}
});
this.filterStateSubject.next(resetState);
}
// Save current filter state as a preset
savePreset(name: string): void {
this.presets[name] = this.filterStateSubject.value;
}
// Load a preset
loadPreset(name: string): void {
if (this.presets[name]) {
this.filterStateSubject.next(this.presets[name]);
this.activePresetSubject.next(name);
}
}
// Get all presets
getPresets(): string[] {
return Object.keys(this.presets);
}
// Delete a preset
deletePreset(name: string): void {
delete this.presets[name];
if (this.activePresetSubject.value === name) {
this.activePresetSubject.next(null);
}
}
// Clear all presets
clearPresets(): void {
this.presets = {};
this.activePresetSubject.next(null);
}
// Build query parameters for API calls
buildQueryParams(): string {
const filterValues = this.getFilterValues();
const params = new URLSearchParams();
Object.keys(filterValues).forEach(key => {
const value = filterValues[key];
if (value !== undefined && value !== null && value !== '') {
if (typeof value === 'object') {
// Handle date ranges and other objects
if (value.hasOwnProperty('start') && value.hasOwnProperty('end')) {
// Date range
if (value.start) params.append(`${key}_start`, value.start);
if (value.end) params.append(`${key}_end`, value.end);
} else {
// Other objects as JSON
params.append(key, JSON.stringify(value));
}
} else {
// Simple values
params.append(key, value.toString());
}
}
});
return params.toString();
}
// Get filter definitions
getFilters(): Filter[] {
return this.filtersSubject.value;
}
// Update filter definitions
setFilters(filters: Filter[]): void {
this.filtersSubject.next(filters);
// Initialize filter state with default values
const initialState: FilterState = {};
filters.forEach(filter => {
switch (filter.type) {
case 'multiselect':
initialState[filter.id] = filter.value || [];
break;
case 'date-range':
initialState[filter.id] = filter.value || { start: null, end: null };
break;
case 'toggle':
initialState[filter.id] = filter.value || false;
break;
default:
initialState[filter.id] = filter.value || '';
}
});
this.filterStateSubject.next(initialState);
}
}

View File

@@ -0,0 +1,4 @@
export * from './filter.service';
export * from './common-filter.component';
export * from './chart-wrapper.component';
export * from './compact-filter.component';

View File

@@ -22,6 +22,10 @@ import { AlertsService } from 'src/app/services/fnd/alerts.service';
import { isArray } from 'highcharts'; import { isArray } from 'highcharts';
// Add the SureconnectService import // Add the SureconnectService import
import { SureconnectService } from '../sureconnect/sureconnect.service'; import { SureconnectService } from '../sureconnect/sureconnect.service';
// Add the CommonFilterComponent import
import { CommonFilterComponent } from '../common-filter/common-filter.component';
// Add the CompactFilterComponent import
import { CompactFilterComponent } from '../common-filter';
function isNullArray(arr) { function isNullArray(arr) {
return !Array.isArray(arr) || arr.length === 0; return !Array.isArray(arr) || arr.length === 0;
@@ -45,6 +49,10 @@ export class EditnewdashComponent implements OnInit {
public commonFilterForm: FormGroup; // Add common filter form public commonFilterForm: FormGroup; // Add common filter form
WidgetsMock: WidgetModel[] = [ WidgetsMock: WidgetModel[] = [
{
name: 'Common Filter',
identifier: 'common_filter'
},
{ {
name: 'Radar Chart', name: 'Radar Chart',
identifier: 'radar_chart' identifier: 'radar_chart'
@@ -92,6 +100,10 @@ export class EditnewdashComponent implements OnInit {
{ {
name: 'Grid View', name: 'Grid View',
identifier: 'grid_view' identifier: 'grid_view'
},
{
name: 'Compact Filter',
identifier: 'compact_filter'
} }
] ]
@@ -104,6 +116,7 @@ export class EditnewdashComponent implements OnInit {
public dashArr: []; public dashArr: [];
protected componentCollection = [ protected componentCollection = [
{ name: "Common Filter", componentInstance: CommonFilterComponent },
{ name: "Line Chart", componentInstance: LineChartComponent }, { name: "Line Chart", componentInstance: LineChartComponent },
{ name: "Doughnut Chart", componentInstance: DoughnutChartComponent }, { name: "Doughnut Chart", componentInstance: DoughnutChartComponent },
{ name: "Radar Chart", componentInstance: RadarChartComponent }, { name: "Radar Chart", componentInstance: RadarChartComponent },
@@ -116,6 +129,7 @@ export class EditnewdashComponent implements OnInit {
{ name: "Financial Chart", componentInstance: FinancialChartComponent }, { name: "Financial Chart", componentInstance: FinancialChartComponent },
{ name: "To Do Chart", componentInstance: ToDoChartComponent }, { name: "To Do Chart", componentInstance: ToDoChartComponent },
{ name: "Grid View", componentInstance: GridViewComponent }, { name: "Grid View", componentInstance: GridViewComponent },
{ name: "Compact Filter", componentInstance: CompactFilterComponent }, // Add this line
]; ];
model: any; model: any;
linesdata: any; linesdata: any;
@@ -161,7 +175,12 @@ export class EditnewdashComponent implements OnInit {
drilldownLayers: [] as any[], drilldownLayers: [] as any[],
// Common filter properties // Common filter properties
commonFilterEnabled: false, commonFilterEnabled: false,
commonFilterEnabledDrilldown: false commonFilterEnabledDrilldown: false,
// Compact filter properties
filterKey: '',
filterType: 'text',
filterLabel: '',
filterOptions: [] as string[]
}; };
// Add sureconnect data property // Add sureconnect data property
@@ -201,7 +220,9 @@ export class EditnewdashComponent implements OnInit {
}, },
displayGrid: "always", displayGrid: "always",
minCols: 10, minCols: 10,
minRows: 10 minRows: 10,
// Add resize callback to handle chart resizing
itemResizeCallback: this.itemResize.bind(this)
}; };
this.editId = this.route.snapshot.params.id; this.editId = this.route.snapshot.params.id;
@@ -500,6 +521,31 @@ export class EditnewdashComponent implements OnInit {
component: ToDoChartComponent, component: ToDoChartComponent,
name: "To Do Chart" name: "To Do Chart"
}); });
case "common_filter":
return this.dashboardArray.push({
cols: 10,
rows: 3,
x: 0,
y: 0,
chartid: maxChartId + 1,
component: CommonFilterComponent,
name: "Common Filter"
});
case "compact_filter":
return this.dashboardArray.push({
cols: 3,
rows: 2,
x: 0,
y: 0,
chartid: maxChartId + 1,
component: CompactFilterComponent,
name: "Compact Filter",
// Add default configuration for compact filter
filterKey: '',
filterType: 'text',
filterLabel: '',
filterOptions: []
});
case "grid_view": case "grid_view":
return this.dashboardArray.push({ return this.dashboardArray.push({
cols: 5, cols: 5,
@@ -542,6 +588,19 @@ export class EditnewdashComponent implements OnInit {
if (item['commonFilterEnabledDrilldown'] === undefined) { if (item['commonFilterEnabledDrilldown'] === undefined) {
this.gadgetsEditdata['commonFilterEnabledDrilldown'] = false; this.gadgetsEditdata['commonFilterEnabledDrilldown'] = false;
} }
// Initialize compact filter properties if not present
if (item['filterKey'] === undefined) {
this.gadgetsEditdata['filterKey'] = '';
}
if (item['filterType'] === undefined) {
this.gadgetsEditdata['filterType'] = 'text';
}
if (item['filterLabel'] === undefined) {
this.gadgetsEditdata['filterLabel'] = '';
}
if (item['filterOptions'] === undefined) {
this.gadgetsEditdata['filterOptions'] = [];
}
this.getStores(); this.getStores();
// Set default connection if none is set and we have connections // Set default connection if none is set and we have connections
@@ -717,6 +776,16 @@ export class EditnewdashComponent implements OnInit {
xyz.drilldownLayers = this.gadgetsEditdata.drilldownLayers; xyz.drilldownLayers = this.gadgetsEditdata.drilldownLayers;
xyz.commonFilterEnabled = this.gadgetsEditdata.commonFilterEnabled; // Add common filter property xyz.commonFilterEnabled = this.gadgetsEditdata.commonFilterEnabled; // Add common filter property
// For compact filter, preserve filter configuration properties
if (item.component && item.component.name === 'CompactFilterComponent') {
xyz.filterKey = this.gadgetsEditdata.filterKey || '';
xyz.filterType = this.gadgetsEditdata.filterType || 'text';
xyz.filterLabel = this.gadgetsEditdata.filterLabel || '';
xyz.filterOptions = this.gadgetsEditdata.filterOptions || [];
xyz.table = this.gadgetsEditdata.table || '';
xyz.connection = this.gadgetsEditdata.connection || undefined;
}
console.log(xyz); console.log(xyz);
return xyz; return xyz;
} }
@@ -751,7 +820,97 @@ export class EditnewdashComponent implements OnInit {
* This prevents errors when trying to set properties that don't exist on the components * This prevents errors when trying to set properties that don't exist on the components
*/ */
getChartInputs(item: any): any { getChartInputs(item: any): any {
// Only pass properties that are relevant to chart components // For CompactFilterComponent, pass only filter configuration properties
if (item.component && item.component.name === 'CompactFilterComponent') {
const filterInputs = {
filterKey: item['filterKey'] || '',
filterType: item['filterType'] || 'text',
filterLabel: item['filterLabel'] || '',
filterOptions: item['filterOptions'] || [],
apiUrl: item['table'] || '', // Use table as API URL
connectionId: item['connection'] ? parseInt(item['connection'], 10) : undefined
};
// Preserve configuration in the item itself
item['filterKey'] = filterInputs['filterKey'];
item['filterType'] = filterInputs['filterType'];
item['filterLabel'] = filterInputs['filterLabel'];
item['filterOptions'] = filterInputs['filterOptions'];
item['table'] = filterInputs['apiUrl'];
item['connection'] = item['connection'];
// Remove undefined properties to avoid passing unnecessary data
Object.keys(filterInputs).forEach(key => {
if (filterInputs[key] === undefined) {
delete filterInputs[key];
}
});
return filterInputs;
}
// For CommonFilterComponent, pass only filter-related properties
if (item.component && item.component.name === 'CommonFilterComponent') {
const commonFilterInputs = {
baseFilters: item['baseFilters'] || [],
drilldownFilters: item['drilldownFilters'] || [],
drilldownLayers: item['drilldownLayers'] || [],
fieldName: item['name'] || '',
connection: item['connection'] || undefined
};
// Remove undefined properties to avoid passing unnecessary data
Object.keys(commonFilterInputs).forEach(key => {
if (commonFilterInputs[key] === undefined) {
delete commonFilterInputs[key];
}
});
return commonFilterInputs;
}
// For GridViewComponent, pass chart properties with drilldown support
if (item.component && item.component.name === 'GridViewComponent') {
const gridInputs = {
xAxis: item.xAxis,
yAxis: item.yAxis,
table: item.table,
datastore: item.datastore,
charttitle: item.charttitle,
chartlegend: item.chartlegend,
showlabel: item.showlabel,
chartcolor: item.chartcolor,
slices: item.slices,
donut: item.donut,
charturl: item.charturl,
chartparameter: item.chartparameter,
datasource: item.datasource,
fieldName: item.name, // Using item.name as fieldName
connection: item['connection'], // Add connection field using bracket notation
// Base drilldown configuration properties
drilldownEnabled: item['drilldownEnabled'],
drilldownApiUrl: item['drilldownApiUrl'],
// Removed drilldownParameterKey since we're using URL templates
drilldownXAxis: item['drilldownXAxis'],
drilldownYAxis: item['drilldownYAxis'],
drilldownParameter: item['drilldownParameter'], // Add drilldown parameter
baseFilters: item['baseFilters'] || [], // Add base filters
drilldownFilters: item['drilldownFilters'] || [], // Add drilldown filters
// Multi-layer drilldown configurations
drilldownLayers: item['drilldownLayers'] || []
};
// Remove undefined properties to avoid passing unnecessary data
Object.keys(gridInputs).forEach(key => {
if (gridInputs[key] === undefined) {
delete gridInputs[key];
}
});
return gridInputs;
}
// For all other chart components, pass chart-specific properties
const chartInputs = { const chartInputs = {
xAxis: item.xAxis, xAxis: item.xAxis,
yAxis: item.yAxis, yAxis: item.yAxis,
@@ -835,6 +994,22 @@ export class EditnewdashComponent implements OnInit {
updatedItem.commonFilterEnabled = this.gadgetsEditdata.commonFilterEnabled; // Add common filter property updatedItem.commonFilterEnabled = this.gadgetsEditdata.commonFilterEnabled; // Add common filter property
updatedItem.commonFilterEnabledDrilldown = this.gadgetsEditdata.commonFilterEnabledDrilldown; // Add drilldown common filter property updatedItem.commonFilterEnabledDrilldown = this.gadgetsEditdata.commonFilterEnabledDrilldown; // Add drilldown common filter property
// For compact filter, preserve filter configuration properties
if (item.component && item.component.name === 'CompactFilterComponent') {
updatedItem.filterKey = this.gadgetsEditdata.filterKey || '';
updatedItem.filterType = this.gadgetsEditdata.filterType || 'text';
updatedItem.filterLabel = this.gadgetsEditdata.filterLabel || '';
updatedItem.filterOptions = this.gadgetsEditdata.filterOptions || [];
updatedItem.table = this.gadgetsEditdata.table || ''; // API URL
updatedItem.connection = this.gadgetsEditdata.connection || undefined; // Connection ID
// Also preserve these properties in gadgetsEditdata for consistency
this.gadgetsEditdata.filterKey = updatedItem.filterKey;
this.gadgetsEditdata.filterType = updatedItem.filterType;
this.gadgetsEditdata.filterLabel = updatedItem.filterLabel;
this.gadgetsEditdata.filterOptions = updatedItem.filterOptions;
}
console.log('Updated item:', updatedItem); console.log('Updated item:', updatedItem);
return updatedItem; return updatedItem;
} }
@@ -1247,4 +1422,17 @@ export class EditnewdashComponent implements OnInit {
// When disabling, the user can edit the filters normally // When disabling, the user can edit the filters normally
} }
} }
// Add method to handle item resize events
itemResize(item: any, itemComponent: any) {
console.log('Item resized:', item);
// Trigger a window resize event to notify charts to resize
window.dispatchEvent(new Event('resize'));
// Also try to directly notify the chart component if possible
if (itemComponent && itemComponent.item && itemComponent.item.component) {
// If the resized item contains a chart, we could try to call its resize method directly
// This would require the chart component to have a public resize method
}
}
} }

View File

@@ -1,4 +1,4 @@
<div style="display: block"> <div style="display: block; height: 100%; width: 100%;">
<!-- No filter controls needed with the new simplified approach --> <!-- No filter controls needed with the new simplified approach -->
<!-- Filters are now configured at the drilldown level --> <!-- Filters are now configured at the drilldown level -->
@@ -19,11 +19,12 @@
</div> </div>
<!-- Chart display --> <!-- Chart display -->
<div *ngIf="!noDataAvailable"> <div *ngIf="!noDataAvailable" style="position: relative; height: calc(100% - 50px);">
<canvas baseChart <canvas baseChart
[datasets]="barChartData" [datasets]="barChartData"
[labels]="barChartLabels" [labels]="barChartLabels"
[type]="barChartType" [type]="barChartType"
[options]="barChartOptions"
(chartHover)="chartHovered($event)" (chartHover)="chartHovered($event)"
(chartClick)="chartClicked($event)"> (chartClick)="chartClicked($event)">
</canvas> </canvas>

View File

@@ -0,0 +1,31 @@
// Bar Chart Component Styles
:host {
display: block;
height: 100%;
width: 100%;
}
.bar-chart-container {
position: relative;
height: 100%;
width: 100%;
}
canvas {
display: block;
max-width: 100%;
max-height: 100%;
}
// Responsive design for chart container
@media (max-width: 768px) {
.bar-chart-container {
height: 300px;
}
}
@media (max-width: 480px) {
.bar-chart-container {
height: 250px;
}
}

View File

@@ -1,6 +1,9 @@
import { Component, Input, OnInit, OnChanges, OnDestroy, SimpleChanges } from '@angular/core'; import { Component, Input, OnInit, OnChanges, OnDestroy, SimpleChanges, ViewChild } from '@angular/core';
import { Dashboard3Service } from '../../../../../../services/builder/dashboard3.service'; import { Dashboard3Service } from '../../../../../../services/builder/dashboard3.service';
import { Subscription } from 'rxjs'; import { Subscription } from 'rxjs';
import { FilterService } from '../../common-filter/filter.service';
// Add BaseChartDirective import for chart resizing
import { BaseChartDirective } from 'ng2-charts';
@Component({ @Component({
selector: 'app-bar-chart', selector: 'app-bar-chart',
@@ -34,6 +37,9 @@ export class BarChartComponent implements OnInit, OnChanges, OnDestroy {
// Multi-layer drilldown configuration inputs // Multi-layer drilldown configuration inputs
@Input() drilldownLayers: any[] = []; // Array of drilldown layer configurations @Input() drilldownLayers: any[] = []; // Array of drilldown layer configurations
// Add ViewChild to access the chart directive
@ViewChild(BaseChartDirective) chart?: BaseChartDirective;
barChartLabels: string[] = ['Apple', 'Banana', 'Kiwifruit', 'Blueberry', 'Orange', 'Grapes']; barChartLabels: string[] = ['Apple', 'Banana', 'Kiwifruit', 'Blueberry', 'Orange', 'Grapes'];
barChartType: string = 'bar'; barChartType: string = 'bar';
barChartPlugins = []; barChartPlugins = [];
@@ -42,6 +48,33 @@ export class BarChartComponent implements OnInit, OnChanges, OnDestroy {
]; ];
barChartLegend: boolean = true; barChartLegend: boolean = true;
// Add responsive chart options
barChartOptions: any = {
responsive: true,
maintainAspectRatio: false,
scales: {
x: {
ticks: {
autoSkip: false,
maxRotation: 45,
minRotation: 45
}
},
y: {
beginAtZero: true
}
},
plugins: {
legend: {
display: true,
position: 'top',
},
tooltip: {
enabled: true
}
}
};
// Multi-layer drilldown state tracking // Multi-layer drilldown state tracking
drilldownStack: any[] = []; // Stack to track drilldown navigation history drilldownStack: any[] = []; // Stack to track drilldown navigation history
currentDrilldownLevel: number = 0; // Current drilldown level (0 = base level) currentDrilldownLevel: number = 0; // Current drilldown level (0 = base level)
@@ -57,9 +90,20 @@ export class BarChartComponent implements OnInit, OnChanges, OnDestroy {
// Subscriptions to unsubscribe on destroy // Subscriptions to unsubscribe on destroy
private subscriptions: Subscription[] = []; private subscriptions: Subscription[] = [];
constructor(private dashboardService: Dashboard3Service) { } constructor(
private dashboardService: Dashboard3Service,
private filterService: FilterService
) { }
ngOnInit(): void { ngOnInit(): void {
// Subscribe to filter changes
this.subscriptions.push(
this.filterService.filterState$.subscribe(filters => {
// When filters change, refresh the chart data
this.fetchChartData();
})
);
// Initialize with default data // Initialize with default data
this.fetchChartData(); this.fetchChartData();
} }
@@ -92,6 +136,7 @@ export class BarChartComponent implements OnInit, OnChanges, OnDestroy {
// Update legend visibility if it changed // Update legend visibility if it changed
if (changes.chartlegend !== undefined) { if (changes.chartlegend !== undefined) {
this.barChartLegend = changes.chartlegend.currentValue; this.barChartLegend = changes.chartlegend.currentValue;
this.barChartOptions.plugins.legend.display = this.barChartLegend;
console.log('Chart legend changed to:', this.barChartLegend); console.log('Chart legend changed to:', this.barChartLegend);
} }
} }
@@ -110,10 +155,15 @@ export class BarChartComponent implements OnInit, OnChanges, OnDestroy {
// If we have the necessary data, fetch chart data from the service // If we have the necessary data, fetch chart data from the service
if (this.table && this.xAxis && this.yAxis) { if (this.table && this.xAxis && this.yAxis) {
console.log('Fetching bar chart data for:', { table: this.table, xAxis: this.xAxis, yAxis: this.yAxis, connection: this.connection }); console.log('=== BAR CHART DEBUG INFO ===');
console.log('Table:', this.table);
console.log('X-Axis:', this.xAxis);
console.log('Y-Axis:', this.yAxis);
console.log('Connection:', this.connection);
// Convert yAxis to string if it's an array // Convert yAxis to string if it's an array
const yAxisString = Array.isArray(this.yAxis) ? this.yAxis.join(',') : this.yAxis; const yAxisString = Array.isArray(this.yAxis) ? this.yAxis.join(',') : this.yAxis;
console.log('Y-Axis String:', yAxisString);
// Get the parameter value from the drilldown stack for base level (should be empty) // Get the parameter value from the drilldown stack for base level (should be empty)
let parameterValue = ''; let parameterValue = '';
@@ -123,23 +173,46 @@ export class BarChartComponent implements OnInit, OnChanges, OnDestroy {
console.log('Bar chart data URL:', url); console.log('Bar chart data URL:', url);
// Convert baseFilters to filter parameters // Convert baseFilters to filter parameters
let filterParams = '';
if (this.baseFilters && this.baseFilters.length > 0) {
const filterObj = {}; const filterObj = {};
// Add base filters
if (this.baseFilters && this.baseFilters.length > 0) {
this.baseFilters.forEach(filter => { this.baseFilters.forEach(filter => {
if (filter.field && filter.value) { if (filter.field && filter.value) {
filterObj[filter.field] = filter.value; filterObj[filter.field] = filter.value;
} }
}); });
}
// Add common filters
const commonFilters = this.filterService.getFilterValues();
const filterDefinitions = this.filterService.getFilters();
Object.keys(commonFilters).forEach(filterId => {
const filterValue = commonFilters[filterId];
// Find the filter definition to get the field name
const filterDef = this.filterService.getFilters().find(f => f.id === filterId);
if (filterDef && filterDef.field) {
const fieldName = filterDef.field;
if (filterValue !== undefined && filterValue !== null && filterValue !== '') {
filterObj[fieldName] = filterValue;
}
}
});
// Convert to JSON string for API call
let filterParams = '';
if (Object.keys(filterObj).length > 0) { if (Object.keys(filterObj).length > 0) {
filterParams = JSON.stringify(filterObj); filterParams = JSON.stringify(filterObj);
} }
}
console.log('Base filter parameters:', filterParams); console.log('Final filter object:', filterObj);
// Fetch data from the dashboard service with parameter field and value // Fetch data from the dashboard service with parameter field and value
// For base level, we pass empty parameter and value, but now also pass filters // For base level, we pass empty parameter and value, but now also pass filters
const subscription = this.dashboardService.getChartData(this.table, 'bar', this.xAxis, yAxisString, this.connection, '', '', filterParams).subscribe( const subscription = this.dashboardService.getChartData(this.table, 'bar', this.xAxis, yAxisString, this.connection, '', '', filterParams).subscribe(
(data: any) => { (data: any) => {
console.log('=== BAR CHART DATA RESPONSE ===');
console.log('Received bar chart data:', data); console.log('Received bar chart data:', data);
if (data === null) { if (data === null) {
console.warn('Bar chart API returned null data. Check if the API endpoint is working correctly.'); console.warn('Bar chart API returned null data. Check if the API endpoint is working correctly.');
@@ -160,6 +233,7 @@ export class BarChartComponent implements OnInit, OnChanges, OnDestroy {
// Trigger change detection // Trigger change detection
// this.barChartData = [...this.barChartData]; // this.barChartData = [...this.barChartData];
console.log('Updated bar chart with data:', { labels: this.barChartLabels, data: this.barChartData }); console.log('Updated bar chart with data:', { labels: this.barChartLabels, data: this.barChartData });
console.log('=== CHART UPDATED SUCCESSFULLY ===');
} else if (data && data.labels && data.datasets) { } else if (data && data.labels && data.datasets) {
// Backend has already filtered the data, just display it // Backend has already filtered the data, just display it
this.noDataAvailable = data.labels.length === 0; this.noDataAvailable = data.labels.length === 0;
@@ -168,6 +242,7 @@ export class BarChartComponent implements OnInit, OnChanges, OnDestroy {
// Trigger change detection // Trigger change detection
// this.barChartData = [...this.barChartData]; // this.barChartData = [...this.barChartData];
console.log('Updated bar chart with legacy data format:', { labels: this.barChartLabels, data: this.barChartData }); console.log('Updated bar chart with legacy data format:', { labels: this.barChartLabels, data: this.barChartData });
console.log('=== CHART UPDATED SUCCESSFULLY (LEGACY) ===');
} else { } else {
console.warn('Bar chart received data does not have expected structure', data); console.warn('Bar chart received data does not have expected structure', data);
this.noDataAvailable = true; this.noDataAvailable = true;
@@ -178,6 +253,7 @@ export class BarChartComponent implements OnInit, OnChanges, OnDestroy {
this.isFetchingData = false; this.isFetchingData = false;
}, },
(error) => { (error) => {
console.error('=== BAR CHART ERROR ===');
console.error('Error fetching bar chart data:', error); console.error('Error fetching bar chart data:', error);
this.noDataAvailable = true; this.noDataAvailable = true;
this.barChartLabels = []; this.barChartLabels = [];
@@ -286,33 +362,49 @@ export class BarChartComponent implements OnInit, OnChanges, OnDestroy {
console.log('Drilldown data URL:', url); console.log('Drilldown data URL:', url);
// Convert drilldown layer filters to filter parameters (if applicable) // Convert drilldown layer filters to filter parameters (if applicable)
let filterParams = '';
if (drilldownConfig.filters && drilldownConfig.filters.length > 0) {
const filterObj = {}; const filterObj = {};
// Add drilldown layer filters
if (drilldownConfig.filters && drilldownConfig.filters.length > 0) {
drilldownConfig.filters.forEach((filter: any) => { drilldownConfig.filters.forEach((filter: any) => {
if (filter.field && filter.value) { if (filter.field && filter.value) {
filterObj[filter.field] = filter.value; filterObj[filter.field] = filter.value;
} }
}); });
if (Object.keys(filterObj).length > 0) {
filterParams = JSON.stringify(filterObj);
} }
}
console.log('Drilldown layer filter parameters:', filterParams);
// Convert drilldownFilters to filter parameters for drilldown level // Add drilldownFilters
let drilldownFilterParams = '';
if (this.drilldownFilters && this.drilldownFilters.length > 0) { if (this.drilldownFilters && this.drilldownFilters.length > 0) {
const filterObj = {};
this.drilldownFilters.forEach(filter => { this.drilldownFilters.forEach(filter => {
if (filter.field && filter.value) { if (filter.field && filter.value) {
filterObj[filter.field] = filter.value; filterObj[filter.field] = filter.value;
} }
}); });
}
// Add common filters
const commonFilters = this.filterService.getFilterValues();
const filterDefinitions = this.filterService.getFilters();
Object.keys(commonFilters).forEach(filterId => {
const filterValue = commonFilters[filterId];
// Find the filter definition to get the field name
const filterDef = this.filterService.getFilters().find(f => f.id === filterId);
if (filterDef && filterDef.field) {
const fieldName = filterDef.field;
if (filterValue !== undefined && filterValue !== null && filterValue !== '') {
filterObj[fieldName] = filterValue;
}
}
});
// Convert to JSON string for API call
let drilldownFilterParams = '';
if (Object.keys(filterObj).length > 0) { if (Object.keys(filterObj).length > 0) {
drilldownFilterParams = JSON.stringify(filterObj); drilldownFilterParams = JSON.stringify(filterObj);
} }
}
console.log('Drilldown filter parameters:', drilldownFilterParams); console.log('Drilldown filter parameters:', drilldownFilterParams);
// For drilldown level, we pass the parameter value from the drilldown stack and drilldown filters // For drilldown level, we pass the parameter value from the drilldown stack and drilldown filters
@@ -426,6 +518,18 @@ export class BarChartComponent implements OnInit, OnChanges, OnDestroy {
} }
} }
// Public method to refresh data when filters change
refreshData(): void {
this.fetchChartData();
}
// Method to handle window resize events
onResize(): void {
if (this.chart) {
this.chart.chart?.resize();
}
}
// Ensure labels and data arrays have the same length // Ensure labels and data arrays have the same length
private syncLabelAndDataArrays(): void { private syncLabelAndDataArrays(): void {
// For bar charts, we need to ensure all datasets have the same number of data points // For bar charts, we need to ensure all datasets have the same number of data points

View File

@@ -1,23 +1,30 @@
<div class="doughnut-chart-container"> <div class="doughnut-chart-container">
<!-- Compact Filters -->
<div class="compact-filters-container" *ngIf="baseFilters && baseFilters.length > 0">
<app-compact-filter
*ngFor="let filter of baseFilters"
[filterKey]="filter.field"
(filterChange)="onFilterChange($event)">
</app-compact-filter>
</div>
<!-- Drilldown mode indicator --> <!-- Drilldown mode indicator -->
<div *ngIf="currentDrilldownLevel > 0" style="background-color: #e0e0e0; padding: 5px; margin-bottom: 10px; border-radius: 4px; text-align: center;"> <div *ngIf="currentDrilldownLevel > 0" class="drilldown-indicator">
<span style="font-weight: bold; color: #333;">Drilldown Level: {{currentDrilldownLevel}}</span> <span class="drilldown-text">Drilldown Level: {{currentDrilldownLevel}}</span>
<button (click)="navigateBack()" style="margin-left: 10px; padding: 2px 8px; background-color: #007cba; color: white; border: none; border-radius: 3px; cursor: pointer;"> <button class="btn btn-secondary btn-sm" (click)="navigateBack()">
Back to Level {{currentDrilldownLevel - 1}} Back to Level {{currentDrilldownLevel - 1}}
</button> </button>
<button (click)="resetToOriginalData()" style="margin-left: 10px; padding: 2px 8px; background-color: #dc3545; color: white; border: none; border-radius: 3px; cursor: pointer;"> <button class="btn btn-danger btn-sm" (click)="resetToOriginalData()">
Back to Main View Back to Main View
</button> </button>
</div> </div>
<div class="chart-header">
<h3 class="chart-title" *ngIf="charttitle">{{ charttitle }}</h3> <h3 class="chart-title" *ngIf="charttitle">{{ charttitle }}</h3>
<div class="chart-wrapper">
<!-- Show loading indicator -->
<div class="loading-indicator" *ngIf="doughnutChartLabels.length === 0 && doughnutChartData.length === 0 && !noDataAvailable">
<div class="spinner"></div>
<p>Loading chart data...</p>
</div> </div>
<div class="chart-wrapper">
<div class="chart-content" [class.loading]="doughnutChartLabels.length === 0 && doughnutChartData.length === 0 && !noDataAvailable">
<!-- Show no data message --> <!-- Show no data message -->
<div class="no-data-message" *ngIf="noDataAvailable"> <div class="no-data-message" *ngIf="noDataAvailable">
<p>No chart data available</p> <p>No chart data available</p>
@@ -33,7 +40,14 @@
(chartHover)="chartHovered($event)" (chartHover)="chartHovered($event)"
(chartClick)="chartClicked($event)"> (chartClick)="chartClicked($event)">
</canvas> </canvas>
<!-- Loading overlay -->
<div class="loading-overlay" *ngIf="doughnutChartLabels.length === 0 && doughnutChartData.length === 0 && !noDataAvailable">
<div class="shimmer-donut"></div>
</div> </div>
</div>
</div>
<div class="chart-legend" *ngIf="!noDataAvailable && showlabel && doughnutChartLabels && doughnutChartLabels.length > 0"> <div class="chart-legend" *ngIf="!noDataAvailable && showlabel && doughnutChartLabels && doughnutChartLabels.length > 0">
<div class="legend-item" *ngFor="let label of doughnutChartLabels; let i = index"> <div class="legend-item" *ngFor="let label of doughnutChartLabels; let i = index">
<span class="legend-color" [style.background-color]="getLegendColor(i)"></span> <span class="legend-color" [style.background-color]="getLegendColor(i)"></span>

View File

@@ -17,17 +17,78 @@
transform: translateY(-2px); transform: translateY(-2px);
} }
.compact-filters-container {
display: flex;
flex-wrap: wrap;
gap: 5px;
margin-bottom: 10px;
padding: 5px;
background: #ffffff;
border: 1px solid #e9ecef;
border-radius: 6px;
min-height: 40px;
}
.drilldown-indicator {
background-color: #e0e0e0;
padding: 10px;
margin-bottom: 15px;
border-radius: 8px;
text-align: center;
display: flex;
justify-content: center;
align-items: center;
gap: 10px;
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
}
.drilldown-text {
font-weight: bold;
color: #333;
font-size: 16px;
}
.btn {
padding: 6px 12px;
border: none;
border-radius: 4px;
cursor: pointer;
font-size: 14px;
transition: all 0.3s ease;
}
.btn:hover {
transform: translateY(-2px);
box-shadow: 0 4px 8px rgba(0, 0, 0, 0.2);
}
.btn-sm {
padding: 4px 8px;
font-size: 12px;
}
.btn-secondary {
background-color: #007cba;
color: white;
}
.btn-danger {
background-color: #dc3545;
color: white;
}
.chart-header {
margin-bottom: 20px;
.chart-title { .chart-title {
font-size: 26px; font-size: 22px;
font-weight: 700; font-weight: 600;
color: #2c3e50; color: #0a192f;
margin-bottom: 20px; margin: 0;
text-align: center; text-align: center;
padding-bottom: 15px; padding-bottom: 10px;
border-bottom: 2px solid #3498db; border-bottom: 2px solid #3498db;
text-shadow: 0 1px 2px rgba(0, 0, 0, 0.1); }
} }
.chart-wrapper { .chart-wrapper {
@@ -56,6 +117,62 @@
transition: all 0.3s ease; transition: all 0.3s ease;
} }
.chart-content {
position: relative;
height: 100%;
width: 100%;
display: flex;
align-items: center;
justify-content: center;
&.loading {
opacity: 0.7;
canvas {
filter: blur(2px);
}
}
.no-data-message {
text-align: center;
padding: 30px;
color: #666;
font-size: 18px;
font-style: italic;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
height: 100%;
width: 100%;
}
.no-data-message p {
margin: 0;
}
.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;
}
}
}
.chart-legend { .chart-legend {
display: flex; display: flex;
flex-wrap: wrap; flex-wrap: wrap;
@@ -119,36 +236,13 @@
text-align: center; text-align: center;
} }
.loading-indicator, .no-data-message { @keyframes shimmer {
text-align: center; 0% {
padding: 30px; background-position: -200% 0;
color: #666;
font-size: 18px;
font-style: italic;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
height: 100%;
} }
100% {
.loading-indicator p, .no-data-message p { background-position: 200% 0;
margin: 10px 0 0 0;
} }
.spinner {
border: 4px solid #f3f3f3;
border-top: 4px solid #3498db;
border-radius: 50%;
width: 40px;
height: 40px;
animation: spin 1s linear infinite;
margin-bottom: 10px;
}
@keyframes spin {
0% { transform: rotate(0deg); }
100% { transform: rotate(360deg); }
} }
/* Responsive design */ /* Responsive design */
@@ -157,9 +251,17 @@
padding: 15px; padding: 15px;
} }
.chart-title { .chart-header .chart-title {
font-size: 20px; font-size: 18px;
margin-bottom: 15px; }
.drilldown-indicator {
flex-direction: column;
gap: 5px;
}
.drilldown-text {
font-size: 14px;
} }
.chart-wrapper { .chart-wrapper {
@@ -181,4 +283,8 @@
font-size: 16px; font-size: 16px;
padding: 20px; padding: 20px;
} }
.compact-filters-container {
flex-wrap: wrap;
}
} }

View File

@@ -1,5 +1,7 @@
import { Component, OnInit, Input, OnChanges, SimpleChanges, AfterViewChecked } from '@angular/core'; import { Component, OnInit, Input, OnChanges, SimpleChanges, AfterViewChecked, OnDestroy } from '@angular/core';
import { Dashboard3Service } from 'src/app/services/builder/dashboard3.service'; import { Dashboard3Service } from 'src/app/services/builder/dashboard3.service';
import { FilterService } from '../../common-filter/filter.service';
import { Subscription } from 'rxjs';
@Component({ @Component({
selector: 'app-doughnut-chart', selector: 'app-doughnut-chart',
@@ -97,9 +99,23 @@ export class DoughnutChartComponent implements OnInit, OnChanges, AfterViewCheck
// Flag to prevent infinite loops // Flag to prevent infinite loops
private isFetchingData: boolean = false; private isFetchingData: boolean = false;
constructor(private dashboardService: Dashboard3Service) { } // Subscriptions to unsubscribe on destroy
private subscriptions: Subscription[] = [];
constructor(
private dashboardService: Dashboard3Service,
private filterService: FilterService
) { }
ngOnInit(): void { ngOnInit(): void {
// Subscribe to filter changes
this.subscriptions.push(
this.filterService.filterState$.subscribe(filters => {
// When filters change, refresh the chart data
this.fetchChartData();
})
);
// Validate initial data // Validate initial data
this.validateChartData(); this.validateChartData();
this.fetchChartData(); this.fetchChartData();
@@ -180,6 +196,21 @@ export class DoughnutChartComponent implements OnInit, OnChanges, AfterViewCheck
}); });
} }
ngOnDestroy(): void {
this.subscriptions.forEach(sub => sub.unsubscribe());
}
// Handle filter changes from compact filters
onFilterChange(event: { filterId: string, value: any }): void {
console.log('Compact filter changed:', event);
// The filter service will automatically trigger chart updates through the subscription
}
// Public method to refresh data when filters change
refreshData(): void {
this.fetchChartData();
}
fetchChartData(): void { fetchChartData(): void {
// Set flag to prevent recursive calls // Set flag to prevent recursive calls
this.isFetchingData = true; this.isFetchingData = true;
@@ -212,7 +243,49 @@ export class DoughnutChartComponent implements OnInit, OnChanges, AfterViewCheck
filterParams = JSON.stringify(filterObj); filterParams = JSON.stringify(filterObj);
} }
} }
console.log('Base filter parameters:', filterParams);
// Add common filters to filter parameters
const commonFilters = this.filterService.getFilterValues();
console.log('Common filters from service:', commonFilters);
if (Object.keys(commonFilters).length > 0) {
// Merge common filters with base filters
const mergedFilterObj = {};
// Add base filters first
if (filterParams) {
try {
const baseFilterObj = JSON.parse(filterParams);
Object.assign(mergedFilterObj, baseFilterObj);
} catch (e) {
console.warn('Failed to parse base filter parameters:', e);
}
}
// Add common filters using the field name as the key, not the filter id
Object.keys(commonFilters).forEach(filterId => {
const filterValue = commonFilters[filterId];
// Find the filter definition to get the field name
const filterDef = this.filterService.getFilters().find(f => f.id === filterId);
if (filterDef && filterDef.field) {
const fieldName = filterDef.field;
if (filterValue !== undefined && filterValue !== null && filterValue !== '') {
mergedFilterObj[fieldName] = filterValue;
}
} else {
// Fallback to using filterId as field name if no field is defined
if (filterValue !== undefined && filterValue !== null && filterValue !== '') {
mergedFilterObj[filterId] = filterValue;
}
}
});
if (Object.keys(mergedFilterObj).length > 0) {
filterParams = JSON.stringify(mergedFilterObj);
}
}
console.log('Final filter parameters:', filterParams);
// Log the URL that will be called // Log the URL that will be called
const url = `chart/getdashjson/doughnut?tableName=${this.table}&xAxis=${this.xAxis}&yAxes=${yAxisString}${this.connection ? `&sureId=${this.connection}` : ''}`; const url = `chart/getdashjson/doughnut?tableName=${this.table}&xAxis=${this.xAxis}&yAxes=${yAxisString}${this.connection ? `&sureId=${this.connection}` : ''}`;

View File

@@ -4,9 +4,41 @@
<div class="clr-col-8"> <div class="clr-col-8">
<h3>{{charttitle || 'Data Grid'}}</h3> <h3>{{charttitle || 'Data Grid'}}</h3>
</div> </div>
<!-- Add drilldown navigation controls -->
<div class="clr-col-4" *ngIf="drilldownEnabled && drilldownStack.length > 0" style="text-align: right;">
<button class="btn btn-sm btn-link" (click)="navigateBack()">
<cds-icon shape="arrow" direction="left"></cds-icon>
Back to {{drilldownStack.length > 1 ? 'Previous Level' : 'Main Data'}}
</button>
</div> </div>
</div>
<!-- Show current drilldown level -->
<div class="clr-row" *ngIf="drilldownEnabled && drilldownStack.length > 0">
<div class="clr-col-12">
<div class="alert alert-info" style="padding: 8px 12px; margin-bottom: 12px;">
<div class="alert-items">
<div class="alert-item static">
<div class="alert-icon-wrapper">
<cds-icon class="alert-icon" shape="info-circle"></cds-icon>
</div>
<span class="alert-text">
Drilldown Level: {{currentDrilldownLevel}}
<span *ngIf="drilldownStack.length > 0">
(Clicked on: {{drilldownStack[drilldownStack.length - 1].clickedKey}} = {{drilldownStack[drilldownStack.length - 1].clickedValue}})
</span>
</span>
</div>
</div>
</div>
</div>
</div>
<clr-datagrid [clrDgLoading]="loading"> <clr-datagrid [clrDgLoading]="loading">
<clr-dg-placeholder> <ng-template #loadingSpinner><clr-spinner>Loading ... </clr-spinner></ng-template> <clr-dg-placeholder>
<ng-template #loadingSpinner>
<clr-spinner>Loading ... </clr-spinner>
</ng-template>
<div *ngIf="error;else loadingSpinner">{{error}}</div> <div *ngIf="error;else loadingSpinner">{{error}}</div>
</clr-dg-placeholder> </clr-dg-placeholder>
@@ -19,7 +51,7 @@
<clr-dg-row *clrDgItems="let item of givendata" [clrDgItem]="item"> <clr-dg-row *clrDgItems="let item of givendata" [clrDgItem]="item">
<!-- Dynamic cells based on response keys --> <!-- Dynamic cells based on response keys -->
<clr-dg-cell *ngFor="let header of dynamicHeaders"> <clr-dg-cell *ngFor="let header of dynamicHeaders" (click)="onRowClick(item, header.key)">
{{item[header.key]}} {{item[header.key]}}
</clr-dg-cell> </clr-dg-cell>
</clr-dg-row> </clr-dg-row>

View File

@@ -1,12 +1,28 @@
@import '../../../../../../../styles1.scss'; // Add styles for drilldown navigation
input.ng-invalid.ng-touched { .alert-info {
border-color: red; background-color: #dcedf7;
border-color: #a3d4f5;
color: #21333b;
} }
.error_mess { .alert-info .alert-icon {
color: red; color: #0072a3;
} }
clr-datagrid{
height: 400px; /* Adjust the height as needed */ .btn-link {
overflow-y: auto; color: #0072a3;
text-decoration: none;
}
.btn-link:hover {
color: #00567a;
text-decoration: underline;
}
.dg-wrapper {
padding: 12px;
}
.clr-row {
margin-bottom: 12px;
} }

View File

@@ -1,13 +1,17 @@
import { Component, OnInit, Input, OnChanges, SimpleChanges } from '@angular/core'; import { Component, OnInit, Input, OnChanges, OnDestroy, SimpleChanges, ViewChild } from '@angular/core';
import { UsergrpmaintainceService } from 'src/app/services/admin/usergrpmaintaince.service'; import { UsergrpmaintainceService } from 'src/app/services/admin/usergrpmaintaince.service';
import { Dashboard3Service } from 'src/app/services/builder/dashboard3.service'; import { Dashboard3Service } from 'src/app/services/builder/dashboard3.service';
// Add FilterService import
import { FilterService } from '../../common-filter/filter.service';
// Add Subscription import
import { Subscription } from 'rxjs';
@Component({ @Component({
selector: 'app-grid-view', selector: 'app-grid-view',
templateUrl: './grid-view.component.html', templateUrl: './grid-view.component.html',
styleUrls: ['./grid-view.component.scss'] styleUrls: ['./grid-view.component.scss']
}) })
export class GridViewComponent implements OnInit, OnChanges { export class GridViewComponent implements OnInit, OnChanges, OnDestroy {
@Input() xAxis: string; @Input() xAxis: string;
@Input() yAxis: string | string[]; @Input() yAxis: string | string[];
@Input() table: string; @Input() table: string;
@@ -23,6 +27,16 @@ export class GridViewComponent implements OnInit, OnChanges {
@Input() datasource: string; @Input() datasource: string;
@Input() fieldName: string; @Input() fieldName: string;
@Input() connection: number; // Add connection input @Input() connection: number; // Add connection input
// Drilldown configuration inputs
@Input() drilldownEnabled: boolean = false;
@Input() drilldownApiUrl: string;
@Input() drilldownXAxis: string;
@Input() drilldownYAxis: string;
@Input() drilldownParameter: string; // Add drilldown parameter input
@Input() baseFilters: any[] = []; // Add base filters input
@Input() drilldownFilters: any[] = []; // Add drilldown filters input
// Multi-layer drilldown configuration inputs
@Input() drilldownLayers: any[] = []; // Array of drilldown layer configurations
loading = false; loading = false;
givendata: any[] = []; givendata: any[] = [];
@@ -38,13 +52,37 @@ export class GridViewComponent implements OnInit, OnChanges {
submitted = false; submitted = false;
dynamicHeaders: any[] = []; dynamicHeaders: any[] = [];
constructor( // Multi-layer drilldown state tracking
drilldownStack: any[] = []; // Stack to track drilldown navigation history
currentDrilldownLevel: number = 0; // Current drilldown level (0 = base level)
originalGridData: any[] = [];
// No data state
noDataAvailable: boolean = false;
// Flag to prevent infinite loops
private isFetchingData: boolean = false;
// Add subscriptions to unsubscribe on destroy
private subscriptions: Subscription[] = [];
constructor(
private mainservice: UsergrpmaintainceService, private mainservice: UsergrpmaintainceService,
private dashboardService: Dashboard3Service, private dashboardService: Dashboard3Service,
// Add FilterService to constructor
private filterService: FilterService
) { } ) { }
ngOnInit(): void { ngOnInit(): void {
// Subscribe to filter changes
this.subscriptions.push(
this.filterService.filterState$.subscribe(filters => {
// When filters change, refresh the grid data
console.log('GridView: Filter state changed:', filters);
this.fetchGridData();
})
);
this.fetchGridData(); this.fetchGridData();
} }
@@ -55,12 +93,21 @@ export class GridViewComponent implements OnInit, OnChanges {
const xAxisChanged = changes.xAxis && !changes.xAxis.firstChange; const xAxisChanged = changes.xAxis && !changes.xAxis.firstChange;
const yAxisChanged = changes.yAxis && !changes.yAxis.firstChange; const yAxisChanged = changes.yAxis && !changes.yAxis.firstChange;
const tableChanged = changes.table && !changes.table.firstChange; const tableChanged = changes.table && !changes.table.firstChange;
const connectionChanged = changes.connection && !changes.connection.firstChange; // Add connection change detection const connectionChanged = changes.connection && !changes.connection.firstChange;
const baseFiltersChanged = changes.baseFilters && !changes.baseFilters.firstChange;
// Drilldown configuration changes
const drilldownEnabledChanged = changes.drilldownEnabled && !changes.drilldownEnabled.firstChange;
const drilldownApiUrlChanged = changes.drilldownApiUrl && !changes.drilldownApiUrl.firstChange;
const drilldownXAxisChanged = changes.drilldownXAxis && !changes.drilldownXAxis.firstChange;
const drilldownYAxisChanged = changes.drilldownYAxis && !changes.drilldownYAxis.firstChange;
const drilldownLayersChanged = changes.drilldownLayers && !changes.drilldownLayers.firstChange;
// Respond to input changes // Respond to input changes
if (xAxisChanged || yAxisChanged || tableChanged || connectionChanged) { if (!this.isFetchingData && (xAxisChanged || yAxisChanged || tableChanged || connectionChanged || baseFiltersChanged ||
console.log('X or Y axis or table or connection changed, fetching new data'); drilldownEnabledChanged || drilldownApiUrlChanged || drilldownXAxisChanged || drilldownYAxisChanged ||
// Only fetch data if xAxis, yAxis, table, or connection has changed (and it's not the first change) drilldownLayersChanged)) {
console.log('X or Y axis or table or connection or base filters or drilldown config changed, fetching new data');
// Only fetch data if xAxis, yAxis, table, connection, baseFilters or drilldown config has changed (and it's not the first change)
this.fetchGridData(); this.fetchGridData();
} }
} }
@@ -68,20 +115,87 @@ export class GridViewComponent implements OnInit, OnChanges {
// Dynamic headers for the grid // Dynamic headers for the grid
fetchGridData(): void { fetchGridData(): void {
// Set flag to prevent recursive calls
this.isFetchingData = true;
// If we're in drilldown mode, fetch the appropriate drilldown data
if (this.currentDrilldownLevel > 0 && this.drilldownStack.length > 0) {
this.fetchDrilldownData();
// Reset flag after fetching
this.isFetchingData = false;
return;
}
// If we have the necessary data, fetch grid data from the service // If we have the necessary data, fetch grid data from the service
if (this.table && this.xAxis) { // if (this.table && this.xAxis) {
console.log('Fetching grid data for:', { table: this.table, xAxis: this.xAxis, yAxis: this.yAxis, connection: this.connection }); if (this.table) {
console.log('=== GRID VIEW DEBUG INFO ===');
console.log('Table:', this.table);
console.log('X-Axis:', this.xAxis);
console.log('Y-Axis:', this.yAxis);
console.log('Connection:', this.connection);
// Convert yAxis to string if it's an array // Convert yAxis to string if it's an array
const yAxisString = Array.isArray(this.yAxis) ? this.yAxis.join(',') : this.yAxis; const yAxisString = Array.isArray(this.yAxis) ? this.yAxis.join(',') : this.yAxis;
// Get the parameter value from the drilldown stack for base level (should be empty)
let parameterValue = '';
// Log the URL that will be called
let url = `chart/getdashjson/grid?tableName=${this.table}&xAxis=${this.xAxis}&yAxes=${yAxisString}${this.connection ? `&sureId=${this.connection}` : ''}`;
console.log('Grid data URL:', url);
// Get filter parameters from base filters
const filterObj = {};
// Add base filters
if (this.baseFilters && this.baseFilters.length > 0) {
this.baseFilters.forEach(filter => {
if (filter.field && filter.value) {
filterObj[filter.field] = filter.value;
}
});
}
// Add common filters directly as key-value pairs
const commonFilters = this.filterService.getFilterValues();
const filterDefinitions = this.filterService.getFilters();
// Add common filters using the field name as the key
Object.keys(commonFilters).forEach(filterId => {
const filterValue = commonFilters[filterId];
// Find the filter definition to get the field name
const filterDef = this.filterService.getFilters().find(f => f.id === filterId);
if (filterDef && filterDef.field) {
const fieldName = filterDef.field;
if (filterValue !== undefined && filterValue !== null && filterValue !== '') {
filterObj[fieldName] = filterValue;
}
}
});
// Convert to JSON string for API call
let filterParams = '';
if (Object.keys(filterObj).length > 0) {
filterParams = JSON.stringify(filterObj);
}
console.log('GridView: Final filter object to send to API:', filterObj);
// Fetch data from the dashboard service, similar to other chart components // Fetch data from the dashboard service, similar to other chart components
this.dashboardService.getChartData(this.table, 'grid', this.xAxis, yAxisString, this.connection).subscribe( this.dashboardService.getChartData(this.table, 'grid', this.xAxis, yAxisString, this.connection, '', '', filterParams).subscribe(
(data: any) => { (data: any) => {
console.log('=== GRID VIEW DATA RESPONSE ===');
console.log('Received grid data:', data); console.log('Received grid data:', data);
if (data === null) { if (data === null) {
console.warn('Grid API returned null data. Check if the API endpoint is working correctly.'); console.warn('Grid API returned null data. Check if the API endpoint is working correctly.');
this.error = "No data Available"; this.error = "No data Available";
this.noDataAvailable = true;
// Reset flag after fetching
this.isFetchingData = false;
return; return;
} }
@@ -90,27 +204,36 @@ export class GridViewComponent implements OnInit, OnChanges {
this.givendata = data.chartData; this.givendata = data.chartData;
this.extractDynamicHeaders(data.chartData); this.extractDynamicHeaders(data.chartData);
this.error = this.givendata.length === 0 ? "No data Available" : undefined; this.error = this.givendata.length === 0 ? "No data Available" : undefined;
this.noDataAvailable = this.givendata.length === 0;
console.log('Updated grid with data:', this.givendata); console.log('Updated grid with data:', this.givendata);
} else if (data && data.data) { } else if (data && data.data) {
// Handle the original expected format as fallback // Handle the original expected format as fallback
this.givendata = data.data; this.givendata = data.data;
this.extractDynamicHeaders(data.data); this.extractDynamicHeaders(data.data);
this.error = this.givendata.length === 0 ? "No data Available" : undefined; this.error = this.givendata.length === 0 ? "No data Available" : undefined;
this.noDataAvailable = this.givendata.length === 0;
console.log('Updated grid with legacy data format:', this.givendata); console.log('Updated grid with legacy data format:', this.givendata);
} else if (Array.isArray(data)) { } else if (Array.isArray(data)) {
// Handle case where data is directly an array // Handle case where data is directly an array
this.givendata = data; this.givendata = data;
this.extractDynamicHeaders(data); this.extractDynamicHeaders(data);
this.error = this.givendata.length === 0 ? "No data Available" : undefined; this.error = this.givendata.length === 0 ? "No data Available" : undefined;
this.noDataAvailable = this.givendata.length === 0;
console.log('Updated grid with array data:', this.givendata); console.log('Updated grid with array data:', this.givendata);
} else { } else {
console.warn('Grid received data does not have expected structure', data); console.warn('Grid received data does not have expected structure', data);
this.error = "No valid data received"; this.error = "No valid data received";
this.givendata = []; this.givendata = [];
this.noDataAvailable = true;
} }
// Reset flag after fetching
this.isFetchingData = false;
}, (error) => { }, (error) => {
console.log('Error fetching grid data:', error); console.log('Error fetching grid data:', error);
this.error = "Server Error"; this.error = "Server Error";
this.noDataAvailable = true;
// Reset flag after fetching
this.isFetchingData = false;
}); });
} else if (this.table) { } else if (this.table) {
console.log('Missing xAxis, falling back to default data fetching'); console.log('Missing xAxis, falling back to default data fetching');
@@ -126,13 +249,342 @@ export class GridViewComponent implements OnInit, OnChanges {
this.givendata = Array.isArray(data) ? data : []; this.givendata = Array.isArray(data) ? data : [];
this.extractDynamicHeaders(data); this.extractDynamicHeaders(data);
this.error = this.givendata && this.givendata.length === 0 ? "No data Available" : undefined; this.error = this.givendata && this.givendata.length === 0 ? "No data Available" : undefined;
this.noDataAvailable = this.givendata && this.givendata.length === 0;
// Reset flag after fetching
this.isFetchingData = false;
}, (error) => { }, (error) => {
console.log(error); console.log(error);
this.error = "Server Error"; this.error = "Server Error";
this.noDataAvailable = true;
// Reset flag after fetching
this.isFetchingData = false;
}); });
} else { } else {
console.log('Missing required data for grid:', { table: this.table }); console.log('Missing required data for grid:', { table: this.table });
this.error = "Table name is required"; this.error = "Table name is required";
this.noDataAvailable = true;
// Reset flag after fetching
this.isFetchingData = false;
}
}
// Fetch drilldown data based on current drilldown level
fetchDrilldownData(): void {
console.log('Fetching drilldown data, current level:', this.currentDrilldownLevel);
console.log('Drilldown stack:', this.drilldownStack);
// Get the current drilldown configuration based on the current level
let drilldownConfig;
if (this.currentDrilldownLevel === 1) {
// Base drilldown level
drilldownConfig = {
apiUrl: this.drilldownApiUrl,
xAxis: this.drilldownXAxis,
yAxis: this.drilldownYAxis,
parameter: this.drilldownParameter
};
} else {
// Multi-layer drilldown level
const layerIndex = this.currentDrilldownLevel - 2; // -2 because level 1 is base drilldown
if (layerIndex >= 0 && layerIndex < this.drilldownLayers.length) {
drilldownConfig = this.drilldownLayers[layerIndex];
} else {
console.warn('Invalid drilldown layer index:', layerIndex);
this.error = "Invalid drilldown configuration";
this.givendata = [];
this.noDataAvailable = true;
return;
}
}
console.log('Drilldown config for level', this.currentDrilldownLevel, ':', drilldownConfig);
// Check if we have valid drilldown configuration
if (!drilldownConfig || !drilldownConfig.apiUrl || !drilldownConfig.xAxis || !drilldownConfig.yAxis) {
console.warn('Missing drilldown configuration for level:', this.currentDrilldownLevel);
this.error = "Missing drilldown configuration";
this.givendata = [];
this.noDataAvailable = true;
return;
}
// Get the parameter value from the drilldown stack
let parameterValue = '';
if (this.drilldownStack.length > 0) {
const lastEntry = this.drilldownStack[this.drilldownStack.length - 1];
parameterValue = lastEntry.clickedValue || '';
console.log('Parameter value from last click:', parameterValue);
}
// Get the parameter field from drilldown config
const parameterField = drilldownConfig.parameter || '';
console.log('Parameter field:', parameterField);
console.log('Fetching drilldown data for level:', this.currentDrilldownLevel, {
apiUrl: drilldownConfig.apiUrl,
xAxis: drilldownConfig.xAxis,
yAxis: drilldownConfig.yAxis,
parameterField: parameterField,
parameterValue: parameterValue,
connection: this.connection
});
// Build the actual API URL with parameter replacement
let actualApiUrl = drilldownConfig.apiUrl;
console.log('Original API URL:', actualApiUrl);
console.log('Parameter value to use:', parameterValue);
console.log('Parameter field:', parameterField);
// Check if the URL contains angle brackets for parameter replacement
const hasAngleBrackets = /<[^>]+>/.test(actualApiUrl);
if (hasAngleBrackets && parameterValue) {
// Replace angle brackets placeholder with actual value
console.log('Replacing angle brackets with parameter value');
const encodedValue = encodeURIComponent(parameterValue);
actualApiUrl = actualApiUrl.replace(/<[^>]+>/g, encodedValue);
console.log('URL after angle bracket replacement:', actualApiUrl);
}
// Log the URL that will be called
let url = `chart/getdashjson/grid?tableName=${actualApiUrl}&xAxis=${drilldownConfig.xAxis}&yAxes=${drilldownConfig.yAxis}${this.connection ? `&sureId=${this.connection}` : ''}`;
if (parameterField && parameterValue) {
url += `&parameter=${encodeURIComponent(parameterField)}&parameterValue=${encodeURIComponent(parameterValue)}`;
}
console.log('Drilldown data URL:', url);
// Convert drilldown layer filters to filter parameters (if applicable)
const filterObj = {};
// Add drilldown layer filters
if (drilldownConfig.filters && drilldownConfig.filters.length > 0) {
drilldownConfig.filters.forEach((filter: any) => {
if (filter.field && filter.value) {
filterObj[filter.field] = filter.value;
}
});
}
// Add drilldownFilters
if (this.drilldownFilters && this.drilldownFilters.length > 0) {
this.drilldownFilters.forEach(filter => {
if (filter.field && filter.value) {
filterObj[filter.field] = filter.value;
}
});
}
// Add common filters
const commonFilters = this.filterService.getFilterValues();
const filterDefinitions = this.filterService.getFilters();
Object.keys(commonFilters).forEach(filterId => {
const filterValue = commonFilters[filterId];
// Find the filter definition to get the field name
const filterDef = this.filterService.getFilters().find(f => f.id === filterId);
if (filterDef && filterDef.field) {
const fieldName = filterDef.field;
if (filterValue !== undefined && filterValue !== null && filterValue !== '') {
filterObj[fieldName] = filterValue;
}
}
});
// Convert to JSON string for API call
let drilldownFilterParams = '';
if (Object.keys(filterObj).length > 0) {
drilldownFilterParams = JSON.stringify(filterObj);
}
console.log('Drilldown filter parameters:', drilldownFilterParams);
// For drilldown level, we pass the parameter value from the drilldown stack and drilldown filters
this.dashboardService.getChartData(
drilldownConfig.apiUrl, 'grid',
drilldownConfig.xAxis, drilldownConfig.yAxis,
this.connection,
parameterField, parameterValue,
drilldownFilterParams
).subscribe(
(data: any) => {
console.log('Received drilldown data:', data);
if (data === null) {
console.warn('Drilldown API returned null data. Check if the API endpoint is working correctly.');
this.error = "No data Available";
this.givendata = [];
this.noDataAvailable = true;
return;
}
// Handle the actual data structure returned by the API
if (data && data.chartData) {
this.givendata = data.chartData;
this.extractDynamicHeaders(data.chartData);
this.error = this.givendata.length === 0 ? "No data Available" : undefined;
this.noDataAvailable = this.givendata.length === 0;
console.log('Updated grid with drilldown data:', this.givendata);
} else if (data && data.data) {
// Handle the original expected format as fallback
this.givendata = data.data;
this.extractDynamicHeaders(data.data);
this.error = this.givendata.length === 0 ? "No data Available" : undefined;
this.noDataAvailable = this.givendata.length === 0;
console.log('Updated grid with drilldown legacy data format:', this.givendata);
} else if (Array.isArray(data)) {
// Handle case where data is directly an array
this.givendata = data;
this.extractDynamicHeaders(data);
this.error = this.givendata.length === 0 ? "No data Available" : undefined;
this.noDataAvailable = this.givendata.length === 0;
console.log('Updated grid with drilldown array data:', this.givendata);
} else {
console.warn('Drilldown received data does not have expected structure', data);
this.error = "No valid data received";
this.givendata = [];
this.noDataAvailable = true;
}
},
(error) => {
console.error('Error fetching drilldown data:', error);
this.error = "Server Error";
this.givendata = [];
this.noDataAvailable = true;
}
);
}
// Reset to original data (go back to base level)
resetToOriginalData(): void {
console.log('Resetting to original data');
console.log('Current stack before reset:', this.drilldownStack);
console.log('Current level before reset:', this.currentDrilldownLevel);
this.currentDrilldownLevel = 0;
this.drilldownStack = [];
if (this.originalGridData.length > 0) {
// Create a deep copy to avoid reference issues
this.givendata = JSON.parse(JSON.stringify(this.originalGridData));
this.extractDynamicHeaders(this.givendata);
console.log('Restored original data');
}
console.log('After reset - data:', this.givendata);
// Re-fetch original data
this.fetchGridData();
}
// Navigate back to previous drilldown level
navigateBack(): void {
console.log('Navigating back, current stack:', this.drilldownStack);
console.log('Current level:', this.currentDrilldownLevel);
if (this.drilldownStack.length > 0) {
// Remove the last entry from the stack
const removedEntry = this.drilldownStack.pop();
console.log('Removed entry from stack:', removedEntry);
// Update the current drilldown level
this.currentDrilldownLevel = this.drilldownStack.length;
console.log('New level after pop:', this.currentDrilldownLevel);
console.log('Stack after pop:', this.drilldownStack);
if (this.drilldownStack.length > 0) {
// Fetch data for the previous level
console.log('Fetching data for previous level');
this.fetchDrilldownData();
} else {
// Back to base level
console.log('Back to base level, resetting to original data');
this.resetToOriginalData();
}
} else {
// Already at base level, reset to original data
console.log('Already at base level, resetting to original data');
this.resetToOriginalData();
}
}
// Method to handle grid row clicks for drilldown
onRowClick(item: any, key: string): void {
console.log('Grid row clicked:', { item, key });
// If drilldown is enabled
if (this.drilldownEnabled) {
// Get the value for the clicked key
const clickedValue = item[key];
console.log('Clicked on row value:', { key, value: clickedValue });
// If we're not at the base level, store original data
if (this.currentDrilldownLevel === 0) {
// Store original data before entering drilldown mode
// Create a deep copy to avoid reference issues
this.originalGridData = JSON.parse(JSON.stringify(this.givendata));
console.log('Stored original data for drilldown');
}
// Determine the next drilldown level
const nextDrilldownLevel = this.currentDrilldownLevel + 1;
console.log('Next drilldown level will be:', nextDrilldownLevel);
// Check if there's a drilldown configuration for this level
let hasDrilldownConfig = false;
let drilldownConfig;
if (nextDrilldownLevel === 1) {
// Base drilldown level
drilldownConfig = {
apiUrl: this.drilldownApiUrl,
xAxis: this.drilldownXAxis,
yAxis: this.drilldownYAxis,
parameter: this.drilldownParameter
};
hasDrilldownConfig = !!this.drilldownApiUrl && !!this.drilldownXAxis && !!this.drilldownYAxis;
} else {
// Multi-layer drilldown level
const layerIndex = nextDrilldownLevel - 2; // -2 because level 1 is base drilldown
if (layerIndex < this.drilldownLayers.length) {
drilldownConfig = this.drilldownLayers[layerIndex];
hasDrilldownConfig = drilldownConfig.enabled &&
!!drilldownConfig.apiUrl &&
!!drilldownConfig.xAxis &&
!!drilldownConfig.yAxis;
}
}
console.log('Drilldown config for next level:', drilldownConfig);
console.log('Has drilldown config:', hasDrilldownConfig);
// If there's a drilldown configuration for the next level, proceed
if (hasDrilldownConfig) {
// Add this click to the drilldown stack
const stackEntry = {
level: nextDrilldownLevel,
clickedKey: key,
clickedValue: clickedValue
};
this.drilldownStack.push(stackEntry);
console.log('Added to drilldown stack:', stackEntry);
console.log('Current drilldown stack:', this.drilldownStack);
// Update the current drilldown level
this.currentDrilldownLevel = nextDrilldownLevel;
console.log('Entering drilldown level:', this.currentDrilldownLevel);
// Fetch drilldown data for the new level
this.fetchDrilldownData();
} else {
console.log('No drilldown configuration for level:', nextDrilldownLevel);
}
} else {
console.log('Drilldown not enabled');
} }
} }
@@ -175,4 +627,23 @@ export class GridViewComponent implements OnInit, OnChanges {
.replace(/([A-Z])/g, ' $1') .replace(/([A-Z])/g, ' $1')
.replace(/^./, str => str.toUpperCase()); .replace(/^./, str => str.toUpperCase());
} }
ngOnDestroy() {
// Unsubscribe from all subscriptions to prevent memory leaks
console.log('GridViewComponent ngOnDestroy called, unsubscribing from', this.subscriptions.length, 'subscriptions');
this.subscriptions.forEach(subscription => {
if (subscription && !subscription.closed) {
subscription.unsubscribe();
}
});
this.subscriptions = [];
// Clear data to help with garbage collection
this.givendata = [];
this.dynamicHeaders = [];
this.drilldownStack = [];
this.originalGridData = [];
console.log('GridViewComponent destroyed and cleaned up');
}
} }

View File

@@ -1,5 +1,7 @@
import { Component, OnInit, Input, OnChanges, SimpleChanges } from '@angular/core'; import { Component, OnInit, Input, OnChanges, SimpleChanges, OnDestroy } from '@angular/core';
import { Dashboard3Service } from 'src/app/services/builder/dashboard3.service'; import { Dashboard3Service } from 'src/app/services/builder/dashboard3.service';
import { FilterService } from '../../common-filter/filter.service';
import { Subscription } from 'rxjs';
@Component({ @Component({
selector: 'app-line-chart', selector: 'app-line-chart',
@@ -83,9 +85,23 @@ export class LineChartComponent implements OnInit, OnChanges {
// Flag to prevent infinite loops // Flag to prevent infinite loops
private isFetchingData: boolean = false; private isFetchingData: boolean = false;
constructor(private dashboardService: Dashboard3Service) { } // Subscriptions to unsubscribe on destroy
private subscriptions: Subscription[] = [];
constructor(
private dashboardService: Dashboard3Service,
private filterService: FilterService
) { }
ngOnInit(): void { ngOnInit(): void {
// Subscribe to filter changes
this.subscriptions.push(
this.filterService.filterState$.subscribe(filters => {
// When filters change, refresh the chart data
this.fetchChartData();
})
);
// Initialize with default data // Initialize with default data
this.fetchChartData(); this.fetchChartData();
} }
@@ -122,6 +138,15 @@ export class LineChartComponent implements OnInit, OnChanges {
} }
} }
ngOnDestroy(): void {
this.subscriptions.forEach(sub => sub.unsubscribe());
}
// Public method to refresh data when filters change
refreshData(): void {
this.fetchChartData();
}
fetchChartData(): void { fetchChartData(): void {
// Set flag to prevent recursive calls // Set flag to prevent recursive calls
this.isFetchingData = true; this.isFetchingData = true;
@@ -154,7 +179,49 @@ export class LineChartComponent implements OnInit, OnChanges {
filterParams = JSON.stringify(filterObj); filterParams = JSON.stringify(filterObj);
} }
} }
console.log('Base filter parameters:', filterParams);
// Add common filters to filter parameters
const commonFilters = this.filterService.getFilterValues();
console.log('Common filters from service:', commonFilters);
if (Object.keys(commonFilters).length > 0) {
// Merge common filters with base filters
const mergedFilterObj = {};
// Add base filters first
if (filterParams) {
try {
const baseFilterObj = JSON.parse(filterParams);
Object.assign(mergedFilterObj, baseFilterObj);
} catch (e) {
console.warn('Failed to parse base filter parameters:', e);
}
}
// Add common filters using the field name as the key, not the filter id
Object.keys(commonFilters).forEach(filterId => {
const filterValue = commonFilters[filterId];
// Find the filter definition to get the field name
const filterDef = this.filterService.getFilters().find(f => f.id === filterId);
if (filterDef && filterDef.field) {
const fieldName = filterDef.field;
if (filterValue !== undefined && filterValue !== null && filterValue !== '') {
mergedFilterObj[fieldName] = filterValue;
}
} else {
// Fallback to using filterId as field name if no field is defined
if (filterValue !== undefined && filterValue !== null && filterValue !== '') {
mergedFilterObj[filterId] = filterValue;
}
}
});
if (Object.keys(mergedFilterObj).length > 0) {
filterParams = JSON.stringify(mergedFilterObj);
}
}
console.log('Final filter parameters:', filterParams);
// Log the URL that will be called // Log the URL that will be called
const url = `chart/getdashjson/line?tableName=${this.table}&xAxis=${this.xAxis}&yAxes=${yAxisString}${this.connection ? `&sureId=${this.connection}` : ''}`; const url = `chart/getdashjson/line?tableName=${this.table}&xAxis=${this.xAxis}&yAxes=${yAxisString}${this.connection ? `&sureId=${this.connection}` : ''}`;
@@ -313,6 +380,35 @@ export class LineChartComponent implements OnInit, OnChanges {
} }
} }
// Add common filters to drilldown filter parameters
const commonFilters = this.filterService.getFilterValues();
if (Object.keys(commonFilters).length > 0) {
// Merge common filters with drilldown filters
const mergedFilterObj = {};
// Add drilldown filters first
if (filterParams) {
try {
const drilldownFilterObj = JSON.parse(filterParams);
Object.assign(mergedFilterObj, drilldownFilterObj);
} catch (e) {
console.warn('Failed to parse drilldown filter parameters:', e);
}
}
// Add common filters
Object.keys(commonFilters).forEach(key => {
const value = commonFilters[key];
if (value !== undefined && value !== null && value !== '') {
mergedFilterObj[key] = value;
}
});
if (Object.keys(mergedFilterObj).length > 0) {
filterParams = JSON.stringify(mergedFilterObj);
}
}
// Log the URL that will be called // Log the URL that will be called
const url = `chart/getdashjson/line?tableName=${actualApiUrl}&xAxis=${drilldownConfig.xAxis}&yAxes=${drilldownConfig.yAxis}${this.connection ? `&sureId=${this.connection}` : ''}`; const url = `chart/getdashjson/line?tableName=${actualApiUrl}&xAxis=${drilldownConfig.xAxis}&yAxes=${drilldownConfig.yAxis}${this.connection ? `&sureId=${this.connection}` : ''}`;
console.log('Drilldown data URL:', url); console.log('Drilldown data URL:', url);

View File

@@ -1,5 +1,7 @@
import { Component, OnInit, Input, OnChanges, SimpleChanges, AfterViewChecked } from '@angular/core'; import { Component, OnInit, Input, OnChanges, SimpleChanges, AfterViewChecked, OnDestroy } from '@angular/core';
import { Dashboard3Service } from 'src/app/services/builder/dashboard3.service'; import { Dashboard3Service } from 'src/app/services/builder/dashboard3.service';
import { FilterService } from '../../common-filter/filter.service';
import { Subscription } from 'rxjs';
@Component({ @Component({
selector: 'app-pie-chart', selector: 'app-pie-chart',
@@ -96,7 +98,13 @@ export class PieChartComponent implements OnInit, OnChanges, AfterViewChecked {
// Flag to prevent infinite loops // Flag to prevent infinite loops
private isFetchingData: boolean = false; private isFetchingData: boolean = false;
constructor(private dashboardService: Dashboard3Service) { } // Subscriptions to unsubscribe on destroy
private subscriptions: Subscription[] = [];
constructor(
private dashboardService: Dashboard3Service,
private filterService: FilterService
) { }
/** /**
* Force chart redraw * Force chart redraw
@@ -108,6 +116,14 @@ export class PieChartComponent implements OnInit, OnChanges, AfterViewChecked {
} }
ngOnInit(): void { ngOnInit(): void {
// Subscribe to filter changes
this.subscriptions.push(
this.filterService.filterState$.subscribe(filters => {
// When filters change, refresh the chart data
this.fetchChartData();
})
);
console.log('PieChartComponent initialized with default data:', { labels: this.pieChartLabels, data: this.pieChartData }); console.log('PieChartComponent initialized with default data:', { labels: this.pieChartLabels, data: this.pieChartData });
// Validate initial data // Validate initial data
this.validateChartData(); this.validateChartData();
@@ -140,6 +156,15 @@ export class PieChartComponent implements OnInit, OnChanges, AfterViewChecked {
} }
} }
ngOnDestroy(): void {
this.subscriptions.forEach(sub => sub.unsubscribe());
}
// Public method to refresh data when filters change
refreshData(): void {
this.fetchChartData();
}
fetchChartData(): void { fetchChartData(): void {
// Set flag to prevent recursive calls // Set flag to prevent recursive calls
this.isFetchingData = true; this.isFetchingData = true;
@@ -172,7 +197,49 @@ export class PieChartComponent implements OnInit, OnChanges, AfterViewChecked {
filterParams = JSON.stringify(filterObj); filterParams = JSON.stringify(filterObj);
} }
} }
console.log('Base filter parameters:', filterParams);
// Add common filters to filter parameters
const commonFilters = this.filterService.getFilterValues();
console.log('Common filters from service:', commonFilters);
if (Object.keys(commonFilters).length > 0) {
// Merge common filters with base filters
const mergedFilterObj = {};
// Add base filters first
if (filterParams) {
try {
const baseFilterObj = JSON.parse(filterParams);
Object.assign(mergedFilterObj, baseFilterObj);
} catch (e) {
console.warn('Failed to parse base filter parameters:', e);
}
}
// Add common filters using the field name as the key, not the filter id
Object.keys(commonFilters).forEach(filterId => {
const filterValue = commonFilters[filterId];
// Find the filter definition to get the field name
const filterDef = this.filterService.getFilters().find(f => f.id === filterId);
if (filterDef && filterDef.field) {
const fieldName = filterDef.field;
if (filterValue !== undefined && filterValue !== null && filterValue !== '') {
mergedFilterObj[fieldName] = filterValue;
}
} else {
// Fallback to using filterId as field name if no field is defined
if (filterValue !== undefined && filterValue !== null && filterValue !== '') {
mergedFilterObj[filterId] = filterValue;
}
}
});
if (Object.keys(mergedFilterObj).length > 0) {
filterParams = JSON.stringify(mergedFilterObj);
}
}
console.log('Final filter parameters:', filterParams);
// Log the URL that will be called // Log the URL that will be called
const url = `chart/getdashjson/pie?tableName=${this.table}&xAxis=${this.xAxis}&yAxes=${yAxisString}${this.connection ? `&sureId=${this.connection}` : ''}`; const url = `chart/getdashjson/pie?tableName=${this.table}&xAxis=${this.xAxis}&yAxes=${yAxisString}${this.connection ? `&sureId=${this.connection}` : ''}`;
@@ -362,6 +429,35 @@ export class PieChartComponent implements OnInit, OnChanges, AfterViewChecked {
} }
} }
// Add common filters to drilldown filter parameters
const commonFilters = this.filterService.getFilterValues();
if (Object.keys(commonFilters).length > 0) {
// Merge common filters with drilldown filters
const mergedFilterObj = {};
// Add drilldown filters first
if (filterParams) {
try {
const drilldownFilterObj = JSON.parse(filterParams);
Object.assign(mergedFilterObj, drilldownFilterObj);
} catch (e) {
console.warn('Failed to parse drilldown filter parameters:', e);
}
}
// Add common filters
Object.keys(commonFilters).forEach(key => {
const value = commonFilters[key];
if (value !== undefined && value !== null && value !== '') {
mergedFilterObj[key] = value;
}
});
if (Object.keys(mergedFilterObj).length > 0) {
filterParams = JSON.stringify(mergedFilterObj);
}
}
// Log the URL that will be called // Log the URL that will be called
const url = `chart/getdashjson/pie?tableName=${actualApiUrl}&xAxis=${drilldownConfig.xAxis}&yAxes=${drilldownConfig.yAxis}${this.connection ? `&sureId=${this.connection}` : ''}`; const url = `chart/getdashjson/pie?tableName=${actualApiUrl}&xAxis=${drilldownConfig.xAxis}&yAxes=${drilldownConfig.yAxis}${this.connection ? `&sureId=${this.connection}` : ''}`;
console.log('Drilldown data URL:', url); console.log('Drilldown data URL:', url);

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,3 @@
<div class="sidebar-filters">
<!-- Component Palette Button and List have been moved to the main shield dashboard component -->
</div>

View File

@@ -0,0 +1,25 @@
<div class="sidebar-filters">
<!-- Component Palette Button -->
<div class="component-palette-section">
<button class="component-palette-button" (click)="toggleComponentPalette()">
<clr-icon shape="plus-circle"></clr-icon>
Add Components
</button>
<!-- Component Palette (hidden by default) -->
<div class="component-palette" *ngIf="showComponentPalette">
<h3 class="palette-title">Available Components</h3>
<div class="component-list">
<div
*ngFor="let component of availableComponents"
class="component-item"
draggable="true"
(dragstart)="onComponentDragStart($event, component)"
(dragend)="onComponentDragEnd($event)">
<clr-icon shape="drag-handle" class="drag-icon"></clr-icon>
{{ component.name }}
</div>
</div>
</div>
</div>
</div>

View File

@@ -0,0 +1,216 @@
.sidebar-filters {
height: 100%;
display: flex;
flex-direction: column;
.componentbtn {
margin: 10px;
width: calc(100% - 20px);
}
.nav-list {
padding: 0;
margin: 0 10px;
.nav-link {
display: flex;
align-items: center;
padding: 8px 12px;
margin-bottom: 5px;
background: #f8f9fa;
border: 1px solid #e9ecef;
border-radius: 4px;
cursor: move;
&:hover {
background: #e9ecef;
}
.has-badge {
margin-left: auto;
}
}
}
.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;
}
}
.component-palette-section {
margin: 20px;
.component-palette-button {
width: 100%;
padding: 12px;
background: rgba(255, 107, 53, 0.2);
color: white;
border: 1px solid rgba(255, 107, 53, 0.5);
border-radius: 8px;
font-size: 16px;
font-weight: 500;
cursor: pointer;
transition: all 0.2s ease;
display: flex;
align-items: center;
justify-content: center;
gap: 8px;
&:hover {
background: rgba(255, 107, 53, 0.3);
border-color: #ff6b35;
}
}
.component-palette {
margin-top: 15px;
background: rgba(255, 255, 255, 0.05);
border-radius: 8px;
padding: 15px;
.palette-title {
font-size: 16px;
color: white;
margin: 0 0 15px 0;
text-align: center;
}
.component-list {
display: flex;
flex-direction: column;
gap: 10px;
.component-item {
padding: 10px 15px;
background: rgba(255, 255, 255, 0.1);
border: 1px solid rgba(255, 255, 255, 0.2);
border-radius: 6px;
color: white;
cursor: move;
display: flex;
align-items: center;
gap: 10px;
transition: all 0.2s ease;
&:hover {
background: rgba(255, 107, 53, 0.2);
border-color: #ff6b35;
}
.drag-icon {
font-size: 16px;
}
}
}
}
}
.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,16 @@
import { Component, OnInit } from '@angular/core';
import { DashboardFilterService } from '../../services/dashboard-filter.service';
@Component({
selector: 'app-shield-sidebar-filters',
templateUrl: './sidebar-filters.component.html',
styleUrls: ['./sidebar-filters.component.scss']
})
export class SidebarFiltersComponent implements OnInit {
constructor(private filterService: DashboardFilterService) { }
ngOnInit(): void {
// Component initialization
}
}

View File

@@ -0,0 +1,16 @@
import { Component, OnInit } from '@angular/core';
import { DashboardFilterService } from '../../services/dashboard-filter.service';
@Component({
selector: 'app-shield-sidebar-filters',
templateUrl: './sidebar-filters.component.html',
styleUrls: ['./sidebar-filters.component.scss']
})
export class SidebarFiltersComponent implements OnInit {
constructor(private filterService: DashboardFilterService) { }
ngOnInit(): void {
// Component initialization
}
}

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,493 @@
<div class="shield-dashboard">
<div class="dashboard-container">
<!-- Sidebar Filters -->
<div class="sidebar">
<button class="btn componentbtn" (click)="toggleComponentPalette()">
<clr-icon shape="plus"></clr-icon> Component
</button>
<ul class="nav-list" style="list-style-type: none;" *ngIf="showComponentPalette">
<li *ngFor="let widget of WidgetsMock">
<!--
Draggable widget from store using vanilla javascript event (dragstart)
onDrag() is call, it take $event and a widget identifier as parameters
-->
<a draggable="true" class="nav-link" (dragstart)="onDrag($event, widget.identifier)">
<clr-icon shape="drag-handle" style="margin-right: 10px;"></clr-icon>
{{ widget.name }}
<clr-icon shape="plugin" class="has-badge"></clr-icon>
</a>
</li>
</ul>
</div>
<!-- Main Dashboard Content -->
<div class="main-content">
<!-- Deleted Items Section -->
<div class="deleted-items-section" *ngIf="deletedItems.length > 0">
<h3>Deleted Items</h3>
<div class="deleted-items-list">
<div *ngFor="let item of deletedItems" class="deleted-item">
<span>{{ item.name }}</span>
<button class="btn btn-sm btn-primary" (click)="restoreItem(item)">
<clr-icon shape="undo"></clr-icon> Restore
</button>
</div>
</div>
<button class="btn btn-sm btn-danger" (click)="clearDeletedItems()">
<clr-icon shape="trash"></clr-icon> Clear All
</button>
</div>
<!-- Dashboard Grid with Drag and Drop -->
<gridster [options]="options" (drop)="onDrop($event)" style="background-color: transparent; min-height: 500px;">
<gridster-item [item]="item" *ngFor="let item of dashboard">
<!-- Remove Button -->
<button class="btn btn-icon btn-danger" style="margin-left: 10px; margin-top: 10px;" (click)="removeItem(item)">
<clr-icon shape="trash"></clr-icon>
</button>
<!-- Edit Button -->
<button class="btn btn-icon" style="margin-top: 10px; float: right;" (click)="editGadget(item)">
<clr-icon shape="pencil"></clr-icon>
</button>
<!-- Drag Handle -->
<button class="btn btn-icon drag-handler" style="margin-left: 10px; margin-top: 10px;">
<clr-icon shape="drag-handle"></clr-icon>
</button>
<!-- Chart Components -->
<div class="grid-item-content">
<h4 style="margin-top: 0px; margin-left: 10px;">{{item.name}}</h4>
<div *ngIf="item.chartType === 'bar-chart'">
<app-shield-bar-chart></app-shield-bar-chart>
</div>
<div *ngIf="item.chartType === 'donut-chart' && item.name === 'End Customer Donut'">
<app-shield-donut-chart chartType="endCustomer"></app-shield-donut-chart>
</div>
<div *ngIf="item.chartType === 'donut-chart' && item.name === 'Segment Penetration Donut'">
<app-shield-donut-chart chartType="segmentPenetration"></app-shield-donut-chart>
</div>
<div *ngIf="item.chartType === 'map-chart'">
<app-shield-map-chart></app-shield-map-chart>
</div>
<div *ngIf="item.chartType === 'data-table'">
<app-shield-data-table></app-shield-data-table>
</div>
<div *ngIf="item.chartType === 'deal-details'">
<app-shield-deal-details-card></app-shield-deal-details-card>
</div>
<div *ngIf="item.chartType === 'quarterwise-flow'">
<app-shield-quarterwise-flow></app-shield-quarterwise-flow>
</div>
</div>
</gridster-item>
</gridster>
</div>
</div>
</div>
<!-- Configuration Modal -->
<clr-modal [(clrModalOpen)]="modeledit" [clrModalStaticBackdrop]="true" clrModalSize="lg">
<h3 class="modal-title">Configure Chart</h3>
<div class="modal-body" *ngIf="selectedItem">
<form [formGroup]="configForm" class="clr-form-horizontal">
<!-- Chart Title -->
<div class="clr-row">
<div class="clr-col-12">
<clr-input-container>
<label for="charttitle">Chart Title</label>
<input id="charttitle" type="text" formControlName="charttitle" clrInput />
<clr-control-helper>Enter a descriptive title for your chart</clr-control-helper>
</clr-input-container>
</div>
</div>
<!-- Connection Selection Field -->
<div class="clr-row">
<div class="clr-col-12">
<clr-select-container>
<label for="connection">Connection</label>
<select id="connection" formControlName="connection" clrSelect>
<option value="">Select Connection</option>
<option *ngFor="let conn of sureconnectData" [value]="conn.id">
{{conn.connection_name || conn.id}}
</option>
</select>
<clr-control-helper>Select a SureConnect connection to use for this chart</clr-control-helper>
</clr-select-container>
</div>
</div>
<!-- Chart Options (except for Data Table and Deal Details) -->
<div class="clr-row" *ngIf="selectedItem?.name !== 'Data Table' && selectedItem?.name !== 'Deal Details'">
<div class="clr-col-12">
<div class="clr-form-control clr-row">
<div class="clr-col-12">
<clr-checkbox-container>
<clr-checkbox-wrapper>
<input type="checkbox" id="chartlegend" formControlName="chartlegend" clrCheckbox />
<label for="chartlegend">Show Chart Legend</label>
</clr-checkbox-wrapper>
<clr-checkbox-wrapper>
<input type="checkbox" id="showlabel" formControlName="showlabel" clrCheckbox />
<label for="showlabel">Show Chart Label</label>
</clr-checkbox-wrapper>
</clr-checkbox-container>
</div>
</div>
</div>
</div>
<!-- API URL -->
<div class="clr-row">
<div class="clr-col-12">
<clr-input-container>
<label for="table">API URL</label>
<div class="clr-input-group">
<input type="text" id="table" formControlName="table" clrInput />
<button class="btn btn-icon btn-primary" (click)="getColumns(configForm.value.connection, configForm.value.table)" type="button">
<clr-icon shape="refresh"></clr-icon>
</button>
</div>
<clr-control-helper>Enter the API endpoint for your data source</clr-control-helper>
</clr-input-container>
</div>
</div>
<!-- X-Axis (except for Data Table and Deal Details) -->
<div class="clr-row" *ngIf="selectedItem?.name !== 'Data Table' && selectedItem?.name !== 'Deal Details'">
<div class="clr-col-12">
<clr-select-container>
<label for="xAxis">X-Axis</label>
<select id="xAxis" formControlName="xAxis" clrSelect>
<option value="">Select Column</option>
<option *ngFor="let data of columnData" [value]="data">{{data}}</option>
</select>
<clr-control-helper>Select the column to use for the X-axis</clr-control-helper>
</clr-select-container>
</div>
</div>
<!-- Y-Axis (except for Data Table and Deal Details) -->
<div class="clr-row" *ngIf="selectedItem?.name !== 'Data Table' && selectedItem?.name !== 'Deal Details'">
<div class="clr-col-12">
<clr-combobox-container>
<label for="yAxis">Y-Axis (Numeric)</label>
<clr-combobox id="yAxis" formControlName="yAxis" clrMulti="true" required>
<ng-container *clrOptionSelected="let selected">
{{selected}}
</ng-container>
<clr-options>
<clr-option *clrOptionItems="let state of columnData" [clrValue]="state">
{{state}}
</clr-option>
</clr-options>
</clr-combobox>
<clr-control-helper>Select one or more columns for the Y-axis</clr-control-helper>
</clr-combobox-container>
</div>
</div>
<!-- Base API Filters Section -->
<div class="clr-row section-divider">
<div class="clr-col-12">
<h4>Base API Filters</h4>
<p class="clr-subtext">Configure filters for the main API (applied regardless of drilldown settings)</p>
<!-- Add Base Filter Button -->
<button class="btn btn-sm btn-primary" (click)="addBaseFilter()" type="button">
<clr-icon shape="plus"></clr-icon> Add Filter
</button>
<!-- Base Filter Fields List -->
<div *ngFor="let filter of selectedItem?.baseFilters; let i = index" class="filter-item">
<div class="filter-header">
<span>Filter {{i + 1}}</span>
<button class="btn btn-icon btn-danger btn-sm" (click)="removeBaseFilter(i)" type="button">
<clr-icon shape="trash"></clr-icon>
</button>
</div>
<div class="clr-row filter-content">
<div class="clr-col-5">
<clr-input-container>
<label [attr.for]="'baseFilterField' + i">Field Name</label>
<input type="text" [id]="'baseFilterField' + i" [(ngModel)]="filter.field" [ngModelOptions]="{standalone: true}" clrInput />
</clr-input-container>
</div>
<div class="clr-col-5">
<clr-input-container>
<label [attr.for]="'baseFilterValue' + i">Filter Value</label>
<input type="text" [id]="'baseFilterValue' + i" [(ngModel)]="filter.value" [ngModelOptions]="{standalone: true}" clrInput />
</clr-input-container>
</div>
<div class="clr-col-2">
<button class="btn btn-icon btn-danger btn-sm" (click)="removeBaseFilter(i)" type="button">
<clr-icon shape="trash"></clr-icon>
</button>
</div>
</div>
</div>
</div>
</div>
<!-- Base Drilldown Configuration Section -->
<div class="clr-row section-divider">
<div class="clr-col-12">
<h4>Base Drilldown Configuration</h4>
<clr-checkbox-container>
<clr-checkbox-wrapper>
<input type="checkbox" id="drilldownEnabled" [(ngModel)]="selectedItem.drilldownEnabled" [ngModelOptions]="{standalone: true}" clrCheckbox />
<label for="drilldownEnabled">Enable Base Drilldown</label>
</clr-checkbox-wrapper>
</clr-checkbox-container>
<p class="clr-subtext">Enable drilldown functionality for this chart</p>
</div>
</div>
<!-- Drilldown API URL -->
<div class="clr-row" *ngIf="selectedItem?.drilldownEnabled">
<div class="clr-col-12">
<clr-input-container>
<label for="drilldownApiUrl">Base Drilldown API URL</label>
<div class="clr-input-group">
<input type="text" id="drilldownApiUrl" [(ngModel)]="selectedItem.drilldownApiUrl" [ngModelOptions]="{standalone: true}" clrInput />
<button class="btn btn-icon btn-primary" (click)="refreshDrilldownColumns()" [disabled]="!selectedItem.drilldownApiUrl" type="button">
<clr-icon shape="refresh"></clr-icon>
</button>
</div>
<clr-control-helper>Enter the API URL for base drilldown data. Use angle brackets for parameters, e.g., http://api.example.com/data/&lt;country&gt;</clr-control-helper>
</clr-input-container>
</div>
</div>
<!-- Drilldown X-Axis -->
<div class="clr-row" *ngIf="selectedItem?.drilldownEnabled">
<div class="clr-col-12">
<clr-select-container>
<label for="drilldownXAxis">Base Drilldown X-Axis</label>
<select id="drilldownXAxis" [(ngModel)]="selectedItem.drilldownXAxis" [ngModelOptions]="{standalone: true}" clrSelect>
<option value="">Select X-Axis Column</option>
<option *ngFor="let column of drilldownColumnData" [value]="column">{{column}}</option>
</select>
<clr-control-helper>Select the column to use for X-axis in base drilldown view</clr-control-helper>
</clr-select-container>
</div>
</div>
<!-- Drilldown Y-Axis (except for Deal Details) -->
<div class="clr-row" *ngIf="selectedItem?.drilldownEnabled && selectedItem?.name !== 'Deal Details'">
<div class="clr-col-12">
<clr-select-container>
<label for="drilldownYAxis">Base Drilldown Y-Axis</label>
<select id="drilldownYAxis" [(ngModel)]="selectedItem.drilldownYAxis" [ngModelOptions]="{standalone: true}" clrSelect>
<option value="">Select Y-Axis Column</option>
<option *ngFor="let column of drilldownColumnData" [value]="column">{{column}}</option>
</select>
<clr-control-helper>Select the column to use for Y-axis in base drilldown view</clr-control-helper>
</clr-select-container>
</div>
</div>
<!-- Drilldown Parameter -->
<div class="clr-row" *ngIf="selectedItem?.drilldownEnabled">
<div class="clr-col-12">
<clr-select-container>
<label for="drilldownParameter">Base Drilldown Parameter</label>
<select id="drilldownParameter" [(ngModel)]="selectedItem.drilldownParameter" [ngModelOptions]="{standalone: true}" clrSelect>
<option value="">Select Parameter Column</option>
<option *ngFor="let column of drilldownColumnData" [value]="column">{{column}}</option>
</select>
<clr-control-helper>Select the column to use as parameter for URL template replacement in base drilldown</clr-control-helper>
</clr-select-container>
</div>
</div>
<!-- Base Drilldown Filter Configuration -->
<div class="clr-row section-divider" *ngIf="selectedItem?.drilldownEnabled">
<div class="clr-col-12">
<h5>Base Drilldown Filters</h5>
<p class="clr-subtext">Configure filters for the base drilldown level</p>
<!-- Add Drilldown Filter Button -->
<button class="btn btn-sm btn-primary" (click)="addDrilldownFilter()" type="button">
<clr-icon shape="plus"></clr-icon> Add Filter
</button>
<!-- Drilldown Filter Fields List -->
<div *ngFor="let filter of selectedItem?.drilldownFilters; let i = index" class="filter-item">
<div class="filter-header">
<span>Filter {{i + 1}}</span>
<button class="btn btn-icon btn-danger btn-sm" (click)="removeDrilldownFilter(i)" type="button">
<clr-icon shape="trash"></clr-icon>
</button>
</div>
<div class="clr-row filter-content">
<div class="clr-col-5">
<clr-input-container>
<label [attr.for]="'drilldownFilterField' + i">Field Name</label>
<input type="text" [id]="'drilldownFilterField' + i" [(ngModel)]="filter.field" [ngModelOptions]="{standalone: true}" clrInput />
</clr-input-container>
</div>
<div class="clr-col-5">
<clr-input-container>
<label [attr.for]="'drilldownFilterValue' + i">Filter Value</label>
<input type="text" [id]="'drilldownFilterValue' + i" [(ngModel)]="filter.value" [ngModelOptions]="{standalone: true}" clrInput />
</clr-input-container>
</div>
<div class="clr-col-2">
<button class="btn btn-icon btn-danger btn-sm" (click)="removeDrilldownFilter(i)" type="button">
<clr-icon shape="trash"></clr-icon>
</button>
</div>
</div>
</div>
</div>
</div>
<!-- Multi-Layer Drilldown Configurations -->
<div class="clr-row section-divider" *ngIf="selectedItem?.drilldownEnabled">
<div class="clr-col-12">
<h4>Multi-Layer Drilldown Configurations</h4>
<button class="btn btn-sm btn-primary" (click)="addDrilldownLayer()" type="button">
<clr-icon shape="plus"></clr-icon> Add Drilldown Layer
</button>
<p class="clr-subtext">Add additional drilldown layers for multi-level navigation</p>
</div>
</div>
<!-- Dynamic Drilldown Layers -->
<div class="clr-row" *ngFor="let layer of selectedItem?.drilldownLayers; let i = index">
<div class="clr-col-12 drilldown-layer">
<div class="layer-header">
<h5>Drilldown Layer {{i + 1}}</h5>
<button class="btn btn-icon btn-danger btn-sm" (click)="removeDrilldownLayer(i)" type="button">
<clr-icon shape="trash"></clr-icon>
</button>
</div>
<clr-checkbox-container>
<clr-checkbox-wrapper>
<input type="checkbox" [id]="'layerEnabled' + i" [(ngModel)]="layer.enabled" [ngModelOptions]="{standalone: true}" clrCheckbox />
<label [for]="'layerEnabled' + i">Enable Layer {{i + 1}} Drilldown</label>
</clr-checkbox-wrapper>
</clr-checkbox-container>
<!-- Layer API URL -->
<div class="clr-row">
<div class="clr-col-12">
<clr-input-container>
<label [for]="'layerApiUrl' + i">Layer {{i + 1}} API URL</label>
<div class="clr-input-group">
<input type="text" [id]="'layerApiUrl' + i" [(ngModel)]="layer.apiUrl" clrInput [ngModelOptions]="{standalone: true}" />
<button class="btn btn-icon btn-primary" (click)="refreshDrilldownLayerColumns(i)" [disabled]="!layer.apiUrl" type="button">
<clr-icon shape="refresh"></clr-icon>
</button>
</div>
<clr-control-helper>Enter the API URL for layer {{i + 1}} drilldown data. Use angle brackets for parameters, e.g., http://api.example.com/data/&lt;state&gt;</clr-control-helper>
</clr-input-container>
</div>
</div>
<!-- Layer X-Axis -->
<div class="clr-row">
<div class="clr-col-12">
<clr-select-container>
<label [for]="'layerXAxis' + i">Layer {{i + 1}} X-Axis</label>
<select [id]="'layerXAxis' + i" [(ngModel)]="layer.xAxis" [ngModelOptions]="{standalone: true}" clrSelect>
<option value="">Select X-Axis Column</option>
<option *ngFor="let column of layerColumnData[i] || []" [value]="column">{{column}}</option>
</select>
<clr-control-helper>Select the column to use for X-axis in layer {{i + 1}} drilldown view</clr-control-helper>
</clr-select-container>
</div>
</div>
<!-- Layer Y-Axis (except for Deal Details) -->
<div class="clr-row" *ngIf="selectedItem?.name !== 'Deal Details'">
<div class="clr-col-12">
<clr-select-container>
<label [for]="'layerYAxis' + i">Layer {{i + 1}} Y-Axis</label>
<select [id]="'layerYAxis' + i" [(ngModel)]="layer.yAxis" [ngModelOptions]="{standalone: true}" clrSelect>
<option value="">Select Y-Axis Column</option>
<option *ngFor="let column of layerColumnData[i] || []" [value]="column">{{column}}</option>
</select>
<clr-control-helper>Select the column to use for Y-axis in layer {{i + 1}} drilldown view</clr-control-helper>
</clr-select-container>
</div>
</div>
<!-- Parameter Selection for Drilldown Layer -->
<div class="clr-row">
<div class="clr-col-12">
<clr-select-container>
<label [for]="'layerParameter' + i">Layer {{i + 1}} Parameter</label>
<select [id]="'layerParameter' + i" [(ngModel)]="layer.parameter" [ngModelOptions]="{standalone: true}" clrSelect>
<option value="">Select Parameter Column</option>
<option *ngFor="let column of layerColumnData[i] || []" [value]="column">{{column}}</option>
</select>
<clr-control-helper>Select the column to use as parameter for URL template replacement in layer {{i + 1}} drilldown</clr-control-helper>
</clr-select-container>
</div>
</div>
<!-- Layer Filter Configuration -->
<div class="clr-row section-divider">
<div class="clr-col-12">
<h5>Layer {{i + 1}} Filters</h5>
<p class="clr-subtext">Configure filters for this drilldown layer</p>
<!-- Add Layer Filter Button -->
<button class="btn btn-sm btn-primary" (click)="addLayerFilter(i)" type="button">
<clr-icon shape="plus"></clr-icon> Add Filter
</button>
<!-- Layer Filter Fields List -->
<div *ngFor="let filter of layer.filters; let j = index" class="filter-item">
<div class="filter-header">
<span>Filter {{j + 1}}</span>
<button class="btn btn-icon btn-danger btn-sm" (click)="removeLayerFilter(i, j)" type="button">
<clr-icon shape="trash"></clr-icon>
</button>
</div>
<div class="clr-row filter-content">
<div class="clr-col-5">
<clr-input-container>
<label [attr.for]="'layerFilterField' + i + '_' + j">Field Name</label>
<input type="text" [id]="'layerFilterField' + i + '_' + j" [(ngModel)]="filter.field" [ngModelOptions]="{standalone: true}" clrInput />
</clr-input-container>
</div>
<div class="clr-col-5">
<clr-input-container>
<label [attr.for]="'layerFilterValue' + i + '_' + j">Filter Value</label>
<input type="text" [id]="'layerFilterValue' + i + '_' + j" [(ngModel)]="filter.value" [ngModelOptions]="{standalone: true}" clrInput />
</clr-input-container>
</div>
<div class="clr-col-2">
<button class="btn btn-icon btn-danger btn-sm" (click)="removeLayerFilter(i, j)" type="button">
<clr-icon shape="trash"></clr-icon>
</button>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
</form>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-outline" (click)="cancelConfiguration()">Cancel</button>
<button type="button" class="btn btn-primary" (click)="saveConfiguration()">Save</button>
</div>
</clr-modal>

View File

@@ -0,0 +1,220 @@
.shield-dashboard {
padding: 20px;
background-color: #f5f5f5;
min-height: 100vh;
}
.dashboard-container {
display: flex;
gap: 20px;
}
.sidebar {
flex: 0 0 250px;
background: white;
border-radius: 4px;
box-shadow: 0 2px 4px rgba(0,0,0,0.1);
padding: 15px;
}
.main-content {
flex: 1;
display: flex;
flex-direction: column;
gap: 20px;
}
.kpi-section {
display: flex;
gap: 20px;
margin-bottom: 20px;
}
.kpi-card {
flex: 1;
background: white;
border-radius: 4px;
box-shadow: 0 2px 4px rgba(0,0,0,0.1);
padding: 20px;
text-align: center;
}
.kpi-title {
font-size: 14px;
color: #666;
margin-bottom: 10px;
}
.kpi-value {
font-size: 24px;
font-weight: bold;
color: #333;
}
.total-leads {
border-top: 4px solid #4285f4;
}
.total-deals {
border-top: 4px solid #0f9d58;
}
.deleted-items-section {
background: white;
border-radius: 4px;
box-shadow: 0 2px 4px rgba(0,0,0,0.1);
padding: 20px;
h3 {
margin-top: 0;
color: #333;
border-bottom: 1px solid #eee;
padding-bottom: 10px;
}
.deleted-items-list {
display: flex;
flex-wrap: wrap;
gap: 10px;
margin-bottom: 15px;
.deleted-item {
display: flex;
align-items: center;
gap: 10px;
background: #f8f9fa;
border: 1px solid #e9ecef;
border-radius: 4px;
padding: 8px 12px;
span {
font-size: 14px;
color: #495057;
}
}
}
}
.dashboard-grid {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(400px, 1fr));
gap: 20px;
}
.grid-item {
background: white;
border-radius: 4px;
box-shadow: 0 2px 4px rgba(0,0,0,0.1);
padding: 15px;
position: relative;
}
.grid-item-content {
margin-top: 30px;
}
.drag-handler {
cursor: move;
}
.drop-zone-indicator {
display: flex;
align-items: center;
justify-content: center;
min-height: 500px;
background-color: #f8f9fa;
border: 2px dashed #dee2e6;
border-radius: 4px;
color: #6c757d;
font-size: 18px;
text-align: center;
p {
margin: 0;
}
}
/* Gridster specific styles */
gridster {
background: transparent !important;
}
gridster-item {
background: white;
border-radius: 4px;
box-shadow: 0 2px 4px rgba(0,0,0,0.1);
overflow: hidden;
}
/* Configuration Modal Styles */
.section-divider {
margin-top: 20px;
padding-top: 15px;
border-top: 1px solid #eee;
}
.section-divider:first-child {
border-top: none;
padding-top: 0;
margin-top: 0;
}
.filter-item {
margin-bottom: 15px;
padding: 12px;
border: 1px solid #e9ecef;
border-radius: 4px;
background-color: #f8f9fa;
}
.filter-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 10px;
}
.filter-content {
margin-top: 10px;
}
.drilldown-layer {
margin-top: 20px;
padding: 15px;
border: 1px solid #dee2e6;
border-radius: 4px;
background-color: #ffffff;
}
.layer-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 15px;
padding-bottom: 10px;
border-bottom: 1px solid #eee;
}
.layer-header h5 {
margin: 0;
color: #495057;
}
/* Responsive design */
@media (max-width: 768px) {
.dashboard-container {
flex-direction: column;
}
.sidebar {
flex: 0 0 auto;
}
.kpi-section {
flex-direction: column;
}
.filter-item .clr-row > div {
margin-bottom: 10px;
}
}

View File

@@ -0,0 +1,596 @@
import { Component, OnInit } from '@angular/core';
import { GridsterConfig, GridsterItem } from 'angular-gridster2';
import { FormBuilder, FormGroup } from '@angular/forms';
import { DatastoreService } from 'src/app/services/fnd/datastore.service';
import { AlertsService } from 'src/app/services/fnd/alerts.service';
import { SureconnectService } from '../../sureconnect/sureconnect.service';
interface ShieldDashboardItem extends GridsterItem {
chartType: string;
name: string;
id: number;
component?: any;
// Configuration properties
charttitle?: string;
connection?: string;
table?: string;
xAxis?: string;
yAxis?: string[];
chartlegend?: boolean;
showlabel?: boolean;
baseFilters?: any[];
drilldownEnabled?: boolean;
drilldownApiUrl?: string;
drilldownXAxis?: string;
drilldownYAxis?: string;
drilldownParameter?: string;
drilldownFilters?: any[];
drilldownLayers?: any[];
}
interface WidgetModel {
name: string;
identifier: string;
}
@Component({
selector: 'app-shield-dashboard',
templateUrl: './shield-dashboard.component.html',
styleUrls: ['./shield-dashboard.component.scss']
})
export class ShieldDashboardComponent implements OnInit {
options: GridsterConfig;
dashboard: Array<ShieldDashboardItem>;
// Configuration modal
configModalOpen = false;
modeledit = false; // Add this to match the main dashboard pattern
configForm: FormGroup;
selectedItem: ShieldDashboardItem | null = null;
// Component palette
showComponentPalette = false;
WidgetsMock: WidgetModel[] = [
{
name: 'Bar Chart',
identifier: 'bar_chart'
},
{
name: 'Doughnut Chart',
identifier: 'doughnut_chart'
},
{
name: 'Map Chart',
identifier: 'map_chart'
},
{
name: 'Data Table',
identifier: 'grid_view'
},
{
name: 'Deal Details',
identifier: 'to_do_chart'
},
{
name: 'Quarterwise Flow',
identifier: 'line_chart'
},
{
name: 'Compact Filter',
identifier: 'compact_filter'
}
];
// Services data
storedata: any[] = [];
columnData: any[] = [];
sureconnectData: any[] = [];
drilldownColumnData: any[] = [];
layerColumnData: { [key: number]: any[] } = {};
// Keep track of deleted items
deletedItems: Array<ShieldDashboardItem> = [];
constructor(
private _fb: FormBuilder,
private datastoreService: DatastoreService,
private alertService: AlertsService,
private sureconnectService: SureconnectService
) { }
ngOnInit(): void {
this.options = {
gridType: 'fit',
enableEmptyCellDrop: true,
emptyCellDropCallback: this.onDrop,
pushItems: true,
swap: true,
pushDirections: { north: true, east: true, south: true, west: true },
resizable: { enabled: true },
itemChangeCallback: this.itemChange.bind(this),
draggable: {
enabled: true,
ignoreContent: true,
dropOverItems: true,
dragHandleClass: 'drag-handler',
ignoreContentClass: 'no-drag',
},
displayGrid: 'always',
minCols: 10,
minRows: 10,
itemResizeCallback: this.itemResize.bind(this)
};
// Initialize the dashboard with empty canvas
this.dashboard = [];
// Initialize form
this.configForm = this._fb.group({
charttitle: [''],
connection: [''],
table: [''],
xAxis: [''],
yAxis: [''],
chartlegend: [true],
showlabel: [true]
});
// Load service data
this.loadServicesData();
}
// Load initial data from services
loadServicesData() {
// Load sureconnect data
this.sureconnectService.getAll().subscribe((data: any[]) => {
this.sureconnectData = data;
});
// Load datastore data
this.datastoreService.getAll().subscribe((data) => {
this.storedata = data as any[];
});
}
// Toggle component palette visibility
toggleComponentPalette(): void {
this.showComponentPalette = !this.showComponentPalette;
}
// Handle drag start event for components - matching the working implementation
onDrag(event: DragEvent, identifier: string): void {
console.log("on drag", identifier);
console.log("on drag ", event);
if (event.dataTransfer) {
event.dataTransfer.setData('widgetIdentifier', identifier);
}
}
onDrop(ev: any) {
// Handle dropping new components onto the dashboard
console.log('Item dropped:', ev);
// Get the component identifier from the drag event
const componentType = ev.dataTransfer ? ev.dataTransfer.getData('widgetIdentifier') : '';
console.log('Component type dropped:', componentType);
if (componentType) {
this.addComponentToDashboard(componentType);
} else {
console.log('No component type found in drag data');
}
}
// Add a new component to the dashboard
addComponentToDashboard(componentType: string) {
// Generate a new ID for the component
const newId = this.dashboard.length > 0 ? Math.max(...this.dashboard.map(item => item.id), 0) + 1 : 1;
let newItem: ShieldDashboardItem;
switch (componentType) {
case "bar_chart":
newItem = {
cols: 5,
rows: 6,
y: 0,
x: 0,
chartType: 'bar-chart',
name: 'Bar Chart',
id: newId,
chartlegend: true,
showlabel: true,
baseFilters: [],
drilldownEnabled: false,
drilldownLayers: []
};
break;
case "doughnut_chart":
// For doughnut charts, we'll need to determine which one based on existing items
const donutCount = this.dashboard.filter(item => item.chartType === 'donut-chart').length;
if (donutCount % 2 === 0) {
newItem = {
cols: 5,
rows: 6,
y: 0,
x: 0,
chartType: 'donut-chart',
name: 'End Customer Donut',
id: newId,
chartlegend: true,
showlabel: true,
baseFilters: [],
drilldownEnabled: false,
drilldownLayers: []
};
} else {
newItem = {
cols: 5,
rows: 6,
y: 0,
x: 0,
chartType: 'donut-chart',
name: 'Segment Penetration Donut',
id: newId,
chartlegend: true,
showlabel: true,
baseFilters: [],
drilldownEnabled: false,
drilldownLayers: []
};
}
break;
case "map_chart":
newItem = {
cols: 5,
rows: 6,
y: 0,
x: 0,
chartType: 'map-chart',
name: 'Map Chart',
id: newId,
chartlegend: true,
showlabel: true,
baseFilters: [],
drilldownEnabled: false,
drilldownLayers: []
};
break;
case "grid_view":
newItem = {
cols: 10,
rows: 6,
y: 0,
x: 0,
chartType: 'data-table',
name: 'Data Table',
id: newId,
chartlegend: true,
showlabel: true,
baseFilters: [],
drilldownEnabled: false,
drilldownLayers: []
};
break;
case "to_do_chart":
newItem = {
cols: 5,
rows: 6,
y: 0,
x: 0,
chartType: 'deal-details',
name: 'Deal Details',
id: newId,
chartlegend: true,
showlabel: true,
baseFilters: [],
drilldownEnabled: false,
drilldownLayers: []
};
break;
case "line_chart":
newItem = {
cols: 5,
rows: 6,
y: 0,
x: 0,
chartType: 'quarterwise-flow',
name: 'Quarterwise Flow',
id: newId,
chartlegend: true,
showlabel: true,
baseFilters: [],
drilldownEnabled: false,
drilldownLayers: []
};
break;
case "compact_filter":
newItem = {
cols: 3,
rows: 2,
y: 0,
x: 0,
chartType: 'compact-filter',
name: 'Compact Filter',
id: newId,
baseFilters: [],
drilldownEnabled: false,
drilldownLayers: []
};
break;
default:
newItem = {
cols: 5,
rows: 6,
y: 0,
x: 0,
chartType: componentType,
name: componentType,
id: newId,
chartlegend: true,
showlabel: true,
baseFilters: [],
drilldownEnabled: false,
drilldownLayers: []
};
}
// Add the new item to the dashboard
this.dashboard.push(newItem);
}
removeItem(item: ShieldDashboardItem) {
// Add the item to deleted items list before removing
this.deletedItems.push({...item});
// Remove the item from the dashboard
this.dashboard.splice(this.dashboard.indexOf(item), 1);
}
// Restore a deleted item
restoreItem(item: ShieldDashboardItem) {
// Remove from deleted items
this.deletedItems.splice(this.deletedItems.indexOf(item), 1);
// Add back to dashboard
this.dashboard.push(item);
}
// Clear all deleted items
clearDeletedItems() {
this.deletedItems = [];
}
itemChange() {
console.log('Item changed:', this.dashboard);
}
itemResize(item: any, itemComponent: any) {
console.log('Item resized:', item);
// Trigger a window resize event to notify charts to resize
window.dispatchEvent(new Event('resize'));
}
/**
* Extract only the relevant chart configuration properties to pass to chart components
* This prevents errors when trying to set properties that don't exist on the components
*/
getChartInputs(item: any): any {
// Only pass properties that are relevant to chart components
const chartInputs = {
chartType: item.chartType,
name: item.name,
charttitle: item.charttitle,
connection: item.connection,
table: item.table,
xAxis: item.xAxis,
yAxis: item.yAxis,
chartlegend: item.chartlegend,
showlabel: item.showlabel,
baseFilters: item.baseFilters || [],
drilldownEnabled: item.drilldownEnabled,
drilldownApiUrl: item.drilldownApiUrl,
drilldownXAxis: item.drilldownXAxis,
drilldownYAxis: item.drilldownYAxis,
drilldownParameter: item.drilldownParameter,
drilldownFilters: item.drilldownFilters || [],
drilldownLayers: item.drilldownLayers || []
};
// Remove undefined properties to avoid passing unnecessary data
Object.keys(chartInputs).forEach(key => {
if (chartInputs[key] === undefined) {
delete chartInputs[key];
}
});
return chartInputs;
}
// Open configuration modal for a chart
editGadget(item: ShieldDashboardItem) {
console.log('Opening configuration modal for item:', item);
this.selectedItem = item;
this.modeledit = true; // Use modeledit instead of configModalOpen
// Initialize form with item data
this.configForm.patchValue({
charttitle: item.charttitle || '',
connection: item.connection || '',
table: item.table || '',
xAxis: item.xAxis || '',
yAxis: item.yAxis || [],
chartlegend: item.chartlegend !== undefined ? item.chartlegend : true,
showlabel: item.showlabel !== undefined ? item.showlabel : true
});
console.log('Form values after patch:', this.configForm.value);
// Load columns if table is set
if (item.table) {
this.getColumns(item.connection, item.table);
}
// Load drilldown columns if drilldown API URL is set
if (item.drilldownApiUrl) {
this.refreshDrilldownColumns();
}
}
// Save configuration changes
saveConfiguration() {
if (this.selectedItem) {
const formData = this.configForm.value;
console.log('Saving configuration:', formData);
// Update the selected item with form data
Object.assign(this.selectedItem, formData);
// Close the modal
this.modeledit = false; // Use modeledit instead of configModalOpen
this.selectedItem = null;
}
}
// Cancel configuration changes
cancelConfiguration() {
console.log('Canceling configuration');
this.modeledit = false; // Use modeledit instead of configModalOpen
this.selectedItem = null;
}
// Get tables from datastore
getTables(id: string) {
this.alertService.getTablefromstore(parseInt(id, 10)).subscribe(gateway => {
console.log(gateway);
// Handle table data
}, (error) => {
console.log(error);
});
}
// Get columns from API
getColumns(connectionId: string, table: string) {
const connId = connectionId ? parseInt(connectionId, 10) : undefined;
this.alertService.getColumnfromurl(table, connId).subscribe(data => {
console.log('Column data:', data);
this.columnData = data;
}, (error) => {
console.log(error);
this.columnData = [];
});
}
// Refresh drilldown columns
refreshDrilldownColumns() {
if (this.selectedItem && this.selectedItem.drilldownApiUrl) {
const connId = this.selectedItem.connection ? parseInt(this.selectedItem.connection, 10) : undefined;
this.alertService.getColumnfromurl(this.selectedItem.drilldownApiUrl, connId).subscribe(data => {
console.log('Drilldown column data:', data);
this.drilldownColumnData = data;
}, (error) => {
console.log('Error fetching drilldown columns:', error);
this.drilldownColumnData = [];
});
}
}
// Add base filter
addBaseFilter() {
if (this.selectedItem) {
if (!this.selectedItem.baseFilters) {
this.selectedItem.baseFilters = [];
}
this.selectedItem.baseFilters.push({ field: '', value: '' });
}
}
// Remove base filter
removeBaseFilter(index: number) {
if (this.selectedItem && this.selectedItem.baseFilters) {
this.selectedItem.baseFilters.splice(index, 1);
}
}
// Add drilldown filter
addDrilldownFilter() {
if (this.selectedItem) {
if (!this.selectedItem.drilldownFilters) {
this.selectedItem.drilldownFilters = [];
}
this.selectedItem.drilldownFilters.push({ field: '', value: '' });
}
}
// Remove drilldown filter
removeDrilldownFilter(index: number) {
if (this.selectedItem && this.selectedItem.drilldownFilters) {
this.selectedItem.drilldownFilters.splice(index, 1);
}
}
// Add drilldown layer
addDrilldownLayer() {
if (this.selectedItem) {
if (!this.selectedItem.drilldownLayers) {
this.selectedItem.drilldownLayers = [];
}
this.selectedItem.drilldownLayers.push({
enabled: false,
apiUrl: '',
xAxis: '',
yAxis: '',
parameter: '',
filters: []
});
}
}
// Remove drilldown layer
removeDrilldownLayer(index: number) {
if (this.selectedItem && this.selectedItem.drilldownLayers) {
this.selectedItem.drilldownLayers.splice(index, 1);
}
}
// Add layer filter
addLayerFilter(layerIndex: number) {
if (this.selectedItem && this.selectedItem.drilldownLayers) {
const layer = this.selectedItem.drilldownLayers[layerIndex];
if (layer) {
if (!layer.filters) {
layer.filters = [];
}
layer.filters.push({ field: '', value: '' });
}
}
}
// Refresh drilldown columns for a specific layer
refreshDrilldownLayerColumns(layerIndex: number) {
if (this.selectedItem && this.selectedItem.drilldownLayers && this.selectedItem.drilldownLayers[layerIndex]) {
const layer = this.selectedItem.drilldownLayers[layerIndex];
if (layer && layer.apiUrl) {
const connId = this.selectedItem.connection ? parseInt(this.selectedItem.connection, 10) : undefined;
this.alertService.getColumnfromurl(layer.apiUrl, connId).subscribe(data => {
console.log(`Drilldown layer ${layerIndex} column data:`, data);
// Store layer column data in the layerColumnData property
this.layerColumnData[layerIndex] = data;
}, (error) => {
console.log(`Error fetching drilldown layer ${layerIndex} columns:`, error);
this.layerColumnData[layerIndex] = [];
});
}
}
}
// Remove layer filter
removeLayerFilter(layerIndex: number, filterIndex: number) {
if (this.selectedItem && this.selectedItem.drilldownLayers) {
const layer = this.selectedItem.drilldownLayers[layerIndex];
if (layer && layer.filters) {
layer.filters.splice(filterIndex, 1);
}
}
}
}

View File

@@ -0,0 +1,223 @@
import { Component, OnInit } from '@angular/core';
import { GridsterConfig, GridsterItem } from 'angular-gridster2';
interface ShieldDashboardItem extends GridsterItem {
chartType: string;
name: string;
id: number;
component?: any;
}
@Component({
selector: 'app-shield-dashboard',
templateUrl: './shield-dashboard.component.html',
styleUrls: ['./shield-dashboard.component.scss']
})
export class ShieldDashboardComponent implements OnInit {
options: GridsterConfig;
dashboard: Array<ShieldDashboardItem>;
// Keep track of deleted items
deletedItems: Array<ShieldDashboardItem> = [];
constructor() { }
ngOnInit(): void {
this.options = {
gridType: 'fit',
enableEmptyCellDrop: true,
emptyCellDropCallback: this.onDrop,
pushItems: true,
swap: true,
pushDirections: { north: true, east: true, south: true, west: true },
resizable: { enabled: true },
itemChangeCallback: this.itemChange.bind(this),
draggable: {
enabled: true,
ignoreContent: true,
dropOverItems: true,
dragHandleClass: 'drag-handler',
ignoreContentClass: 'no-drag',
},
displayGrid: 'always',
minCols: 10,
minRows: 10,
itemResizeCallback: this.itemResize.bind(this)
};
// Initialize the dashboard with empty canvas
this.dashboard = [];
}
onDrop = (event: any) => {
// Handle dropping new components onto the dashboard
console.log('Item dropped:', event);
// Get the component identifier from the drag event
const componentType = event.dataTransfer ? event.dataTransfer.getData('widgetIdentifier') : '';
console.log('Component type dropped:', componentType);
if (componentType) {
this.addComponentToDashboard(componentType);
} else {
console.log('No component type found in drag data');
}
}
// Add a new component to the dashboard
addComponentToDashboard(componentType: string) {
// Generate a new ID for the component
const newId = this.dashboard.length > 0 ? Math.max(...this.dashboard.map(item => item.id), 0) + 1 : 1;
let newItem: ShieldDashboardItem;
switch (componentType) {
case "bar_chart":
newItem = {
cols: 5,
rows: 6,
y: 0,
x: 0,
chartType: 'bar-chart',
name: 'Bar Chart',
id: newId
};
break;
case "doughnut_chart":
// For doughnut charts, we'll need to determine which one based on existing items
const donutCount = this.dashboard.filter(item => item.chartType === 'donut-chart').length;
if (donutCount % 2 === 0) {
newItem = {
cols: 5,
rows: 6,
y: 0,
x: 0,
chartType: 'donut-chart',
name: 'End Customer Donut',
id: newId
};
} else {
newItem = {
cols: 5,
rows: 6,
y: 0,
x: 0,
chartType: 'donut-chart',
name: 'Segment Penetration Donut',
id: newId
};
}
break;
case "map_chart":
newItem = {
cols: 5,
rows: 6,
y: 0,
x: 0,
chartType: 'map-chart',
name: 'Map Chart',
id: newId
};
break;
case "grid_view":
newItem = {
cols: 10,
rows: 6,
y: 0,
x: 0,
chartType: 'data-table',
name: 'Data Table',
id: newId
};
break;
case "to_do_chart":
newItem = {
cols: 5,
rows: 6,
y: 0,
x: 0,
chartType: 'deal-details',
name: 'Deal Details',
id: newId
};
break;
case "line_chart":
newItem = {
cols: 5,
rows: 6,
y: 0,
x: 0,
chartType: 'quarterwise-flow',
name: 'Quarterwise Flow',
id: newId
};
break;
default:
newItem = {
cols: 5,
rows: 6,
y: 0,
x: 0,
chartType: componentType,
name: componentType,
id: newId
};
}
// Add the new item to the dashboard
this.dashboard.push(newItem);
}
removeItem(item: ShieldDashboardItem) {
// Add the item to deleted items list before removing
this.deletedItems.push({...item});
// Remove the item from the dashboard
this.dashboard.splice(this.dashboard.indexOf(item), 1);
}
// Restore a deleted item
restoreItem(item: ShieldDashboardItem) {
// Remove from deleted items
this.deletedItems.splice(this.deletedItems.indexOf(item), 1);
// Add back to dashboard
this.dashboard.push(item);
}
// Clear all deleted items
clearDeletedItems() {
this.deletedItems = [];
}
itemChange() {
console.log('Item changed:', this.dashboard);
}
itemResize(item: any, itemComponent: any) {
console.log('Item resized:', item);
// Trigger a window resize event to notify charts to resize
window.dispatchEvent(new Event('resize'));
}
/**
* Extract only the relevant chart configuration properties to pass to chart components
* This prevents errors when trying to set properties that don't exist on the components
*/
getChartInputs(item: any): any {
// Only pass properties that are relevant to chart components
const chartInputs = {
chartType: item.chartType,
name: item.name
};
// Remove undefined properties to avoid passing unnecessary data
Object.keys(chartInputs).forEach(key => {
if (chartInputs[key] === undefined) {
delete chartInputs[key];
}
});
return chartInputs;
}
}

View File

@@ -0,0 +1,44 @@
import { NgModule, CUSTOM_ELEMENTS_SCHEMA, NO_ERRORS_SCHEMA } from '@angular/core';
import { CommonModule } from '@angular/common';
import { FormsModule, ReactiveFormsModule } from '@angular/forms';
import { ClarityModule } from '@clr/angular';
import { GridsterModule } from 'angular-gridster2';
import { NgChartsModule } from 'ng2-charts';
import { DynamicModule } from 'ng-dynamic-component';
import { ShieldDashboardRoutingModule } from './shield-dashboard-routing.module';
import { ShieldDashboardComponent } from './shield-dashboard.component';
import { SidebarFiltersComponent } from './components/sidebar-filters/sidebar-filters.component';
import { BarChartComponent } from './components/bar-chart/bar-chart.component';
import { DonutChartComponent } from './components/donut-chart/donut-chart.component';
import { MapChartComponent } from './components/map-chart/map-chart.component';
import { DataTableComponent } from './components/data-table/data-table.component';
import { DealDetailsCardComponent } from './components/deal-details-card/deal-details-card.component';
import { QuarterwiseFlowComponent } from './components/quarterwise-flow/quarterwise-flow.component';
import { LoadingShimmerComponent } from './components/loading-shimmer/loading-shimmer.component';
@NgModule({
declarations: [
ShieldDashboardComponent,
SidebarFiltersComponent,
BarChartComponent,
DonutChartComponent,
MapChartComponent,
DataTableComponent,
DealDetailsCardComponent,
QuarterwiseFlowComponent,
LoadingShimmerComponent
],
imports: [
CommonModule,
FormsModule,
ReactiveFormsModule,
ClarityModule,
GridsterModule,
NgChartsModule,
DynamicModule,
ShieldDashboardRoutingModule
],
schemas: [CUSTOM_ELEMENTS_SCHEMA, NO_ERRORS_SCHEMA]
})
export class ShieldDashboardModule { }

View File

@@ -11,6 +11,9 @@
<h3>{{ 'all_dashboard' | translate }}</h3> <h3>{{ 'all_dashboard' | translate }}</h3>
</div> </div>
<div class="clr-col-4" style="text-align: right;"> <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)="gotoadd()"> <button id="add" class="btn btn-primary" (click)="gotoadd()">
<clr-icon shape="plus"></clr-icon>{{ 'dashboard_builder' | translate }} <clr-icon shape="plus"></clr-icon>{{ 'dashboard_builder' | translate }}
</button> </button>
@@ -112,6 +115,3 @@
</div> </div>
</div> </div>
</clr-modal> </clr-modal>

View File

@@ -3,6 +3,10 @@ import { DashrunnerService } from '../dashrunner.service';
import { ActivatedRoute, Router } from '@angular/router'; import { ActivatedRoute, Router } from '@angular/router';
import { Dashboard3Service } from 'src/app/services/builder/dashboard3.service'; import { Dashboard3Service } from 'src/app/services/builder/dashboard3.service';
import { DashboardContentModel } from 'src/app/models/builder/dashboard'; import { DashboardContentModel } from 'src/app/models/builder/dashboard';
// Add FilterService import
import { FilterService } from '../../../dashboardnew/common-filter/filter.service';
// Add Subscription import
import { Subscription } from 'rxjs';
@Component({ @Component({
selector: 'app-bar-runner', selector: 'app-bar-runner',
@@ -24,8 +28,19 @@ export class BarRunnerComponent implements OnInit {
JsonData; JsonData;
barData; barData;
constructor(private Dashtestservive:DashrunnerService,private route: ActivatedRoute,private dashboardService: Dashboard3Service, ConnectionId: number; // Add ConnectionId property
private router : Router,) { }
// Add subscriptions to unsubscribe on destroy
private subscriptions: Subscription[] = [];
constructor(
private Dashtestservive:DashrunnerService,
private route: ActivatedRoute,
private dashboardService: Dashboard3Service,
private router : Router,
// Add FilterService to constructor
private filterService: FilterService
) { }
barChartLabels: any[] = []; barChartLabels: any[] = [];
barChartType: string = 'bar'; barChartType: string = 'bar';
@@ -47,6 +62,13 @@ export class BarRunnerComponent implements OnInit {
this.editId = this.route.snapshot.params.id; this.editId = this.route.snapshot.params.id;
console.log(this.editId); console.log(this.editId);
// Subscribe to filter changes
this.subscriptions.push(
this.filterService.filterState$.subscribe(filters => {
// When filters change, refresh the chart data
this.fetchChartData();
})
);
this.dashboardService.getById(this.editId).subscribe((data)=>{ this.dashboardService.getById(this.editId).subscribe((data)=>{
console.log(data); console.log(data);
@@ -74,22 +96,62 @@ export class BarRunnerComponent implements OnInit {
this.YAxis = ChartObject[i].yAxis; this.YAxis = ChartObject[i].yAxis;
this.showlabel = ChartObject[i].showlabel; this.showlabel = ChartObject[i].showlabel;
this.barChartLegend = ChartObject[i].chartlegend; this.barChartLegend = ChartObject[i].chartlegend;
this.ConnectionId = ChartObject[i].connection; // Add connection ID
console.log(this.TableName); console.log(this.TableName);
this.Dashtestservive.getChartData(this.TableName,"Bar Chart",this.XAxis,this.YAxis).subscribe((Ldata) => { // Fetch data with filters
console.log(Ldata); this.fetchChartData();
this.JsonData = Ldata;
this.barChartData = this.JsonData.barChartData;
this.barChartLabels = this.JsonData.barChartLabels;
},(error) => {
console.log(error);
});
break; // No need to continue the loop once the correct placeholder is found break; // No need to continue the loop once the correct placeholder is found
} }
} }
}); });
} }
// Fetch chart data with filter support
fetchChartData(): void {
if (this.TableName && this.XAxis && this.YAxis) {
// Convert YAxis to string if it's an array
const yAxisString = Array.isArray(this.YAxis) ? this.YAxis.join(',') : this.YAxis;
// Get filter parameters from common filters
const commonFilters = this.filterService.getFilterValues();
const filterDefinitions = this.filterService.getFilters();
// Build filter object using field names as keys
const filterObj = {};
Object.keys(commonFilters).forEach(filterId => {
const filterValue = commonFilters[filterId];
// Find the filter definition to get the field name
const filterDef = this.filterService.getFilters().find(f => f.id === filterId);
if (filterDef && filterDef.field) {
const fieldName = filterDef.field;
if (filterValue !== undefined && filterValue !== null && filterValue !== '') {
filterObj[fieldName] = filterValue;
}
}
});
// Convert to JSON string for API call
let filterParams = '';
if (Object.keys(filterObj).length > 0) {
filterParams = JSON.stringify(filterObj);
}
console.log('BarRunner: Final filter object to send to API:', filterObj);
// Fetch data from the dashboard service with filters
this.Dashtestservive.getChartDataWithFilters(this.TableName, "Bar Chart", this.XAxis, this.YAxis, this.ConnectionId, '', '', filterParams).subscribe((Ldata) => {
console.log(Ldata);
this.JsonData = Ldata;
this.barChartData = this.JsonData.barChartData;
this.barChartLabels = this.JsonData.barChartLabels;
},(error) => {
console.log(error);
});
}
}
generatePDFFile(){ generatePDFFile(){
this.buttonClicked.emit(); this.buttonClicked.emit();
const content = this.contentContainerRef.nativeElement; const content = this.contentContainerRef.nativeElement;
@@ -98,4 +160,16 @@ export class BarRunnerComponent implements OnInit {
this.Dashtestservive.generatePDF(content, filename); this.Dashtestservive.generatePDF(content, filename);
} }
ngOnDestroy() {
// Unsubscribe from all subscriptions to prevent memory leaks
console.log('BarRunnerComponent ngOnDestroy called, unsubscribing from', this.subscriptions.length, 'subscriptions');
this.subscriptions.forEach(subscription => {
if (subscription && !subscription.closed) {
subscription.unsubscribe();
}
});
this.subscriptions = [];
console.log('BarRunnerComponent destroyed and cleaned up');
}
} }

View File

@@ -5,6 +5,10 @@ import { ChartConfiguration, ChartDataset, ChartOptions } from 'chart.js';
import { DashboardContentModel } from 'src/app/models/builder/dashboard'; import { DashboardContentModel } from 'src/app/models/builder/dashboard';
import { DashrunnerService } from '../dashrunner.service'; import { DashrunnerService } from '../dashrunner.service';
import { Dashboard3Service } from 'src/app/services/builder/dashboard3.service'; import { Dashboard3Service } from 'src/app/services/builder/dashboard3.service';
// Add FilterService import
import { FilterService } from '../../../dashboardnew/common-filter/filter.service';
// Add Subscription import
import { Subscription } from 'rxjs';
@Component({ @Component({
selector: 'app-bubble-runner', selector: 'app-bubble-runner',
@@ -25,9 +29,15 @@ export class BubbleRunnerComponent implements OnInit {
JsonData; JsonData;
lineChartNoLabels: [] = []; lineChartNoLabels: [] = [];
ChartLegend = false; ChartLegend = false;
ConnectionId: number; // Add ConnectionId property
// Add subscriptions to unsubscribe on destroy
private subscriptions: Subscription[] = [];
constructor(private Dashtestservive:DashrunnerService,private route: ActivatedRoute,private dashboardService: Dashboard3Service, constructor(private Dashtestservive:DashrunnerService,private route: ActivatedRoute,private dashboardService: Dashboard3Service,
private router : Router,) { } private router : Router,
// Add FilterService to constructor
private filterService: FilterService) { }
public bubbleChartOptions: ChartConfiguration['options'] = { public bubbleChartOptions: ChartConfiguration['options'] = {
// scales: { // scales: {
@@ -87,6 +97,13 @@ export class BubbleRunnerComponent implements OnInit {
this.editId = this.route.snapshot.params.id; this.editId = this.route.snapshot.params.id;
console.log(this.editId); console.log(this.editId);
// Subscribe to filter changes
this.subscriptions.push(
this.filterService.filterState$.subscribe(filters => {
// When filters change, refresh the chart data
this.fetchChartData();
})
);
this.dashboardService.getById(this.editId).subscribe((data)=>{ this.dashboardService.getById(this.editId).subscribe((data)=>{
console.log(data); console.log(data);
@@ -112,22 +129,62 @@ export class BubbleRunnerComponent implements OnInit {
this.YAxis = ChartObject[i].yAxis; this.YAxis = ChartObject[i].yAxis;
this.showlabel = ChartObject[i].showlabel; this.showlabel = ChartObject[i].showlabel;
this.ChartLegend = ChartObject[i].chartlegend; this.ChartLegend = ChartObject[i].chartlegend;
this.ConnectionId = ChartObject[i].connection; // Add connection ID
console.log(this.TableName); console.log(this.TableName);
this.Dashtestservive.getChartData(this.TableName,"Bubble Chart",this.XAxis,this.YAxis).subscribe((Ldata) => { // Fetch data with filters
console.log(Ldata); this.fetchChartData();
this.JsonData = Ldata;
this.bubbleChartData = this.JsonData.bubbleChartData;
// this.radarChartLabels = this.JsonData.radarChartLabels;
},(error) => {
console.log(error);
});
break; // No need to continue the loop once the correct placeholder is found break; // No need to continue the loop once the correct placeholder is found
} }
} }
}); });
} }
// Fetch chart data with filter support
fetchChartData(): void {
if (this.TableName && this.XAxis && this.YAxis) {
// Convert YAxis to string if it's an array
const yAxisString = Array.isArray(this.YAxis) ? this.YAxis.join(',') : this.YAxis;
// Get filter parameters from common filters
const commonFilters = this.filterService.getFilterValues();
const filterDefinitions = this.filterService.getFilters();
// Build filter object using field names as keys
const filterObj = {};
Object.keys(commonFilters).forEach(filterId => {
const filterValue = commonFilters[filterId];
// Find the filter definition to get the field name
const filterDef = this.filterService.getFilters().find(f => f.id === filterId);
if (filterDef && filterDef.field) {
const fieldName = filterDef.field;
if (filterValue !== undefined && filterValue !== null && filterValue !== '') {
filterObj[fieldName] = filterValue;
}
}
});
// Convert to JSON string for API call
let filterParams = '';
if (Object.keys(filterObj).length > 0) {
filterParams = JSON.stringify(filterObj);
}
console.log('BubbleRunner: Final filter object to send to API:', filterObj);
// Fetch data from the dashboard service with filters
this.Dashtestservive.getChartDataWithFilters(this.TableName, "Bubble Chart", this.XAxis, this.YAxis, this.ConnectionId, '', '', filterParams).subscribe((Ldata) => {
console.log(Ldata);
this.JsonData = Ldata;
this.bubbleChartData = this.JsonData.bubbleChartData;
// this.radarChartLabels = this.JsonData.radarChartLabels;
},(error) => {
console.log(error);
});
}
}
generatePDFFile(){ generatePDFFile(){
this.buttonClicked.emit(); this.buttonClicked.emit();
const content = this.contentContainerRef.nativeElement; const content = this.contentContainerRef.nativeElement;
@@ -136,5 +193,18 @@ export class BubbleRunnerComponent implements OnInit {
this.Dashtestservive.generatePDF(content, filename); this.Dashtestservive.generatePDF(content, filename);
} }
ngOnDestroy() {
// Unsubscribe from all subscriptions to prevent memory leaks
console.log('BubbleRunnerComponent ngOnDestroy called, unsubscribing from', this.subscriptions.length, 'subscriptions');
this.subscriptions.forEach(subscription => {
if (subscription && !subscription.closed) {
subscription.unsubscribe();
}
});
this.subscriptions = [];
console.log('BubbleRunnerComponent destroyed and cleaned up');
}
} }

View File

@@ -0,0 +1,64 @@
<!-- Display Mode - No configuration UI in runner -->
<div class="compact-filter">
<div class="filter-header">
<span class="filter-label" *ngIf="filterLabel">{{ filterLabel }}</span>
<span class="filter-key" *ngIf="!filterLabel && filterKey">{{ filterKey }}</span>
<span class="filter-type">({{ filterType }})</span>
</div>
<!-- Text Filter -->
<div class="filter-control" *ngIf="filterType === 'text'">
<input type="text"
[(ngModel)]="filterValue"
(ngModelChange)="onFilterValueChange($event)"
[placeholder]="filterLabel || filterKey"
class="clr-input compact-input">
</div>
<!-- Dropdown Filter -->
<div class="filter-control" *ngIf="filterType === 'dropdown'">
<select [(ngModel)]="filterValue"
(ngModelChange)="onFilterValueChange($event)"
class="clr-select compact-select">
<option value="">{{ filterLabel || filterKey }}</option>
<option *ngFor="let option of filterOptions" [value]="option">{{ option }}</option>
</select>
</div>
<!-- Multi-Select Filter -->
<div class="filter-control" *ngIf="filterType === 'multiselect'">
<div class="checkbox-group">
<div *ngFor="let option of filterOptions" class="checkbox-item">
<input type="checkbox"
[checked]="filterValue && filterValue.includes(option)"
(change)="onMultiSelectChange(option, $event)"
[id]="'checkbox-' + option">
<label [for]="'checkbox-' + option">{{ option }}</label>
</div>
</div>
</div>
<!-- Date Range Filter -->
<div class="filter-control date-range" *ngIf="filterType === 'date-range'">
<input type="date"
[(ngModel)]="filterValue.start"
(ngModelChange)="onDateRangeChange({ start: $event, end: filterValue.end })"
placeholder="Start Date"
class="clr-input compact-date">
<input type="date"
[(ngModel)]="filterValue.end"
(ngModelChange)="onDateRangeChange({ start: filterValue.start, end: $event })"
placeholder="End Date"
class="clr-input compact-date">
</div>
<!-- Toggle Filter -->
<div class="filter-control toggle" *ngIf="filterType === 'toggle'">
<input type="checkbox"
[(ngModel)]="filterValue"
(ngModelChange)="onToggleChange($event)"
clrToggle
class="clr-toggle">
<label class="toggle-label">{{ filterLabel || filterKey }}</label>
</div>
</div>

View File

@@ -0,0 +1,74 @@
.compact-filter {
padding: 10px;
border: 1px solid #ddd;
border-radius: 4px;
margin-bottom: 10px;
background-color: #f8f8f8;
.filter-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 10px;
.filter-label, .filter-key {
font-weight: bold;
}
.filter-type {
font-size: 0.8em;
color: #666;
}
}
.filter-control {
margin-bottom: 10px;
.compact-input, .compact-select, .compact-date {
width: 100%;
padding: 5px;
border: 1px solid #ccc;
border-radius: 4px;
}
.compact-multiselect {
width: 100%;
height: 100px;
}
.checkbox-group {
display: flex;
flex-direction: column;
.checkbox-item {
margin: 5px 0;
input[type="checkbox"] {
margin-right: 5px;
}
}
}
&.date-range {
display: flex;
gap: 10px;
.compact-date {
flex: 1;
}
}
&.toggle {
display: flex;
align-items: center;
.clr-toggle {
margin-right: 10px;
}
.toggle-label {
margin: 0;
}
}
}
}

View File

@@ -0,0 +1,206 @@
import { Component, OnInit, Input, Output, EventEmitter, OnChanges, SimpleChanges } from '@angular/core';
import { FilterService, Filter } from '../../../dashboardnew/common-filter/filter.service';
@Component({
selector: 'app-compact-filter-runner',
templateUrl: './compact-filter-runner.component.html',
styleUrls: ['./compact-filter-runner.component.scss']
})
export class CompactFilterRunnerComponent implements OnInit, OnChanges {
@Input() filterKey: string = '';
@Input() filterType: string = 'text';
@Input() filterOptions: string[] = [];
@Input() filterLabel: string = '';
@Input() apiUrl: string = '';
@Input() connection: number | undefined;
@Output() filterChange = new EventEmitter<any>();
selectedFilter: Filter | null = null;
filterValue: any = '';
availableFilters: Filter[] = [];
availableKeys: string[] = [];
availableValues: string[] = [];
constructor(
private filterService: FilterService
) { }
ngOnInit(): void {
console.log('CompactFilterRunnerComponent initialized with inputs:', {
filterKey: this.filterKey,
filterType: this.filterType,
filterOptions: this.filterOptions,
filterLabel: this.filterLabel,
apiUrl: this.apiUrl,
connection: this.connection
});
// Register this filter with the filter service
this.registerFilter();
// Subscribe to filter definitions to get available filters
this.filterService.filters$.subscribe(filters => {
this.availableFilters = filters;
this.updateSelectedFilter();
});
// Subscribe to filter state changes
this.filterService.filterState$.subscribe(state => {
if (this.selectedFilter && state.hasOwnProperty(this.selectedFilter.id)) {
this.filterValue = state[this.selectedFilter.id];
}
});
}
ngOnChanges(changes: SimpleChanges): void {
console.log('CompactFilterRunnerComponent inputs changed:', changes);
// If filterKey or filterType changes, re-register the filter
if (changes.filterKey || changes.filterType || changes.filterOptions) {
this.registerFilter();
}
}
// Register this filter with the filter service
registerFilter(): void {
console.log('Registering filter with key:', this.filterKey, 'type:', this.filterType);
if (this.filterKey) {
// Get current filter values from the service
const currentFilterValues = this.filterService.getFilterValues();
// Create a filter definition for this compact filter
const filterDef: Filter = {
id: `${this.filterKey}`,
field: this.filterKey,
label: this.filterLabel || this.filterKey,
type: this.filterType as any,
options: this.filterOptions,
value: this.filterValue // Use the current filter value
};
console.log('Created filter definition:', filterDef);
// Get current filters
const currentFilters = this.filterService.getFilters();
// Check if this filter is already registered
const existingFilterIndex = currentFilters.findIndex(f => f.id === filterDef.id);
if (existingFilterIndex >= 0) {
// Preserve the existing filter configuration
const existingFilter = currentFilters[existingFilterIndex];
console.log('Found existing filter:', existingFilter);
// Preserve the existing filter value if it exists in the service
if (currentFilterValues.hasOwnProperty(existingFilter.id)) {
filterDef.value = currentFilterValues[existingFilter.id];
this.filterValue = filterDef.value; // Update local value
console.log('Using value from service:', filterDef.value);
} else if (existingFilter.value !== undefined) {
// Fallback to existing filter's value if no service value
filterDef.value = existingFilter.value;
this.filterValue = filterDef.value;
console.log('Using value from existing filter:', filterDef.value);
}
// Preserve other configuration properties
filterDef.label = existingFilter.label;
filterDef.options = existingFilter.options || this.filterOptions;
// Update existing filter
currentFilters[existingFilterIndex] = filterDef;
console.log('Updated existing filter:', filterDef);
} else {
// For new filters, check if there's already a value in the service
if (currentFilterValues.hasOwnProperty(filterDef.id)) {
filterDef.value = currentFilterValues[filterDef.id];
this.filterValue = filterDef.value; // Update local value
console.log('Using value from service for new filter:', filterDef.value);
}
// Add new filter
currentFilters.push(filterDef);
console.log('Added new filter:', filterDef);
}
// Update the filter service with the new filter list
this.filterService.setFilters(currentFilters);
// Update the selected filter reference
this.selectedFilter = filterDef;
console.log('Selected filter set to:', this.selectedFilter);
}
}
updateSelectedFilter(): void {
if (this.filterKey && this.availableFilters.length > 0) {
this.selectedFilter = this.availableFilters.find(f => f.field === this.filterKey) || null;
if (this.selectedFilter) {
// Get current value for this filter from the service
const currentState = this.filterService.getFilterValues();
const filterValue = currentState[this.selectedFilter.id];
if (filterValue !== undefined) {
this.filterValue = filterValue;
} else if (this.selectedFilter.value !== undefined) {
// Use the filter's default value if no service value
this.filterValue = this.selectedFilter.value;
} else {
// Use the current filter value as fallback
this.filterValue = this.filterValue || '';
}
console.log('Updated selected filter value:', this.filterValue);
}
}
}
onFilterValueChange(value: any): void {
console.log('Filter value changed:', value);
if (this.selectedFilter) {
this.filterValue = value;
this.filterService.updateFilterValue(this.selectedFilter.id, value);
this.filterChange.emit({ filterId: this.selectedFilter.id, value: value });
// Update the filter definition in the service to reflect the new value
const currentFilters = this.filterService.getFilters();
const filterIndex = currentFilters.findIndex(f => f.id === this.selectedFilter.id);
if (filterIndex >= 0) {
currentFilters[filterIndex].value = value;
this.filterService.setFilters(currentFilters);
}
}
}
onToggleChange(checked: boolean): void {
this.onFilterValueChange(checked);
}
onDateRangeChange(dateRange: { start: string | null, end: string | null }): void {
this.onFilterValueChange(dateRange);
}
// Handle multi-select changes
onMultiSelectChange(option: string, event: any): void {
const checked = event.target.checked;
// Initialize filterValue as array if it's not already
if (!Array.isArray(this.filterValue)) {
this.filterValue = [];
}
if (checked) {
// Add option to array if not already present
if (!this.filterValue.includes(option)) {
this.filterValue.push(option);
}
} else {
// Remove option from array
this.filterValue = this.filterValue.filter((item: string) => item !== option);
}
// Emit the change
this.onFilterValueChange(this.filterValue);
}
}

View File

@@ -160,7 +160,27 @@ getlinechart(): any[] {
return this._http.get(url); return this._http.get(url);
} }
// New method to support filters
public getChartDataWithFilters(tableName: string, jobType: string, xAxis:any, yAxes:any, sureId: number | undefined, parameterField: string, parameterValue: string, filterParams: string): Observable<any> {
let url = `${baseUrl}/chart/getdashjson/${jobType}?tableName=${tableName}&xAxis=${xAxis}&yAxes=${yAxes}`;
// Add sureId if provided
if (sureId) {
url += `&sureId=${sureId}`;
}
// Add parameter field and value if provided
if (parameterField && parameterValue) {
url += `&parameter=${encodeURIComponent(parameterField)}&parameterValue=${encodeURIComponent(parameterValue)}`;
}
// Add filter parameters if provided
if (filterParams) {
url += `&filters=${encodeURIComponent(filterParams)}`;
}
return this._http.get(url);
}
////////////////////////////////////////////// //////////////////////////////////////////////

View File

@@ -26,7 +26,11 @@
<!-- <span><button class="btn btn-primary" (click)="Export(item.name)">Export</button></span> --> <!-- <span><button class="btn btn-primary" (click)="Export(item.name)">Export</button></span> -->
<!-- <span><app-line-runner (buttonClicked)="generatePDFFile()"></app-line-runner></span> --> <!-- <span><app-line-runner (buttonClicked)="generatePDFFile()"></app-line-runner></span> -->
<!-- <h4 style="margin-top: 10px; margin-left: 10px;">{{ item.charttitle }}</h4> --> <!-- <h4 style="margin-top: 10px; margin-left: 10px;">{{ item.charttitle }}</h4> -->
<ndc-dynamic class="no-drag" [ndcDynamicComponent]="item.component" (moduleInfo)="display($event)"></ndc-dynamic> <ndc-dynamic class="no-drag"
[ndcDynamicComponent]="item.component"
[ndcDynamicInputs]="getComponentInputs(item)"
(moduleInfo)="display($event)">
</ndc-dynamic>
</gridster-item> </gridster-item>
</gridster> </gridster>
</div> </div>

View File

@@ -17,6 +17,10 @@ import { BubbleRunnerComponent } from './bubble-runner/bubble-runner.component';
import { ScatterRunnerComponent } from './scatter-runner/scatter-runner.component'; import { ScatterRunnerComponent } from './scatter-runner/scatter-runner.component';
import { PolarRunnerComponent } from './polar-runner/polar-runner.component'; import { PolarRunnerComponent } from './polar-runner/polar-runner.component';
import { RadarRunnerComponent } from './radar-runner/radar-runner.component'; import { RadarRunnerComponent } from './radar-runner/radar-runner.component';
// Add FilterService import
import { FilterService } from '../../dashboardnew/common-filter/filter.service';
// Add CompactFilterRunnerComponent import
import { CompactFilterRunnerComponent } from './compact-filter-runner/compact-filter-runner.component';
@Component({ @Component({
selector: 'app-dashrunnerline', selector: 'app-dashrunnerline',
@@ -44,10 +48,13 @@ export class DashrunnerlineComponent implements OnInit {
{ name: "Radar Chart", componentInstance: RadarRunnerComponent }, { name: "Radar Chart", componentInstance: RadarRunnerComponent },
{ name: "Grid View", componentInstance: GridRunnerComponent }, { name: "Grid View", componentInstance: GridRunnerComponent },
{ name: "To Do Chart", componentInstance: TodoRunnerComponent }, { name: "To Do Chart", componentInstance: TodoRunnerComponent },
{ name: "Compact Filter", componentInstance: CompactFilterRunnerComponent }, // Add Compact Filter Runner
]; ];
constructor(private Dashtestservive:DashrunnerService, private dashboardService: Dashboard3Service,private route: ActivatedRoute, constructor(private Dashtestservive:DashrunnerService, private dashboardService: Dashboard3Service,private route: ActivatedRoute,
private router : Router,) { } private router : Router,
// Add FilterService to constructor
private filterService: FilterService) { }
ngOnInit(): void { ngOnInit(): void {
@@ -288,4 +295,48 @@ dashboard_name = "Dashtest";
console.log('Button clicked in SomeComponent'); console.log('Button clicked in SomeComponent');
// Add your custom logic here when the button is clicked in SomeComponent // Add your custom logic here when the button is clicked in SomeComponent
} }
// Method to provide inputs for dynamic components based on their type
getComponentInputs(item: any): any {
const inputs: any = {};
// Common inputs for all components
if (item.table !== undefined) inputs.table = item.table;
if (item.xAxis !== undefined) inputs.xAxis = item.xAxis;
if (item.yAxis !== undefined) inputs.yAxis = item.yAxis;
if (item.connection !== undefined) inputs.connection = item.connection;
if (item.charttitle !== undefined) inputs.charttitle = item.charttitle;
if (item.chartlegend !== undefined) inputs.chartlegend = item.chartlegend;
if (item.showlabel !== undefined) inputs.showlabel = item.showlabel;
// Compact Filter specific inputs
if (item.name === 'Compact Filter') {
if (item.filterKey !== undefined) inputs.filterKey = item.filterKey;
if (item.filterType !== undefined) inputs.filterType = item.filterType;
if (item.filterLabel !== undefined) inputs.filterLabel = item.filterLabel;
if (item.filterOptions !== undefined) inputs.filterOptions = item.filterOptions;
if (item.table !== undefined) inputs.apiUrl = item.table; // Use table as API URL for compact filter
if (item.connection !== undefined) inputs.connection = item.connection ? parseInt(item.connection, 10) : undefined;
}
// Grid View specific inputs
if (item.name === 'Grid View') {
if (item.baseFilters !== undefined) inputs.baseFilters = item.baseFilters;
}
// Chart specific inputs
if (item.name.includes('Chart') && item.name !== 'Compact Filter') {
if (item.baseFilters !== undefined) inputs.baseFilters = item.baseFilters;
if (item.drilldownEnabled !== undefined) inputs.drilldownEnabled = item.drilldownEnabled;
if (item.drilldownApiUrl !== undefined) inputs.drilldownApiUrl = item.drilldownApiUrl;
if (item.drilldownXAxis !== undefined) inputs.drilldownXAxis = item.drilldownXAxis;
if (item.drilldownYAxis !== undefined) inputs.drilldownYAxis = item.drilldownYAxis;
if (item.drilldownParameter !== undefined) inputs.drilldownParameter = item.drilldownParameter;
if (item.drilldownFilters !== undefined) inputs.drilldownFilters = item.drilldownFilters;
if (item.drilldownLayers !== undefined) inputs.drilldownLayers = item.drilldownLayers;
}
console.log('Component inputs for', item.name, ':', inputs);
return inputs;
}
} }

View File

@@ -5,6 +5,10 @@ import { ChartDataset, ChartType, } from 'chart.js';
import { ActivatedRoute, Router } from '@angular/router'; import { ActivatedRoute, Router } from '@angular/router';
import { Dashboard3Service } from 'src/app/services/builder/dashboard3.service'; import { Dashboard3Service } from 'src/app/services/builder/dashboard3.service';
import { DashboardContentModel } from 'src/app/models/builder/dashboard'; import { DashboardContentModel } from 'src/app/models/builder/dashboard';
// Add FilterService import
import { FilterService } from '../../../dashboardnew/common-filter/filter.service';
// Add Subscription import
import { Subscription } from 'rxjs';
@Component({ @Component({
selector: 'app-doughnut-runner', selector: 'app-doughnut-runner',
@@ -33,10 +37,16 @@ export class DoughnutRunnerComponent implements OnInit {
"chartLabels": ["Project", "Repository", "Wireframe"] "chartLabels": ["Project", "Repository", "Wireframe"]
} }
doughnutChartType: ChartType = 'doughnut'; doughnutChartType: ChartType = 'doughnut';
ConnectionId: number; // Add ConnectionId property
// Add subscriptions to unsubscribe on destroy
private subscriptions: Subscription[] = [];
constructor(private Dashtestservive:DashrunnerService,private route: ActivatedRoute,private dashboardService: Dashboard3Service, constructor(private Dashtestservive:DashrunnerService,private route: ActivatedRoute,private dashboardService: Dashboard3Service,
private router : Router,) { } private router : Router,
// Add FilterService to constructor
private filterService: FilterService) { }
ngOnInit(): void { ngOnInit(): void {
this.doughnutChartData = this.doughnutData.chartData; this.doughnutChartData = this.doughnutData.chartData;
this.doughnutChartLabels = this.doughnutData.chartLabels; this.doughnutChartLabels = this.doughnutData.chartLabels;
@@ -44,6 +54,14 @@ export class DoughnutRunnerComponent implements OnInit {
this.editId = this.route.snapshot.params.id; this.editId = this.route.snapshot.params.id;
console.log(this.editId); console.log(this.editId);
// Subscribe to filter changes
this.subscriptions.push(
this.filterService.filterState$.subscribe(filters => {
// When filters change, refresh the chart data
this.fetchChartData();
})
);
this.dashboardService.getById(this.editId).subscribe((data)=>{ this.dashboardService.getById(this.editId).subscribe((data)=>{
console.log(data); console.log(data);
this.workflowLine = data.dashbord1_Line[0].model; this.workflowLine = data.dashbord1_Line[0].model;
@@ -70,22 +88,62 @@ export class DoughnutRunnerComponent implements OnInit {
this.YAxis = ChartObject[i].yAxis; this.YAxis = ChartObject[i].yAxis;
this.showlabel = ChartObject[i].showlabel; this.showlabel = ChartObject[i].showlabel;
this.doughnutChartLegend = ChartObject[i].chartlegend; this.doughnutChartLegend = ChartObject[i].chartlegend;
this.ConnectionId = ChartObject[i].connection; // Add connection ID
console.log(this.TableName); console.log(this.TableName);
this.Dashtestservive.getChartData(this.TableName,"Doughnut Chart",this.XAxis,this.YAxis).subscribe((Ldata) => { // Fetch data with filters
console.log(Ldata); this.fetchChartData();
this.JsonData = Ldata;
this.doughnutChartData = this.JsonData.chartData;
this.doughnutChartLabels = this.JsonData.chartLabels;
},(error) => {
console.log(error);
});
break; // No need to continue the loop once the correct placeholder is found break; // No need to continue the loop once the correct placeholder is found
} }
} }
}); });
} }
// Fetch chart data with filter support
fetchChartData(): void {
if (this.TableName && this.XAxis && this.YAxis) {
// Convert YAxis to string if it's an array
const yAxisString = Array.isArray(this.YAxis) ? this.YAxis.join(',') : this.YAxis;
// Get filter parameters from common filters
const commonFilters = this.filterService.getFilterValues();
const filterDefinitions = this.filterService.getFilters();
// Build filter object using field names as keys
const filterObj = {};
Object.keys(commonFilters).forEach(filterId => {
const filterValue = commonFilters[filterId];
// Find the filter definition to get the field name
const filterDef = this.filterService.getFilters().find(f => f.id === filterId);
if (filterDef && filterDef.field) {
const fieldName = filterDef.field;
if (filterValue !== undefined && filterValue !== null && filterValue !== '') {
filterObj[fieldName] = filterValue;
}
}
});
// Convert to JSON string for API call
let filterParams = '';
if (Object.keys(filterObj).length > 0) {
filterParams = JSON.stringify(filterObj);
}
console.log('DoughnutRunner: Final filter object to send to API:', filterObj);
// Fetch data from the dashboard service with filters
this.Dashtestservive.getChartDataWithFilters(this.TableName, "Doughnut Chart", this.XAxis, this.YAxis, this.ConnectionId, '', '', filterParams).subscribe((Ldata) => {
console.log(Ldata);
this.JsonData = Ldata;
this.doughnutChartData = this.JsonData.chartData;
this.doughnutChartLabels = this.JsonData.chartLabels;
},(error) => {
console.log(error);
});
}
}
generatePDFFile(){ generatePDFFile(){
// this.buttonClicked.emit(); // this.buttonClicked.emit();
const content = this.contentContainerRef.nativeElement; const content = this.contentContainerRef.nativeElement;
@@ -94,6 +152,19 @@ export class DoughnutRunnerComponent implements OnInit {
this.Dashtestservive.generatePDF(content, filename); this.Dashtestservive.generatePDF(content, filename);
} }
ngOnDestroy() {
// Unsubscribe from all subscriptions to prevent memory leaks
console.log('DoughnutRunnerComponent ngOnDestroy called, unsubscribing from', this.subscriptions.length, 'subscriptions');
this.subscriptions.forEach(subscription => {
if (subscription && !subscription.closed) {
subscription.unsubscribe();
}
});
this.subscriptions = [];
console.log('DoughnutRunnerComponent destroyed and cleaned up');
}
} }

View File

@@ -39,16 +39,35 @@
</div> --> </div> -->
<div><button class="btn btn-primary" (click)="generatePDFFile()">Export</button></div> <div><button class="btn btn-primary" (click)="generatePDFFile()">Export</button></div>
<div style="max-height: 400px; overflow: auto; padding: 10px;"> <div style="max-height: 400px; overflow: auto; padding: 10px;">
<table class="table"> <!-- Debug information -->
<div *ngIf="false" style="background-color: #f0f0f0; padding: 10px; margin-bottom: 10px;">
<h4>Debug Information</h4>
<p><strong>TableName:</strong> {{ TableName }}</p>
<p><strong>XAxis:</strong> {{ XAxis }}</p>
<p><strong>YAxis:</strong> {{ YAxis }}</p>
<p><strong>Rows:</strong> {{ rows?.length }} items</p>
<p><strong>Headers:</strong> {{ getHeaders() | json }}</p>
<div *ngIf="error"><strong>Error:</strong> {{ error }}</div>
</div>
<div *ngIf="error" class="error_mess">
{{ error }}
</div>
<table class="table" *ngIf="rows && rows.length > 0; else noData">
<thead> <thead>
<tr> <tr>
<th *ngFor="let co of getHeaders();let i=index">{{co}}</th> <th *ngFor="let co of getHeaders();let i=index">{{co}}</th>
</tr> </tr>
</thead> </thead>
<tbody> <tbody>
<tr *ngFor="let item of rows?.slice()?.reverse()"> <tr *ngFor="let item of rows">
<td *ngFor="let key of getHeaders()">{{item[key]}}</td> <td *ngFor="let key of getHeaders()">{{item[key]}}</td>
</tr> </tr>
</tbody> </tbody>
</table> </table>
<ng-template #noData>
<p *ngIf="!error">No data available</p>
</ng-template>
</div> </div>

View File

@@ -3,6 +3,10 @@ import { DashrunnerService } from '../dashrunner.service';
import { DashboardContentModel } from 'src/app/models/builder/dashboard'; import { DashboardContentModel } from 'src/app/models/builder/dashboard';
import { ActivatedRoute, Router } from '@angular/router'; import { ActivatedRoute, Router } from '@angular/router';
import { Dashboard3Service } from 'src/app/services/builder/dashboard3.service'; import { Dashboard3Service } from 'src/app/services/builder/dashboard3.service';
// Add FilterService import
import { FilterService } from '../../../dashboardnew/common-filter/filter.service';
// Add Subscription import
import { Subscription } from 'rxjs';
@Component({ @Component({
selector: 'app-grid-runner', selector: 'app-grid-runner',
@@ -26,57 +30,150 @@ export class GridRunnerComponent implements OnInit {
public DashtestboardArray: DashboardContentModel[] = []; public DashtestboardArray: DashboardContentModel[] = [];
workflowLine; workflowLine;
TableName; TableName;
ConnectionId: number; // Add ConnectionId property
// Add subscriptions to unsubscribe on destroy
private subscriptions: Subscription[] = [];
constructor( constructor(
private Dashtestservive:DashrunnerService,private route: ActivatedRoute,private dashboardService: Dashboard3Service, private Dashtestservive:DashrunnerService,
private router : Router private route: ActivatedRoute,
private dashboardService: Dashboard3Service,
private router : Router,
// Add FilterService to constructor
private filterService: FilterService
) { } ) { }
ngOnInit(): void { ngOnInit(): void {
this.editId = this.route.snapshot.params.id; this.editId = this.route.snapshot.params.id;
console.log(this.editId); console.log('GridRunner: Component initialized with editId:', this.editId);
// this.getbyId(); // this.getbyId();
// Subscribe to filter changes
this.subscriptions.push(
this.filterService.filterState$.subscribe(filters => {
console.log('GridRunner: Filter state changed:', filters);
// When filters change, refresh the grid data
this.fetchGridData();
})
);
this.dashboardService.getById(this.editId).subscribe((data) => { this.dashboardService.getById(this.editId).subscribe((data) => {
console.log(data); console.log('GridRunner: Received dashboard data:', data);
this.workflowLine = data.dashbord1_Line[0].model; this.workflowLine = data.dashbord1_Line[0].model;
const dash = JSON.parse(this.workflowLine); const dash = JSON.parse(this.workflowLine);
// this.DashtestboardArray = dash.dashboard; // this.DashtestboardArray = dash.dashboard;
// console.log(this.DashtestboardArray); // console.log(this.DashtestboardArray);
const ChartObject = dash.dashboard.filter(obj => obj.name === "Grid View"); const ChartObject = dash.dashboard.filter(obj => obj.name === "Grid View");
console.log(ChartObject); console.log('GridRunner: ChartObject for Grid View:', ChartObject);
for (let i = 0; i < ChartObject.length; i++) { for (let i = 0; i < ChartObject.length; i++) {
const ids = this.Dashtestservive.getgridview(); const ids = this.Dashtestservive.getgridview();
console.log('GridRunner: Current gridview ids:', ids);
console.log('GridRunner: Checking chartid:', ChartObject[i].chartid);
// console.log(ids); // console.log(ids);
if (ids.includes(ChartObject[i].chartid)) { if (ids.includes(ChartObject[i].chartid)) {
// If the chartid is already in the ids array, continue to the next iteration // If the chartid is already in the ids array, continue to the next iteration
console.log('GridRunner: Skipping chartid as it already exists:', ChartObject[i].chartid);
continue; continue;
} }
console.log('GridRunner: Adding new chartid:', ChartObject[i].chartid);
this.Dashtestservive.setgridview(ChartObject[i].chartid); this.Dashtestservive.setgridview(ChartObject[i].chartid);
const id = ids[i];
console.log(id);
if (ChartObject[i].chartid === id) {
this.TableName = ChartObject[i].table; this.TableName = ChartObject[i].table;
this.XAxis = ChartObject[i].xAxis; this.XAxis = ChartObject[i].xAxis;
this.YAxis = ChartObject[i].yAxis; this.YAxis = ChartObject[i].yAxis;
console.log(this.TableName); // Add connection ID if available
this.Dashtestservive.getChartData(this.TableName,"Grid View",this.XAxis,this.YAxis).subscribe((Ldata) => { this.ConnectionId = ChartObject[i].connection;
console.log(Ldata); console.log('GridRunner: TableName:', this.TableName);
this.rows = Ldata; console.log('GridRunner: XAxis:', this.XAxis);
this.rowdata = this.rows console.log('GridRunner: YAxis:', this.YAxis);
console.log('GridRunner: ConnectionId:', this.ConnectionId);
},(error) => { // Fetch data with filters
console.log(error); this.fetchGridData();
});
break; // No need to continue the loop once the correct placeholder is found break; // No need to continue the loop once the correct placeholder is found
} }
} }, (error) => {
console.log('GridRunner: Error fetching dashboard data:', error);
}); });
} }
// Fetch grid data with filter support
fetchGridData(): void {
console.log('fetching grid data ...')
if (this.TableName) {
console.log('GridRunner: Fetching data for TableName:', this.TableName, 'XAxis:', this.XAxis, 'YAxis:', this.YAxis);
// Convert YAxis to string if it's an array
const yAxisString = Array.isArray(this.YAxis) ? this.YAxis.join(',') : this.YAxis;
// Get filter parameters from common filters
const commonFilters = this.filterService.getFilterValues();
const filterDefinitions = this.filterService.getFilters();
// Build filter object using field names as keys
const filterObj = {};
Object.keys(commonFilters).forEach(filterId => {
const filterValue = commonFilters[filterId];
// Find the filter definition to get the field name
const filterDef = this.filterService.getFilters().find(f => f.id === filterId);
if (filterDef && filterDef.field) {
const fieldName = filterDef.field;
if (filterValue !== undefined && filterValue !== null && filterValue !== '') {
filterObj[fieldName] = filterValue;
}
}
});
// Convert to JSON string for API call
let filterParams = '';
if (Object.keys(filterObj).length > 0) {
filterParams = JSON.stringify(filterObj);
}
console.log('GridRunner: Final filter object to send to API:', filterObj);
// Fetch data from the dashboard service with filters
this.Dashtestservive.getChartDataWithFilters(this.TableName, "grid", this.XAxis, this.YAxis, this.ConnectionId, '', '', filterParams).subscribe((Ldata) => {
console.log('GridRunner: Received data from API:', Ldata);
// Handle the actual data structure returned by the API
if (Ldata && Ldata.chartData) {
this.rows = Ldata.chartData;
this.rowdata = this.rows;
} else if (Ldata && Ldata.data) {
// Handle the original expected format as fallback
this.rows = Ldata.data;
this.rowdata = this.rows;
} else if (Array.isArray(Ldata)) {
// Handle case where data is directly an array
this.rows = Ldata;
this.rowdata = this.rows;
} else {
console.warn('GridRunner: Received data does not have expected structure', Ldata);
this.rows = [];
this.rowdata = [];
}
// Log the structure of the received data
if (this.rows) {
console.log('GridRunner: Rows length:', this.rows.length);
if (this.rows.length > 0) {
console.log('GridRunner: First row structure:', this.rows[0]);
}
} else {
console.log('GridRunner: No data received');
}
}, (error) => {
console.log('GridRunner: Error fetching data:', error);
this.error = error;
});
} else {
console.log('GridRunner: Missing TableName or XAxis');
}
}
//dynamic table //dynamic table
@@ -87,16 +184,16 @@ getTableData(id){
getHeaders() { getHeaders() {
let headers: string[] = []; let headers: string[] = [];
if (this.rows) { if (this.rows) {
console.log('GridRunner: Getting headers from rows:', this.rows);
this.rows.forEach((value) => { this.rows.forEach((value) => {
Object.keys(value).forEach((key) => { Object.keys(value).forEach((key) => {
if (!headers.find((header) => header == key)) { if (!headers.find((header) => header == key)) {
headers.push(key) headers.push(key)
} }
}) })
}) })
} }
console.log('GridRunner: Computed headers:', headers);
return headers; return headers;
} }
@@ -107,5 +204,17 @@ generatePDFFile(){
this.Dashtestservive.generatePDF(content, filename); this.Dashtestservive.generatePDF(content, filename);
} }
}
ngOnDestroy() {
// Unsubscribe from all subscriptions to prevent memory leaks
console.log('GridRunnerComponent ngOnDestroy called, unsubscribing from', this.subscriptions.length, 'subscriptions');
this.subscriptions.forEach(subscription => {
if (subscription && !subscription.closed) {
subscription.unsubscribe();
}
});
this.subscriptions = [];
console.log('GridRunnerComponent destroyed and cleaned up');
}
}

View File

@@ -7,6 +7,10 @@ import { Dashboard3Service } from 'src/app/services/builder/dashboard3.service';
import { DashboardContentModel } from 'src/app/models/builder/dashboard'; import { DashboardContentModel } from 'src/app/models/builder/dashboard';
import { jsPDF } from 'jspdf'; import { jsPDF } from 'jspdf';
import domtoimage from 'dom-to-image'; import domtoimage from 'dom-to-image';
// Add FilterService import
import { FilterService } from '../../../dashboardnew/common-filter/filter.service';
// Add Subscription import
import { Subscription } from 'rxjs';
@Component({ @Component({
selector: 'app-line-runner', selector: 'app-line-runner',
templateUrl: './line-runner.component.html', templateUrl: './line-runner.component.html',
@@ -54,8 +58,14 @@ export class LineRunnerComponent implements OnInit {
lineChartLegend = false; lineChartLegend = false;
lineChartPlugins = []; lineChartPlugins = [];
lineChartType = 'line'; lineChartType = 'line';
ConnectionId: number; // Add ConnectionId property
// Add subscriptions to unsubscribe on destroy
private subscriptions: Subscription[] = [];
constructor(private Dashtestservive:DashrunnerService,private route: ActivatedRoute,private dashboardService: Dashboard3Service, constructor(private Dashtestservive:DashrunnerService,private route: ActivatedRoute,private dashboardService: Dashboard3Service,
private router : Router,) { } private router : Router,
// Add FilterService to constructor
private filterService: FilterService) { }
ngOnInit(): void { ngOnInit(): void {
@@ -65,6 +75,13 @@ export class LineRunnerComponent implements OnInit {
this.editId = this.route.snapshot.params.id; this.editId = this.route.snapshot.params.id;
console.log(this.editId); console.log(this.editId);
// Subscribe to filter changes
this.subscriptions.push(
this.filterService.filterState$.subscribe(filters => {
// When filters change, refresh the chart data
this.fetchChartData();
})
);
this.dashboardService.getById(this.editId).subscribe((data)=>{ this.dashboardService.getById(this.editId).subscribe((data)=>{
console.log(data); console.log(data);
@@ -92,16 +109,10 @@ export class LineRunnerComponent implements OnInit {
this.YAxis = ChartObject[i].yAxis; this.YAxis = ChartObject[i].yAxis;
this.showlabel = ChartObject[i].showlabel; this.showlabel = ChartObject[i].showlabel;
this.lineChartLegend = ChartObject[i].chartlegend; this.lineChartLegend = ChartObject[i].chartlegend;
this.ConnectionId = ChartObject[i].connection; // Add connection ID
console.log(this.TableName); console.log(this.TableName);
this.Dashtestservive.getChartData(this.TableName,"Line Chart",this.XAxis,this.YAxis).subscribe((Ldata) => { // Fetch data with filters
console.log(Ldata); this.fetchChartData();
this.JsonData = Ldata;
this.lineChartData = this.JsonData.chartData;
this.lineChartLabels = this.JsonData.chartLabels;
},(error) => {
console.log(error);
});
break; // No need to continue the loop once the correct placeholder is found break; // No need to continue the loop once the correct placeholder is found
} }
} }
@@ -128,6 +139,52 @@ export class LineRunnerComponent implements OnInit {
// } // }
} }
// Fetch chart data with filter support
fetchChartData(): void {
if (this.TableName && this.XAxis && this.YAxis) {
// Convert YAxis to string if it's an array
const yAxisString = Array.isArray(this.YAxis) ? this.YAxis.join(',') : this.YAxis;
// Get filter parameters from common filters
const commonFilters = this.filterService.getFilterValues();
const filterDefinitions = this.filterService.getFilters();
// Build filter object using field names as keys
const filterObj = {};
Object.keys(commonFilters).forEach(filterId => {
const filterValue = commonFilters[filterId];
// Find the filter definition to get the field name
const filterDef = this.filterService.getFilters().find(f => f.id === filterId);
if (filterDef && filterDef.field) {
const fieldName = filterDef.field;
if (filterValue !== undefined && filterValue !== null && filterValue !== '') {
filterObj[fieldName] = filterValue;
}
}
});
// Convert to JSON string for API call
let filterParams = '';
if (Object.keys(filterObj).length > 0) {
filterParams = JSON.stringify(filterObj);
}
console.log('LineRunner: Final filter object to send to API:', filterObj);
// Fetch data from the dashboard service with filters
this.Dashtestservive.getChartDataWithFilters(this.TableName, "Line Chart", this.XAxis, this.YAxis, this.ConnectionId, '', '', filterParams).subscribe((Ldata) => {
console.log(Ldata);
this.JsonData = Ldata;
this.lineChartData = this.JsonData.chartData;
this.lineChartLabels = this.JsonData.chartLabels;
},(error) => {
console.log(error);
});
}
}
generatePDFFile(){ generatePDFFile(){
this.buttonClicked.emit(); this.buttonClicked.emit();
const content = this.contentContainerRef.nativeElement; const content = this.contentContainerRef.nativeElement;
@@ -166,4 +223,17 @@ export class LineRunnerComponent implements OnInit {
// } // }
// } // }
ngOnDestroy() {
// Unsubscribe from all subscriptions to prevent memory leaks
console.log('LineRunnerComponent ngOnDestroy called, unsubscribing from', this.subscriptions.length, 'subscriptions');
this.subscriptions.forEach(subscription => {
if (subscription && !subscription.closed) {
subscription.unsubscribe();
}
});
this.subscriptions = [];
console.log('LineRunnerComponent destroyed and cleaned up');
}
} }

View File

@@ -3,6 +3,10 @@ import { DashrunnerService } from '../dashrunner.service';
import { ActivatedRoute, Router } from '@angular/router'; import { ActivatedRoute, Router } from '@angular/router';
import { Dashboard3Service } from 'src/app/services/builder/dashboard3.service'; import { Dashboard3Service } from 'src/app/services/builder/dashboard3.service';
import { DashboardContentModel } from 'src/app/models/builder/dashboard'; import { DashboardContentModel } from 'src/app/models/builder/dashboard';
// Add FilterService import
import { FilterService } from '../../../dashboardnew/common-filter/filter.service';
// Add Subscription import
import { Subscription } from 'rxjs';
@Component({ @Component({
@@ -23,9 +27,15 @@ export class PieRunnerComponent implements OnInit {
showlabel; showlabel;
JsonData; JsonData;
lineChartNoLabels: any[] = []; lineChartNoLabels: any[] = [];
ConnectionId: number; // Add ConnectionId property
// Add subscriptions to unsubscribe on destroy
private subscriptions: Subscription[] = [];
constructor(private Dashtestservive:DashrunnerService,private route: ActivatedRoute,private dashboardService: Dashboard3Service, constructor(private Dashtestservive:DashrunnerService,private route: ActivatedRoute,private dashboardService: Dashboard3Service,
private router : Router,) { } private router : Router,
// Add FilterService to constructor
private filterService: FilterService) { }
public pieChartLabels: string[] = ['SciFi', 'Drama', 'Comedy']; public pieChartLabels: string[] = ['SciFi', 'Drama', 'Comedy'];
public pieChartData: number[] = [30, 50, 20]; public pieChartData: number[] = [30, 50, 20];
@@ -39,6 +49,13 @@ export class PieRunnerComponent implements OnInit {
this.editId = this.route.snapshot.params.id; this.editId = this.route.snapshot.params.id;
console.log(this.editId); console.log(this.editId);
// Subscribe to filter changes
this.subscriptions.push(
this.filterService.filterState$.subscribe(filters => {
// When filters change, refresh the chart data
this.fetchChartData();
})
);
this.dashboardService.getById(this.editId).subscribe((data)=>{ this.dashboardService.getById(this.editId).subscribe((data)=>{
console.log(data); console.log(data);
@@ -66,22 +83,62 @@ export class PieRunnerComponent implements OnInit {
this.YAxis = ChartObject[i].yAxis; this.YAxis = ChartObject[i].yAxis;
this.showlabel = ChartObject[i].showlabel; this.showlabel = ChartObject[i].showlabel;
this.ChartLegend = ChartObject[i].chartlegend; this.ChartLegend = ChartObject[i].chartlegend;
this.ConnectionId = ChartObject[i].connection; // Add connection ID
console.log(this.TableName); console.log(this.TableName);
this.Dashtestservive.getChartData(this.TableName,"Pie Chart",this.XAxis,this.YAxis).subscribe((Ldata) => { // Fetch data with filters
console.log(Ldata); this.fetchChartData();
this.JsonData = Ldata;
this.pieChartData = this.JsonData.pieChartData;
this.pieChartLabels = this.JsonData.pieChartLabels;
},(error) => {
console.log(error);
});
break; // No need to continue the loop once the correct placeholder is found break; // No need to continue the loop once the correct placeholder is found
} }
} }
}); });
} }
// Fetch chart data with filter support
fetchChartData(): void {
if (this.TableName && this.XAxis && this.YAxis) {
// Convert YAxis to string if it's an array
const yAxisString = Array.isArray(this.YAxis) ? this.YAxis.join(',') : this.YAxis;
// Get filter parameters from common filters
const commonFilters = this.filterService.getFilterValues();
const filterDefinitions = this.filterService.getFilters();
// Build filter object using field names as keys
const filterObj = {};
Object.keys(commonFilters).forEach(filterId => {
const filterValue = commonFilters[filterId];
// Find the filter definition to get the field name
const filterDef = this.filterService.getFilters().find(f => f.id === filterId);
if (filterDef && filterDef.field) {
const fieldName = filterDef.field;
if (filterValue !== undefined && filterValue !== null && filterValue !== '') {
filterObj[fieldName] = filterValue;
}
}
});
// Convert to JSON string for API call
let filterParams = '';
if (Object.keys(filterObj).length > 0) {
filterParams = JSON.stringify(filterObj);
}
console.log('PieRunner: Final filter object to send to API:', filterObj);
// Fetch data from the dashboard service with filters
this.Dashtestservive.getChartDataWithFilters(this.TableName, "Pie Chart", this.XAxis, this.YAxis, this.ConnectionId, '', '', filterParams).subscribe((Ldata) => {
console.log(Ldata);
this.JsonData = Ldata;
this.pieChartData = this.JsonData.pieChartData;
this.pieChartLabels = this.JsonData.pieChartLabels;
},(error) => {
console.log(error);
});
}
}
generatePDFFile(){ generatePDFFile(){
this.buttonClicked.emit(); this.buttonClicked.emit();
const content = this.contentContainerRef.nativeElement; const content = this.contentContainerRef.nativeElement;
@@ -89,4 +146,17 @@ export class PieRunnerComponent implements OnInit {
this.Dashtestservive.generatePDF(content, filename); this.Dashtestservive.generatePDF(content, filename);
} }
ngOnDestroy() {
// Unsubscribe from all subscriptions to prevent memory leaks
console.log('PieRunnerComponent ngOnDestroy called, unsubscribing from', this.subscriptions.length, 'subscriptions');
this.subscriptions.forEach(subscription => {
if (subscription && !subscription.closed) {
subscription.unsubscribe();
}
});
this.subscriptions = [];
console.log('PieRunnerComponent destroyed and cleaned up');
}
} }

View File

@@ -4,6 +4,10 @@ import { ActivatedRoute, Router } from '@angular/router';
import { Dashboard3Service } from 'src/app/services/builder/dashboard3.service'; import { Dashboard3Service } from 'src/app/services/builder/dashboard3.service';
import { DashboardContentModel } from 'src/app/models/builder/dashboard'; import { DashboardContentModel } from 'src/app/models/builder/dashboard';
// import { Label } from 'ng2-charts'; // import { Label } from 'ng2-charts';
// Add FilterService import
import { FilterService } from '../../../dashboardnew/common-filter/filter.service';
// Add Subscription import
import { Subscription } from 'rxjs';
@Component({ @Component({
selector: 'app-polar-runner', selector: 'app-polar-runner',
@@ -23,9 +27,15 @@ export class PolarRunnerComponent implements OnInit {
showlabel; showlabel;
JsonData; JsonData;
lineChartNoLabels: any[] = []; lineChartNoLabels: any[] = [];
ConnectionId: number; // Add ConnectionId property
// Add subscriptions to unsubscribe on destroy
private subscriptions: Subscription[] = [];
constructor(private Dashtestservive:DashrunnerService,private route: ActivatedRoute,private dashboardService: Dashboard3Service, constructor(private Dashtestservive:DashrunnerService,private route: ActivatedRoute,private dashboardService: Dashboard3Service,
private router : Router,) { } private router : Router,
// Add FilterService to constructor
private filterService: FilterService) { }
public polarAreaChartLabels: string[] = [ 'Download Sales', 'In-Store Sales', 'Mail Sales', 'Telesales', 'Corporate Sales' ]; public polarAreaChartLabels: string[] = [ 'Download Sales', 'In-Store Sales', 'Mail Sales', 'Telesales', 'Corporate Sales' ];
public polarAreaChartData: any = [ public polarAreaChartData: any = [
@@ -41,6 +51,13 @@ export class PolarRunnerComponent implements OnInit {
this.editId = this.route.snapshot.params.id; this.editId = this.route.snapshot.params.id;
console.log(this.editId); console.log(this.editId);
// Subscribe to filter changes
this.subscriptions.push(
this.filterService.filterState$.subscribe(filters => {
// When filters change, refresh the chart data
this.fetchChartData();
})
);
this.dashboardService.getById(this.editId).subscribe((data)=>{ this.dashboardService.getById(this.editId).subscribe((data)=>{
console.log(data); console.log(data);
@@ -66,22 +83,62 @@ export class PolarRunnerComponent implements OnInit {
this.YAxis = ChartObject[i].yAxis; this.YAxis = ChartObject[i].yAxis;
this.showlabel = ChartObject[i].showlabel; this.showlabel = ChartObject[i].showlabel;
this.ChartLegend = ChartObject[i].chartlegend; this.ChartLegend = ChartObject[i].chartlegend;
this.ConnectionId = ChartObject[i].connection; // Add connection ID
console.log(this.TableName); console.log(this.TableName);
this.Dashtestservive.getChartData(this.TableName,"PolarArea Chart",this.XAxis,this.YAxis).subscribe((Ldata) => { // Fetch data with filters
console.log(Ldata); this.fetchChartData();
this.JsonData = Ldata;
this.polarAreaChartData = this.JsonData.polarAreaChartData;
this.polarAreaChartLabels = this.JsonData.polarAreaChartLabels;
},(error) => {
console.log(error);
});
break; // No need to continue the loop once the correct placeholder is found break; // No need to continue the loop once the correct placeholder is found
} }
} }
}); });
} }
// Fetch chart data with filter support
fetchChartData(): void {
if (this.TableName && this.XAxis && this.YAxis) {
// Convert YAxis to string if it's an array
const yAxisString = Array.isArray(this.YAxis) ? this.YAxis.join(',') : this.YAxis;
// Get filter parameters from common filters
const commonFilters = this.filterService.getFilterValues();
const filterDefinitions = this.filterService.getFilters();
// Build filter object using field names as keys
const filterObj = {};
Object.keys(commonFilters).forEach(filterId => {
const filterValue = commonFilters[filterId];
// Find the filter definition to get the field name
const filterDef = this.filterService.getFilters().find(f => f.id === filterId);
if (filterDef && filterDef.field) {
const fieldName = filterDef.field;
if (filterValue !== undefined && filterValue !== null && filterValue !== '') {
filterObj[fieldName] = filterValue;
}
}
});
// Convert to JSON string for API call
let filterParams = '';
if (Object.keys(filterObj).length > 0) {
filterParams = JSON.stringify(filterObj);
}
console.log('PolarRunner: Final filter object to send to API:', filterObj);
// Fetch data from the dashboard service with filters
this.Dashtestservive.getChartDataWithFilters(this.TableName, "PolarArea Chart", this.XAxis, this.YAxis, this.ConnectionId, '', '', filterParams).subscribe((Ldata) => {
console.log(Ldata);
this.JsonData = Ldata;
this.polarAreaChartData = this.JsonData.polarAreaChartData;
this.polarAreaChartLabels = this.JsonData.polarAreaChartLabels;
},(error) => {
console.log(error);
});
}
}
generatePDFFile(){ generatePDFFile(){
this.buttonClicked.emit(); this.buttonClicked.emit();
const content = this.contentContainerRef.nativeElement; const content = this.contentContainerRef.nativeElement;
@@ -90,4 +147,17 @@ export class PolarRunnerComponent implements OnInit {
this.Dashtestservive.generatePDF(content, filename); this.Dashtestservive.generatePDF(content, filename);
} }
ngOnDestroy() {
// Unsubscribe from all subscriptions to prevent memory leaks
console.log('PolarRunnerComponent ngOnDestroy called, unsubscribing from', this.subscriptions.length, 'subscriptions');
this.subscriptions.forEach(subscription => {
if (subscription && !subscription.closed) {
subscription.unsubscribe();
}
});
this.subscriptions = [];
console.log('PolarRunnerComponent destroyed and cleaned up');
}
} }

View File

@@ -4,6 +4,10 @@ import { ActivatedRoute, Router } from '@angular/router';
import { Dashboard3Service } from 'src/app/services/builder/dashboard3.service'; import { Dashboard3Service } from 'src/app/services/builder/dashboard3.service';
import { DashboardContentModel } from 'src/app/models/builder/dashboard'; import { DashboardContentModel } from 'src/app/models/builder/dashboard';
// import { Label } from 'ng2-charts'; // import { Label } from 'ng2-charts';
// Add FilterService import
import { FilterService } from '../../../dashboardnew/common-filter/filter.service';
// Add Subscription import
import { Subscription } from 'rxjs';
@Component({ @Component({
selector: 'app-radar-runner', selector: 'app-radar-runner',
@@ -24,9 +28,15 @@ export class RadarRunnerComponent implements OnInit {
JsonData; JsonData;
lineChartNoLabels: any[] = []; lineChartNoLabels: any[] = [];
ChartLegend = false; ChartLegend = false;
ConnectionId: number; // Add ConnectionId property
// Add subscriptions to unsubscribe on destroy
private subscriptions: Subscription[] = [];
constructor(private Dashtestservive:DashrunnerService,private route: ActivatedRoute,private dashboardService: Dashboard3Service, constructor(private Dashtestservive:DashrunnerService,private route: ActivatedRoute,private dashboardService: Dashboard3Service,
private router : Router,) { } private router : Router,
// Add FilterService to constructor
private filterService: FilterService) { }
public radarChartLabels: string[] = [ public radarChartLabels: string[] = [
"Eating", "Eating",
@@ -50,6 +60,13 @@ export class RadarRunnerComponent implements OnInit {
this.editId = this.route.snapshot.params.id; this.editId = this.route.snapshot.params.id;
console.log(this.editId); console.log(this.editId);
// Subscribe to filter changes
this.subscriptions.push(
this.filterService.filterState$.subscribe(filters => {
// When filters change, refresh the chart data
this.fetchChartData();
})
);
this.dashboardService.getById(this.editId).subscribe((data)=>{ this.dashboardService.getById(this.editId).subscribe((data)=>{
console.log(data); console.log(data);
@@ -75,22 +92,62 @@ export class RadarRunnerComponent implements OnInit {
this.YAxis = ChartObject[i].yAxis; this.YAxis = ChartObject[i].yAxis;
this.showlabel = ChartObject[i].showlabel; this.showlabel = ChartObject[i].showlabel;
this.ChartLegend = ChartObject[i].chartlegend; this.ChartLegend = ChartObject[i].chartlegend;
this.ConnectionId = ChartObject[i].connection; // Add connection ID
console.log(this.TableName); console.log(this.TableName);
this.Dashtestservive.getChartData(this.TableName,"Radar Chart",this.XAxis,this.YAxis).subscribe((Ldata) => { // Fetch data with filters
console.log(Ldata); this.fetchChartData();
this.JsonData = Ldata;
this.radarChartData = this.JsonData.radarChartData;
this.radarChartLabels = this.JsonData.radarChartLabels;
},(error) => {
console.log(error);
});
break; // No need to continue the loop once the correct placeholder is found break; // No need to continue the loop once the correct placeholder is found
} }
} }
}); });
} }
// Fetch chart data with filter support
fetchChartData(): void {
if (this.TableName && this.XAxis && this.YAxis) {
// Convert YAxis to string if it's an array
const yAxisString = Array.isArray(this.YAxis) ? this.YAxis.join(',') : this.YAxis;
// Get filter parameters from common filters
const commonFilters = this.filterService.getFilterValues();
const filterDefinitions = this.filterService.getFilters();
// Build filter object using field names as keys
const filterObj = {};
Object.keys(commonFilters).forEach(filterId => {
const filterValue = commonFilters[filterId];
// Find the filter definition to get the field name
const filterDef = this.filterService.getFilters().find(f => f.id === filterId);
if (filterDef && filterDef.field) {
const fieldName = filterDef.field;
if (filterValue !== undefined && filterValue !== null && filterValue !== '') {
filterObj[fieldName] = filterValue;
}
}
});
// Convert to JSON string for API call
let filterParams = '';
if (Object.keys(filterObj).length > 0) {
filterParams = JSON.stringify(filterObj);
}
console.log('RadarRunner: Final filter object to send to API:', filterObj);
// Fetch data from the dashboard service with filters
this.Dashtestservive.getChartDataWithFilters(this.TableName, "Radar Chart", this.XAxis, this.YAxis, this.ConnectionId, '', '', filterParams).subscribe((Ldata) => {
console.log(Ldata);
this.JsonData = Ldata;
this.radarChartData = this.JsonData.radarChartData;
this.radarChartLabels = this.JsonData.radarChartLabels;
},(error) => {
console.log(error);
});
}
}
generatePDFFile(){ generatePDFFile(){
this.buttonClicked.emit(); this.buttonClicked.emit();
const content = this.contentContainerRef.nativeElement; const content = this.contentContainerRef.nativeElement;
@@ -99,4 +156,17 @@ export class RadarRunnerComponent implements OnInit {
this.Dashtestservive.generatePDF(content, filename); this.Dashtestservive.generatePDF(content, filename);
} }
ngOnDestroy() {
// Unsubscribe from all subscriptions to prevent memory leaks
console.log('RadarRunnerComponent ngOnDestroy called, unsubscribing from', this.subscriptions.length, 'subscriptions');
this.subscriptions.forEach(subscription => {
if (subscription && !subscription.closed) {
subscription.unsubscribe();
}
});
this.subscriptions = [];
console.log('RadarRunnerComponent destroyed and cleaned up');
}
} }

View File

@@ -5,6 +5,10 @@ import { Dashboard3Service } from 'src/app/services/builder/dashboard3.service';
import { DashboardContentModel } from 'src/app/models/builder/dashboard'; import { DashboardContentModel } from 'src/app/models/builder/dashboard';
// import { Label } from 'ng2-charts'; // import { Label } from 'ng2-charts';
import { ChartDataset } from 'chart.js'; import { ChartDataset } from 'chart.js';
// Add FilterService import
import { FilterService } from '../../../dashboardnew/common-filter/filter.service';
// Add Subscription import
import { Subscription } from 'rxjs';
@Component({ @Component({
selector: 'app-scatter-runner', selector: 'app-scatter-runner',
@@ -25,9 +29,15 @@ export class ScatterRunnerComponent implements OnInit {
JsonData; JsonData;
lineChartNoLabels: any[] = []; lineChartNoLabels: any[] = [];
ChartLegend = false; ChartLegend = false;
ConnectionId: number; // Add ConnectionId property
// Add subscriptions to unsubscribe on destroy
private subscriptions: Subscription[] = [];
constructor(private Dashtestservive:DashrunnerService,private route: ActivatedRoute,private dashboardService: Dashboard3Service, constructor(private Dashtestservive:DashrunnerService,private route: ActivatedRoute,private dashboardService: Dashboard3Service,
private router : Router,) { } private router : Router,
// Add FilterService to constructor
private filterService: FilterService) { }
public scatterChartLabels: string[] = [ 'Eating', 'Drinking', 'Sleeping', 'Designing', 'Coding', 'Cycling', 'Running' ]; public scatterChartLabels: string[] = [ 'Eating', 'Drinking', 'Sleeping', 'Designing', 'Coding', 'Cycling', 'Running' ];
@@ -69,6 +79,13 @@ export class ScatterRunnerComponent implements OnInit {
this.editId = this.route.snapshot.params.id; this.editId = this.route.snapshot.params.id;
console.log(this.editId); console.log(this.editId);
// Subscribe to filter changes
this.subscriptions.push(
this.filterService.filterState$.subscribe(filters => {
// When filters change, refresh the chart data
this.fetchChartData();
})
);
this.dashboardService.getById(this.editId).subscribe((data)=>{ this.dashboardService.getById(this.editId).subscribe((data)=>{
console.log(data); console.log(data);
@@ -94,22 +111,62 @@ export class ScatterRunnerComponent implements OnInit {
this.YAxis = ChartObject[i].yAxis; this.YAxis = ChartObject[i].yAxis;
this.showlabel = ChartObject[i].showlabel; this.showlabel = ChartObject[i].showlabel;
this.ChartLegend = ChartObject[i].chartlegend; this.ChartLegend = ChartObject[i].chartlegend;
this.ConnectionId = ChartObject[i].connection; // Add connection ID
console.log(this.TableName); console.log(this.TableName);
this.Dashtestservive.getChartData(this.TableName,"Scatter Chart",this.XAxis,this.YAxis).subscribe((Ldata) => { // Fetch data with filters
console.log(Ldata); this.fetchChartData();
this.JsonData = Ldata;
this.scatterChartData = this.JsonData.scatterChartData;
this.scatterChartLabels = this.JsonData.scatterChartLabels;
},(error) => {
console.log(error);
});
break; // No need to continue the loop once the correct placeholder is found break; // No need to continue the loop once the correct placeholder is found
} }
} }
}); });
} }
// Fetch chart data with filter support
fetchChartData(): void {
if (this.TableName && this.XAxis && this.YAxis) {
// Convert YAxis to string if it's an array
const yAxisString = Array.isArray(this.YAxis) ? this.YAxis.join(',') : this.YAxis;
// Get filter parameters from common filters
const commonFilters = this.filterService.getFilterValues();
const filterDefinitions = this.filterService.getFilters();
// Build filter object using field names as keys
const filterObj = {};
Object.keys(commonFilters).forEach(filterId => {
const filterValue = commonFilters[filterId];
// Find the filter definition to get the field name
const filterDef = this.filterService.getFilters().find(f => f.id === filterId);
if (filterDef && filterDef.field) {
const fieldName = filterDef.field;
if (filterValue !== undefined && filterValue !== null && filterValue !== '') {
filterObj[fieldName] = filterValue;
}
}
});
// Convert to JSON string for API call
let filterParams = '';
if (Object.keys(filterObj).length > 0) {
filterParams = JSON.stringify(filterObj);
}
console.log('ScatterRunner: Final filter object to send to API:', filterObj);
// Fetch data from the dashboard service with filters
this.Dashtestservive.getChartDataWithFilters(this.TableName, "Scatter Chart", this.XAxis, this.YAxis, this.ConnectionId, '', '', filterParams).subscribe((Ldata) => {
console.log(Ldata);
this.JsonData = Ldata;
this.scatterChartData = this.JsonData.scatterChartData;
this.scatterChartLabels = this.JsonData.scatterChartLabels;
},(error) => {
console.log(error);
});
}
}
generatePDFFile(){ generatePDFFile(){
this.buttonClicked.emit(); this.buttonClicked.emit();
const content = this.contentContainerRef.nativeElement; const content = this.contentContainerRef.nativeElement;
@@ -118,4 +175,17 @@ export class ScatterRunnerComponent implements OnInit {
this.Dashtestservive.generatePDF(content, filename); this.Dashtestservive.generatePDF(content, filename);
} }
ngOnDestroy() {
// Unsubscribe from all subscriptions to prevent memory leaks
console.log('ScatterRunnerComponent ngOnDestroy called, unsubscribing from', this.subscriptions.length, 'subscriptions');
this.subscriptions.forEach(subscription => {
if (subscription && !subscription.closed) {
subscription.unsubscribe();
}
});
this.subscriptions = [];
console.log('ScatterRunnerComponent destroyed and cleaned up');
}
} }

View File

@@ -3,6 +3,10 @@ import { DashrunnerService } from '../dashrunner.service';
import { DashboardContentModel } from 'src/app/models/builder/dashboard'; import { DashboardContentModel } from 'src/app/models/builder/dashboard';
import { ActivatedRoute, Router } from '@angular/router'; import { ActivatedRoute, Router } from '@angular/router';
import { Dashboard3Service } from 'src/app/services/builder/dashboard3.service'; import { Dashboard3Service } from 'src/app/services/builder/dashboard3.service';
// Add FilterService import
import { FilterService } from '../../../dashboardnew/common-filter/filter.service';
// Add Subscription import
import { Subscription } from 'rxjs';
@Component({ @Component({
selector: 'app-todo-runner', selector: 'app-todo-runner',
@@ -12,8 +16,9 @@ import { Dashboard3Service } from 'src/app/services/builder/dashboard3.service';
export class TodoRunnerComponent implements OnInit { export class TodoRunnerComponent implements OnInit {
@ViewChild('contentContainer') contentContainerRef!: ElementRef; @ViewChild('contentContainer') contentContainerRef!: ElementRef;
@Output() buttonClicked = new EventEmitter<void>(); @Output() buttonClicked = new EventEmitter<void>();
constructor( private Dashtestservive:DashrunnerService,private route: ActivatedRoute,private dashboardService: Dashboard3Service,
private router : Router) { } // Add subscriptions to unsubscribe on destroy
private subscriptions: Subscription[] = [];
loading = false; loading = false;
givendata; givendata;
@@ -25,6 +30,7 @@ export class TodoRunnerComponent implements OnInit {
public DashtestboardArray: DashboardContentModel[] = []; public DashtestboardArray: DashboardContentModel[] = [];
workflowLine; workflowLine;
TableName; TableName;
ConnectionId: number; // Add ConnectionId property
list; list;
data: any; data: any;
@@ -34,11 +40,25 @@ export class TodoRunnerComponent implements OnInit {
listName: "title123", listName: "title123",
List:['todo 1','todo 2'], List:['todo 1','todo 2'],
} }
constructor( private Dashtestservive:DashrunnerService,private route: ActivatedRoute,private dashboardService: Dashboard3Service,
private router : Router,
// Add FilterService to constructor
private filterService: FilterService) { }
ngOnInit(): void { ngOnInit(): void {
this.editId = this.route.snapshot.params.id; this.editId = this.route.snapshot.params.id;
console.log(this.editId); console.log(this.editId);
// this.getbyId(); // this.getbyId();
// Subscribe to filter changes
this.subscriptions.push(
this.filterService.filterState$.subscribe(filters => {
// When filters change, refresh the todo data
this.fetchTodoData();
})
);
this.dashboardService.getById(this.editId).subscribe((data)=>{ this.dashboardService.getById(this.editId).subscribe((data)=>{
console.log(data); console.log(data);
this.workflowLine = data.dashbord1_Line[0].model; this.workflowLine = data.dashbord1_Line[0].model;
@@ -63,15 +83,10 @@ export class TodoRunnerComponent implements OnInit {
this.TableName = ChartObject[i].table; this.TableName = ChartObject[i].table;
this.XAxis = ChartObject[i].xAxis; this.XAxis = ChartObject[i].xAxis;
this.YAxis = ChartObject[i].yAxis; this.YAxis = ChartObject[i].yAxis;
this.ConnectionId = ChartObject[i].connection; // Add connection ID
console.log(this.TableName); console.log(this.TableName);
this.Dashtestservive.getChartData(this.TableName,"Todo List",this.XAxis,this.YAxis).subscribe((Ldata) => { // Fetch data with filters
console.log(Ldata); this.fetchTodoData();
this.todoList.listName = Ldata.listName;
this.todoList.List = Ldata.List;
},(error) => {
console.log(error);
});
break; // No need to continue the loop once the correct placeholder is found break; // No need to continue the loop once the correct placeholder is found
} }
} }
@@ -100,4 +115,58 @@ generatePDFFile(){
this.Dashtestservive.generatePDF(content, filename); this.Dashtestservive.generatePDF(content, filename);
} }
// Fetch todo data with filter support
fetchTodoData(): void {
if (this.TableName && this.XAxis && this.YAxis) {
// Get filter parameters from common filters
const commonFilters = this.filterService.getFilterValues();
// Build filter object using field names as keys
const filterObj = {};
Object.keys(commonFilters).forEach(filterId => {
const filterValue = commonFilters[filterId];
// Find the filter definition to get the field name
const filterDef = this.filterService.getFilters().find(f => f.id === filterId);
if (filterDef && filterDef.field) {
const fieldName = filterDef.field;
if (filterValue !== undefined && filterValue !== null && filterValue !== '') {
filterObj[fieldName] = filterValue;
}
}
});
// Convert to JSON string for API call
let filterParams = '';
if (Object.keys(filterObj).length > 0) {
filterParams = JSON.stringify(filterObj);
}
console.log('TodoRunner: Final filter object to send to API:', filterObj);
// Fetch data from the dashboard service with filters
this.Dashtestservive.getChartDataWithFilters(this.TableName, "Todo List", this.XAxis, this.YAxis, this.ConnectionId, '', '', filterParams).subscribe((Ldata) => {
console.log(Ldata);
this.todoList.listName = Ldata.listName;
this.todoList.List = Ldata.List;
},(error) => {
console.log(error);
});
}
}
ngOnDestroy() {
// Unsubscribe from all subscriptions to prevent memory leaks
console.log('TodoRunnerComponent ngOnDestroy called, unsubscribing from', this.subscriptions.length, 'subscriptions');
this.subscriptions.forEach(subscription => {
if (subscription && !subscription.closed) {
subscription.unsubscribe();
}
});
this.subscriptions = [];
console.log('TodoRunnerComponent destroyed and cleaned up');
}
} }

View File

@@ -1,7 +1,7 @@
import { Injectable } from '@angular/core'; import { Injectable } from '@angular/core';
import { Router } from '@angular/router'; import { Router } from '@angular/router';
import { Observable, BehaviorSubject } from 'rxjs'; import { Observable, BehaviorSubject, Subject } from 'rxjs';
import { UserInfoService, LoginInfoInStorage} from '../user-info.service'; import { UserInfoService, LoginInfoInStorage} from '../user-info.service';
import { ApiRequestService } from './api-request.service'; import { ApiRequestService } from './api-request.service';
import { HttpClient } from '@angular/common/http'; import { HttpClient } from '@angular/common/http';
@@ -43,11 +43,14 @@ export class LoginService {
*/ */
let loginDataSubject:BehaviorSubject<any> = new BehaviorSubject<any>([]); // Will use this BehaviorSubject to emit data that we want after ajax login attempt // let loginDataSubject:BehaviorSubject<any> = new BehaviorSubject<any>([]); // Will use this BehaviorSubject to emit data that we want after ajax login attempt
let loginDataSubject: Subject<any> = new Subject<any>();
let loginInfoReturn:LoginInfoInStorage; // Object that we want to send back to Login Page let loginInfoReturn:LoginInfoInStorage; // Object that we want to send back to Login Page
this.apiRequest.loginAuthentication('token/session', bodyData) this.apiRequest.loginAuthentication('token/session', bodyData)
.subscribe(jsonResp => { .subscribe({
next: (jsonResp) => {
console.log('login response in service : ', jsonResp); console.log('login response in service : ', jsonResp);
if (jsonResp.operationMessage=='Login Failed') { if (jsonResp.operationMessage=='Login Failed') {
this.toastr.warning('Not Login Getting Error check your Username and password'); this.toastr.warning('Not Login Getting Error check your Username and password');
@@ -78,7 +81,7 @@ export class LoginService {
this.userInfoService.storeUserInfo(JSON.stringify(loginInfoReturn.user)); this.userInfoService.storeUserInfo(JSON.stringify(loginInfoReturn.user));
} }
else { else {
//Create a faliure object that we want to send back to login page //Create a failure object that we want to send back to login page
loginInfoReturn = { loginInfoReturn = {
"success":false, "success":false,
"message":jsonResp.operationMessage, "message":jsonResp.operationMessage,
@@ -86,8 +89,9 @@ export class LoginService {
}; };
} }
loginDataSubject.next(loginInfoReturn); loginDataSubject.next(loginInfoReturn);
loginDataSubject.complete(); // Complete the subject
}, },
err => { error: (err) => {
console.log('login error ', err); console.log('login error ', err);
loginInfoReturn = { loginInfoReturn = {
"success": false, "success": false,
@@ -97,6 +101,9 @@ export class LoginService {
if (err) { if (err) {
this.toastr.error('Getting Server Error'); this.toastr.error('Getting Server Error');
} }
loginDataSubject.next(loginInfoReturn); // Send the error response
loginDataSubject.complete(); // Complete the subject
}
}); });
return loginDataSubject; return loginDataSubject;

View File

@@ -281,7 +281,7 @@ export class Dashboard3Service {
return this.apiRequest.get(`Dashboard/Dashboard`); return this.apiRequest.get(`Dashboard/Dashboard`);
} }
public getChartData(tableName: string, jobType: string, xAxis?: any, yAxes?: any, sureId?: number, parameter?: string, parameterValue?: string): Observable<any> { public getChartData(tableName: string, jobType: string, xAxis?: any, yAxes?: any, sureId?: number, parameter?: string, parameterValue?: string, filters?: string): Observable<any> {
let url = `${baseUrl}/chart/getdashjson/${jobType}?tableName=${tableName}&xAxis=${xAxis}&yAxes=${yAxes}`; let url = `${baseUrl}/chart/getdashjson/${jobType}?tableName=${tableName}&xAxis=${xAxis}&yAxes=${yAxes}`;
if (sureId) { if (sureId) {
url += `&sureId=${sureId}`; url += `&sureId=${sureId}`;
@@ -293,7 +293,26 @@ export class Dashboard3Service {
url += `&parameterValue=${encodeURIComponent(parameterValue)}`; url += `&parameterValue=${encodeURIComponent(parameterValue)}`;
} }
console.log('=== DASHBOARD SERVICE DEBUG INFO ===');
console.log('Base URL:', url);
console.log('Filters parameter:', filters);
// Parse filters JSON and add as a single "filters" parameter
if (filters) {
try {
const filterObj = JSON.parse(filters);
console.log('Parsed filter object:', filterObj);
// Add all filters as a single "filters" parameter with JSON object
url += `&filters=${encodeURIComponent(JSON.stringify(filterObj))}`;
console.log('Added filters parameter:', JSON.stringify(filterObj));
} catch (e) {
console.warn('Failed to parse filter parameters:', e);
}
}
console.log('Final constructed URL:', url);
console.log('=== END DASHBOARD SERVICE DEBUG ===');
return this._http.get(url); return this._http.get(url);
} }

View File

@@ -372,5 +372,9 @@
"select_Field2": "select_Field2", "select_Field2": "select_Field2",
"Password_Field": "Password_Field", "Password_Field": "Password_Field",
"age": "age", "age": "age",
"Button_Field": "Button_Field" "Button_Field": "Button_Field",
"API_REGISTERY": "API Registry",
"API_REGISTERY_DESCRIPTION": "API Registry Description",
"TOKEN_REGISTERY": "Token Registry",
"TOKEN_REGISTERY_DESCRIPTION": "Token Registry Description"
} }

View File

@@ -105,6 +105,8 @@
"REPORT_DESCRIPTION": "रिपोर्ट विवरण", "REPORT_DESCRIPTION": "रिपोर्ट विवरण",
"API_REGISTERY": "एपीआई रजिस्ट्री", "API_REGISTERY": "एपीआई रजिस्ट्री",
"API_REGISTERY_DESCRIPTION": "एपीआई रजिस्ट्री विवरण", "API_REGISTERY_DESCRIPTION": "एपीआई रजिस्ट्री विवरण",
"TOKEN_REGISTERY": "टोकन रजिस्ट्री",
"TOKEN_REGISTERY_DESCRIPTION": "टोकन रजिस्ट्री विवरण",
"ACTIVE": "सक्रिय", "ACTIVE": "सक्रिय",
"FOLDER_NAME": "फ़ोल्डर नाम", "FOLDER_NAME": "फ़ोल्डर नाम",
"ACTION": "क्रिया", "ACTION": "क्रिया",