52 Commits
main ... build8

Author SHA1 Message Date
Gaurav Kumar
807058e40d chart 2025-11-01 16:20:59 +05:30
Gaurav Kumar
c6022b0e22 bar chart 2025-11-01 12:34:16 +05:30
Gaurav Kumar
9aed6e0d43 Update unified-chart.component.html 2025-11-01 12:18:01 +05:30
Gaurav Kumar
64b664e625 Update editnewdash.component.ts 2025-11-01 11:00:10 +05:30
Gaurav Kumar
49f1a2fbf2 Update unified-chart.component.ts 2025-11-01 10:54:41 +05:30
Gaurav Kumar
6bfe890cd9 Update editnewdash.component.ts 2025-11-01 10:03:00 +05:30
Gaurav Kumar
3e70f85644 dashboard 2025-10-31 17:19:18 +05:30
Gaurav Kumar
482805b5cf Update editnewdash.component.ts 2025-10-31 17:17:01 +05:30
Gaurav Kumar
ffda17e6b1 unified 2025-10-31 16:35:15 +05:30
Gaurav Kumar
7396843bc6 Update report-build2all.component.html 2025-10-31 16:03:37 +05:30
Gaurav Kumar
4f75ecb3e0 builder 2025-10-31 15:51:28 +05:30
Gaurav Kumar
7c1a487114 dashboard 2025-10-31 10:47:43 +05:30
Gaurav Kumar
47e9fb92e3 Update compact-filter.component.ts 2025-10-30 12:46:39 +05:30
Gaurav Kumar
7f735dcada Update editnewdash.component.ts 2025-10-30 12:29:22 +05:30
Gaurav Kumar
bd315f42a3 compact 2025-10-29 17:55:05 +05:30
Gaurav Kumar
e8c1f46430 Update editnewdash.component.html 2025-10-29 17:30:13 +05:30
Gaurav Kumar
fa96ca81bd runner 2025-10-29 17:18:26 +05:30
Gaurav Kumar
c384f44c0c doughnut 2025-10-29 17:01:50 +05:30
Gaurav Kumar
50df914ca9 pie 2025-10-29 16:52:45 +05:30
Gaurav Kumar
e0bd888c45 polr 2025-10-29 16:14:45 +05:30
Gaurav Kumar
02b82fcaf8 Update polar-chart.component.ts 2025-10-29 16:07:59 +05:30
Gaurav Kumar
557afc348f bubble 2025-10-29 15:55:01 +05:30
Gaurav Kumar
1dec787062 Update bubble-chart.component.ts 2025-10-29 15:50:08 +05:30
Gaurav Kumar
8853cf75cf Update bubble-chart.component.ts 2025-10-29 15:42:21 +05:30
Gaurav Kumar
ad57f11f8a bubble 2025-10-29 15:34:10 +05:30
Gaurav Kumar
9b775a8c63 fin 2025-10-29 12:59:20 +05:30
Gaurav Kumar
cf4fc1be93 todo 2025-10-29 12:21:24 +05:30
Gaurav Kumar
4c135c4901 to do 2025-10-29 12:16:54 +05:30
Gaurav Kumar
0b738ca7ca scatter 2025-10-29 11:35:09 +05:30
Gaurav Kumar
1b17bb706d scatter 2025-10-29 11:11:36 +05:30
Gaurav Kumar
f740076d60 bar chart 2025-10-28 17:04:02 +05:30
Gaurav Kumar
bedcc0822d chart 2025-10-28 16:31:45 +05:30
Gaurav Kumar
87810acc9e line 2025-10-28 15:41:36 +05:30
Gaurav Kumar
96b90e5dbd barchart 2025-10-28 15:32:37 +05:30
Gaurav Kumar
ced99e0940 grid view 2025-10-28 15:11:40 +05:30
Gaurav Kumar
4f82ae8698 grid view 2025-10-28 12:40:01 +05:30
Gaurav Kumar
e6779e8f34 filter 2025-10-28 12:29:10 +05:30
Gaurav Kumar
82425d5377 Update editnewdash.component.html 2025-10-28 12:13:15 +05:30
Gaurav Kumar
2995328ec1 compact filter 2025-10-27 20:36:22 +05:30
Gaurav Kumar
afc2c1f8a1 filter with runner 2025-10-27 20:12:22 +05:30
Gaurav Kumar
418b02acd7 checkbox multislect filter, in edit new dash 2025-10-27 19:02:47 +05:30
Gaurav Kumar
cdd752469c builder 2025-10-27 18:48:16 +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
133 changed files with 26552 additions and 2542 deletions

View File

@@ -4,5 +4,6 @@ public report_name:string;
public description: string;
public report_tags: string;
public servicename: string;
// Add SureConnect reference
public sureConnectId: number | null;
}

View File

@@ -23,6 +23,39 @@ export interface DashboardContentModel {
component?: any;
name: string;
type?:string;
// Chart properties
xAxis?: string;
yAxis?: string | string[];
chartType?: string;
charttitle?: string;
chartlegend?: boolean;
showlabel?: boolean;
chartcolor?: boolean;
slices?: boolean;
donut?: boolean;
charturl?: string;
chartparameter?: string;
datastore?: string;
table?: string;
datasource?: string;
fieldName?: 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[];
// Compact filter properties
filterKey?: string;
filterType?: string;
filterLabel?: string;
filterOptions?: string[];
}
export interface DashboardModel {
@@ -67,6 +100,10 @@ export class value1{
export const WidgetsMock: WidgetModel[] = [
{
name: 'Common Filter',
identifier: 'common_filter'
},
{
name: 'Radar Chart',
identifier: 'radar_chart'

View File

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

View File

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

View File

@@ -0,0 +1,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,122 @@
<!-- 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'">
<div class="compact-multiselect-display" (click)="toggleMultiselectDropdown()" style="padding: 5px; border: 1px solid #ddd; cursor: pointer; background-color: #f8f8f8;">
<span *ngIf="filterValue && filterValue.length > 0">{{ filterValue.length }} selected</span>
<span *ngIf="!filterValue || filterValue.length === 0">{{ filterLabel || filterKey || 'Select options' }}</span>
<clr-icon shape="caret down" style="float: right; margin-top: 3px;"></clr-icon>
</div>
<div class="compact-multiselect-dropdown" *ngIf="showMultiselectDropdown" style="max-height: 200px; overflow-y: auto; border: 1px solid #ddd; border-top: none; padding: 10px; background-color: white; position: absolute; z-index: 1000; width: calc(100% - 2px); box-shadow: 0 2px 4px rgba(0,0,0,0.1);">
<div *ngFor="let option of filterOptions" class="clr-checkbox-wrapper" style="margin-bottom: 5px;">
<input type="checkbox"
[id]="'multiselect-' + option"
[value]="option"
[checked]="isOptionSelected(option)"
(change)="onMultiselectOptionChange($event, option)">
<label [for]="'multiselect-' + option" class="clr-control-label">{{ 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,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,441 @@
import { Component, OnInit, Input, Output, EventEmitter, OnChanges, SimpleChanges, OnDestroy } 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, OnDestroy {
@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[] = [];
// Multiselect dropdown state
showMultiselectDropdown: boolean = false;
// 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 changes, clear the previous filter value and remove old filter from service
if (changes.filterKey) {
// Clear the previous filter value
this.filterValue = '';
// Clear filter options
this.filterOptions = [];
// Clear available values
this.availableValues = [];
// If we had a previous selected filter, clear its value in the service
if (this.selectedFilter && changes.filterKey.previousValue) {
const oldFilterId = changes.filterKey.previousValue;
this.filterService.updateFilterValue(oldFilterId, '');
}
}
// 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);
}
ngOnDestroy(): void {
// Component cleanup - remove this filter from the filter service
if (this.selectedFilter) {
// Use the proper removeFilter method which handles both filter definition and state
this.filterService.removeFilter(this.selectedFilter.id);
}
}
// 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;
// Clear filter value when changing configuration
this.filterValue = '';
// 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 {
// Clear the previous filter value when changing keys
this.filterValue = '';
// Clear filter options until new values are loaded
this.filterOptions = [];
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);
}
// Clear the filter service value for the previous filter key
if (this.selectedFilter) {
this.filterService.updateFilterValue(this.selectedFilter.id, '');
}
}
// 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);
}
}
// Add method to check if an option is selected for checkboxes
isOptionSelected(option: string): boolean {
if (!this.filterValue) {
return false;
}
// Ensure filterValue is an array for multiselect
if (!Array.isArray(this.filterValue)) {
this.filterValue = [];
return false;
}
return this.filterValue.includes(option);
}
// need to check this
// Add method to handle multiselect option change
onMultiselectOptionChange(event: any, option: string): void {
// Initialize filterValue array if it doesn't exist
if (!this.filterValue) {
this.filterValue = [];
}
// Ensure filterValue is an array
if (!Array.isArray(this.filterValue)) {
this.filterValue = [];
}
if (event.target.checked) {
// Add option if not already in array
if (!this.filterValue.includes(option)) {
this.filterValue.push(option);
}
} else {
// Remove option from array
const index = this.filterValue.indexOf(option);
if (index > -1) {
this.filterValue.splice(index, 1);
}
}
// Emit the change event
this.onFilterValueChange(this.filterValue);
}
// Add method to toggle multiselect dropdown visibility
toggleMultiselectDropdown(): void {
this.showMultiselectDropdown = !this.showMultiselectDropdown;
// Add document click handler to close dropdown when clicking outside
if (this.showMultiselectDropdown) {
setTimeout(() => {
const handleClick = (event: MouseEvent) => {
const target = event.target as HTMLElement;
if (!target.closest('.compact-multiselect-display') && !target.closest('.compact-multiselect-dropdown')) {
this.showMultiselectDropdown = false;
document.removeEventListener('click', handleClick);
}
};
document.addEventListener('click', handleClick);
}, 0);
}
}
}

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

@@ -7,8 +7,8 @@
</ol> -->
<div style="display: inline;">
<button class="btn componentbtn" (click)="toggleMenu()"><clr-icon shape="plus"></clr-icon>component</button>
<button class="btn btn-primary" (click)="openCommonFilterModal()" style="margin-left: 10px;">
<button class="btn componentbtn" (click)="toggleMenu()" *ngIf="!fromRunner"><clr-icon shape="plus"></clr-icon>component</button>
<button class="btn btn-primary" (click)="openCommonFilterModal()" style="margin-left: 10px;" *ngIf="!fromRunner">
<clr-icon shape="filter"></clr-icon> Common Filter
</button>
<div style="display: inline;">
@@ -22,7 +22,7 @@
</div>
<div class="content-container">
<nav class="sidenav" *ngIf="toggle" style="width: 16%;">
<nav class="sidenav" *ngIf="toggle && !fromRunner" style="width: 16%;">
<ul class="nav-list" style="list-style-type: none;">
<li *ngFor="let widget of WidgetsMock">
@@ -42,14 +42,14 @@
<gridster [options]="options" (drop)="onDrop($event)" style="background-color: transparent;">
<gridster-item [item]="item" *ngFor="let item of dashboardArray">
<!-- <ng-container *ngIf="addToDashboard && item.addToDashboard"> -->
<button class="btn btn-icon btn-danger" style="margin-left: 10px; margin-top: 10px;" (click)="removeItem(item)">
<button class="btn btn-icon btn-danger" style="margin-left: 10px; margin-top: 10px;" (click)="removeItem(item)" *ngIf="!fromRunner">
<clr-icon shape="trash"></clr-icon>
</button>
<button class="btn btn-icon drag-handler" style="margin-left: 10px; margin-top: 10px;">
<button class="btn btn-icon drag-handler" style="margin-left: 10px; margin-top: 10px;" *ngIf="!fromRunner">
<clr-icon shape="drag-handle"></clr-icon>
</button>
<button class="btn btn-icon" style="margin-top: 10px; float: right;">
<button class="btn btn-icon" style="margin-top: 10px; float: right;" *ngIf="!fromRunner">
<input type="checkbox" clrToggle [(ngModel)]="item.addToDashboard" name="addToDashboardSwitch"
(change)="toggleAddToDashboard(item)" />
</button>
@@ -57,7 +57,7 @@
<!-- <label for="workflow_name">Add to Dasboard</label>
<input class="btn btn-icon" style="margin-top: 10px;float: right;" type="checkbox" clrToggle value="billable" name="billable" />
-->
<button class="btn btn-icon" style="margin-top: 10px;float: right;" (click)="editGadget(item)">
<button class="btn btn-icon" style="margin-top: 10px;float: right;" (click)="editGadget(item)" *ngIf="!fromRunner">
<clr-icon shape="pencil"></clr-icon>
</button>
@@ -72,7 +72,7 @@
</div>
<div style="text-align: center;">
<button class="btn btn-outline" (click)="goBack()">Back</button>
<button type="submit" class="btn btn-primary btn-adddata " (click)="UpdateLine()">
<button type="submit" class="btn btn-primary btn-adddata " (click)="UpdateLine()" *ngIf="!fromRunner">
<b>Update</b>
</button>
</div>
@@ -90,11 +90,106 @@
</div>
</div>
<!-- Compact Filter Configuration (shown only for Compact Filter components) -->
<div class="clr-row" *ngIf="gadgetsEditdata?.fieldName === 'Compact Filter'">
<div class="clr-col-sm-12">
<h4>Compact Filter Configuration</h4>
<div class="clr-row">
<div class="clr-col-sm-12">
<label for="compactFilterConnection">Connection</label>
<select id="compactFilterConnection" class="clr-select" [(ngModel)]="gadgetsEditdata.connection"
(ngModelChange)="onCompactFilterConnectionChange($event)" [ngModelOptions]="{standalone: true}">
<option value="">Select Connection</option>
<option *ngFor="let conn of sureconnectData" [value]="conn.id">
{{conn.connection_name || conn.id}}
</option>
</select>
<div class="clr-subtext">Select a connection for this compact filter</div>
</div>
</div>
<div class="clr-row" style="margin-top: 10px;">
<div class="clr-col-sm-12">
<label for="compactFilterApiUrl">API URL</label>
<div>
<input type="text" id="compactFilterApiUrl" class="clr-input" [(ngModel)]="gadgetsEditdata.table"
(ngModelChange)="onCompactFilterApiUrlChange($event)" [ngModelOptions]="{standalone: true}"
placeholder="Enter API URL">
<span>
<button class="btn btn-icon btn-primary" style="margin: 0px;"
(click)="loadAvailableKeys(gadgetsEditdata.table, gadgetsEditdata.connection)"
[disabled]="!gadgetsEditdata.table">
<clr-icon shape="redo"></clr-icon>
</button>
</span>
</div>
<div class="clr-subtext">Enter the API URL to fetch data for this filter</div>
</div>
</div>
<div class="clr-row" style="margin-top: 10px;">
<div class="clr-col-sm-12">
<label for="filterKey">Filter Key</label>
<select id="filterKey" class="clr-select" [(ngModel)]="gadgetsEditdata.filterKey"
(ngModelChange)="onFilterKeyChange($event)" [ngModelOptions]="{standalone: true}">
<option value="">Select Filter Key</option>
<option *ngFor="let key of availableKeys" [value]="key">{{ key }}</option>
</select>
<div class="clr-subtext">Select the field name to filter on</div>
</div>
</div>
<div class="clr-row" style="margin-top: 10px;">
<div class="clr-col-sm-12">
<label for="filterType">Filter Type</label>
<select id="filterType" class="clr-select" [(ngModel)]="gadgetsEditdata.filterType"
(ngModelChange)="onFilterTypeChange($event)" [ngModelOptions]="{standalone: true}">
<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 class="clr-subtext">Select the type of filter control to display</div>
</div>
</div>
<div class="clr-row" style="margin-top: 10px;">
<div class="clr-col-sm-12">
<label for="filterLabel">Filter Label (Optional)</label>
<input type="text" id="filterLabel" class="clr-input" [(ngModel)]="gadgetsEditdata.filterLabel"
[ngModelOptions]="{standalone: true}" placeholder="Enter filter label">
<div class="clr-subtext">Label to display for this filter in the UI (if not provided, filter key will be
used)</div>
</div>
</div>
<div class="clr-row" style="margin-top: 10px;"
*ngIf="gadgetsEditdata.filterType === 'dropdown' || gadgetsEditdata.filterType === 'multiselect'">
<div class="clr-col-sm-12">
<label for="filterOptions">Filter Options (comma separated)</label>
<input type="text" id="filterOptions" class="clr-input" [(ngModel)]="filterOptionsString"
[ngModelOptions]="{standalone: true}" placeholder="Option1,Option2,Option3">
<div class="clr-subtext">Comma-separated list of options for dropdown/multiselect filters</div>
<div class="clr-subtext" *ngIf="gadgetsEditdata.filterKey">
<strong>Available values for "{{ gadgetsEditdata.filterKey }}":</strong> {{ filterOptionsString }}
</div>
</div>
</div>
</div>
</div>
<div class="clr-row" *ngIf="gadgetsEditdata?.fieldName !== 'Compact Filter'"
style="margin-top: 20px; padding-top: 15px; border-top: 1px solid #ddd;">
<!-- Add Connection Selection Field -->
<div class="clr-row">
<div class="clr-col-sm-12">
<label for="connection">Connection</label>
<select id="connection" formControlName="connection" [(ngModel)]="gadgetsEditdata.connection" class="clr-select">
<select id="connection" formControlName="connection" [(ngModel)]="gadgetsEditdata.connection"
class="clr-select">
<option value="">Select Connection</option>
<option *ngFor="let conn of sureconnectData" [value]="conn.id">
{{conn.connection_name || conn.id}}
@@ -149,8 +244,8 @@
<div class="clr-col-sm-12">
<label for="table">Api Url</label>
<div><input type="urk" id="table" formControlName="table" class="clr-input"
[(ngModel)]="gadgetsEditdata.table" style="width:90%">&nbsp;<span><button class="btn btn-icon btn-primary"
style="margin: 0px;" (click)="callApi(gadgetsEditdata.table)">
[(ngModel)]="gadgetsEditdata.table" style="width:90%">&nbsp;<span><button
class="btn btn-icon btn-primary" style="margin: 0px;" (click)="callApi(gadgetsEditdata.table)">
<clr-icon shape="redo"></clr-icon> </button></span></div>
<!-- <select id="table" formControlName="table" [(ngModel)]="gadgetsEditdata.table" (change)="tablename($event.target.value)">
<option value="null">choose Table</option>
@@ -223,8 +318,8 @@
</div>
<!-- Add Base Filter Button -->
<button class="btn btn-sm btn-primary" (click)="addBaseFilter()" style="margin-top: 10px; margin-bottom: 10px;"
[disabled]="gadgetsEditdata.commonFilterEnabled">
<button class="btn btn-sm btn-primary" (click)="addBaseFilter()"
style="margin-top: 10px; margin-bottom: 10px;" [disabled]="gadgetsEditdata.commonFilterEnabled">
<clr-icon shape="plus"></clr-icon> Add Filter
</button>
@@ -240,19 +335,38 @@
</div>
<div class="clr-row" style="margin-top: 8px;">
<div class="clr-col-sm-5">
<select [(ngModel)]="filter.field" [ngModelOptions]="{standalone: true}" class="clr-select"
<div class="clr-col-sm-4">
<select [(ngModel)]="filter.field" (ngModelChange)="onBaseFilterFieldChange(i, $event)" [ngModelOptions]="{standalone: true}" class="clr-select"
[disabled]="gadgetsEditdata.commonFilterEnabled">
<option value="">Select Field</option>
<!-- Base API filters should always use columnData, not drilldownColumnData -->
<option *ngFor="let column of getAvailableFields(gadgetsEditdata.baseFilters, i, columnData)" [value]="column">{{column}}</option>
<option *ngFor="let column of getAvailableFields(gadgetsEditdata.baseFilters, i, columnData)"
[value]="column">{{column}}</option>
</select>
</div>
<div class="clr-col-sm-5">
<input type="text" [(ngModel)]="filter.value" [ngModelOptions]="{standalone: true}"
class="clr-input" placeholder="Filter Value"
[disabled]="gadgetsEditdata.commonFilterEnabled"/>
<div class="clr-col-sm-3">
<select [(ngModel)]="filter.type" (ngModelChange)="onBaseFilterTypeChange(i, $event)" [ngModelOptions]="{standalone: true}" class="clr-select"
[disabled]="gadgetsEditdata.commonFilterEnabled">
<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>
<div class="clr-col-sm-3" *ngIf="filter.type === 'dropdown' || filter.type === 'multiselect'">
<input type="text" [(ngModel)]="filter.options" [ngModelOptions]="{standalone: true}" class="clr-input"
placeholder="Option1,Option2,Option3" [disabled]="gadgetsEditdata.commonFilterEnabled" />
<div class="clr-subtext" *ngIf="filter.availableValues">
Available: {{ filter.availableValues }}
</div>
</div>
<div class="clr-col-sm-3" *ngIf="filter.type !== 'dropdown' && filter.type !== 'multiselect'">
<input type="text" [(ngModel)]="filter.value" [ngModelOptions]="{standalone: true}" class="clr-input"
placeholder="Filter Value" [disabled]="gadgetsEditdata.commonFilterEnabled" />
</div>
<div class="clr-col-sm-2">
@@ -288,22 +402,25 @@
<label for="drilldownApiUrl">Base Drilldown API URL</label>
<div>
<input type="text" id="drilldownApiUrl" formControlName="drilldownApiUrl" class="clr-input"
[(ngModel)]="gadgetsEditdata.drilldownApiUrl" style="width:90%" [ngModelOptions]="{standalone: true}">&nbsp;
[(ngModel)]="gadgetsEditdata.drilldownApiUrl" style="width:90%"
[ngModelOptions]="{standalone: true}">&nbsp;
<span>
<button class="btn btn-icon btn-primary" style="margin: 0px;"
(click)="refreshBaseDrilldownColumns()" [disabled]="!gadgetsEditdata.drilldownApiUrl">
<button class="btn btn-icon btn-primary" style="margin: 0px;" (click)="refreshBaseDrilldownColumns()"
[disabled]="!gadgetsEditdata.drilldownApiUrl">
<clr-icon shape="redo"></clr-icon>
</button>
</span>
</div>
<div class="clr-subtext">Enter the API URL for base drilldown data. Use angle brackets for parameters, e.g., http://api.example.com/data/&lt;country&gt;</div>
<div class="clr-subtext">Enter the API URL for base drilldown data. Use angle brackets for parameters, e.g.,
http://api.example.com/data/&lt;country&gt;</div>
</div>
</div>
<div class="clr-row" *ngIf="gadgetsEditdata.drilldownEnabled">
<div class="clr-col-sm-12">
<label for="drilldownXAxis">Base Drilldown X-Axis</label>
<select id="drilldownXAxis" formControlName="drilldownXAxis" [(ngModel)]="gadgetsEditdata.drilldownXAxis" [ngModelOptions]="{standalone: true}">
<select id="drilldownXAxis" formControlName="drilldownXAxis" [(ngModel)]="gadgetsEditdata.drilldownXAxis"
[ngModelOptions]="{standalone: true}">
<option value="">Select X-Axis Column</option>
<option *ngFor="let column of drilldownColumnData" [value]="column">{{column}}</option>
</select>
@@ -317,7 +434,8 @@
gadgetsEditdata?.fieldName !== 'To Do Chart'">
<div class="clr-col-sm-12">
<label for="drilldownYAxis">Base Drilldown Y-Axis</label>
<select id="drilldownYAxis" formControlName="drilldownYAxis" [(ngModel)]="gadgetsEditdata.drilldownYAxis" [ngModelOptions]="{standalone: true}">
<select id="drilldownYAxis" formControlName="drilldownYAxis" [(ngModel)]="gadgetsEditdata.drilldownYAxis"
[ngModelOptions]="{standalone: true}">
<option value="">Select Y-Axis Column</option>
<option *ngFor="let column of drilldownColumnData" [value]="column">{{column}}</option>
</select>
@@ -329,11 +447,14 @@
<div class="clr-row" *ngIf="gadgetsEditdata.drilldownEnabled">
<div class="clr-col-sm-12">
<label for="drilldownParameter">Base Drilldown Parameter</label>
<select id="drilldownParameter" [(ngModel)]="gadgetsEditdata.drilldownParameter" [ngModelOptions]="{standalone: true}">
<select id="drilldownParameter" [(ngModel)]="gadgetsEditdata.drilldownParameter"
[ngModelOptions]="{standalone: true}">
<option value="">Select Parameter Column</option>
<option *ngFor="let column of drilldownColumnData" [value]="column">{{column}}</option>
</select>
<div class="clr-subtext">Select the column to use as parameter for URL template replacement in base drilldown</div>
<div class="clr-subtext">Select the column to use as parameter for URL template replacement in base
drilldown
</div>
</div>
</div>
@@ -347,16 +468,18 @@
<div class="clr-form-control" style="margin-top: 10px;">
<div class="clr-control-container">
<div class="clr-checkbox-wrapper">
<input type="checkbox" id="commonFilterToggleDrilldown" [(ngModel)]="gadgetsEditdata.commonFilterEnabledDrilldown"
(change)="onCommonFilterToggleDrilldown()" [ngModelOptions]="{standalone: true}" class="clr-checkbox" />
<input type="checkbox" id="commonFilterToggleDrilldown"
[(ngModel)]="gadgetsEditdata.commonFilterEnabledDrilldown"
(change)="onCommonFilterToggleDrilldown()" [ngModelOptions]="{standalone: true}"
class="clr-checkbox" />
<label for="commonFilterToggleDrilldown" class="clr-control-label">Use Common Filter</label>
</div>
</div>
</div>
<!-- Add Drilldown Filter Button -->
<button class="btn btn-sm btn-primary" (click)="addDrilldownFilter()" style="margin-top: 10px; margin-bottom: 10px;"
[disabled]="gadgetsEditdata.commonFilterEnabledDrilldown">
<button class="btn btn-sm btn-primary" (click)="addDrilldownFilter()"
style="margin-top: 10px; margin-bottom: 10px;" [disabled]="gadgetsEditdata.commonFilterEnabledDrilldown">
<clr-icon shape="plus"></clr-icon> Add Filter
</button>
@@ -372,18 +495,38 @@
</div>
<div class="clr-row" style="margin-top: 8px;">
<div class="clr-col-sm-5">
<select [(ngModel)]="filter.field" [ngModelOptions]="{standalone: true}" class="clr-select"
<div class="clr-col-sm-4">
<select [(ngModel)]="filter.field" (ngModelChange)="onDrilldownFilterFieldChange(i, $event)" [ngModelOptions]="{standalone: true}" class="clr-select"
[disabled]="gadgetsEditdata.commonFilterEnabledDrilldown">
<option value="">Select Field</option>
<option *ngFor="let column of getAvailableFields(gadgetsEditdata.drilldownFilters, i, drilldownColumnData)" [value]="column">{{column}}</option>
<option
*ngFor="let column of getAvailableFields(gadgetsEditdata.drilldownFilters, i, drilldownColumnData)"
[value]="column">{{column}}</option>
</select>
</div>
<div class="clr-col-sm-5">
<input type="text" [(ngModel)]="filter.value" [ngModelOptions]="{standalone: true}"
class="clr-input" placeholder="Filter Value"
[disabled]="gadgetsEditdata.commonFilterEnabledDrilldown"/>
<div class="clr-col-sm-3">
<select [(ngModel)]="filter.type" (ngModelChange)="onDrilldownFilterTypeChange(i, $event)" [ngModelOptions]="{standalone: true}" class="clr-select"
[disabled]="gadgetsEditdata.commonFilterEnabledDrilldown">
<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>
<div class="clr-col-sm-3" *ngIf="filter.type === 'dropdown' || filter.type === 'multiselect'">
<input type="text" [(ngModel)]="filter.options" [ngModelOptions]="{standalone: true}" class="clr-input"
placeholder="Option1,Option2,Option3" [disabled]="gadgetsEditdata.commonFilterEnabledDrilldown" />
<div class="clr-subtext" *ngIf="filter.availableValues">
Available: {{ filter.availableValues }}
</div>
</div>
<div class="clr-col-sm-3" *ngIf="filter.type !== 'dropdown' && filter.type !== 'multiselect'">
<input type="text" [(ngModel)]="filter.value" [ngModelOptions]="{standalone: true}" class="clr-input"
placeholder="Filter Value" [disabled]="gadgetsEditdata.commonFilterEnabledDrilldown" />
</div>
<div class="clr-col-sm-2">
@@ -400,7 +543,8 @@
<!-- Multi-Layer Drilldown Configurations -->
<div class="clr-row" *ngIf="gadgetsEditdata.drilldownEnabled" style="margin-top: 20px; padding-top: 15px; border-top: 1px solid #ddd;">
<div class="clr-row" *ngIf="gadgetsEditdata.drilldownEnabled"
style="margin-top: 20px; padding-top: 15px; border-top: 1px solid #ddd;">
<div class="clr-col-sm-12">
<h4>Multi-Layer Drilldown Configurations</h4>
<button class="btn btn-sm btn-primary" (click)="addDrilldownLayer()">
@@ -412,7 +556,8 @@
<!-- Dynamic Drilldown Layers -->
<div class="clr-row" *ngFor="let layer of gadgetsEditdata.drilldownLayers; let i = index">
<div class="clr-col-sm-12" style="margin-top: 15px; padding: 10px; border: 1px solid #eee; border-radius: 4px;">
<div class="clr-col-sm-12"
style="margin-top: 15px; padding: 10px; border: 1px solid #eee; border-radius: 4px;">
<div style="display: flex; justify-content: space-between; align-items: center;">
<h5>Drilldown Layer {{i + 1}}</h5>
<button class="btn btn-icon btn-danger btn-sm" (click)="removeDrilldownLayer(i)">
@@ -423,7 +568,8 @@
<div class="clr-form-control">
<div class="clr-control-container">
<div class="clr-checkbox-wrapper">
<input type="checkbox" [id]="'layerEnabled' + i" [(ngModel)]="layer.enabled" class="clr-checkbox" [ngModelOptions]="{standalone: true}" />
<input type="checkbox" [id]="'layerEnabled' + i" [(ngModel)]="layer.enabled" class="clr-checkbox"
[ngModelOptions]="{standalone: true}" />
<label [for]="'layerEnabled' + i" class="clr-control-label">Enable Layer {{i + 1}} Drilldown</label>
</div>
</div>
@@ -433,8 +579,8 @@
<div class="clr-col-sm-12">
<label [for]="'layerApiUrl' + i">Layer {{i + 1}} API URL</label>
<div>
<input type="text" [id]="'layerApiUrl' + i" class="clr-input"
[(ngModel)]="layer.apiUrl" style="width:90%" [ngModelOptions]="{standalone: true}">&nbsp;
<input type="text" [id]="'layerApiUrl' + i" class="clr-input" [(ngModel)]="layer.apiUrl"
style="width:90%" [ngModelOptions]="{standalone: true}">&nbsp;
<span>
<button class="btn btn-icon btn-primary" style="margin: 0px;"
(click)="refreshDrilldownLayerColumns(i)" [disabled]="!layer.apiUrl">
@@ -442,7 +588,8 @@
</button>
</span>
</div>
<div class="clr-subtext">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;</div>
<div class="clr-subtext">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;</div>
</div>
</div>
@@ -478,7 +625,9 @@
<option value="">Select Parameter Column</option>
<option *ngFor="let column of layerColumnData[i] || []" [value]="column">{{column}}</option>
</select>
<div class="clr-subtext">Select the column to use as parameter for URL template replacement in layer {{i + 1}} drilldown</div>
<div class="clr-subtext">Select the column to use as parameter for URL template replacement in layer {{i
+
1}} drilldown</div>
</div>
</div>
@@ -492,16 +641,17 @@
<div class="clr-form-control" style="margin-top: 10px;">
<div class="clr-control-container">
<div class="clr-checkbox-wrapper">
<input type="checkbox" [id]="'commonFilterToggleLayer' + i" [(ngModel)]="layer.commonFilterEnabled"
(change)="onCommonFilterToggleLayer(i)" [ngModelOptions]="{standalone: true}" class="clr-checkbox" />
<input type="checkbox" [id]="'commonFilterToggleLayer' + i"
[(ngModel)]="layer.commonFilterEnabled" (change)="onCommonFilterToggleLayer(i)"
[ngModelOptions]="{standalone: true}" class="clr-checkbox" />
<label [for]="'commonFilterToggleLayer' + i" class="clr-control-label">Use Common Filter</label>
</div>
</div>
</div>
<!-- Add Layer Filter Button -->
<button class="btn btn-sm btn-primary" (click)="addLayerFilter(i)" style="margin-top: 10px; margin-bottom: 10px;"
[disabled]="layer.commonFilterEnabled">
<button class="btn btn-sm btn-primary" (click)="addLayerFilter(i)"
style="margin-top: 10px; margin-bottom: 10px;" [disabled]="layer.commonFilterEnabled">
<clr-icon shape="plus"></clr-icon> Add Filter
</button>
@@ -517,18 +667,37 @@
</div>
<div class="clr-row" style="margin-top: 8px;">
<div class="clr-col-sm-5">
<select [(ngModel)]="filter.field" [ngModelOptions]="{standalone: true}" class="clr-select"
<div class="clr-col-sm-4">
<select [(ngModel)]="filter.field" (ngModelChange)="onLayerFilterFieldChange(i, j, $event)" [ngModelOptions]="{standalone: true}" class="clr-select"
[disabled]="layer.commonFilterEnabled">
<option value="">Select Field</option>
<option *ngFor="let column of getAvailableFields(layer.filters, j, layerColumnData[i] || [])" [value]="column">{{column}}</option>
<option *ngFor="let column of getAvailableFields(layer.filters, j, layerColumnData[i] || [])"
[value]="column">{{column}}</option>
</select>
</div>
<div class="clr-col-sm-5">
<input type="text" [(ngModel)]="filter.value" [ngModelOptions]="{standalone: true}"
class="clr-input" placeholder="Filter Value"
[disabled]="layer.commonFilterEnabled"/>
<div class="clr-col-sm-3">
<select [(ngModel)]="filter.type" (ngModelChange)="onLayerFilterTypeChange(i, j, $event)" [ngModelOptions]="{standalone: true}" class="clr-select"
[disabled]="layer.commonFilterEnabled">
<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>
<div class="clr-col-sm-3" *ngIf="filter.type === 'dropdown' || filter.type === 'multiselect'">
<input type="text" [(ngModel)]="filter.options" [ngModelOptions]="{standalone: true}" class="clr-input"
placeholder="Option1,Option2,Option3" [disabled]="layer.commonFilterEnabled" />
<div class="clr-subtext" *ngIf="filter.availableValues">
Available: {{ filter.availableValues }}
</div>
</div>
<div class="clr-col-sm-3" *ngIf="filter.type !== 'dropdown' && filter.type !== 'multiselect'">
<input type="text" [(ngModel)]="filter.value" [ngModelOptions]="{standalone: true}" class="clr-input"
placeholder="Filter Value" [disabled]="layer.commonFilterEnabled" />
</div>
<div class="clr-col-sm-2">
@@ -550,6 +719,9 @@
<input id="chartparameter" type="text" formControlName="chartparameter" class="clr-input" [(ngModel)]="gadgetsEditdata.chartparameter">
</div>
</div> -->
</div>
<div class="modal-footer">
<button type="button" class="btn btn-outline" (click)="modeledit = false">Cancel</button>
<button type="button" class="btn btn-primary" (click)="applyChanges(modelid)">Apply</button>
@@ -568,7 +740,8 @@
<div class="clr-row">
<div class="clr-col-sm-12">
<label for="commonFilterConnection">Connection</label>
<select id="commonFilterConnection" formControlName="connection" [(ngModel)]="commonFilterData.connection" class="clr-select">
<select id="commonFilterConnection" formControlName="connection" [(ngModel)]="commonFilterData.connection"
class="clr-select">
<option value="">Select Connection</option>
<option *ngFor="let conn of sureconnectData" [value]="conn.id">
{{conn.connection_name || conn.id}}
@@ -584,8 +757,8 @@
<input type="text" id="commonFilterApiUrl" formControlName="apiUrl" class="clr-input"
[(ngModel)]="commonFilterData.apiUrl" style="width:90%">&nbsp;
<span>
<button class="btn btn-icon btn-primary" style="margin: 0px;"
(click)="refreshCommonFilterColumns()" [disabled]="!commonFilterData.apiUrl">
<button class="btn btn-icon btn-primary" style="margin: 0px;" (click)="refreshCommonFilterColumns()"
[disabled]="!commonFilterData.apiUrl">
<clr-icon shape="redo"></clr-icon>
</button>
</span>
@@ -599,7 +772,8 @@
<h5>Common Filters</h5>
<!-- Add Common Filter Button -->
<button class="btn btn-sm btn-primary" (click)="addCommonFilter()" style="margin-top: 10px; margin-bottom: 10px;">
<button class="btn btn-sm btn-primary" (click)="addCommonFilter()"
style="margin-top: 10px; margin-bottom: 10px;">
<clr-icon shape="plus"></clr-icon> Add Filter
</button>
@@ -622,8 +796,8 @@
</div>
<div class="clr-col-sm-5">
<input type="text" [(ngModel)]="filter.value" [ngModelOptions]="{standalone: true}"
class="clr-input" placeholder="Filter Value" />
<input type="text" [(ngModel)]="filter.value" [ngModelOptions]="{standalone: true}" class="clr-input"
placeholder="Filter Value" />
</div>
<div class="clr-col-sm-2">

View File

@@ -1,31 +1,334 @@
<div style="display: block">
<!-- No filter controls needed with the new simplified approach -->
<!-- Filters are now configured at the drilldown level -->
<div class="chart-container">
<!-- Filter Controls Section -->
<div class="filter-section" *ngIf="hasActiveFilters()">
<!-- Base Filters -->
<div class="filter-group" *ngIf="baseFilters && baseFilters.length > 0">
<h4>Base Filters</h4>
<div class="filter-controls">
<div *ngFor="let filter of baseFilters" class="filter-item">
<div class="filter-label">{{ filter.field }} ({{ filter.type || 'text' }})</div>
<!-- Drilldown mode indicator -->
<div *ngIf="currentDrilldownLevel > 0" style="background-color: #e0e0e0; padding: 5px; margin-bottom: 10px; border-radius: 4px; text-align: center;">
<span style="font-weight: bold; color: #333;">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;">
Back to Level {{currentDrilldownLevel - 1}}
</button>
<button (click)="resetToOriginalData()" style="margin-left: 10px; padding: 2px 8px; background-color: #dc3545; color: white; border: none; border-radius: 3px; cursor: pointer;">
Back to Main View
</button>
<!-- Text Filter -->
<div *ngIf="!filter.type || filter.type === 'text'" class="filter-input">
<input type="text"
[(ngModel)]="filter.value"
(ngModelChange)="onBaseFilterChange(filter)"
[placeholder]="filter.field"
class="clr-input filter-text-input">
</div>
<!-- No data message -->
<div *ngIf="noDataAvailable" style="text-align: center; padding: 20px; color: #666; font-style: italic;">
<!-- Dropdown Filter -->
<div *ngIf="filter.type === 'dropdown'" class="filter-input">
<select [(ngModel)]="filter.value"
(ngModelChange)="onBaseFilterChange(filter)"
class="clr-select filter-select">
<option value="">Select {{ filter.field }}</option>
<option *ngFor="let option of getFilterOptions(filter)" [value]="option">{{ option }}</option>
</select>
</div>
<!-- Multi-Select Filter - Updated to show key first, then dropdown on click -->
<div *ngIf="filter.type === 'multiselect'" class="filter-input multiselect-container">
<div class="multiselect-display" (click)="toggleMultiselect(filter, 'base')">
<span class="multiselect-label">{{ filter.field }}</span>
<span class="multiselect-value" *ngIf="getSelectedOptionsCount(filter) > 0">
({{ getSelectedOptionsCount(filter) }} selected)
</span>
<clr-icon shape="caret down" class="dropdown-icon"></clr-icon>
</div>
<div class="multiselect-dropdown" *ngIf="isMultiselectOpen(filter, 'base')">
<div class="checkbox-group">
<div *ngFor="let option of getFilterOptions(filter); let i = index" class="checkbox-item">
<input type="checkbox"
[checked]="isOptionSelected(filter, option)"
(change)="onMultiSelectChange(filter, option, $event)"
[id]="'base-' + filter.field + '-' + i"
class="clr-checkbox">
<label [for]="'base-' + filter.field + '-' + i" class="checkbox-label">{{ option }}</label>
</div>
</div>
</div>
</div>
<!-- Date Range Filter -->
<div *ngIf="filter.type === 'date-range'" class="filter-input date-range">
<div class="date-input-group">
<input type="date"
[(ngModel)]="filter.value.start"
(ngModelChange)="onDateRangeChange(filter, { start: $event, end: filter.value.end })"
placeholder="Start Date"
class="clr-input filter-date">
<span class="date-separator">to</span>
<input type="date"
[(ngModel)]="filter.value.end"
(ngModelChange)="onDateRangeChange(filter, { start: filter.value.start, end: $event })"
placeholder="End Date"
class="clr-input filter-date">
</div>
</div>
<!-- Toggle Filter -->
<div *ngIf="filter.type === 'toggle'" class="filter-input toggle">
<input type="checkbox"
[(ngModel)]="filter.value"
(ngModelChange)="onToggleChange(filter, $event)"
clrToggle
class="clr-toggle">
<label class="toggle-label">{{ filter.field }}</label>
</div>
</div>
</div>
</div>
<!-- Drilldown Filters -->
<div class="filter-group" *ngIf="drilldownFilters && drilldownFilters.length > 0 && currentDrilldownLevel > 0">
<h4>Drilldown Filters</h4>
<div class="filter-controls">
<div *ngFor="let filter of drilldownFilters" class="filter-item">
<div class="filter-label">{{ filter.field }} ({{ filter.type || 'text' }})</div>
<!-- Text Filter -->
<div *ngIf="!filter.type || filter.type === 'text'" class="filter-input">
<input type="text"
[(ngModel)]="filter.value"
(ngModelChange)="onDrilldownFilterChange(filter)"
[placeholder]="filter.field"
class="clr-input filter-text-input">
</div>
<!-- Dropdown Filter -->
<div *ngIf="filter.type === 'dropdown'" class="filter-input">
<select [(ngModel)]="filter.value"
(ngModelChange)="onDrilldownFilterChange(filter)"
class="clr-select filter-select">
<option value="">Select {{ filter.field }}</option>
<option *ngFor="let option of getFilterOptions(filter)" [value]="option">{{ option }}</option>
</select>
</div>
<!-- Multi-Select Filter - Updated to show key first, then dropdown on click -->
<div *ngIf="filter.type === 'multiselect'" class="filter-input multiselect-container">
<div class="multiselect-display" (click)="toggleMultiselect(filter, 'drilldown')">
<span class="multiselect-label">{{ filter.field }}</span>
<span class="multiselect-value" *ngIf="getSelectedOptionsCount(filter) > 0">
({{ getSelectedOptionsCount(filter) }} selected)
</span>
<clr-icon shape="caret down" class="dropdown-icon"></clr-icon>
</div>
<div class="multiselect-dropdown" *ngIf="isMultiselectOpen(filter, 'drilldown')">
<div class="checkbox-group">
<div *ngFor="let option of getFilterOptions(filter); let i = index" class="checkbox-item">
<input type="checkbox"
[checked]="isOptionSelected(filter, option)"
(change)="onMultiSelectChange(filter, option, $event)"
[id]="'drilldown-' + filter.field + '-' + i"
class="clr-checkbox">
<label [for]="'drilldown-' + filter.field + '-' + i" class="checkbox-label">{{ option }}</label>
</div>
</div>
</div>
</div>
<!-- Date Range Filter -->
<div *ngIf="filter.type === 'date-range'" class="filter-input date-range">
<div class="date-input-group">
<input type="date"
[(ngModel)]="filter.value.start"
(ngModelChange)="onDateRangeChange(filter, { start: $event, end: filter.value.end })"
placeholder="Start Date"
class="clr-input filter-date">
<span class="date-separator">to</span>
<input type="date"
[(ngModel)]="filter.value.end"
(ngModelChange)="onDateRangeChange(filter, { start: filter.value.start, end: $event })"
placeholder="End Date"
class="clr-input filter-date">
</div>
</div>
<!-- Toggle Filter -->
<div *ngIf="filter.type === 'toggle'" class="filter-input toggle">
<input type="checkbox"
[(ngModel)]="filter.value"
(ngModelChange)="onToggleChange(filter, $event)"
clrToggle
class="clr-toggle">
<label class="toggle-label">{{ filter.field }}</label>
</div>
</div>
</div>
</div>
<!-- Layer Filters -->
<div class="filter-group" *ngIf="hasActiveLayerFilters()">
<h4>Layer Filters</h4>
<div class="filter-controls">
<div *ngFor="let filter of getActiveLayerFilters()" class="filter-item">
<div class="filter-label">{{ filter.field }} ({{ filter.type || 'text' }})</div>
<!-- Text Filter -->
<div *ngIf="!filter.type || filter.type === 'text'" class="filter-input">
<input type="text"
[(ngModel)]="filter.value"
(ngModelChange)="onLayerFilterChange(filter)"
[placeholder]="filter.field"
class="clr-input filter-text-input">
</div>
<!-- Dropdown Filter -->
<div *ngIf="filter.type === 'dropdown'" class="filter-input">
<select [(ngModel)]="filter.value"
(ngModelChange)="onLayerFilterChange(filter)"
class="clr-select filter-select">
<option value="">Select {{ filter.field }}</option>
<option *ngFor="let option of getFilterOptions(filter)" [value]="option">{{ option }}</option>
</select>
</div>
<!-- Multi-Select Filter - Updated to show key first, then dropdown on click -->
<div *ngIf="filter.type === 'multiselect'" class="filter-input multiselect-container">
<div class="multiselect-display" (click)="toggleMultiselect(filter, 'layer')">
<span class="multiselect-label">{{ filter.field }}</span>
<span class="multiselect-value" *ngIf="getSelectedOptionsCount(filter) > 0">
({{ getSelectedOptionsCount(filter) }} selected)
</span>
<clr-icon shape="caret down" class="dropdown-icon"></clr-icon>
</div>
<div class="multiselect-dropdown" *ngIf="isMultiselectOpen(filter, 'layer')">
<div class="checkbox-group">
<div *ngFor="let option of getFilterOptions(filter); let i = index" class="checkbox-item">
<input type="checkbox"
[checked]="isOptionSelected(filter, option)"
(change)="onMultiSelectChange(filter, option, $event)"
[id]="'layer-' + filter.field + '-' + i"
class="clr-checkbox">
<label [for]="'layer-' + filter.field + '-' + i" class="checkbox-label">{{ option }}</label>
</div>
</div>
</div>
</div>
<!-- Date Range Filter -->
<div *ngIf="filter.type === 'date-range'" class="filter-input date-range">
<div class="date-input-group">
<input type="date"
[(ngModel)]="filter.value.start"
(ngModelChange)="onDateRangeChange(filter, { start: $event, end: filter.value.end })"
placeholder="Start Date"
class="clr-input filter-date">
<span class="date-separator">to</span>
<input type="date"
[(ngModel)]="filter.value.end"
(ngModelChange)="onDateRangeChange(filter, { start: filter.value.start, end: $event })"
placeholder="End Date"
class="clr-input filter-date">
</div>
</div>
<!-- Toggle Filter -->
<div *ngIf="filter.type === 'toggle'" class="filter-input toggle">
<input type="checkbox"
[(ngModel)]="filter.value"
(ngModelChange)="onToggleChange(filter, $event)"
clrToggle
class="clr-toggle">
<label class="toggle-label">{{ filter.field }}</label>
</div>
</div>
</div>
</div>
<!-- Clear Filters Button -->
<div class="filter-actions">
<button class="btn btn-sm btn-outline" (click)="clearAllFilters()">Clear All Filters</button>
</div>
</div>
<!-- Header row with chart title and drilldown navigation -->
<div class="chart-header">
<div class="clr-row header-row">
<div class="clr-col-6">
<h3 class="chart-title">{{charttitle || 'Bar Chart'}}</h3>
</div>
<div class="clr-col-6" style="text-align: right;">
<!-- Add drilldown navigation controls -->
<button class="btn btn-sm btn-link" *ngIf="drilldownEnabled && drilldownStack.length > 0" (click)="navigateBack()">
<cds-icon shape="arrow" direction="left"></cds-icon>
Back to {{drilldownStack.length > 1 ? 'Previous Level' : 'Main Data'}}
</button>
</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].clickedLabel}})
</span>
</span>
</div>
</div>
</div>
</div>
</div>
</div>
<div class="chart-wrapper">
<div class="chart-content" [class.loading]="isLoading">
<div *ngIf="noDataAvailable" class="no-data-message">
No data available
</div>
<!-- Chart display -->
<div *ngIf="!noDataAvailable">
<div *ngIf="!noDataAvailable" class="chart-display">
<canvas baseChart
[datasets]="barChartData"
[labels]="barChartLabels"
[type]="barChartType"
[options]="barChartOptions"
(chartHover)="chartHovered($event)"
(chartClick)="chartClicked($event)">
</canvas>
</div>
<div class="loading-overlay" *ngIf="isLoading">
<div class="shimmer-bar"></div>
</div>
</div>
</div>
<!-- sheield dashboard -->
<!--
<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> -->
</div>

View File

@@ -0,0 +1,278 @@
// Chart container structure
.chart-container {
height: 100%;
display: flex;
flex-direction: column;
// Filter section styling
.filter-section {
margin-bottom: 20px;
padding: 15px;
border: 1px solid #ddd;
border-radius: 4px;
background-color: #f9f9f9;
}
.filter-group {
margin-bottom: 15px;
h4 {
margin-top: 0;
margin-bottom: 10px;
color: #333;
font-weight: 600;
}
}
.filter-controls {
display: flex;
flex-wrap: wrap;
gap: 15px;
}
.filter-item {
flex: 1 1 300px;
min-width: 250px;
padding: 10px;
background: white;
border: 1px solid #e0e0e0;
border-radius: 4px;
}
.filter-label {
font-weight: 500;
margin-bottom: 8px;
color: #555;
font-size: 14px;
}
.filter-input {
width: 100%;
.filter-text-input,
.filter-select,
.filter-date {
width: 100%;
padding: 6px 12px;
border: 1px solid #ccc;
border-radius: 4px;
font-size: 14px;
}
.filter-select {
height: 34px;
}
}
.multiselect-container {
position: relative;
}
.multiselect-display {
display: flex;
align-items: center;
justify-content: space-between;
padding: 6px 12px;
border: 1px solid #ccc;
border-radius: 4px;
background: white;
cursor: pointer;
min-height: 34px;
.multiselect-label {
flex: 1;
font-size: 14px;
}
.multiselect-value {
color: #666;
font-size: 12px;
margin-right: 8px;
}
.dropdown-icon {
flex-shrink: 0;
transition: transform 0.2s ease;
}
&:hover {
border-color: #999;
}
}
.multiselect-dropdown {
position: absolute;
top: 100%;
left: 0;
right: 0;
z-index: 1000;
background: white;
border: 1px solid #ccc;
border-top: none;
border-radius: 0 0 4px 4px;
box-shadow: 0 2px 4px rgba(0,0,0,0.1);
max-height: 200px;
overflow-y: auto;
.checkbox-group {
padding: 8px;
.checkbox-item {
display: flex;
align-items: center;
gap: 8px;
padding: 4px 0;
.checkbox-label {
margin: 0;
font-size: 14px;
cursor: pointer;
}
}
}
}
.date-range {
.date-input-group {
display: flex;
align-items: center;
gap: 8px;
}
.date-separator {
margin: 0 5px;
color: #777;
}
}
.toggle {
display: flex;
align-items: center;
gap: 8px;
.toggle-label {
margin: 0;
font-size: 14px;
cursor: pointer;
}
}
.filter-actions {
margin-top: 15px;
padding-top: 15px;
border-top: 1px solid #eee;
.btn {
font-size: 13px;
}
}
// Chart header styling
.chart-header {
margin-bottom: 20px;
.header-row {
margin-bottom: 15px;
padding-bottom: 10px;
border-bottom: 1px solid #eee;
.chart-title {
margin: 0;
font-size: 18px;
font-weight: 600;
color: #333;
}
}
}
// Chart wrapper and content
.chart-wrapper {
flex: 1;
position: relative;
.chart-content {
position: relative;
height: 100%;
min-height: 300px; // Ensure minimum height for chart
&.loading {
opacity: 0.7;
.chart-display {
filter: blur(2px);
}
}
.no-data-message {
text-align: center;
padding: 20px;
color: #666;
font-style: italic;
}
.chart-display {
position: relative;
height: 100%;
width: 100%;
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;
}
}
// Responsive design
@media (max-width: 768px) {
.chart-container {
.filter-controls {
flex-direction: column;
}
.filter-item {
min-width: 100%;
}
.chart-header {
.header-row {
.chart-title {
font-size: 16px;
}
}
}
.chart-content {
min-height: 250px; // Adjust for mobile
}
}
}

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 { Subscription } from 'rxjs';
import { FilterService } from '../../common-filter/filter.service';
// Add BaseChartDirective import for chart resizing
import { BaseChartDirective } from 'ng2-charts';
@Component({
selector: 'app-bar-chart',
@@ -34,6 +37,9 @@ export class BarChartComponent implements OnInit, OnChanges, OnDestroy {
// Multi-layer drilldown configuration inputs
@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'];
barChartType: string = 'bar';
barChartPlugins = [];
@@ -42,6 +48,58 @@ export class BarChartComponent implements OnInit, OnChanges, OnDestroy {
];
barChartLegend: boolean = true;
// Add responsive chart options
barChartOptions: any = {
responsive: true,
maintainAspectRatio: false,
scales: {
x: {
ticks: {
autoSkip: false,
maxRotation: 45,
minRotation: 45,
padding: 15,
font: {
size: 12
}
},
grid: {
display: false
}
},
y: {
beginAtZero: true,
ticks: {
font: {
size: 12
}
}
}
},
plugins: {
legend: {
display: true,
position: 'top',
labels: {
font: {
size: 12
}
}
},
tooltip: {
enabled: true
}
},
layout: {
padding: {
bottom: 60,
left: 15,
right: 15,
top: 15
}
}
};
// Multi-layer drilldown state tracking
drilldownStack: any[] = []; // Stack to track drilldown navigation history
currentDrilldownLevel: number = 0; // Current drilldown level (0 = base level)
@@ -51,15 +109,34 @@ export class BarChartComponent implements OnInit, OnChanges, OnDestroy {
// No data state
noDataAvailable: boolean = false;
// Loading state
isLoading: boolean = false;
// Flag to prevent infinite loops
private isFetchingData: boolean = false;
// Subscriptions to unsubscribe on destroy
private subscriptions: Subscription[] = [];
constructor(private dashboardService: Dashboard3Service) { }
// Add properties for filter functionality
private openMultiselects: Map<string, string> = new Map(); // Map of filterId -> context
private documentClickHandler: ((event: MouseEvent) => void) | null = null;
private filtersInitialized: boolean = false;
constructor(
private dashboardService: Dashboard3Service,
private filterService: FilterService
) { }
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
this.fetchChartData();
}
@@ -67,6 +144,12 @@ export class BarChartComponent implements OnInit, OnChanges, OnDestroy {
ngOnChanges(changes: SimpleChanges): void {
console.log('BarChartComponent input changes:', changes);
// Initialize filter values if they haven't been initialized yet
if (!this.filtersInitialized && (changes.baseFilters || changes.drilldownFilters || changes.drilldownLayers)) {
this.initializeFilterValues();
this.filtersInitialized = true;
}
// Check if any of the key properties have changed
const xAxisChanged = changes.xAxis && !changes.xAxis.firstChange;
const yAxisChanged = changes.yAxis && !changes.yAxis.firstChange;
@@ -92,28 +175,348 @@ export class BarChartComponent implements OnInit, OnChanges, OnDestroy {
// Update legend visibility if it changed
if (changes.chartlegend !== undefined) {
this.barChartLegend = changes.chartlegend.currentValue;
this.barChartOptions.plugins.legend.display = this.barChartLegend;
console.log('Chart legend changed to:', this.barChartLegend);
}
}
// Initialize filter values with proper default values based on type
private initializeFilterValues(): void {
console.log('Initializing filter values');
// Initialize base filters
if (this.baseFilters) {
this.baseFilters.forEach(filter => {
if (filter.value === undefined || filter.value === null) {
switch (filter.type) {
case 'multiselect':
filter.value = [];
break;
case 'date-range':
filter.value = { start: null, end: null };
break;
case 'toggle':
filter.value = false;
break;
default:
filter.value = '';
}
}
});
}
// Initialize drilldown filters
if (this.drilldownFilters) {
this.drilldownFilters.forEach(filter => {
if (filter.value === undefined || filter.value === null) {
switch (filter.type) {
case 'multiselect':
filter.value = [];
break;
case 'date-range':
filter.value = { start: null, end: null };
break;
case 'toggle':
filter.value = false;
break;
default:
filter.value = '';
}
}
});
}
// Initialize layer filters
if (this.drilldownLayers) {
this.drilldownLayers.forEach(layer => {
if (layer.filters) {
layer.filters.forEach((filter: any) => {
if (filter.value === undefined || filter.value === null) {
switch (filter.type) {
case 'multiselect':
filter.value = [];
break;
case 'date-range':
filter.value = { start: null, end: null };
break;
case 'toggle':
filter.value = false;
break;
default:
filter.value = '';
}
}
});
}
});
}
console.log('Filter values initialized:', {
baseFilters: this.baseFilters,
drilldownFilters: this.drilldownFilters,
drilldownLayers: this.drilldownLayers
});
}
// Check if there are active filters
hasActiveFilters(): boolean {
return (this.baseFilters && this.baseFilters.length > 0) ||
(this.drilldownFilters && this.drilldownFilters.length > 0) ||
this.hasActiveLayerFilters();
}
// Check if there are active layer filters for current drilldown level
hasActiveLayerFilters(): boolean {
if (this.currentDrilldownLevel > 1 && this.drilldownLayers && this.drilldownLayers.length > 0) {
const layerIndex = this.currentDrilldownLevel - 2; // -2 because level 1 is base drilldown
return layerIndex < this.drilldownLayers.length &&
this.drilldownLayers[layerIndex].filters &&
this.drilldownLayers[layerIndex].filters.length > 0;
}
return false;
}
// Get active layer filters for current drilldown level
getActiveLayerFilters(): any[] {
if (this.currentDrilldownLevel > 1 && this.drilldownLayers && this.drilldownLayers.length > 0) {
const layerIndex = this.currentDrilldownLevel - 2; // -2 because level 1 is base drilldown
if (layerIndex < this.drilldownLayers.length &&
this.drilldownLayers[layerIndex].filters) {
return this.drilldownLayers[layerIndex].filters;
}
}
return [];
}
// Get filter options for dropdown/multiselect filters
getFilterOptions(filter: any): string[] {
if (filter.options) {
if (Array.isArray(filter.options)) {
return filter.options;
} else if (typeof filter.options === 'string') {
return filter.options.split(',').map(opt => opt.trim()).filter(opt => opt);
}
}
return [];
}
// Check if an option is selected for multiselect filters
isOptionSelected(filter: any, option: string): boolean {
if (!filter.value) {
return false;
}
if (Array.isArray(filter.value)) {
return filter.value.includes(option);
}
return filter.value === option;
}
// Handle base filter changes
onBaseFilterChange(filter: any): void {
console.log('Base filter changed:', filter);
// Refresh data when filter changes
this.fetchChartData();
}
// Handle drilldown filter changes
onDrilldownFilterChange(filter: any): void {
console.log('Drilldown filter changed:', filter);
// Refresh data when filter changes
this.fetchChartData();
}
// Handle layer filter changes
onLayerFilterChange(filter: any): void {
console.log('Layer filter changed:', filter);
// Refresh data when filter changes
this.fetchChartData();
}
// Handle multiselect changes
onMultiSelectChange(filter: any, option: string, event: any): void {
const checked = event.target.checked;
// Initialize filter.value as array if it's not already
if (!Array.isArray(filter.value)) {
filter.value = [];
}
if (checked) {
// Add option to array if not already present
if (!filter.value.includes(option)) {
filter.value.push(option);
}
} else {
// Remove option from array
filter.value = filter.value.filter((item: string) => item !== option);
}
// Refresh data when filter changes
this.fetchChartData();
}
// Handle date range changes
onDateRangeChange(filter: any, dateRange: { start: string | null, end: string | null }): void {
filter.value = dateRange;
// Refresh data when filter changes
this.fetchChartData();
}
// Handle toggle changes
onToggleChange(filter: any, checked: boolean): void {
filter.value = checked;
// Refresh data when filter changes
this.fetchChartData();
}
// Toggle multiselect dropdown visibility
toggleMultiselect(filter: any, context: string): void {
const filterId = `${context}-${filter.field}`;
if (this.isMultiselectOpen(filter, context)) {
this.openMultiselects.delete(filterId);
} else {
// Close all other multiselects first
this.openMultiselects.clear();
this.openMultiselects.set(filterId, context);
// Add document click handler to close dropdown when clicking outside
this.addDocumentClickHandler();
}
}
// Add document click handler to close dropdowns when clicking outside
private addDocumentClickHandler(): void {
if (!this.documentClickHandler) {
this.documentClickHandler = (event: MouseEvent) => {
const target = event.target as HTMLElement;
// Check if click is outside any multiselect dropdown
if (!target.closest('.multiselect-container')) {
this.openMultiselects.clear();
this.removeDocumentClickHandler();
}
};
// Use setTimeout to ensure the click event that opened the dropdown doesn't immediately close it
setTimeout(() => {
document.addEventListener('click', this.documentClickHandler!);
}, 0);
}
}
// Remove document click handler
private removeDocumentClickHandler(): void {
if (this.documentClickHandler) {
document.removeEventListener('click', this.documentClickHandler);
this.documentClickHandler = null;
}
}
// Check if multiselect dropdown is open
isMultiselectOpen(filter: any, context: string): boolean {
const filterId = `${context}-${filter.field}`;
return this.openMultiselects.has(filterId);
}
// Get count of selected options for a multiselect filter
getSelectedOptionsCount(filter: any): number {
if (!filter.value) {
return 0;
}
if (Array.isArray(filter.value)) {
return filter.value.length;
}
return 0;
}
// Clear all filters
clearAllFilters(): void {
// Clear base filters
if (this.baseFilters) {
this.baseFilters.forEach(filter => {
if (filter.type === 'multiselect') {
filter.value = [];
} else if (filter.type === 'date-range') {
filter.value = { start: null, end: null };
} else if (filter.type === 'toggle') {
filter.value = false;
} else {
filter.value = '';
}
});
}
// Clear drilldown filters
if (this.drilldownFilters) {
this.drilldownFilters.forEach(filter => {
if (filter.type === 'multiselect') {
filter.value = [];
} else if (filter.type === 'date-range') {
filter.value = { start: null, end: null };
} else if (filter.type === 'toggle') {
filter.value = false;
} else {
filter.value = '';
}
});
}
// Clear layer filters
if (this.drilldownLayers) {
this.drilldownLayers.forEach(layer => {
if (layer.filters) {
layer.filters.forEach((filter: any) => {
if (filter.type === 'multiselect') {
filter.value = [];
} else if (filter.type === 'date-range') {
filter.value = { start: null, end: null };
} else if (filter.type === 'toggle') {
filter.value = false;
} else {
filter.value = '';
}
});
}
});
}
// Close all multiselect dropdowns
this.openMultiselects.clear();
// Refresh data
this.fetchChartData();
}
fetchChartData(): void {
// Set loading state
this.isLoading = true;
// 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
// Reset flags after fetching
this.isFetchingData = false;
this.isLoading = false;
return;
}
// If we have the necessary data, fetch chart data from the service
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
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)
let parameterValue = '';
@@ -123,31 +526,55 @@ export class BarChartComponent implements OnInit, OnChanges, OnDestroy {
console.log('Bar chart data URL:', url);
// Convert baseFilters to filter parameters
let filterParams = '';
if (this.baseFilters && this.baseFilters.length > 0) {
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
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) {
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
// 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(
(data: any) => {
console.log('=== BAR CHART DATA RESPONSE ===');
console.log('Received bar chart data:', data);
if (data === null) {
console.warn('Bar chart API returned null data. Check if the API endpoint is working correctly.');
this.noDataAvailable = true;
this.barChartLabels = [];
this.barChartData = [];
// Reset flag after fetching
// Reset flags after fetching
this.isFetchingData = false;
this.isLoading = false;
return;
}
@@ -160,6 +587,7 @@ export class BarChartComponent implements OnInit, OnChanges, OnDestroy {
// Trigger change detection
// this.barChartData = [...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) {
// Backend has already filtered the data, just display it
this.noDataAvailable = data.labels.length === 0;
@@ -168,22 +596,26 @@ export class BarChartComponent implements OnInit, OnChanges, OnDestroy {
// Trigger change detection
// this.barChartData = [...this.barChartData];
console.log('Updated bar chart with legacy data format:', { labels: this.barChartLabels, data: this.barChartData });
console.log('=== CHART UPDATED SUCCESSFULLY (LEGACY) ===');
} else {
console.warn('Bar chart received data does not have expected structure', data);
this.noDataAvailable = true;
this.barChartLabels = [];
this.barChartData = [];
}
// Reset flag after fetching
// Reset flags after fetching
this.isFetchingData = false;
this.isLoading = false;
},
(error) => {
console.error('=== BAR CHART ERROR ===');
console.error('Error fetching bar chart data:', error);
this.noDataAvailable = true;
this.barChartLabels = [];
this.barChartData = [];
// Reset flag after fetching
// Reset flags after fetching
this.isFetchingData = false;
this.isLoading = false;
// Keep default data in case of error
}
);
@@ -195,8 +627,9 @@ export class BarChartComponent implements OnInit, OnChanges, OnDestroy {
this.noDataAvailable = true;
this.barChartLabels = [];
this.barChartData = [];
// Reset flag after fetching
// Reset flags after fetching
this.isFetchingData = false;
this.isLoading = false;
}
}
@@ -286,33 +719,49 @@ export class BarChartComponent implements OnInit, OnChanges, OnDestroy {
console.log('Drilldown data URL:', url);
// Convert drilldown layer filters to filter parameters (if applicable)
let filterParams = '';
if (drilldownConfig.filters && drilldownConfig.filters.length > 0) {
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;
}
});
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
let drilldownFilterParams = '';
// Add drilldownFilters
if (this.drilldownFilters && this.drilldownFilters.length > 0) {
const filterObj = {};
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
@@ -342,17 +791,23 @@ export class BarChartComponent implements OnInit, OnChanges, OnDestroy {
// Trigger change detection
// this.barChartData = [...this.barChartData];
console.log('Updated bar chart with drilldown data:', { labels: this.barChartLabels, data: this.barChartData });
// Set loading state to false
this.isLoading = false;
} else if (data && data.labels && data.datasets) {
// Backend has already filtered the data, just display it
this.noDataAvailable = data.labels.length === 0;
this.barChartLabels = data.labels;
this.barChartData = data.datasets;
console.log('Updated bar chart with drilldown legacy data format:', { labels: this.barChartLabels, data: this.barChartData });
// Set loading state to false
this.isLoading = false;
} else {
console.warn('Drilldown received data does not have expected structure', data);
this.noDataAvailable = true;
this.barChartLabels = [];
this.barChartData = [];
// Set loading state to false
this.isLoading = false;
}
},
(error) => {
@@ -360,12 +815,17 @@ export class BarChartComponent implements OnInit, OnChanges, OnDestroy {
this.noDataAvailable = true;
this.barChartLabels = [];
this.barChartData = [];
// Set loading state to false
this.isLoading = false;
// Keep current data in case of error
}
);
// Add subscription to array for cleanup
this.subscriptions.push(subscription);
// Set loading state
this.isLoading = true;
}
// Reset to original data (go back to base level)
@@ -426,6 +886,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
private syncLabelAndDataArrays(): void {
// For bar charts, we need to ensure all datasets have the same number of data points
@@ -557,6 +1029,12 @@ export class BarChartComponent implements OnInit, OnChanges, OnDestroy {
this.originalBarChartLabels = [];
this.originalBarChartData = [];
// Clear multiselect tracking
this.openMultiselects.clear();
// Remove document click handler
this.removeDocumentClickHandler();
console.log('BarChartComponent destroyed and cleaned up');
}
}

View File

@@ -1,28 +1,310 @@
<div style="display:block">
<!-- Drilldown mode indicator -->
<div *ngIf="currentDrilldownLevel > 0" style="background-color: #e0e0e0; padding: 5px; margin-bottom: 10px; border-radius: 4px; text-align: center;">
<span style="font-weight: bold; color: #333;">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;">
Back to Level {{currentDrilldownLevel - 1}}
</button>
<button (click)="resetToOriginalData()" style="margin-left: 10px; padding: 2px 8px; background-color: #dc3545; color: white; border: none; border-radius: 3px; cursor: pointer;">
Back to Main View
<div style="display:block; height: 100%; width: 100%;">
<!-- Filter Controls Section -->
<div class="filter-section" *ngIf="hasActiveFilters()">
<!-- Base Filters -->
<div class="filter-group" *ngIf="baseFilters && baseFilters.length > 0">
<h4>Base Filters</h4>
<div class="filter-controls">
<div *ngFor="let filter of baseFilters" class="filter-item">
<div class="filter-label">{{ filter.field }} ({{ filter.type || 'text' }})</div>
<!-- Text Filter -->
<div *ngIf="!filter.type || filter.type === 'text'" class="filter-input">
<input type="text"
[(ngModel)]="filter.value"
(ngModelChange)="onBaseFilterChange(filter)"
[placeholder]="filter.field"
class="clr-input filter-text-input">
</div>
<!-- Dropdown Filter -->
<div *ngIf="filter.type === 'dropdown'" class="filter-input">
<select [(ngModel)]="filter.value"
(ngModelChange)="onBaseFilterChange(filter)"
class="clr-select filter-select">
<option value="">Select {{ filter.field }}</option>
<option *ngFor="let option of getFilterOptions(filter)" [value]="option">{{ option }}</option>
</select>
</div>
<!-- Multi-Select Filter - Updated to show key first, then dropdown on click -->
<div *ngIf="filter.type === 'multiselect'" class="filter-input multiselect-container">
<div class="multiselect-display" (click)="toggleMultiselect(filter, 'base')">
<span class="multiselect-label">{{ filter.field }}</span>
<span class="multiselect-value" *ngIf="getSelectedOptionsCount(filter) > 0">
({{ getSelectedOptionsCount(filter) }} selected)
</span>
<clr-icon shape="caret down" class="dropdown-icon"></clr-icon>
</div>
<div class="multiselect-dropdown" *ngIf="isMultiselectOpen(filter, 'base')">
<div class="checkbox-group">
<div *ngFor="let option of getFilterOptions(filter); let i = index" class="checkbox-item">
<input type="checkbox"
[checked]="isOptionSelected(filter, option)"
(change)="onMultiSelectChange(filter, option, $event)"
[id]="'base-' + filter.field + '-' + i"
class="clr-checkbox">
<label [for]="'base-' + filter.field + '-' + i" class="checkbox-label">{{ option }}</label>
</div>
</div>
</div>
</div>
<!-- Date Range Filter -->
<div *ngIf="filter.type === 'date-range'" class="filter-input date-range">
<div class="date-input-group">
<input type="date"
[(ngModel)]="filter.value.start"
(ngModelChange)="onDateRangeChange(filter, { start: $event, end: filter.value.end })"
placeholder="Start Date"
class="clr-input filter-date">
<span class="date-separator">to</span>
<input type="date"
[(ngModel)]="filter.value.end"
(ngModelChange)="onDateRangeChange(filter, { start: filter.value.start, end: $event })"
placeholder="End Date"
class="clr-input filter-date">
</div>
</div>
<!-- Toggle Filter -->
<div *ngIf="filter.type === 'toggle'" class="filter-input toggle">
<input type="checkbox"
[(ngModel)]="filter.value"
(ngModelChange)="onToggleChange(filter, $event)"
clrToggle
class="clr-toggle">
<label class="toggle-label">{{ filter.field }}</label>
</div>
</div>
</div>
</div>
<!-- Drilldown Filters -->
<div class="filter-group" *ngIf="drilldownFilters && drilldownFilters.length > 0 && currentDrilldownLevel > 0">
<h4>Drilldown Filters</h4>
<div class="filter-controls">
<div *ngFor="let filter of drilldownFilters" class="filter-item">
<div class="filter-label">{{ filter.field }} ({{ filter.type || 'text' }})</div>
<!-- Text Filter -->
<div *ngIf="!filter.type || filter.type === 'text'" class="filter-input">
<input type="text"
[(ngModel)]="filter.value"
(ngModelChange)="onDrilldownFilterChange(filter)"
[placeholder]="filter.field"
class="clr-input filter-text-input">
</div>
<!-- Dropdown Filter -->
<div *ngIf="filter.type === 'dropdown'" class="filter-input">
<select [(ngModel)]="filter.value"
(ngModelChange)="onDrilldownFilterChange(filter)"
class="clr-select filter-select">
<option value="">Select {{ filter.field }}</option>
<option *ngFor="let option of getFilterOptions(filter)" [value]="option">{{ option }}</option>
</select>
</div>
<!-- Multi-Select Filter - Updated to show key first, then dropdown on click -->
<div *ngIf="filter.type === 'multiselect'" class="filter-input multiselect-container">
<div class="multiselect-display" (click)="toggleMultiselect(filter, 'drilldown')">
<span class="multiselect-label">{{ filter.field }}</span>
<span class="multiselect-value" *ngIf="getSelectedOptionsCount(filter) > 0">
({{ getSelectedOptionsCount(filter) }} selected)
</span>
<clr-icon shape="caret down" class="dropdown-icon"></clr-icon>
</div>
<div class="multiselect-dropdown" *ngIf="isMultiselectOpen(filter, 'drilldown')">
<div class="checkbox-group">
<div *ngFor="let option of getFilterOptions(filter); let i = index" class="checkbox-item">
<input type="checkbox"
[checked]="isOptionSelected(filter, option)"
(change)="onMultiSelectChange(filter, option, $event)"
[id]="'drilldown-' + filter.field + '-' + i"
class="clr-checkbox">
<label [for]="'drilldown-' + filter.field + '-' + i" class="checkbox-label">{{ option }}</label>
</div>
</div>
</div>
</div>
<!-- Date Range Filter -->
<div *ngIf="filter.type === 'date-range'" class="filter-input date-range">
<div class="date-input-group">
<input type="date"
[(ngModel)]="filter.value.start"
(ngModelChange)="onDateRangeChange(filter, { start: $event, end: filter.value.end })"
placeholder="Start Date"
class="clr-input filter-date">
<span class="date-separator">to</span>
<input type="date"
[(ngModel)]="filter.value.end"
(ngModelChange)="onDateRangeChange(filter, { start: filter.value.start, end: $event })"
placeholder="End Date"
class="clr-input filter-date">
</div>
</div>
<!-- Toggle Filter -->
<div *ngIf="filter.type === 'toggle'" class="filter-input toggle">
<input type="checkbox"
[(ngModel)]="filter.value"
(ngModelChange)="onToggleChange(filter, $event)"
clrToggle
class="clr-toggle">
<label class="toggle-label">{{ filter.field }}</label>
</div>
</div>
</div>
</div>
<!-- Layer Filters -->
<div class="filter-group" *ngIf="hasActiveLayerFilters()">
<h4>Layer Filters</h4>
<div class="filter-controls">
<div *ngFor="let filter of getActiveLayerFilters()" class="filter-item">
<div class="filter-label">{{ filter.field }} ({{ filter.type || 'text' }})</div>
<!-- Text Filter -->
<div *ngIf="!filter.type || filter.type === 'text'" class="filter-input">
<input type="text"
[(ngModel)]="filter.value"
(ngModelChange)="onLayerFilterChange(filter)"
[placeholder]="filter.field"
class="clr-input filter-text-input">
</div>
<!-- Dropdown Filter -->
<div *ngIf="filter.type === 'dropdown'" class="filter-input">
<select [(ngModel)]="filter.value"
(ngModelChange)="onLayerFilterChange(filter)"
class="clr-select filter-select">
<option value="">Select {{ filter.field }}</option>
<option *ngFor="let option of getFilterOptions(filter)" [value]="option">{{ option }}</option>
</select>
</div>
<!-- Multi-Select Filter - Updated to show key first, then dropdown on click -->
<div *ngIf="filter.type === 'multiselect'" class="filter-input multiselect-container">
<div class="multiselect-display" (click)="toggleMultiselect(filter, 'layer')">
<span class="multiselect-label">{{ filter.field }}</span>
<span class="multiselect-value" *ngIf="getSelectedOptionsCount(filter) > 0">
({{ getSelectedOptionsCount(filter) }} selected)
</span>
<clr-icon shape="caret down" class="dropdown-icon"></clr-icon>
</div>
<div class="multiselect-dropdown" *ngIf="isMultiselectOpen(filter, 'layer')">
<div class="checkbox-group">
<div *ngFor="let option of getFilterOptions(filter); let i = index" class="checkbox-item">
<input type="checkbox"
[checked]="isOptionSelected(filter, option)"
(change)="onMultiSelectChange(filter, option, $event)"
[id]="'layer-' + filter.field + '-' + i"
class="clr-checkbox">
<label [for]="'layer-' + filter.field + '-' + i" class="checkbox-label">{{ option }}</label>
</div>
</div>
</div>
</div>
<!-- Date Range Filter -->
<div *ngIf="filter.type === 'date-range'" class="filter-input date-range">
<div class="date-input-group">
<input type="date"
[(ngModel)]="filter.value.start"
(ngModelChange)="onDateRangeChange(filter, { start: $event, end: filter.value.end })"
placeholder="Start Date"
class="clr-input filter-date">
<span class="date-separator">to</span>
<input type="date"
[(ngModel)]="filter.value.end"
(ngModelChange)="onDateRangeChange(filter, { start: filter.value.start, end: $event })"
placeholder="End Date"
class="clr-input filter-date">
</div>
</div>
<!-- Toggle Filter -->
<div *ngIf="filter.type === 'toggle'" class="filter-input toggle">
<input type="checkbox"
[(ngModel)]="filter.value"
(ngModelChange)="onToggleChange(filter, $event)"
clrToggle
class="clr-toggle">
<label class="toggle-label">{{ filter.field }}</label>
</div>
</div>
</div>
</div>
<!-- Clear Filters Button -->
<div class="filter-actions">
<button class="btn btn-sm btn-outline" (click)="clearAllFilters()">Clear All Filters</button>
</div>
</div>
<!-- Header row with chart title and drilldown navigation -->
<div class="clr-row header-row">
<div class="clr-col-6">
<h3 class="chart-title">{{charttitle || 'Bubble Chart'}}</h3>
</div>
<div class="clr-col-6" style="text-align: right;">
<!-- Add drilldown navigation controls -->
<button class="btn btn-sm btn-link" *ngIf="drilldownEnabled && drilldownStack.length > 0" (click)="navigateBack()">
<cds-icon shape="arrow" direction="left"></cds-icon>
Back to {{drilldownStack.length > 1 ? 'Previous Level' : 'Main Data'}}
</button>
</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].clickedLabel}})
</span>
</span>
</div>
</div>
</div>
</div>
</div>
<!-- Chart container -->
<div style="position: relative; height: calc(100% - 80px); width: 100%; padding: 0 10px 30px 10px;">
<!-- Loading indicator -->
<div *ngIf="!dataLoaded" style="text-align: center; padding: 20px; color: #666; position: absolute; top: 50%; left: 50%; transform: translate(-50%, -50%); z-index: 10; width: 100%;">
Loading data...
</div>
<!-- No data message -->
<div *ngIf="noDataAvailable" style="text-align: center; padding: 20px; color: #666; font-style: italic;">
<div *ngIf="dataLoaded && (noDataAvailable || !isChartDataValid())" style="text-align: center; padding: 20px; color: #666; font-style: italic; position: absolute; top: 50%; left: 50%; transform: translate(-50%, -50%); z-index: 10; width: 100%;">
No data available
</div>
<!-- Chart display -->
<div *ngIf="!noDataAvailable">
<!-- Chart display - Always render the canvas but conditionally show/hide with CSS -->
<canvas baseChart
[datasets]="bubbleChartData"
[type]="bubbleChartType"
[options]="bubbleChartOptions"
(chartHover)="chartHovered($event)"
(chartClick)="chartClicked($event)">
(chartClick)="chartClicked($event)"
[style.visibility]="dataLoaded && !noDataAvailable && isChartDataValid() ? 'visible' : 'hidden'"
[style.position]="'absolute'"
[style.top]="'0'"
[style.left]="'0'"
[style.height]="'100%'"
[style.width]="'100%'"
[style.padding]="'0 10px 20px 10px'">
</canvas>
</div>
</div>

View File

@@ -0,0 +1,192 @@
.filter-section {
margin-bottom: 20px;
padding: 15px;
border: 1px solid #ddd;
border-radius: 4px;
background-color: #f9f9f9;
}
.filter-group {
margin-bottom: 15px;
h4 {
margin-top: 0;
margin-bottom: 10px;
color: #333;
font-weight: 600;
}
}
.filter-controls {
display: flex;
flex-wrap: wrap;
gap: 15px;
}
.filter-item {
flex: 1 1 300px;
min-width: 250px;
padding: 10px;
background: white;
border: 1px solid #e0e0e0;
border-radius: 4px;
}
.filter-label {
font-weight: 500;
margin-bottom: 8px;
color: #555;
font-size: 14px;
}
.filter-input {
width: 100%;
.filter-text-input,
.filter-select,
.filter-date {
width: 100%;
padding: 6px 12px;
border: 1px solid #ccc;
border-radius: 4px;
font-size: 14px;
}
.filter-select {
height: 34px;
}
}
.multiselect-container {
position: relative;
}
.multiselect-display {
display: flex;
align-items: center;
justify-content: space-between;
padding: 6px 12px;
border: 1px solid #ccc;
border-radius: 4px;
background: white;
cursor: pointer;
min-height: 34px;
.multiselect-label {
flex: 1;
font-size: 14px;
}
.multiselect-value {
color: #666;
font-size: 12px;
margin-right: 8px;
}
.dropdown-icon {
flex-shrink: 0;
transition: transform 0.2s ease;
}
&:hover {
border-color: #999;
}
}
.multiselect-dropdown {
position: absolute;
top: 100%;
left: 0;
right: 0;
z-index: 1000;
background: white;
border: 1px solid #ccc;
border-top: none;
border-radius: 0 0 4px 4px;
box-shadow: 0 2px 4px rgba(0,0,0,0.1);
max-height: 200px;
overflow-y: auto;
.checkbox-group {
padding: 8px;
.checkbox-item {
display: flex;
align-items: center;
gap: 8px;
padding: 4px 0;
.checkbox-label {
margin: 0;
font-size: 14px;
cursor: pointer;
}
}
}
}
.date-range {
.date-input-group {
display: flex;
align-items: center;
gap: 8px;
}
.date-separator {
margin: 0 5px;
color: #777;
}
}
.toggle {
display: flex;
align-items: center;
gap: 8px;
.toggle-label {
margin: 0;
font-size: 14px;
cursor: pointer;
}
}
.filter-actions {
margin-top: 15px;
padding-top: 15px;
border-top: 1px solid #eee;
.btn {
font-size: 13px;
}
}
// New header row styling
.header-row {
margin-bottom: 15px;
padding-bottom: 10px;
border-bottom: 1px solid #eee;
.chart-title {
margin: 0;
font-size: 18px;
font-weight: 600;
color: #333;
}
}
// Responsive design
@media (max-width: 768px) {
.filter-controls {
flex-direction: column;
}
.filter-item {
min-width: 100%;
}
.header-row {
.chart-title {
font-size: 16px;
}
}
}

View File

@@ -1,44 +1,315 @@
<div class="doughnut-chart-container">
<!-- Drilldown mode indicator -->
<div *ngIf="currentDrilldownLevel > 0" style="background-color: #e0e0e0; padding: 5px; margin-bottom: 10px; border-radius: 4px; text-align: center;">
<span style="font-weight: bold; color: #333;">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;">
Back to Level {{currentDrilldownLevel - 1}}
</button>
<button (click)="resetToOriginalData()" style="margin-left: 10px; padding: 2px 8px; background-color: #dc3545; color: white; border: none; border-radius: 3px; cursor: pointer;">
Back to Main View
</button>
<div class="chart-container">
<!-- Filter Controls Section -->
<div class="filter-section" *ngIf="hasActiveFilters()">
<!-- Base Filters -->
<div class="filter-group" *ngIf="baseFilters && baseFilters.length > 0">
<h4>Base Filters</h4>
<div class="filter-controls">
<div *ngFor="let filter of baseFilters" class="filter-item">
<div class="filter-label">{{ filter.field }} ({{ filter.type || 'text' }})</div>
<!-- Text Filter -->
<div *ngIf="!filter.type || filter.type === 'text'" class="filter-input">
<input type="text"
[(ngModel)]="filter.value"
(ngModelChange)="onBaseFilterChange(filter)"
[placeholder]="filter.field"
class="clr-input filter-text-input">
</div>
<!-- Dropdown Filter -->
<div *ngIf="filter.type === 'dropdown'" class="filter-input">
<select [(ngModel)]="filter.value"
(ngModelChange)="onBaseFilterChange(filter)"
class="clr-select filter-select">
<option value="">Select {{ filter.field }}</option>
<option *ngFor="let option of getFilterOptions(filter)" [value]="option">{{ option }}</option>
</select>
</div>
<!-- Multi-Select Filter - Updated to show key first, then dropdown on click -->
<div *ngIf="filter.type === 'multiselect'" class="filter-input multiselect-container">
<div class="multiselect-display" (click)="toggleMultiselect(filter, 'base')">
<span class="multiselect-label">{{ filter.field }}</span>
<span class="multiselect-value" *ngIf="getSelectedOptionsCount(filter) > 0">
({{ getSelectedOptionsCount(filter) }} selected)
</span>
<clr-icon shape="caret down" class="dropdown-icon"></clr-icon>
</div>
<div class="multiselect-dropdown" *ngIf="isMultiselectOpen(filter, 'base')">
<div class="checkbox-group">
<div *ngFor="let option of getFilterOptions(filter); let i = index" class="checkbox-item">
<input type="checkbox"
[checked]="isOptionSelected(filter, option)"
(change)="onMultiSelectChange(filter, option, $event)"
[id]="'base-' + filter.field + '-' + i"
class="clr-checkbox">
<label [for]="'base-' + filter.field + '-' + i" class="checkbox-label">{{ option }}</label>
</div>
</div>
</div>
</div>
<!-- Date Range Filter -->
<div *ngIf="filter.type === 'date-range'" class="filter-input date-range">
<div class="date-input-group">
<input type="date"
[(ngModel)]="filter.value.start"
(ngModelChange)="onDateRangeChange(filter, { start: $event, end: filter.value.end })"
placeholder="Start Date"
class="clr-input filter-date">
<span class="date-separator">to</span>
<input type="date"
[(ngModel)]="filter.value.end"
(ngModelChange)="onDateRangeChange(filter, { start: filter.value.start, end: $event })"
placeholder="End Date"
class="clr-input filter-date">
</div>
</div>
<!-- Toggle Filter -->
<div *ngIf="filter.type === 'toggle'" class="filter-input toggle">
<input type="checkbox"
[(ngModel)]="filter.value"
(ngModelChange)="onToggleChange(filter, $event)"
clrToggle
class="clr-toggle">
<label class="toggle-label">{{ filter.field }}</label>
</div>
</div>
</div>
</div>
<!-- Drilldown Filters -->
<div class="filter-group" *ngIf="drilldownFilters && drilldownFilters.length > 0 && currentDrilldownLevel > 0">
<h4>Drilldown Filters</h4>
<div class="filter-controls">
<div *ngFor="let filter of drilldownFilters" class="filter-item">
<div class="filter-label">{{ filter.field }} ({{ filter.type || 'text' }})</div>
<!-- Text Filter -->
<div *ngIf="!filter.type || filter.type === 'text'" class="filter-input">
<input type="text"
[(ngModel)]="filter.value"
(ngModelChange)="onDrilldownFilterChange(filter)"
[placeholder]="filter.field"
class="clr-input filter-text-input">
</div>
<!-- Dropdown Filter -->
<div *ngIf="filter.type === 'dropdown'" class="filter-input">
<select [(ngModel)]="filter.value"
(ngModelChange)="onDrilldownFilterChange(filter)"
class="clr-select filter-select">
<option value="">Select {{ filter.field }}</option>
<option *ngFor="let option of getFilterOptions(filter)" [value]="option">{{ option }}</option>
</select>
</div>
<!-- Multi-Select Filter - Updated to show key first, then dropdown on click -->
<div *ngIf="filter.type === 'multiselect'" class="filter-input multiselect-container">
<div class="multiselect-display" (click)="toggleMultiselect(filter, 'drilldown')">
<span class="multiselect-label">{{ filter.field }}</span>
<span class="multiselect-value" *ngIf="getSelectedOptionsCount(filter) > 0">
({{ getSelectedOptionsCount(filter) }} selected)
</span>
<clr-icon shape="caret down" class="dropdown-icon"></clr-icon>
</div>
<div class="multiselect-dropdown" *ngIf="isMultiselectOpen(filter, 'drilldown')">
<div class="checkbox-group">
<div *ngFor="let option of getFilterOptions(filter); let i = index" class="checkbox-item">
<input type="checkbox"
[checked]="isOptionSelected(filter, option)"
(change)="onMultiSelectChange(filter, option, $event)"
[id]="'drilldown-' + filter.field + '-' + i"
class="clr-checkbox">
<label [for]="'drilldown-' + filter.field + '-' + i" class="checkbox-label">{{ option }}</label>
</div>
</div>
</div>
</div>
<!-- Date Range Filter -->
<div *ngIf="filter.type === 'date-range'" class="filter-input date-range">
<div class="date-input-group">
<input type="date"
[(ngModel)]="filter.value.start"
(ngModelChange)="onDateRangeChange(filter, { start: $event, end: filter.value.end })"
placeholder="Start Date"
class="clr-input filter-date">
<span class="date-separator">to</span>
<input type="date"
[(ngModel)]="filter.value.end"
(ngModelChange)="onDateRangeChange(filter, { start: filter.value.start, end: $event })"
placeholder="End Date"
class="clr-input filter-date">
</div>
</div>
<!-- Toggle Filter -->
<div *ngIf="filter.type === 'toggle'" class="filter-input toggle">
<input type="checkbox"
[(ngModel)]="filter.value"
(ngModelChange)="onToggleChange(filter, $event)"
clrToggle
class="clr-toggle">
<label class="toggle-label">{{ filter.field }}</label>
</div>
</div>
</div>
</div>
<!-- Layer Filters -->
<div class="filter-group" *ngIf="hasActiveLayerFilters()">
<h4>Layer Filters</h4>
<div class="filter-controls">
<div *ngFor="let filter of getActiveLayerFilters()" class="filter-item">
<div class="filter-label">{{ filter.field }} ({{ filter.type || 'text' }})</div>
<!-- Text Filter -->
<div *ngIf="!filter.type || filter.type === 'text'" class="filter-input">
<input type="text"
[(ngModel)]="filter.value"
(ngModelChange)="onLayerFilterChange(filter)"
[placeholder]="filter.field"
class="clr-input filter-text-input">
</div>
<!-- Dropdown Filter -->
<div *ngIf="filter.type === 'dropdown'" class="filter-input">
<select [(ngModel)]="filter.value"
(ngModelChange)="onLayerFilterChange(filter)"
class="clr-select filter-select">
<option value="">Select {{ filter.field }}</option>
<option *ngFor="let option of getFilterOptions(filter)" [value]="option">{{ option }}</option>
</select>
</div>
<!-- Multi-Select Filter - Updated to show key first, then dropdown on click -->
<div *ngIf="filter.type === 'multiselect'" class="filter-input multiselect-container">
<div class="multiselect-display" (click)="toggleMultiselect(filter, 'layer')">
<span class="multiselect-label">{{ filter.field }}</span>
<span class="multiselect-value" *ngIf="getSelectedOptionsCount(filter) > 0">
({{ getSelectedOptionsCount(filter) }} selected)
</span>
<clr-icon shape="caret down" class="dropdown-icon"></clr-icon>
</div>
<div class="multiselect-dropdown" *ngIf="isMultiselectOpen(filter, 'layer')">
<div class="checkbox-group">
<div *ngFor="let option of getFilterOptions(filter); let i = index" class="checkbox-item">
<input type="checkbox"
[checked]="isOptionSelected(filter, option)"
(change)="onMultiSelectChange(filter, option, $event)"
[id]="'layer-' + filter.field + '-' + i"
class="clr-checkbox">
<label [for]="'layer-' + filter.field + '-' + i" class="checkbox-label">{{ option }}</label>
</div>
</div>
</div>
</div>
<!-- Date Range Filter -->
<div *ngIf="filter.type === 'date-range'" class="filter-input date-range">
<div class="date-input-group">
<input type="date"
[(ngModel)]="filter.value.start"
(ngModelChange)="onDateRangeChange(filter, { start: $event, end: filter.value.end })"
placeholder="Start Date"
class="clr-input filter-date">
<span class="date-separator">to</span>
<input type="date"
[(ngModel)]="filter.value.end"
(ngModelChange)="onDateRangeChange(filter, { start: filter.value.start, end: $event })"
placeholder="End Date"
class="clr-input filter-date">
</div>
</div>
<!-- Toggle Filter -->
<div *ngIf="filter.type === 'toggle'" class="filter-input toggle">
<input type="checkbox"
[(ngModel)]="filter.value"
(ngModelChange)="onToggleChange(filter, $event)"
clrToggle
class="clr-toggle">
<label class="toggle-label">{{ filter.field }}</label>
</div>
</div>
</div>
</div>
<!-- Clear Filters Button -->
<div class="filter-actions">
<button class="btn btn-sm btn-outline" (click)="clearAllFilters()">Clear All Filters</button>
</div>
</div>
<!-- Header row with chart title and drilldown navigation -->
<div class="chart-header">
<div class="clr-row header-row">
<div class="clr-col-6">
<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 class="clr-col-6" style="text-align: right;">
<!-- Add drilldown navigation controls -->
<button class="btn btn-sm btn-link" *ngIf="drilldownEnabled && drilldownStack.length > 0" (click)="navigateBack()">
<cds-icon shape="arrow" direction="left"></cds-icon>
Back to {{drilldownStack.length > 1 ? 'Previous Level' : 'Main Data'}}
</button>
</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].clickedLabel}})
</span>
</span>
</div>
</div>
</div>
</div>
</div>
</div>
<div class="chart-wrapper">
<div class="chart-content" [class.loading]="isLoading">
<!-- Show no data message -->
<div class="no-data-message" *ngIf="noDataAvailable">
<div class="no-data-message" *ngIf="noDataAvailable && doughnutChartLabels.length === 0">
<p>No chart data available</p>
</div>
<!-- Show chart when data is available -->
<canvas baseChart
*ngIf="!noDataAvailable && doughnutChartLabels.length > 0 && doughnutChartData.length > 0"
[data]="doughnutChartData"
[datasets]="doughnutChartData"
[labels]="doughnutChartLabels"
[type]="doughnutChartType"
[options]="doughnutChartOptions"
(chartHover)="chartHovered($event)"
(chartClick)="chartClicked($event)">
</canvas>
<!-- Loading overlay -->
<div class="loading-overlay" *ngIf="isLoading">
<div class="shimmer-donut"></div>
</div>
<div class="chart-legend" *ngIf="!noDataAvailable && showlabel && doughnutChartLabels && doughnutChartLabels.length > 0">
</div>
</div>
<div class="chart-legend" *ngIf="showlabel && doughnutChartLabels && doughnutChartLabels.length > 0">
<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-label">{{ label }}</span>
<span class="legend-value">{{ doughnutChartData && doughnutChartData[i] !== undefined ? doughnutChartData[i] : 0 }}</span>
<span class="legend-value">{{ doughnutChartData && doughnutChartData[0] && doughnutChartData[0].data && doughnutChartData[0].data[i] !== undefined ? doughnutChartData[0].data[i] : 0 }}</span>
</div>
</div>
</div>

View File

@@ -1,184 +1,343 @@
.doughnut-chart-container {
// Chart container structure - simplified to match shield dashboard
.chart-container {
height: 100%;
min-height: 400px; // Ensure minimum height
display: flex;
flex-direction: column;
height: 400px;
min-height: 400px;
padding: 20px;
background: linear-gradient(135deg, #ffffff 0%, #f8f9fa 100%);
border-radius: 12px;
box-shadow: 0 4px 20px rgba(0, 0, 0, 0.15);
font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
transition: all 0.3s ease;
border: 1px solid #eaeaea;
// Filter section styling
.filter-section {
margin-bottom: 20px;
padding: 15px;
border: 1px solid #ddd;
border-radius: 4px;
background-color: #f9f9f9;
}
.doughnut-chart-container:hover {
box-shadow: 0 6px 25px rgba(0, 0, 0, 0.2);
transform: translateY(-2px);
.filter-group {
margin-bottom: 15px;
h4 {
margin-top: 0;
margin-bottom: 10px;
color: #333;
font-weight: 600;
}
}
.filter-controls {
display: flex;
flex-wrap: wrap;
gap: 15px;
}
.filter-item {
flex: 1 1 300px;
min-width: 250px;
padding: 10px;
background: white;
border: 1px solid #e0e0e0;
border-radius: 4px;
}
.filter-label {
font-weight: 500;
margin-bottom: 8px;
color: #555;
font-size: 14px;
}
.filter-input {
width: 100%;
.filter-text-input,
.filter-select,
.filter-date {
width: 100%;
padding: 6px 12px;
border: 1px solid #ccc;
border-radius: 4px;
font-size: 14px;
}
.filter-select {
height: 34px;
}
}
.multiselect-container {
position: relative;
}
.multiselect-display {
display: flex;
align-items: center;
justify-content: space-between;
padding: 6px 12px;
border: 1px solid #ccc;
border-radius: 4px;
background: white;
cursor: pointer;
min-height: 34px;
.multiselect-label {
flex: 1;
font-size: 14px;
}
.multiselect-value {
color: #666;
font-size: 12px;
margin-right: 8px;
}
.dropdown-icon {
flex-shrink: 0;
transition: transform 0.2s ease;
}
&:hover {
border-color: #999;
}
}
.multiselect-dropdown {
position: absolute;
top: 100%;
left: 0;
right: 0;
z-index: 1000;
background: white;
border: 1px solid #ccc;
border-top: none;
border-radius: 0 0 4px 4px;
box-shadow: 0 2px 4px rgba(0,0,0,0.1);
max-height: 200px;
overflow-y: auto;
.checkbox-group {
padding: 8px;
.checkbox-item {
display: flex;
align-items: center;
gap: 8px;
padding: 4px 0;
.checkbox-label {
margin: 0;
font-size: 14px;
cursor: pointer;
}
}
}
}
.date-range {
.date-input-group {
display: flex;
align-items: center;
gap: 8px;
}
.date-separator {
margin: 0 5px;
color: #777;
}
}
.toggle {
display: flex;
align-items: center;
gap: 8px;
.toggle-label {
margin: 0;
font-size: 14px;
cursor: pointer;
}
}
.filter-actions {
margin-top: 15px;
padding-top: 15px;
border-top: 1px solid #eee;
.btn {
font-size: 13px;
}
}
// Chart header styling
.chart-header {
margin-bottom: 20px;
.header-row {
margin-bottom: 15px;
padding-bottom: 10px;
border-bottom: 1px solid #eee;
.chart-title {
font-size: 26px;
font-weight: 700;
color: #2c3e50;
margin-bottom: 20px;
text-align: center;
padding-bottom: 15px;
border-bottom: 2px solid #3498db;
text-shadow: 0 1px 2px rgba(0, 0, 0, 0.1);
margin: 0;
font-size: 18px;
font-weight: 600;
color: #0a192f;
}
}
}
// Chart wrapper and content - simplified to match shield dashboard
.chart-wrapper {
position: relative;
flex: 1;
min-height: 250px;
margin: 15px 0;
background: #f8f9fa;
border: 1px solid #e9ecef;
border-radius: 8px;
padding: 10px;
position: relative;
.chart-content {
position: relative;
height: 100%;
min-height: 300px;
&.loading {
opacity: 0.7;
canvas {
filter: blur(2px);
}
}
.no-data-message {
text-align: center;
padding: 20px;
color: #666;
font-style: italic;
}
canvas {
max-width: 100%;
max-height: calc(100% - 40px); // Leave space for legend
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;
}
}
}
}
.chart-wrapper canvas {
max-width: 100%;
max-height: 100%;
filter: drop-shadow(0 2px 4px rgba(0,0,0,0.1));
}
.chart-wrapper canvas:hover {
filter: drop-shadow(0 4px 8px rgba(0,0,0,0.15));
transform: scale(1.02);
transition: all 0.3s ease;
}
// Chart legend - simplified
.chart-legend {
display: flex;
flex-wrap: wrap;
justify-content: center;
gap: 15px;
margin-top: 20px;
padding: 20px;
background: linear-gradient(135deg, #f8f9fa 0%, #e9ecef 100%);
border-radius: 8px;
border: 1px solid #dee2e6;
box-shadow: inset 0 1px 3px rgba(0, 0, 0, 0.05);
}
margin: 20px 0;
padding: 15px;
.legend-item {
display: flex;
align-items: center;
padding: 12px 20px;
background: linear-gradient(135deg, #ffffff 0%, #f8f9fa 100%);
border-radius: 25px;
box-shadow: 0 2px 6px rgba(0, 0, 0, 0.1);
transition: all 0.3s ease;
border: 1px solid #eaeaea;
cursor: pointer;
}
.legend-item:hover {
transform: translateY(-3px);
box-shadow: 0 5px 15px rgba(0, 0, 0, 0.2);
border-color: #3498db;
background: linear-gradient(135deg, #f8f9fa 0%, #e9ecef 100%);
padding: 8px 15px;
background: #f8f9fa;
border-radius: 20px;
border: 1px solid #e9ecef;
min-width: 120px;
justify-content: space-between;
}
.legend-color {
width: 20px;
height: 20px;
width: 15px;
height: 15px;
border-radius: 50%;
margin-right: 12px;
margin-right: 8px;
display: inline-block;
border: 2px solid white;
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.2);
flex-shrink: 0;
}
.legend-label {
font-size: 16px;
font-weight: 600;
font-size: 14px;
font-weight: 500;
color: #2c3e50;
margin-right: 15px;
margin-right: 10px;
white-space: nowrap;
text-shadow: 0 1px 1px rgba(0, 0, 0, 0.1);
overflow: hidden;
text-overflow: ellipsis;
}
.legend-value {
font-size: 16px;
font-weight: 700;
font-size: 14px;
font-weight: 600;
color: #3498db;
background: linear-gradient(135deg, #e9ecef 0%, #dde1e5 100%);
padding: 6px 12px;
border-radius: 12px;
box-shadow: 0 1px 2px rgba(0, 0, 0, 0.1);
min-width: 40px;
text-align: center;
min-width: 30px;
text-align: right;
}
}
}
.loading-indicator, .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%;
@keyframes shimmer {
0% {
background-position: -200% 0;
}
100% {
background-position: 200% 0;
}
}
.loading-indicator p, .no-data-message p {
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
@media (max-width: 768px) {
.doughnut-chart-container {
padding: 15px;
.chart-container {
.filter-controls {
flex-direction: column;
}
.filter-item {
min-width: 100%;
}
.chart-header {
.header-row {
.chart-title {
font-size: 20px;
margin-bottom: 15px;
font-size: 16px;
}
}
}
.chart-wrapper {
min-height: 200px;
.chart-content {
min-height: 250px;
}
.chart-legend {
flex-direction: column;
align-items: center;
}
.legend-item {
width: 100%;
max-width: 300px;
justify-content: space-between;
}
}
.no-data-message {
font-size: 16px;
padding: 20px;
.chart-content {
min-height: 250px;
canvas {
max-height: calc(100% - 60px); // More space for legend on mobile
}
}
}
}

View File

@@ -1,12 +1,14 @@
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 { FilterService } from '../../common-filter/filter.service';
import { Subscription } from 'rxjs';
@Component({
selector: 'app-doughnut-chart',
templateUrl: './doughnut-chart.component.html',
styleUrls: ['./doughnut-chart.component.scss']
})
export class DoughnutChartComponent implements OnInit, OnChanges, AfterViewChecked {
export class DoughnutChartComponent implements OnInit, OnChanges, AfterViewChecked, OnDestroy {
@Input() xAxis: string;
@Input() yAxis: string | string[];
@Input() table: string;
@@ -34,7 +36,21 @@ export class DoughnutChartComponent implements OnInit, OnChanges, AfterViewCheck
@Input() drilldownLayers: any[] = []; // Array of drilldown layer configurations
public doughnutChartLabels: string[] = ["Category A", "Category B", "Category C"];
public doughnutChartData: number[] = [30, 50, 20];
public doughnutChartData: any[] = [
{
data: [30, 50, 20],
backgroundColor: [
'#FF6384',
'#36A2EB',
'#FFCE56'
],
hoverBackgroundColor: [
'#FF6384',
'#36A2EB',
'#FFCE56'
]
}
];
public doughnutChartType: string = "doughnut";
public doughnutChartOptions: any = {
responsive: true,
@@ -70,6 +86,14 @@ export class DoughnutChartComponent implements OnInit, OnChanges, AfterViewCheck
borderWidth: 2,
borderColor: '#fff'
}
},
layout: {
padding: {
top: 20,
bottom: 20,
left: 20,
right: 20
}
}
};
@@ -94,16 +118,41 @@ export class DoughnutChartComponent implements OnInit, OnChanges, AfterViewCheck
// No data state
noDataAvailable: boolean = false;
// Loading state
isLoading: boolean = false;
// Flag to prevent infinite loops
private isFetchingData: boolean = false;
constructor(private dashboardService: Dashboard3Service) { }
// Subscriptions to unsubscribe on destroy
private subscriptions: Subscription[] = [];
// Add properties for filter functionality
private openMultiselects: Map<string, string> = new Map(); // Map of filterId -> context
private documentClickHandler: ((event: MouseEvent) => void) | null = null;
private filtersInitialized: boolean = false;
constructor(
private dashboardService: Dashboard3Service,
private filterService: FilterService
) { }
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
this.validateChartData();
// Only fetch data if we have the required inputs, otherwise show default data
if (this.table && this.xAxis && this.yAxis) {
this.fetchChartData();
}
}
/**
* Validate and sanitize chart data
@@ -122,17 +171,33 @@ export class DoughnutChartComponent implements OnInit, OnChanges, AfterViewCheck
if (this.doughnutChartLabels.length === 0 && this.doughnutChartData.length === 0) {
// Add default data to ensure chart visibility
this.doughnutChartLabels = ["Category A", "Category B", "Category C"];
this.doughnutChartData = [30, 50, 20];
this.doughnutChartData = [
{
data: [30, 50, 20],
backgroundColor: [
'#FF6384',
'#36A2EB',
'#FFCE56'
],
hoverBackgroundColor: [
'#FF6384',
'#36A2EB',
'#FFCE56'
]
}
];
}
// Ensure we have matching arrays
if (this.doughnutChartLabels.length !== this.doughnutChartData.length) {
const maxLength = Math.max(this.doughnutChartLabels.length, this.doughnutChartData.length);
if (this.doughnutChartLabels.length !== (this.doughnutChartData[0]?.data?.length || 0)) {
const maxLength = Math.max(this.doughnutChartLabels.length, this.doughnutChartData[0]?.data?.length || 0);
while (this.doughnutChartLabels.length < maxLength) {
this.doughnutChartLabels.push(`Label ${this.doughnutChartLabels.length + 1}`);
}
while (this.doughnutChartData.length < maxLength) {
this.doughnutChartData.push(0);
if (this.doughnutChartData[0]) {
while (this.doughnutChartData[0].data.length < maxLength) {
this.doughnutChartData[0].data.push(0);
}
}
}
}
@@ -148,6 +213,12 @@ export class DoughnutChartComponent implements OnInit, OnChanges, AfterViewCheck
ngOnChanges(changes: SimpleChanges): void {
console.log('DoughnutChartComponent input changes:', changes);
// Initialize filter values if they haven't been initialized yet
if (!this.filtersInitialized && (changes.baseFilters || changes.drilldownFilters || changes.drilldownLayers)) {
this.initializeFilterValues();
this.filtersInitialized = true;
}
// Check if any of the key properties have changed
const xAxisChanged = changes.xAxis && !changes.xAxis.firstChange;
const yAxisChanged = changes.yAxis && !changes.yAxis.firstChange;
@@ -169,6 +240,12 @@ export class DoughnutChartComponent implements OnInit, OnChanges, AfterViewCheck
console.log('Chart configuration changed, fetching new data');
this.fetchChartData();
}
// If we have the required inputs and haven't fetched data yet, fetch it
if ((xAxisChanged || yAxisChanged || tableChanged) && this.table && this.xAxis && this.yAxis && !this.isFetchingData) {
console.log('Required inputs available, fetching data');
this.fetchChartData();
}
}
ngAfterViewChecked() {
@@ -180,15 +257,340 @@ export class DoughnutChartComponent implements OnInit, OnChanges, AfterViewCheck
});
}
ngOnDestroy(): void {
this.subscriptions.forEach(sub => sub.unsubscribe());
// Clean up document click handler
this.removeDocumentClickHandler();
}
// Initialize filter values with proper default values based on type
private initializeFilterValues(): void {
console.log('Initializing filter values');
// Initialize base filters
if (this.baseFilters) {
this.baseFilters.forEach(filter => {
if (filter.value === undefined || filter.value === null) {
switch (filter.type) {
case 'multiselect':
filter.value = [];
break;
case 'date-range':
filter.value = { start: null, end: null };
break;
case 'toggle':
filter.value = false;
break;
default:
filter.value = '';
}
}
});
}
// Initialize drilldown filters
if (this.drilldownFilters) {
this.drilldownFilters.forEach(filter => {
if (filter.value === undefined || filter.value === null) {
switch (filter.type) {
case 'multiselect':
filter.value = [];
break;
case 'date-range':
filter.value = { start: null, end: null };
break;
case 'toggle':
filter.value = false;
break;
default:
filter.value = '';
}
}
});
}
// Initialize layer filters
if (this.drilldownLayers) {
this.drilldownLayers.forEach(layer => {
if (layer.filters) {
layer.filters.forEach((filter: any) => {
if (filter.value === undefined || filter.value === null) {
switch (filter.type) {
case 'multiselect':
filter.value = [];
break;
case 'date-range':
filter.value = { start: null, end: null };
break;
case 'toggle':
filter.value = false;
break;
default:
filter.value = '';
}
}
});
}
});
}
console.log('Filter values initialized:', {
baseFilters: this.baseFilters,
drilldownFilters: this.drilldownFilters,
drilldownLayers: this.drilldownLayers
});
}
// Check if there are active filters
hasActiveFilters(): boolean {
return (this.baseFilters && this.baseFilters.length > 0) ||
(this.drilldownFilters && this.drilldownFilters.length > 0) ||
this.hasActiveLayerFilters();
}
// Check if there are active layer filters for current drilldown level
hasActiveLayerFilters(): boolean {
if (this.currentDrilldownLevel > 1 && this.drilldownLayers && this.drilldownLayers.length > 0) {
const layerIndex = this.currentDrilldownLevel - 2; // -2 because level 1 is base drilldown
return layerIndex < this.drilldownLayers.length &&
this.drilldownLayers[layerIndex].filters &&
this.drilldownLayers[layerIndex].filters.length > 0;
}
return false;
}
// Get active layer filters for current drilldown level
getActiveLayerFilters(): any[] {
if (this.currentDrilldownLevel > 1 && this.drilldownLayers && this.drilldownLayers.length > 0) {
const layerIndex = this.currentDrilldownLevel - 2; // -2 because level 1 is base drilldown
if (layerIndex < this.drilldownLayers.length &&
this.drilldownLayers[layerIndex].filters) {
return this.drilldownLayers[layerIndex].filters;
}
}
return [];
}
// Get filter options for dropdown/multiselect filters
getFilterOptions(filter: any): string[] {
if (filter.options) {
if (Array.isArray(filter.options)) {
return filter.options;
} else if (typeof filter.options === 'string') {
return filter.options.split(',').map(opt => opt.trim()).filter(opt => opt);
}
}
return [];
}
// Check if an option is selected for multiselect filters
isOptionSelected(filter: any, option: string): boolean {
if (!filter.value) {
return false;
}
if (Array.isArray(filter.value)) {
return filter.value.includes(option);
}
return filter.value === option;
}
// Handle base filter changes
onBaseFilterChange(filter: any): void {
console.log('Base filter changed:', filter);
// Refresh data when filter changes
this.fetchChartData();
}
// Handle drilldown filter changes
onDrilldownFilterChange(filter: any): void {
console.log('Drilldown filter changed:', filter);
// Refresh data when filter changes
this.fetchChartData();
}
// Handle layer filter changes
onLayerFilterChange(filter: any): void {
console.log('Layer filter changed:', filter);
// Refresh data when filter changes
this.fetchChartData();
}
// Handle multiselect changes
onMultiSelectChange(filter: any, option: string, event: any): void {
const checked = event.target.checked;
// Initialize filter.value as array if it's not already
if (!Array.isArray(filter.value)) {
filter.value = [];
}
if (checked) {
// Add option to array if not already present
if (!filter.value.includes(option)) {
filter.value.push(option);
}
} else {
// Remove option from array
filter.value = filter.value.filter((item: string) => item !== option);
}
// Refresh data when filter changes
this.fetchChartData();
}
// Handle date range changes
onDateRangeChange(filter: any, dateRange: { start: string | null, end: string | null }): void {
filter.value = dateRange;
// Refresh data when filter changes
this.fetchChartData();
}
// Handle toggle changes
onToggleChange(filter: any, checked: boolean): void {
filter.value = checked;
// Refresh data when filter changes
this.fetchChartData();
}
// Toggle multiselect dropdown visibility
toggleMultiselect(filter: any, context: string): void {
const filterId = `${context}-${filter.field}`;
if (this.isMultiselectOpen(filter, context)) {
this.openMultiselects.delete(filterId);
} else {
// Close all other multiselects first
this.openMultiselects.clear();
this.openMultiselects.set(filterId, context);
// Add document click handler to close dropdown when clicking outside
this.addDocumentClickHandler();
}
}
// Add document click handler to close dropdowns when clicking outside
private addDocumentClickHandler(): void {
if (!this.documentClickHandler) {
this.documentClickHandler = (event: MouseEvent) => {
const target = event.target as HTMLElement;
// Check if click is outside any multiselect dropdown
if (!target.closest('.multiselect-container')) {
this.openMultiselects.clear();
this.removeDocumentClickHandler();
}
};
// Use setTimeout to ensure the click event that opened the dropdown doesn't immediately close it
setTimeout(() => {
document.addEventListener('click', this.documentClickHandler!);
}, 0);
}
}
// Remove document click handler
private removeDocumentClickHandler(): void {
if (this.documentClickHandler) {
document.removeEventListener('click', this.documentClickHandler);
this.documentClickHandler = null;
}
}
// Check if multiselect dropdown is open
isMultiselectOpen(filter: any, context: string): boolean {
const filterId = `${context}-${filter.field}`;
return this.openMultiselects.has(filterId);
}
// Get count of selected options for a multiselect filter
getSelectedOptionsCount(filter: any): number {
if (!filter.value) {
return 0;
}
if (Array.isArray(filter.value)) {
return filter.value.length;
}
return 0;
}
// Clear all filters
clearAllFilters(): void {
// Clear base filters
if (this.baseFilters) {
this.baseFilters.forEach(filter => {
if (filter.type === 'multiselect') {
filter.value = [];
} else if (filter.type === 'date-range') {
filter.value = { start: null, end: null };
} else if (filter.type === 'toggle') {
filter.value = false;
} else {
filter.value = '';
}
});
}
// Clear drilldown filters
if (this.drilldownFilters) {
this.drilldownFilters.forEach(filter => {
if (filter.type === 'multiselect') {
filter.value = [];
} else if (filter.type === 'date-range') {
filter.value = { start: null, end: null };
} else if (filter.type === 'toggle') {
filter.value = false;
} else {
filter.value = '';
}
});
}
// Clear layer filters
if (this.drilldownLayers) {
this.drilldownLayers.forEach(layer => {
if (layer.filters) {
layer.filters.forEach((filter: any) => {
if (filter.type === 'multiselect') {
filter.value = [];
} else if (filter.type === 'date-range') {
filter.value = { start: null, end: null };
} else if (filter.type === 'toggle') {
filter.value = false;
} else {
filter.value = '';
}
});
}
});
}
// Close all multiselect dropdowns
this.openMultiselects.clear();
// Refresh data
this.fetchChartData();
}
// Public method to refresh data when filters change
refreshData(): void {
this.fetchChartData();
}
fetchChartData(): void {
// Set loading state
this.isLoading = true;
// 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
// Reset flags after fetching
this.isFetchingData = false;
this.isLoading = false;
return;
}
@@ -212,11 +614,53 @@ export class DoughnutChartComponent implements OnInit, OnChanges, AfterViewCheck
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
const url = `chart/getdashjson/doughnut?tableName=${this.table}&xAxis=${this.xAxis}&yAxes=${yAxisString}${this.connection ? `&sureId=${this.connection}` : ''}`;
console.log('Doughnut chart data URL:', url);
console.log('Chart data URL:', url);
// 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
@@ -224,89 +668,113 @@ export class DoughnutChartComponent implements OnInit, OnChanges, AfterViewCheck
(data: any) => {
console.log('Received doughnut chart data:', data);
if (data === null) {
console.warn('Doughnut chart API returned null data. Check if the API endpoint is working correctly.');
console.warn('API returned null data. Check if the API endpoint is working correctly.');
this.noDataAvailable = true;
this.doughnutChartLabels = [];
this.doughnutChartData = [];
// Validate and sanitize data to show default data
this.validateChartData();
// Reset flag after fetching
// Reset flags after fetching
this.isFetchingData = false;
this.isLoading = false;
return;
}
// Handle the actual data structure returned by the API
if (data && data.chartLabels && data.chartData) {
// For doughnut charts, we need to extract the data differently
// The first dataset's data array contains the values for the doughnut chart
// Backend has already filtered the data, just display it
this.noDataAvailable = data.chartLabels.length === 0;
this.doughnutChartLabels = data.chartLabels || [];
if (data.chartData && data.chartData.length > 0) {
this.doughnutChartData = data.chartData[0].data.map(value => {
// Convert to number if it's not already
const numValue = Number(value);
return isNaN(numValue) ? 0 : numValue;
});
} else {
this.doughnutChartData = [];
this.doughnutChartLabels = data.chartLabels;
// Handle different data structures
let chartDataValues;
if (Array.isArray(data.chartData)) {
// If chartData is already an array of values
if (data.chartData.length > 0 && typeof data.chartData[0] !== 'object') {
chartDataValues = data.chartData;
}
// Ensure labels and data arrays have the same length
this.syncLabelAndDataArrays();
// Validate and sanitize data
this.validateChartData();
// Trigger change detection
this.doughnutChartData = [...this.doughnutChartData];
// If chartData is an array with one object containing the data
else if (data.chartData.length > 0 && data.chartData[0].data) {
chartDataValues = data.chartData[0].data;
}
// Default case
else {
chartDataValues = data.chartData;
}
} else {
chartDataValues = [data.chartData];
}
this.doughnutChartData = [
{
data: chartDataValues,
backgroundColor: this.chartColors.slice(0, chartDataValues.length),
hoverBackgroundColor: this.chartColors.slice(0, chartDataValues.length)
}
];
console.log('Updated doughnut chart with data:', { labels: this.doughnutChartLabels, data: this.doughnutChartData });
} else if (data && data.labels && data.data) {
// Handle the original expected format as fallback
} else if (data && data.labels && data.datasets) {
// Backend has already filtered the data, just display it
this.noDataAvailable = data.labels.length === 0;
this.doughnutChartLabels = data.labels || [];
this.doughnutChartData = data.data.map(value => {
// Convert to number if it's not already
const numValue = Number(value);
return isNaN(numValue) ? 0 : numValue;
});
// Ensure labels and data arrays have the same length
this.syncLabelAndDataArrays();
// Validate and sanitize data
this.validateChartData();
// Trigger change detection
this.doughnutChartData = [...this.doughnutChartData];
this.doughnutChartLabels = data.labels;
this.doughnutChartData = data.datasets;
console.log('Updated doughnut chart with legacy data format:', { labels: this.doughnutChartLabels, data: this.doughnutChartData });
} else {
console.warn('Doughnut chart received data does not have expected structure', data);
// Reset to default data
console.warn('Received data does not have expected structure', data);
this.noDataAvailable = true;
this.doughnutChartLabels = [];
this.doughnutChartData = [];
// Validate and sanitize data to show default data
this.validateChartData();
// Keep default data instead of empty arrays
this.doughnutChartLabels = ["Category A", "Category B", "Category C"];
this.doughnutChartData = [
{
data: [30, 50, 20],
backgroundColor: [
'#FF6384',
'#36A2EB',
'#FFCE56'
],
hoverBackgroundColor: [
'#FF6384',
'#36A2EB',
'#FFCE56'
]
}
// Reset flag after fetching
];
}
// Reset flags after fetching
this.isFetchingData = false;
this.isLoading = false;
},
(error) => {
console.error('Error fetching doughnut chart data:', error);
this.noDataAvailable = true;
this.doughnutChartLabels = [];
this.doughnutChartData = [];
// Validate and sanitize data to show default data
this.validateChartData();
// Reset flag after fetching
// Keep default data in case of error
this.doughnutChartLabels = ["Category A", "Category B", "Category C"];
this.doughnutChartData = [
{
data: [30, 50, 20],
backgroundColor: [
'#FF6384',
'#36A2EB',
'#FFCE56'
],
hoverBackgroundColor: [
'#FF6384',
'#36A2EB',
'#FFCE56'
]
}
];
// Reset flags after fetching
this.isFetchingData = false;
this.isLoading = false;
}
);
} else {
console.log('Missing required data for doughnut chart, showing default data:', { table: this.table, xAxis: this.xAxis, yAxis: this.yAxis, connection: this.connection });
// Don't set noDataAvailable to true when there's no required data
// This allows static data to be displayed
this.noDataAvailable = false;
// Validate the chart data to ensure we have some data to display
this.validateChartData();
// Force a redraw to ensure the chart displays
this.doughnutChartData = [...this.doughnutChartData];
// Reset flag after fetching
console.log('Missing required data for chart:', { table: this.table, xAxis: this.xAxis, yAxis: this.yAxis, connection: this.connection });
this.noDataAvailable = true;
this.doughnutChartLabels = [];
this.doughnutChartData = [];
// Reset flags after fetching
this.isFetchingData = false;
this.isLoading = false;
}
}
@@ -402,6 +870,35 @@ export class DoughnutChartComponent implements OnInit, OnChanges, AfterViewCheck
}
}
// 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
const url = `chart/getdashjson/doughnut?tableName=${actualApiUrl}&xAxis=${drilldownConfig.xAxis}&yAxes=${drilldownConfig.yAxis}${this.connection ? `&sureId=${this.connection}` : ''}`;
console.log('Drilldown data URL:', url);
@@ -421,59 +918,66 @@ export class DoughnutChartComponent implements OnInit, OnChanges, AfterViewCheck
// Handle the actual data structure returned by the API
if (data && data.chartLabels && data.chartData) {
// For doughnut charts, we need to extract the data differently
// The first dataset's data array contains the values for the doughnut chart
// Backend has already filtered the data, just display it
this.noDataAvailable = data.chartLabels.length === 0;
this.doughnutChartLabels = data.chartLabels || [];
if (data.chartData && data.chartData.length > 0) {
this.doughnutChartData = data.chartData[0].data.map(value => {
// Convert to number if it's not already
const numValue = Number(value);
return isNaN(numValue) ? 0 : numValue;
});
} else {
this.doughnutChartData = [];
this.doughnutChartLabels = data.chartLabels;
// Handle different data structures
let chartDataValues;
if (Array.isArray(data.chartData)) {
// If chartData is already an array of values
if (data.chartData.length > 0 && typeof data.chartData[0] !== 'object') {
chartDataValues = data.chartData;
}
// Ensure labels and data arrays have the same length
this.syncLabelAndDataArrays();
// Validate and sanitize data
this.validateChartData();
// Trigger change detection
this.doughnutChartData = [...this.doughnutChartData];
// If chartData is an array with one object containing the data
else if (data.chartData.length > 0 && data.chartData[0].data) {
chartDataValues = data.chartData[0].data;
}
// Default case
else {
chartDataValues = data.chartData;
}
} else {
chartDataValues = [data.chartData];
}
this.doughnutChartData = [
{
data: chartDataValues,
backgroundColor: this.chartColors.slice(0, chartDataValues.length),
hoverBackgroundColor: this.chartColors.slice(0, chartDataValues.length)
}
];
console.log('Updated doughnut chart with drilldown data:', { labels: this.doughnutChartLabels, data: this.doughnutChartData });
} else if (data && data.labels && data.data) {
// Handle the original expected format as fallback
// Set loading state to false
this.isLoading = false;
} else if (data && data.labels && data.datasets) {
// Backend has already filtered the data, just display it
this.noDataAvailable = data.labels.length === 0;
this.doughnutChartLabels = data.labels || [];
this.doughnutChartData = data.data.map(value => {
// Convert to number if it's not already
const numValue = Number(value);
return isNaN(numValue) ? 0 : numValue;
});
// Ensure labels and data arrays have the same length
this.syncLabelAndDataArrays();
// Validate and sanitize data
this.validateChartData();
// Trigger change detection
this.doughnutChartData = [...this.doughnutChartData];
this.doughnutChartLabels = data.labels;
this.doughnutChartData = data.datasets;
console.log('Updated doughnut chart with drilldown legacy data format:', { labels: this.doughnutChartLabels, data: this.doughnutChartData });
// Set loading state to false
this.isLoading = false;
} else {
console.warn('Drilldown received data does not have expected structure', data);
this.noDataAvailable = true;
this.doughnutChartLabels = [];
this.doughnutChartData = [];
// Validate and sanitize data
this.validateChartData();
// Keep current data instead of empty arrays
// Set loading state to false
this.isLoading = false;
}
},
(error) => {
console.error('Error fetching drilldown data:', error);
this.noDataAvailable = true;
this.doughnutChartLabels = [];
this.doughnutChartData = [];
// Keep current data in case of error
// Set loading state to false
this.isLoading = false;
}
);
// Set loading state
this.isLoading = true;
}
// Reset to original data (go back to base level)
@@ -490,7 +994,7 @@ export class DoughnutChartComponent implements OnInit, OnChanges, AfterViewCheck
console.log('Restored original labels');
}
if (this.originalDoughnutChartData.length > 0) {
this.doughnutChartData = [...this.originalDoughnutChartData];
this.doughnutChartData = JSON.parse(JSON.stringify(this.originalDoughnutChartData));
console.log('Restored original data');
}
@@ -532,44 +1036,11 @@ export class DoughnutChartComponent implements OnInit, OnChanges, AfterViewCheck
}
}
/**
* Get color for legend item
* @param index Index of the legend item
*/
public getLegendColor(index: number): string {
// Get legend color for a specific index
getLegendColor(index: number): string {
return this.chartColors[index % this.chartColors.length];
}
/**
* Ensure labels and data arrays have the same length
*/
private syncLabelAndDataArrays(): void {
// Handle empty arrays
if (this.doughnutChartLabels.length === 0 && this.doughnutChartData.length === 0) {
return;
}
const maxLength = Math.max(this.doughnutChartLabels.length, this.doughnutChartData.length);
// Pad the shorter array with default values
while (this.doughnutChartLabels.length < maxLength) {
this.doughnutChartLabels.push(`Label ${this.doughnutChartLabels.length + 1}`);
}
while (this.doughnutChartData.length < maxLength) {
this.doughnutChartData.push(0);
}
// Truncate the longer array if needed
if (this.doughnutChartLabels.length > maxLength) {
this.doughnutChartLabels = this.doughnutChartLabels.slice(0, maxLength);
}
if (this.doughnutChartData.length > maxLength) {
this.doughnutChartData = this.doughnutChartData.slice(0, maxLength);
}
}
// events
public chartClicked(e: any): void {
console.log('Doughnut chart clicked:', e);
@@ -656,6 +1127,6 @@ export class DoughnutChartComponent implements OnInit, OnChanges, AfterViewCheck
}
public chartHovered(e: any): void {
console.log(e);
console.log('Doughnut chart hovered:', e);
}
}

View File

@@ -1,4 +1,285 @@
<div class="dynamic-chart-container">
<!-- Filter Controls Section -->
<div class="filter-section" *ngIf="hasActiveFilters()">
<!-- Base Filters -->
<div class="filter-group" *ngIf="baseFilters && baseFilters.length > 0">
<h4>Base Filters</h4>
<div class="filter-controls">
<div *ngFor="let filter of baseFilters" class="filter-item">
<div class="filter-label">{{ filter.field }} ({{ filter.type || 'text' }})</div>
<!-- Text Filter -->
<div *ngIf="!filter.type || filter.type === 'text'" class="filter-input">
<input type="text"
[(ngModel)]="filter.value"
(ngModelChange)="onBaseFilterChange(filter)"
[placeholder]="filter.field"
class="clr-input filter-text-input">
</div>
<!-- Dropdown Filter -->
<div *ngIf="filter.type === 'dropdown'" class="filter-input">
<select [(ngModel)]="filter.value"
(ngModelChange)="onBaseFilterChange(filter)"
class="clr-select filter-select">
<option value="">Select {{ filter.field }}</option>
<option *ngFor="let option of getFilterOptions(filter)" [value]="option">{{ option }}</option>
</select>
</div>
<!-- Multi-Select Filter - Updated to show key first, then dropdown on click -->
<div *ngIf="filter.type === 'multiselect'" class="filter-input multiselect-container">
<div class="multiselect-display" (click)="toggleMultiselect(filter, 'base')">
<span class="multiselect-label">{{ filter.field }}</span>
<span class="multiselect-value" *ngIf="getSelectedOptionsCount(filter) > 0">
({{ getSelectedOptionsCount(filter) }} selected)
</span>
<clr-icon shape="caret down" class="dropdown-icon"></clr-icon>
</div>
<div class="multiselect-dropdown" *ngIf="isMultiselectOpen(filter, 'base')">
<div class="checkbox-group">
<div *ngFor="let option of getFilterOptions(filter); let i = index" class="checkbox-item">
<input type="checkbox"
[checked]="isOptionSelected(filter, option)"
(change)="onMultiSelectChange(filter, option, $event)"
[id]="'base-' + filter.field + '-' + i"
class="clr-checkbox">
<label [for]="'base-' + filter.field + '-' + i" class="checkbox-label">{{ option }}</label>
</div>
</div>
</div>
</div>
<!-- Date Range Filter -->
<div *ngIf="filter.type === 'date-range'" class="filter-input date-range">
<div class="date-input-group">
<input type="date"
[(ngModel)]="filter.value.start"
(ngModelChange)="onDateRangeChange(filter, { start: $event, end: filter.value.end })"
placeholder="Start Date"
class="clr-input filter-date">
<span class="date-separator">to</span>
<input type="date"
[(ngModel)]="filter.value.end"
(ngModelChange)="onDateRangeChange(filter, { start: filter.value.start, end: $event })"
placeholder="End Date"
class="clr-input filter-date">
</div>
</div>
<!-- Toggle Filter -->
<div *ngIf="filter.type === 'toggle'" class="filter-input toggle">
<input type="checkbox"
[(ngModel)]="filter.value"
(ngModelChange)="onToggleChange(filter, $event)"
clrToggle
class="clr-toggle">
<label class="toggle-label">{{ filter.field }}</label>
</div>
</div>
</div>
</div>
<!-- Drilldown Filters -->
<div class="filter-group" *ngIf="drilldownFilters && drilldownFilters.length > 0 && currentDrilldownLevel > 0">
<h4>Drilldown Filters</h4>
<div class="filter-controls">
<div *ngFor="let filter of drilldownFilters" class="filter-item">
<div class="filter-label">{{ filter.field }} ({{ filter.type || 'text' }})</div>
<!-- Text Filter -->
<div *ngIf="!filter.type || filter.type === 'text'" class="filter-input">
<input type="text"
[(ngModel)]="filter.value"
(ngModelChange)="onDrilldownFilterChange(filter)"
[placeholder]="filter.field"
class="clr-input filter-text-input">
</div>
<!-- Dropdown Filter -->
<div *ngIf="filter.type === 'dropdown'" class="filter-input">
<select [(ngModel)]="filter.value"
(ngModelChange)="onDrilldownFilterChange(filter)"
class="clr-select filter-select">
<option value="">Select {{ filter.field }}</option>
<option *ngFor="let option of getFilterOptions(filter)" [value]="option">{{ option }}</option>
</select>
</div>
<!-- Multi-Select Filter - Updated to show key first, then dropdown on click -->
<div *ngIf="filter.type === 'multiselect'" class="filter-input multiselect-container">
<div class="multiselect-display" (click)="toggleMultiselect(filter, 'drilldown')">
<span class="multiselect-label">{{ filter.field }}</span>
<span class="multiselect-value" *ngIf="getSelectedOptionsCount(filter) > 0">
({{ getSelectedOptionsCount(filter) }} selected)
</span>
<clr-icon shape="caret down" class="dropdown-icon"></clr-icon>
</div>
<div class="multiselect-dropdown" *ngIf="isMultiselectOpen(filter, 'drilldown')">
<div class="checkbox-group">
<div *ngFor="let option of getFilterOptions(filter); let i = index" class="checkbox-item">
<input type="checkbox"
[checked]="isOptionSelected(filter, option)"
(change)="onMultiSelectChange(filter, option, $event)"
[id]="'drilldown-' + filter.field + '-' + i"
class="clr-checkbox">
<label [for]="'drilldown-' + filter.field + '-' + i" class="checkbox-label">{{ option }}</label>
</div>
</div>
</div>
</div>
<!-- Date Range Filter -->
<div *ngIf="filter.type === 'date-range'" class="filter-input date-range">
<div class="date-input-group">
<input type="date"
[(ngModel)]="filter.value.start"
(ngModelChange)="onDateRangeChange(filter, { start: $event, end: filter.value.end })"
placeholder="Start Date"
class="clr-input filter-date">
<span class="date-separator">to</span>
<input type="date"
[(ngModel)]="filter.value.end"
(ngModelChange)="onDateRangeChange(filter, { start: filter.value.start, end: $event })"
placeholder="End Date"
class="clr-input filter-date">
</div>
</div>
<!-- Toggle Filter -->
<div *ngIf="filter.type === 'toggle'" class="filter-input toggle">
<input type="checkbox"
[(ngModel)]="filter.value"
(ngModelChange)="onToggleChange(filter, $event)"
clrToggle
class="clr-toggle">
<label class="toggle-label">{{ filter.field }}</label>
</div>
</div>
</div>
</div>
<!-- Layer Filters -->
<div class="filter-group" *ngIf="hasActiveLayerFilters()">
<h4>Layer Filters</h4>
<div class="filter-controls">
<div *ngFor="let filter of getActiveLayerFilters()" class="filter-item">
<div class="filter-label">{{ filter.field }} ({{ filter.type || 'text' }})</div>
<!-- Text Filter -->
<div *ngIf="!filter.type || filter.type === 'text'" class="filter-input">
<input type="text"
[(ngModel)]="filter.value"
(ngModelChange)="onLayerFilterChange(filter)"
[placeholder]="filter.field"
class="clr-input filter-text-input">
</div>
<!-- Dropdown Filter -->
<div *ngIf="filter.type === 'dropdown'" class="filter-input">
<select [(ngModel)]="filter.value"
(ngModelChange)="onLayerFilterChange(filter)"
class="clr-select filter-select">
<option value="">Select {{ filter.field }}</option>
<option *ngFor="let option of getFilterOptions(filter)" [value]="option">{{ option }}</option>
</select>
</div>
<!-- Multi-Select Filter - Updated to show key first, then dropdown on click -->
<div *ngIf="filter.type === 'multiselect'" class="filter-input multiselect-container">
<div class="multiselect-display" (click)="toggleMultiselect(filter, 'layer')">
<span class="multiselect-label">{{ filter.field }}</span>
<span class="multiselect-value" *ngIf="getSelectedOptionsCount(filter) > 0">
({{ getSelectedOptionsCount(filter) }} selected)
</span>
<clr-icon shape="caret down" class="dropdown-icon"></clr-icon>
</div>
<div class="multiselect-dropdown" *ngIf="isMultiselectOpen(filter, 'layer')">
<div class="checkbox-group">
<div *ngFor="let option of getFilterOptions(filter); let i = index" class="checkbox-item">
<input type="checkbox"
[checked]="isOptionSelected(filter, option)"
(change)="onMultiSelectChange(filter, option, $event)"
[id]="'layer-' + filter.field + '-' + i"
class="clr-checkbox">
<label [for]="'layer-' + filter.field + '-' + i" class="checkbox-label">{{ option }}</label>
</div>
</div>
</div>
</div>
<!-- Date Range Filter -->
<div *ngIf="filter.type === 'date-range'" class="filter-input date-range">
<div class="date-input-group">
<input type="date"
[(ngModel)]="filter.value.start"
(ngModelChange)="onDateRangeChange(filter, { start: $event, end: filter.value.end })"
placeholder="Start Date"
class="clr-input filter-date">
<span class="date-separator">to</span>
<input type="date"
[(ngModel)]="filter.value.end"
(ngModelChange)="onDateRangeChange(filter, { start: filter.value.start, end: $event })"
placeholder="End Date"
class="clr-input filter-date">
</div>
</div>
<!-- Toggle Filter -->
<div *ngIf="filter.type === 'toggle'" class="filter-input toggle">
<input type="checkbox"
[(ngModel)]="filter.value"
(ngModelChange)="onToggleChange(filter, $event)"
clrToggle
class="clr-toggle">
<label class="toggle-label">{{ filter.field }}</label>
</div>
</div>
</div>
</div>
<!-- Clear Filters Button -->
<div class="filter-actions">
<button class="btn btn-sm btn-outline" (click)="clearAllFilters()">Clear All Filters</button>
</div>
</div>
<!-- Header row with chart title and drilldown navigation -->
<div class="clr-row header-row">
<div class="clr-col-6">
<h3 class="chart-title">{{charttitle || 'Dynamic Chart'}}</h3>
</div>
<div class="clr-col-6" style="text-align: right;">
<!-- Add drilldown navigation controls -->
<button class="btn btn-sm btn-link" *ngIf="drilldownEnabled && drilldownStack.length > 0" (click)="navigateBack()">
<cds-icon shape="arrow" direction="left"></cds-icon>
Back to {{drilldownStack.length > 1 ? 'Previous Level' : 'Main Data'}}
</button>
</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].clickedLabel}})
</span>
</span>
</div>
</div>
</div>
</div>
</div>
<!-- Existing content -->
<!-- Drilldown mode indicator -->
<div *ngIf="currentDrilldownLevel > 0" style="background-color: #e0e0e0; padding: 5px; margin-bottom: 10px; border-radius: 4px; text-align: center;">
<span style="font-weight: bold; color: #333;">Drilldown Level: {{currentDrilldownLevel}}</span>

View File

@@ -0,0 +1,192 @@
.filter-section {
margin-bottom: 20px;
padding: 15px;
border: 1px solid #ddd;
border-radius: 4px;
background-color: #f9f9f9;
}
.filter-group {
margin-bottom: 15px;
h4 {
margin-top: 0;
margin-bottom: 10px;
color: #333;
font-weight: 600;
}
}
.filter-controls {
display: flex;
flex-wrap: wrap;
gap: 15px;
}
.filter-item {
flex: 1 1 300px;
min-width: 250px;
padding: 10px;
background: white;
border: 1px solid #e0e0e0;
border-radius: 4px;
}
.filter-label {
font-weight: 500;
margin-bottom: 8px;
color: #555;
font-size: 14px;
}
.filter-input {
width: 100%;
.filter-text-input,
.filter-select,
.filter-date {
width: 100%;
padding: 6px 12px;
border: 1px solid #ccc;
border-radius: 4px;
font-size: 14px;
}
.filter-select {
height: 34px;
}
}
.multiselect-container {
position: relative;
}
.multiselect-display {
display: flex;
align-items: center;
justify-content: space-between;
padding: 6px 12px;
border: 1px solid #ccc;
border-radius: 4px;
background: white;
cursor: pointer;
min-height: 34px;
.multiselect-label {
flex: 1;
font-size: 14px;
}
.multiselect-value {
color: #666;
font-size: 12px;
margin-right: 8px;
}
.dropdown-icon {
flex-shrink: 0;
transition: transform 0.2s ease;
}
&:hover {
border-color: #999;
}
}
.multiselect-dropdown {
position: absolute;
top: 100%;
left: 0;
right: 0;
z-index: 1000;
background: white;
border: 1px solid #ccc;
border-top: none;
border-radius: 0 0 4px 4px;
box-shadow: 0 2px 4px rgba(0,0,0,0.1);
max-height: 200px;
overflow-y: auto;
.checkbox-group {
padding: 8px;
.checkbox-item {
display: flex;
align-items: center;
gap: 8px;
padding: 4px 0;
.checkbox-label {
margin: 0;
font-size: 14px;
cursor: pointer;
}
}
}
}
.date-range {
.date-input-group {
display: flex;
align-items: center;
gap: 8px;
}
.date-separator {
margin: 0 5px;
color: #777;
}
}
.toggle {
display: flex;
align-items: center;
gap: 8px;
.toggle-label {
margin: 0;
font-size: 14px;
cursor: pointer;
}
}
.filter-actions {
margin-top: 15px;
padding-top: 15px;
border-top: 1px solid #eee;
.btn {
font-size: 13px;
}
}
// New header row styling
.header-row {
margin-bottom: 15px;
padding-bottom: 10px;
border-bottom: 1px solid #eee;
.chart-title {
margin: 0;
font-size: 18px;
font-weight: 600;
color: #333;
}
}
// Responsive design
@media (max-width: 768px) {
.filter-controls {
flex-direction: column;
}
.filter-item {
min-width: 100%;
}
.header-row {
.chart-title {
font-size: 16px;
}
}
}

View File

@@ -2,6 +2,8 @@ import { Component, OnInit, ViewChild, Input, OnChanges, SimpleChanges } from '@
import { ChartConfiguration, ChartData, ChartDataset } from 'chart.js';
import { BaseChartDirective } from 'ng2-charts';
import { Dashboard3Service } from 'src/app/services/builder/dashboard3.service';
import { FilterService } from '../../common-filter/filter.service';
import { Subscription } from 'rxjs';
@Component({
selector: 'app-dynamic-chart',
@@ -37,9 +39,20 @@ export class DynamicChartComponent implements OnInit, OnChanges {
@ViewChild(BaseChartDirective) chart: BaseChartDirective | undefined;
constructor(private dashboardService: Dashboard3Service) { }
constructor(
private dashboardService: Dashboard3Service,
private filterService: FilterService
) { }
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
this.fetchChartData();
}
@@ -47,6 +60,12 @@ export class DynamicChartComponent implements OnInit, OnChanges {
ngOnChanges(changes: SimpleChanges): void {
console.log('DynamicChartComponent input changes:', changes);
// Initialize filter values if they haven't been initialized yet
if (!this.filtersInitialized && (changes.baseFilters || changes.drilldownFilters || changes.drilldownLayers)) {
this.initializeFilterValues();
this.filtersInitialized = true;
}
// Check if any of the key properties have changed
const xAxisChanged = changes.xAxis && !changes.xAxis.firstChange;
const yAxisChanged = changes.yAxis && !changes.yAxis.firstChange;
@@ -107,6 +126,14 @@ export class DynamicChartComponent implements OnInit, OnChanges {
// Flag to prevent infinite loops
private isFetchingData: boolean = false;
// Add properties for filter functionality
private openMultiselects: Map<string, string> = new Map(); // Map of filterId -> context
private documentClickHandler: ((event: MouseEvent) => void) | null = null;
private filtersInitialized: boolean = false;
// Subscriptions to unsubscribe on destroy
private subscriptions: Subscription[] = [];
fetchChartData(): void {
// Set flag to prevent recursive calls
this.isFetchingData = true;
@@ -139,7 +166,49 @@ export class DynamicChartComponent implements OnInit, OnChanges {
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
const url = `chart/getdashjson/dynamic?tableName=${this.table}&xAxis=${this.xAxis}&yAxes=${yAxisString}${this.connection ? `&sureId=${this.connection}` : ''}`;
@@ -504,4 +573,322 @@ export class DynamicChartComponent implements OnInit, OnChanges {
}
this.dynamicChartData = _dynamicChartData;
}
ngOnDestroy(): void {
// Unsubscribe from all subscriptions to prevent memory leaks
this.subscriptions.forEach(subscription => subscription.unsubscribe());
// Remove document click handler if it exists
this.removeDocumentClickHandler();
}
// Initialize filter values with proper default values based on type
private initializeFilterValues(): void {
console.log('Initializing filter values');
// Initialize base filters
if (this.baseFilters) {
this.baseFilters.forEach(filter => {
if (filter.value === undefined || filter.value === null) {
switch (filter.type) {
case 'multiselect':
filter.value = [];
break;
case 'date-range':
filter.value = { start: null, end: null };
break;
case 'toggle':
filter.value = false;
break;
default:
filter.value = '';
}
}
});
}
// Initialize drilldown filters
if (this.drilldownFilters) {
this.drilldownFilters.forEach(filter => {
if (filter.value === undefined || filter.value === null) {
switch (filter.type) {
case 'multiselect':
filter.value = [];
break;
case 'date-range':
filter.value = { start: null, end: null };
break;
case 'toggle':
filter.value = false;
break;
default:
filter.value = '';
}
}
});
}
// Initialize layer filters
if (this.drilldownLayers) {
this.drilldownLayers.forEach(layer => {
if (layer.filters) {
layer.filters.forEach((filter: any) => {
if (filter.value === undefined || filter.value === null) {
switch (filter.type) {
case 'multiselect':
filter.value = [];
break;
case 'date-range':
filter.value = { start: null, end: null };
break;
case 'toggle':
filter.value = false;
break;
default:
filter.value = '';
}
}
});
}
});
}
console.log('Filter values initialized:', {
baseFilters: this.baseFilters,
drilldownFilters: this.drilldownFilters,
drilldownLayers: this.drilldownLayers
});
}
// Check if there are active filters
hasActiveFilters(): boolean {
return (this.baseFilters && this.baseFilters.length > 0) ||
(this.drilldownFilters && this.drilldownFilters.length > 0) ||
this.hasActiveLayerFilters();
}
// Check if there are active layer filters for current drilldown level
hasActiveLayerFilters(): boolean {
if (this.currentDrilldownLevel > 1 && this.drilldownLayers && this.drilldownLayers.length > 0) {
const layerIndex = this.currentDrilldownLevel - 2; // -2 because level 1 is base drilldown
return layerIndex < this.drilldownLayers.length &&
this.drilldownLayers[layerIndex].filters &&
this.drilldownLayers[layerIndex].filters.length > 0;
}
return false;
}
// Get active layer filters for current drilldown level
getActiveLayerFilters(): any[] {
if (this.currentDrilldownLevel > 1 && this.drilldownLayers && this.drilldownLayers.length > 0) {
const layerIndex = this.currentDrilldownLevel - 2; // -2 because level 1 is base drilldown
if (layerIndex < this.drilldownLayers.length &&
this.drilldownLayers[layerIndex].filters) {
return this.drilldownLayers[layerIndex].filters;
}
}
return [];
}
// Get filter options for dropdown/multiselect filters
getFilterOptions(filter: any): string[] {
if (filter.options) {
if (Array.isArray(filter.options)) {
return filter.options;
} else if (typeof filter.options === 'string') {
return filter.options.split(',').map(opt => opt.trim()).filter(opt => opt);
}
}
return [];
}
// Check if an option is selected for multiselect filters
isOptionSelected(filter: any, option: string): boolean {
if (!filter.value) {
return false;
}
if (Array.isArray(filter.value)) {
return filter.value.includes(option);
}
return filter.value === option;
}
// Handle base filter changes
onBaseFilterChange(filter: any): void {
console.log('Base filter changed:', filter);
// Refresh data when filter changes
this.fetchChartData();
}
// Handle drilldown filter changes
onDrilldownFilterChange(filter: any): void {
console.log('Drilldown filter changed:', filter);
// Refresh data when filter changes
this.fetchChartData();
}
// Handle layer filter changes
onLayerFilterChange(filter: any): void {
console.log('Layer filter changed:', filter);
// Refresh data when filter changes
this.fetchChartData();
}
// Handle multiselect changes
onMultiSelectChange(filter: any, option: string, event: any): void {
const checked = event.target.checked;
// Initialize filter.value as array if it's not already
if (!Array.isArray(filter.value)) {
filter.value = [];
}
if (checked) {
// Add option to array if not already present
if (!filter.value.includes(option)) {
filter.value.push(option);
}
} else {
// Remove option from array
filter.value = filter.value.filter((item: string) => item !== option);
}
// Refresh data when filter changes
this.fetchChartData();
}
// Handle date range changes
onDateRangeChange(filter: any, dateRange: { start: string | null, end: string | null }): void {
filter.value = dateRange;
// Refresh data when filter changes
this.fetchChartData();
}
// Handle toggle changes
onToggleChange(filter: any, checked: boolean): void {
filter.value = checked;
// Refresh data when filter changes
this.fetchChartData();
}
// Toggle multiselect dropdown visibility
toggleMultiselect(filter: any, context: string): void {
const filterId = `${context}-${filter.field}`;
if (this.isMultiselectOpen(filter, context)) {
this.openMultiselects.delete(filterId);
} else {
// Close all other multiselects first
this.openMultiselects.clear();
this.openMultiselects.set(filterId, context);
// Add document click handler to close dropdown when clicking outside
this.addDocumentClickHandler();
}
}
// Add document click handler to close dropdowns when clicking outside
private addDocumentClickHandler(): void {
if (!this.documentClickHandler) {
this.documentClickHandler = (event: MouseEvent) => {
const target = event.target as HTMLElement;
// Check if click is outside any multiselect dropdown
if (!target.closest('.multiselect-container')) {
this.openMultiselects.clear();
this.removeDocumentClickHandler();
}
};
// Use setTimeout to ensure the click event that opened the dropdown doesn't immediately close it
setTimeout(() => {
document.addEventListener('click', this.documentClickHandler!);
}, 0);
}
}
// Remove document click handler
private removeDocumentClickHandler(): void {
if (this.documentClickHandler) {
document.removeEventListener('click', this.documentClickHandler);
this.documentClickHandler = null;
}
}
// Check if multiselect dropdown is open
isMultiselectOpen(filter: any, context: string): boolean {
const filterId = `${context}-${filter.field}`;
return this.openMultiselects.has(filterId);
}
// Get count of selected options for a multiselect filter
getSelectedOptionsCount(filter: any): number {
if (!filter.value) {
return 0;
}
if (Array.isArray(filter.value)) {
return filter.value.length;
}
return 0;
}
// Clear all filters
clearAllFilters(): void {
// Clear base filters
if (this.baseFilters) {
this.baseFilters.forEach(filter => {
if (filter.type === 'multiselect') {
filter.value = [];
} else if (filter.type === 'date-range') {
filter.value = { start: null, end: null };
} else if (filter.type === 'toggle') {
filter.value = false;
} else {
filter.value = '';
}
});
}
// Clear drilldown filters
if (this.drilldownFilters) {
this.drilldownFilters.forEach(filter => {
if (filter.type === 'multiselect') {
filter.value = [];
} else if (filter.type === 'date-range') {
filter.value = { start: null, end: null };
} else if (filter.type === 'toggle') {
filter.value = false;
} else {
filter.value = '';
}
});
}
// Clear layer filters
if (this.drilldownLayers) {
this.drilldownLayers.forEach(layer => {
if (layer.filters) {
layer.filters.forEach((filter: any) => {
if (filter.type === 'multiselect') {
filter.value = [];
} else if (filter.type === 'date-range') {
filter.value = { start: null, end: null };
} else if (filter.type === 'toggle') {
filter.value = false;
} else {
filter.value = '';
}
});
}
});
}
// Close all multiselect dropdowns
this.openMultiselects.clear();
// Refresh data
this.fetchChartData();
}
}

View File

@@ -1,4 +1,285 @@
<div class="financial-chart-container">
<!-- Filter Controls Section -->
<div class="filter-section" *ngIf="hasActiveFilters()">
<!-- Base Filters -->
<div class="filter-group" *ngIf="baseFilters && baseFilters.length > 0">
<h4>Base Filters</h4>
<div class="filter-controls">
<div *ngFor="let filter of baseFilters" class="filter-item">
<div class="filter-label">{{ filter.field }} ({{ filter.type || 'text' }})</div>
<!-- Text Filter -->
<div *ngIf="!filter.type || filter.type === 'text'" class="filter-input">
<input type="text"
[(ngModel)]="filter.value"
(ngModelChange)="onBaseFilterChange(filter)"
[placeholder]="filter.field"
class="clr-input filter-text-input">
</div>
<!-- Dropdown Filter -->
<div *ngIf="filter.type === 'dropdown'" class="filter-input">
<select [(ngModel)]="filter.value"
(ngModelChange)="onBaseFilterChange(filter)"
class="clr-select filter-select">
<option value="">Select {{ filter.field }}</option>
<option *ngFor="let option of getFilterOptions(filter)" [value]="option">{{ option }}</option>
</select>
</div>
<!-- Multi-Select Filter - Updated to show key first, then dropdown on click -->
<div *ngIf="filter.type === 'multiselect'" class="filter-input multiselect-container">
<div class="multiselect-display" (click)="toggleMultiselect(filter, 'base')">
<span class="multiselect-label">{{ filter.field }}</span>
<span class="multiselect-value" *ngIf="getSelectedOptionsCount(filter) > 0">
({{ getSelectedOptionsCount(filter) }} selected)
</span>
<clr-icon shape="caret down" class="dropdown-icon"></clr-icon>
</div>
<div class="multiselect-dropdown" *ngIf="isMultiselectOpen(filter, 'base')">
<div class="checkbox-group">
<div *ngFor="let option of getFilterOptions(filter); let i = index" class="checkbox-item">
<input type="checkbox"
[checked]="isOptionSelected(filter, option)"
(change)="onMultiSelectChange(filter, option, $event)"
[id]="'base-' + filter.field + '-' + i"
class="clr-checkbox">
<label [for]="'base-' + filter.field + '-' + i" class="checkbox-label">{{ option }}</label>
</div>
</div>
</div>
</div>
<!-- Date Range Filter -->
<div *ngIf="filter.type === 'date-range'" class="filter-input date-range">
<div class="date-input-group">
<input type="date"
[(ngModel)]="filter.value.start"
(ngModelChange)="onDateRangeChange(filter, { start: $event, end: filter.value.end })"
placeholder="Start Date"
class="clr-input filter-date">
<span class="date-separator">to</span>
<input type="date"
[(ngModel)]="filter.value.end"
(ngModelChange)="onDateRangeChange(filter, { start: filter.value.start, end: $event })"
placeholder="End Date"
class="clr-input filter-date">
</div>
</div>
<!-- Toggle Filter -->
<div *ngIf="filter.type === 'toggle'" class="filter-input toggle">
<input type="checkbox"
[(ngModel)]="filter.value"
(ngModelChange)="onToggleChange(filter, $event)"
clrToggle
class="clr-toggle">
<label class="toggle-label">{{ filter.field }}</label>
</div>
</div>
</div>
</div>
<!-- Drilldown Filters -->
<div class="filter-group" *ngIf="drilldownFilters && drilldownFilters.length > 0 && currentDrilldownLevel > 0">
<h4>Drilldown Filters</h4>
<div class="filter-controls">
<div *ngFor="let filter of drilldownFilters" class="filter-item">
<div class="filter-label">{{ filter.field }} ({{ filter.type || 'text' }})</div>
<!-- Text Filter -->
<div *ngIf="!filter.type || filter.type === 'text'" class="filter-input">
<input type="text"
[(ngModel)]="filter.value"
(ngModelChange)="onDrilldownFilterChange(filter)"
[placeholder]="filter.field"
class="clr-input filter-text-input">
</div>
<!-- Dropdown Filter -->
<div *ngIf="filter.type === 'dropdown'" class="filter-input">
<select [(ngModel)]="filter.value"
(ngModelChange)="onDrilldownFilterChange(filter)"
class="clr-select filter-select">
<option value="">Select {{ filter.field }}</option>
<option *ngFor="let option of getFilterOptions(filter)" [value]="option">{{ option }}</option>
</select>
</div>
<!-- Multi-Select Filter - Updated to show key first, then dropdown on click -->
<div *ngIf="filter.type === 'multiselect'" class="filter-input multiselect-container">
<div class="multiselect-display" (click)="toggleMultiselect(filter, 'drilldown')">
<span class="multiselect-label">{{ filter.field }}</span>
<span class="multiselect-value" *ngIf="getSelectedOptionsCount(filter) > 0">
({{ getSelectedOptionsCount(filter) }} selected)
</span>
<clr-icon shape="caret down" class="dropdown-icon"></clr-icon>
</div>
<div class="multiselect-dropdown" *ngIf="isMultiselectOpen(filter, 'drilldown')">
<div class="checkbox-group">
<div *ngFor="let option of getFilterOptions(filter); let i = index" class="checkbox-item">
<input type="checkbox"
[checked]="isOptionSelected(filter, option)"
(change)="onMultiSelectChange(filter, option, $event)"
[id]="'drilldown-' + filter.field + '-' + i"
class="clr-checkbox">
<label [for]="'drilldown-' + filter.field + '-' + i" class="checkbox-label">{{ option }}</label>
</div>
</div>
</div>
</div>
<!-- Date Range Filter -->
<div *ngIf="filter.type === 'date-range'" class="filter-input date-range">
<div class="date-input-group">
<input type="date"
[(ngModel)]="filter.value.start"
(ngModelChange)="onDateRangeChange(filter, { start: $event, end: filter.value.end })"
placeholder="Start Date"
class="clr-input filter-date">
<span class="date-separator">to</span>
<input type="date"
[(ngModel)]="filter.value.end"
(ngModelChange)="onDateRangeChange(filter, { start: filter.value.start, end: $event })"
placeholder="End Date"
class="clr-input filter-date">
</div>
</div>
<!-- Toggle Filter -->
<div *ngIf="filter.type === 'toggle'" class="filter-input toggle">
<input type="checkbox"
[(ngModel)]="filter.value"
(ngModelChange)="onToggleChange(filter, $event)"
clrToggle
class="clr-toggle">
<label class="toggle-label">{{ filter.field }}</label>
</div>
</div>
</div>
</div>
<!-- Layer Filters -->
<div class="filter-group" *ngIf="hasActiveLayerFilters()">
<h4>Layer Filters</h4>
<div class="filter-controls">
<div *ngFor="let filter of getActiveLayerFilters()" class="filter-item">
<div class="filter-label">{{ filter.field }} ({{ filter.type || 'text' }})</div>
<!-- Text Filter -->
<div *ngIf="!filter.type || filter.type === 'text'" class="filter-input">
<input type="text"
[(ngModel)]="filter.value"
(ngModelChange)="onLayerFilterChange(filter)"
[placeholder]="filter.field"
class="clr-input filter-text-input">
</div>
<!-- Dropdown Filter -->
<div *ngIf="filter.type === 'dropdown'" class="filter-input">
<select [(ngModel)]="filter.value"
(ngModelChange)="onLayerFilterChange(filter)"
class="clr-select filter-select">
<option value="">Select {{ filter.field }}</option>
<option *ngFor="let option of getFilterOptions(filter)" [value]="option">{{ option }}</option>
</select>
</div>
<!-- Multi-Select Filter - Updated to show key first, then dropdown on click -->
<div *ngIf="filter.type === 'multiselect'" class="filter-input multiselect-container">
<div class="multiselect-display" (click)="toggleMultiselect(filter, 'layer')">
<span class="multiselect-label">{{ filter.field }}</span>
<span class="multiselect-value" *ngIf="getSelectedOptionsCount(filter) > 0">
({{ getSelectedOptionsCount(filter) }} selected)
</span>
<clr-icon shape="caret down" class="dropdown-icon"></clr-icon>
</div>
<div class="multiselect-dropdown" *ngIf="isMultiselectOpen(filter, 'layer')">
<div class="checkbox-group">
<div *ngFor="let option of getFilterOptions(filter); let i = index" class="checkbox-item">
<input type="checkbox"
[checked]="isOptionSelected(filter, option)"
(change)="onMultiSelectChange(filter, option, $event)"
[id]="'layer-' + filter.field + '-' + i"
class="clr-checkbox">
<label [for]="'layer-' + filter.field + '-' + i" class="checkbox-label">{{ option }}</label>
</div>
</div>
</div>
</div>
<!-- Date Range Filter -->
<div *ngIf="filter.type === 'date-range'" class="filter-input date-range">
<div class="date-input-group">
<input type="date"
[(ngModel)]="filter.value.start"
(ngModelChange)="onDateRangeChange(filter, { start: $event, end: filter.value.end })"
placeholder="Start Date"
class="clr-input filter-date">
<span class="date-separator">to</span>
<input type="date"
[(ngModel)]="filter.value.end"
(ngModelChange)="onDateRangeChange(filter, { start: filter.value.start, end: $event })"
placeholder="End Date"
class="clr-input filter-date">
</div>
</div>
<!-- Toggle Filter -->
<div *ngIf="filter.type === 'toggle'" class="filter-input toggle">
<input type="checkbox"
[(ngModel)]="filter.value"
(ngModelChange)="onToggleChange(filter, $event)"
clrToggle
class="clr-toggle">
<label class="toggle-label">{{ filter.field }}</label>
</div>
</div>
</div>
</div>
<!-- Clear Filters Button -->
<div class="filter-actions">
<button class="btn btn-sm btn-outline" (click)="clearAllFilters()">Clear All Filters</button>
</div>
</div>
<!-- Header row with chart title and drilldown navigation -->
<div class="clr-row header-row">
<div class="clr-col-6">
<h3 class="chart-title">{{charttitle || 'Financial Chart'}}</h3>
</div>
<div class="clr-col-6" style="text-align: right;">
<!-- Add drilldown navigation controls -->
<button class="btn btn-sm btn-link" *ngIf="drilldownEnabled && drilldownStack.length > 0" (click)="navigateBack()">
<cds-icon shape="arrow" direction="left"></cds-icon>
Back to {{drilldownStack.length > 1 ? 'Previous Level' : 'Main Data'}}
</button>
</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].clickedLabel}})
</span>
</span>
</div>
</div>
</div>
</div>
</div>
<!-- Existing content -->
<!-- Drilldown mode indicator -->
<div *ngIf="currentDrilldownLevel > 0" style="background-color: #e0e0e0; padding: 5px; margin-bottom: 10px; border-radius: 4px; text-align: center;">
<span style="font-weight: bold; color: #333;">Drilldown Level: {{currentDrilldownLevel}}</span>

View File

@@ -1,108 +1,192 @@
.financial-chart-container {
display: flex;
flex-direction: column;
height: 400px;
min-height: 400px;
padding: 20px;
background: linear-gradient(135deg, #ffffff 0%, #f8f9fa 100%);
border-radius: 12px;
box-shadow: 0 4px 20px rgba(0, 0, 0, 0.15);
font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
transition: all 0.3s ease;
border: 1px solid #eaeaea;
}
.financial-chart-container:hover {
box-shadow: 0 6px 25px rgba(0, 0, 0, 0.2);
transform: translateY(-2px);
}
.chart-title {
font-size: 26px;
font-weight: 700;
color: #2c3e50;
.filter-section {
margin-bottom: 20px;
text-align: center;
padding-bottom: 15px;
border-bottom: 2px solid #3498db;
text-shadow: 0 1px 2px rgba(0, 0, 0, 0.1);
}
.chart-wrapper {
position: relative;
flex: 1;
min-height: 250px;
margin: 15px 0;
background: #f8f9fa;
border: 1px solid #e9ecef;
border-radius: 8px;
padding: 10px;
display: flex;
align-items: center;
justify-content: center;
}
.chart-wrapper canvas {
max-width: 100%;
max-height: 100%;
filter: drop-shadow(0 2px 4px rgba(0,0,0,0.1));
}
.chart-wrapper canvas:hover {
filter: drop-shadow(0 4px 8px rgba(0,0,0,0.15));
transform: scale(1.02);
transition: all 0.3s ease;
}
.loading-indicator, .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%;
}
.loading-indicator p, .no-data-message p {
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 */
@media (max-width: 768px) {
.financial-chart-container {
padding: 15px;
border: 1px solid #ddd;
border-radius: 4px;
background-color: #f9f9f9;
}
.filter-group {
margin-bottom: 15px;
h4 {
margin-top: 0;
margin-bottom: 10px;
color: #333;
font-weight: 600;
}
}
.filter-controls {
display: flex;
flex-wrap: wrap;
gap: 15px;
}
.filter-item {
flex: 1 1 300px;
min-width: 250px;
padding: 10px;
background: white;
border: 1px solid #e0e0e0;
border-radius: 4px;
}
.filter-label {
font-weight: 500;
margin-bottom: 8px;
color: #555;
font-size: 14px;
}
.filter-input {
width: 100%;
.filter-text-input,
.filter-select,
.filter-date {
width: 100%;
padding: 6px 12px;
border: 1px solid #ccc;
border-radius: 4px;
font-size: 14px;
}
.filter-select {
height: 34px;
}
}
.multiselect-container {
position: relative;
}
.multiselect-display {
display: flex;
align-items: center;
justify-content: space-between;
padding: 6px 12px;
border: 1px solid #ccc;
border-radius: 4px;
background: white;
cursor: pointer;
min-height: 34px;
.multiselect-label {
flex: 1;
font-size: 14px;
}
.multiselect-value {
color: #666;
font-size: 12px;
margin-right: 8px;
}
.dropdown-icon {
flex-shrink: 0;
transition: transform 0.2s ease;
}
&:hover {
border-color: #999;
}
}
.multiselect-dropdown {
position: absolute;
top: 100%;
left: 0;
right: 0;
z-index: 1000;
background: white;
border: 1px solid #ccc;
border-top: none;
border-radius: 0 0 4px 4px;
box-shadow: 0 2px 4px rgba(0,0,0,0.1);
max-height: 200px;
overflow-y: auto;
.checkbox-group {
padding: 8px;
.checkbox-item {
display: flex;
align-items: center;
gap: 8px;
padding: 4px 0;
.checkbox-label {
margin: 0;
font-size: 14px;
cursor: pointer;
}
}
}
}
.date-range {
.date-input-group {
display: flex;
align-items: center;
gap: 8px;
}
.date-separator {
margin: 0 5px;
color: #777;
}
}
.toggle {
display: flex;
align-items: center;
gap: 8px;
.toggle-label {
margin: 0;
font-size: 14px;
cursor: pointer;
}
}
.filter-actions {
margin-top: 15px;
padding-top: 15px;
border-top: 1px solid #eee;
.btn {
font-size: 13px;
}
}
// New header row styling
.header-row {
margin-bottom: 15px;
padding-bottom: 10px;
border-bottom: 1px solid #eee;
.chart-title {
font-size: 20px;
margin-bottom: 15px;
margin: 0;
font-size: 18px;
font-weight: 600;
color: #333;
}
}
.chart-wrapper {
min-height: 200px;
// Responsive design
@media (max-width: 768px) {
.filter-controls {
flex-direction: column;
}
.no-data-message {
.filter-item {
min-width: 100%;
}
.header-row {
.chart-title {
font-size: 16px;
padding: 20px;
}
}
}

View File

@@ -1,5 +1,8 @@
import { Component, OnInit, Input, OnChanges, SimpleChanges } from '@angular/core';
import { Dashboard3Service } from 'src/app/services/builder/dashboard3.service';
import { FilterService } from '../../common-filter/filter.service';
import { Subscription } from 'rxjs';
import { AlertsService } from 'src/app/services/fnd/alerts.service';
@Component({
selector: 'app-financial-chart',
@@ -33,9 +36,21 @@ export class FinancialChartComponent implements OnInit, OnChanges {
// Multi-layer drilldown configuration inputs
@Input() drilldownLayers: any[] = []; // Array of drilldown layer configurations
constructor(private dashboardService: Dashboard3Service) { }
constructor(
private dashboardService: Dashboard3Service,
private filterService: FilterService,
private alertService: AlertsService
) { }
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
this.fetchChartData();
}
@@ -43,6 +58,14 @@ export class FinancialChartComponent implements OnInit, OnChanges {
ngOnChanges(changes: SimpleChanges): void {
console.log('FinancialChartComponent input changes:', changes);
// Initialize filter values if they haven't been initialized yet
if (!this.filtersInitialized && (changes.baseFilters || changes.drilldownFilters || changes.drilldownLayers)) {
this.initializeFilterValues();
// Load filter options for dropdown/multiselect filters
this.loadFilterOptions();
this.filtersInitialized = true;
}
// Check if any of the key properties have changed
const xAxisChanged = changes.xAxis && !changes.xAxis.firstChange;
const yAxisChanged = changes.yAxis && !changes.yAxis.firstChange;
@@ -86,6 +109,14 @@ export class FinancialChartComponent implements OnInit, OnChanges {
// Flag to prevent infinite loops
private isFetchingData: boolean = false;
// Add properties for filter functionality
private openMultiselects: Map<string, string> = new Map(); // Map of filterId -> context
private documentClickHandler: ((event: MouseEvent) => void) | null = null;
private filtersInitialized: boolean = false;
// Subscriptions to unsubscribe on destroy
private subscriptions: Subscription[] = [];
fetchChartData(): void {
// Set flag to prevent recursive calls
this.isFetchingData = true;
@@ -118,7 +149,49 @@ export class FinancialChartComponent implements OnInit, OnChanges {
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
const url = `chart/getdashjson/financial?tableName=${this.table}&xAxis=${this.xAxis}&yAxes=${yAxisString}${this.connection ? `&sureId=${this.connection}` : ''}`;
@@ -496,4 +569,520 @@ export class FinancialChartComponent implements OnInit, OnChanges {
public chartHovered(e: any): void {
console.log(e);
}
// Initialize filter values with proper default values based on type
private initializeFilterValues(): void {
console.log('Initializing filter values');
// Initialize base filters
if (this.baseFilters) {
this.baseFilters.forEach(filter => {
// Ensure filter has required properties
if (!filter.type) filter.type = 'text';
if (!filter.options) filter.options = '';
if (!filter.availableValues) filter.availableValues = '';
if (filter.value === undefined || filter.value === null) {
switch (filter.type) {
case 'multiselect':
filter.value = [];
break;
case 'date-range':
filter.value = { start: null, end: null };
break;
case 'toggle':
filter.value = false;
break;
default:
filter.value = '';
}
}
});
}
// Initialize drilldown filters
if (this.drilldownFilters) {
this.drilldownFilters.forEach(filter => {
// Ensure filter has required properties
if (!filter.type) filter.type = 'text';
if (!filter.options) filter.options = '';
if (!filter.availableValues) filter.availableValues = '';
if (filter.value === undefined || filter.value === null) {
switch (filter.type) {
case 'multiselect':
filter.value = [];
break;
case 'date-range':
filter.value = { start: null, end: null };
break;
case 'toggle':
filter.value = false;
break;
default:
filter.value = '';
}
}
});
}
// Initialize layer filters
if (this.drilldownLayers) {
this.drilldownLayers.forEach(layer => {
if (layer.filters) {
layer.filters.forEach((filter: any) => {
// Ensure filter has required properties
if (!filter.type) filter.type = 'text';
if (!filter.options) filter.options = '';
if (!filter.availableValues) filter.availableValues = '';
if (filter.value === undefined || filter.value === null) {
switch (filter.type) {
case 'multiselect':
filter.value = [];
break;
case 'date-range':
filter.value = { start: null, end: null };
break;
case 'toggle':
filter.value = false;
break;
default:
filter.value = '';
}
}
});
}
});
}
console.log('Filter values initialized:', {
baseFilters: this.baseFilters,
drilldownFilters: this.drilldownFilters,
drilldownLayers: this.drilldownLayers
});
}
// Check if there are active filters
hasActiveFilters(): boolean {
return (this.baseFilters && this.baseFilters.length > 0) ||
(this.drilldownFilters && this.drilldownFilters.length > 0) ||
this.hasActiveLayerFilters();
}
// Check if there are active layer filters for current drilldown level
hasActiveLayerFilters(): boolean {
if (this.currentDrilldownLevel > 1 && this.drilldownLayers && this.drilldownLayers.length > 0) {
const layerIndex = this.currentDrilldownLevel - 2; // -2 because level 1 is base drilldown
return layerIndex < this.drilldownLayers.length &&
this.drilldownLayers[layerIndex].filters &&
this.drilldownLayers[layerIndex].filters.length > 0;
}
return false;
}
// Get active layer filters for current drilldown level
getActiveLayerFilters(): any[] {
if (this.currentDrilldownLevel > 1 && this.drilldownLayers && this.drilldownLayers.length > 0) {
const layerIndex = this.currentDrilldownLevel - 2; // -2 because level 1 is base drilldown
if (layerIndex < this.drilldownLayers.length &&
this.drilldownLayers[layerIndex].filters) {
return this.drilldownLayers[layerIndex].filters;
}
}
return [];
}
// Get filter options for dropdown/multiselect filters
getFilterOptions(filter: any): string[] {
if (filter.options) {
if (Array.isArray(filter.options)) {
return filter.options;
} else if (typeof filter.options === 'string') {
return filter.options.split(',').map(opt => opt.trim()).filter(opt => opt);
}
}
return [];
}
// Check if an option is selected for multiselect filters
isOptionSelected(filter: any, option: string): boolean {
if (!filter.value) {
return false;
}
if (Array.isArray(filter.value)) {
return filter.value.includes(option);
}
return filter.value === option;
}
// Handle base filter changes
onBaseFilterChange(filter: any): void {
console.log('Base filter changed:', filter);
// Refresh data when filter changes
this.fetchChartData();
}
// Handle drilldown filter changes
onDrilldownFilterChange(filter: any): void {
console.log('Drilldown filter changed:', filter);
// Refresh data when filter changes
this.fetchChartData();
}
// Handle layer filter changes
onLayerFilterChange(filter: any): void {
console.log('Layer filter changed:', filter);
// Refresh data when filter changes
this.fetchChartData();
}
// Handle multiselect changes
onMultiSelectChange(filter: any, option: string, event: any): void {
const checked = event.target.checked;
// Initialize filter.value as array if it's not already
if (!Array.isArray(filter.value)) {
filter.value = [];
}
if (checked) {
// Add option to array if not already present
if (!filter.value.includes(option)) {
filter.value.push(option);
}
} else {
// Remove option from array
filter.value = filter.value.filter((item: string) => item !== option);
}
// Refresh data when filter changes
this.fetchChartData();
}
// Handle date range changes
onDateRangeChange(filter: any, dateRange: { start: string | null, end: string | null }): void {
filter.value = dateRange;
// Refresh data when filter changes
this.fetchChartData();
}
// Handle toggle changes
onToggleChange(filter: any, checked: boolean): void {
filter.value = checked;
// Refresh data when filter changes
this.fetchChartData();
}
// Toggle multiselect dropdown visibility
toggleMultiselect(filter: any, context: string): void {
const filterId = `${context}-${filter.field}`;
if (this.isMultiselectOpen(filter, context)) {
this.openMultiselects.delete(filterId);
} else {
// Close all other multiselects first
this.openMultiselects.clear();
this.openMultiselects.set(filterId, context);
// Add document click handler to close dropdown when clicking outside
this.addDocumentClickHandler();
}
}
// Add document click handler to close dropdowns when clicking outside
private addDocumentClickHandler(): void {
if (!this.documentClickHandler) {
this.documentClickHandler = (event: MouseEvent) => {
const target = event.target as HTMLElement;
// Check if click is outside any multiselect dropdown
if (!target.closest('.multiselect-container')) {
this.openMultiselects.clear();
this.removeDocumentClickHandler();
}
};
// Use setTimeout to ensure the click event that opened the dropdown doesn't immediately close it
setTimeout(() => {
document.addEventListener('click', this.documentClickHandler!);
}, 0);
}
}
// Remove document click handler
private removeDocumentClickHandler(): void {
if (this.documentClickHandler) {
document.removeEventListener('click', this.documentClickHandler);
this.documentClickHandler = null;
}
}
// Check if multiselect dropdown is open
isMultiselectOpen(filter: any, context: string): boolean {
const filterId = `${context}-${filter.field}`;
return this.openMultiselects.has(filterId);
}
// Get count of selected options for a multiselect filter
getSelectedOptionsCount(filter: any): number {
if (!filter.value) {
return 0;
}
if (Array.isArray(filter.value)) {
return filter.value.length;
}
return 0;
}
// Clear all filters
clearAllFilters(): void {
// Clear base filters
if (this.baseFilters) {
this.baseFilters.forEach(filter => {
if (filter.type === 'multiselect') {
filter.value = [];
} else if (filter.type === 'date-range') {
filter.value = { start: null, end: null };
} else if (filter.type === 'toggle') {
filter.value = false;
} else {
filter.value = '';
}
});
}
// Clear drilldown filters
if (this.drilldownFilters) {
this.drilldownFilters.forEach(filter => {
if (filter.type === 'multiselect') {
filter.value = [];
} else if (filter.type === 'date-range') {
filter.value = { start: null, end: null };
} else if (filter.type === 'toggle') {
filter.value = false;
} else {
filter.value = '';
}
});
}
// Clear layer filters
if (this.drilldownLayers) {
this.drilldownLayers.forEach(layer => {
if (layer.filters) {
layer.filters.forEach((filter: any) => {
if (filter.type === 'multiselect') {
filter.value = [];
} else if (filter.type === 'date-range') {
filter.value = { start: null, end: null };
} else if (filter.type === 'toggle') {
filter.value = false;
} else {
filter.value = '';
}
});
}
});
}
// Close all multiselect dropdowns
this.openMultiselects.clear();
// Refresh data
this.fetchChartData();
}
ngOnDestroy(): void {
// Unsubscribe from all subscriptions to prevent memory leaks
this.subscriptions.forEach(subscription => subscription.unsubscribe());
// Remove document click handler if it exists
this.removeDocumentClickHandler();
}
// Load filter options for dropdown and multiselect filters
private loadFilterOptions(): void {
console.log('Loading filter options');
// Load options for base filters
if (this.baseFilters && this.table) {
this.baseFilters.forEach((filter, index) => {
if ((filter.type === 'dropdown' || filter.type === 'multiselect') && filter.field) {
this.loadFilterValuesForField(this.table, this.connection, filter.field, index, 'base');
}
});
}
// Load options for drilldown filters
if (this.drilldownFilters && this.drilldownApiUrl) {
this.drilldownFilters.forEach((filter, index) => {
if ((filter.type === 'dropdown' || filter.type === 'multiselect') && filter.field) {
this.loadFilterValuesForField(this.drilldownApiUrl, this.connection, filter.field, index, 'drilldown');
}
});
}
// Load options for layer filters
if (this.drilldownLayers) {
this.drilldownLayers.forEach((layer, layerIndex) => {
if (layer.filters && layer.apiUrl) {
layer.filters.forEach((filter, filterIndex) => {
if ((filter.type === 'dropdown' || filter.type === 'multiselect') && filter.field) {
this.loadFilterValuesForField(layer.apiUrl, this.connection, filter.field, filterIndex, 'layer', layerIndex);
}
});
}
});
}
}
// Load filter values for a specific field
private loadFilterValuesForField(
apiUrl: string,
connectionId: number | undefined,
field: string,
filterIndex: number,
filterType: 'base' | 'drilldown' | 'layer',
layerIndex?: number
): void {
if (apiUrl && field) {
this.alertService.getValuesFromUrl(apiUrl, connectionId, field).subscribe(
(values: string[]) => {
console.log(`Loaded filter values for ${filterType} filter ${field}:`, values);
// Update the filter with available values
if (filterType === 'base') {
const filter = this.baseFilters[filterIndex];
if (filter) {
filter.availableValues = values.join(', ');
// For dropdown/multiselect types, also update the options
if (filter.type === 'dropdown' || filter.type === 'multiselect') {
filter.options = filter.availableValues;
}
}
} else if (filterType === 'drilldown') {
const filter = this.drilldownFilters[filterIndex];
if (filter) {
filter.availableValues = values.join(', ');
// For dropdown/multiselect types, also update the options
if (filter.type === 'dropdown' || filter.type === 'multiselect') {
filter.options = filter.availableValues;
}
}
} else if (filterType === 'layer' && layerIndex !== undefined) {
const layer = this.drilldownLayers[layerIndex];
if (layer && layer.filters) {
const filter = layer.filters[filterIndex];
if (filter) {
filter.availableValues = values.join(', ');
// For dropdown/multiselect types, also update the options
if (filter.type === 'dropdown' || filter.type === 'multiselect') {
filter.options = filter.availableValues;
}
}
}
}
},
(error) => {
console.error('Error loading available values for field:', field, error);
}
);
}
}
// Handle base filter field change
onBaseFilterFieldChange(index: number, field: string): void {
const filter = this.baseFilters[index];
if (filter) {
filter.field = field;
// If field changes, reset value and options
filter.value = '';
filter.options = '';
filter.availableValues = '';
// If we have a field and table URL, load available values
if (field && this.table && (filter.type === 'dropdown' || filter.type === 'multiselect')) {
this.loadFilterValuesForField(this.table, this.connection, field, index, 'base');
}
}
}
// Handle base filter type change
onBaseFilterTypeChange(index: number, type: string): void {
const filter = this.baseFilters[index];
if (filter) {
filter.type = type;
// If type changes to dropdown/multiselect and we have a field, load available values
if ((type === 'dropdown' || type === 'multiselect') && filter.field && this.table) {
this.loadFilterValuesForField(this.table, this.connection, filter.field, index, 'base');
}
}
}
// Handle drilldown filter field change
onDrilldownFilterFieldChange(index: number, field: string): void {
const filter = this.drilldownFilters[index];
if (filter) {
filter.field = field;
// If field changes, reset value and options
filter.value = '';
filter.options = '';
filter.availableValues = '';
// If we have a field and drilldown API URL, load available values
if (field && this.drilldownApiUrl && (filter.type === 'dropdown' || filter.type === 'multiselect')) {
this.loadFilterValuesForField(this.drilldownApiUrl, this.connection, field, index, 'drilldown');
}
}
}
// Handle drilldown filter type change
onDrilldownFilterTypeChange(index: number, type: string): void {
const filter = this.drilldownFilters[index];
if (filter) {
filter.type = type;
// If type changes to dropdown/multiselect and we have a field, load available values
if ((type === 'dropdown' || type === 'multiselect') && filter.field && this.drilldownApiUrl) {
this.loadFilterValuesForField(this.drilldownApiUrl, this.connection, filter.field, index, 'drilldown');
}
}
}
// Handle layer filter field change
onLayerFilterFieldChange(layerIndex: number, filterIndex: number, field: string): void {
const layer = this.drilldownLayers[layerIndex];
if (layer && layer.filters) {
const filter = layer.filters[filterIndex];
if (filter) {
filter.field = field;
// If field changes, reset value and options
filter.value = '';
filter.options = '';
filter.availableValues = '';
// If we have a field and layer API URL, load available values
if (field && layer.apiUrl && (filter.type === 'dropdown' || filter.type === 'multiselect')) {
this.loadFilterValuesForField(layer.apiUrl, this.connection, field, filterIndex, 'layer', layerIndex);
}
}
}
}
// Handle layer filter type change
onLayerFilterTypeChange(layerIndex: number, filterIndex: number, type: string): void {
const layer = this.drilldownLayers[layerIndex];
if (layer && layer.filters) {
const filter = layer.filters[filterIndex];
if (filter) {
filter.type = type;
// If type changes to dropdown/multiselect and we have a field, load available values
if ((type === 'dropdown' || type === 'multiselect') && filter.field && layer.apiUrl) {
this.loadFilterValuesForField(layer.apiUrl, this.connection, filter.field, filterIndex, 'layer', layerIndex);
}
}
}
}
}

View File

@@ -1,12 +1,289 @@
<div style="display: block;">
<div class="dg-wrapper">
<!-- Filter Controls Section -->
<div class="filter-section" *ngIf="hasActiveFilters()">
<!-- Base Filters -->
<div class="filter-group" *ngIf="baseFilters && baseFilters.length > 0">
<h4>Base Filters</h4>
<div class="filter-controls">
<div *ngFor="let filter of baseFilters" class="filter-item">
<div class="filter-label">{{ filter.field }} ({{ filter.type || 'text' }})</div>
<!-- Text Filter -->
<div *ngIf="!filter.type || filter.type === 'text'" class="filter-input">
<input type="text"
[(ngModel)]="filter.value"
(ngModelChange)="onBaseFilterChange(filter)"
[placeholder]="filter.field"
class="clr-input filter-text-input">
</div>
<!-- Dropdown Filter -->
<div *ngIf="filter.type === 'dropdown'" class="filter-input">
<select [(ngModel)]="filter.value"
(ngModelChange)="onBaseFilterChange(filter)"
class="clr-select filter-select">
<option value="">Select {{ filter.field }}</option>
<option *ngFor="let option of getFilterOptions(filter)" [value]="option">{{ option }}</option>
</select>
</div>
<!-- Multi-Select Filter - Updated to show key first, then dropdown on click -->
<div *ngIf="filter.type === 'multiselect'" class="filter-input multiselect-container">
<div class="multiselect-display" (click)="toggleMultiselect(filter, 'base')">
<span class="multiselect-label">{{ filter.field }}</span>
<span class="multiselect-value" *ngIf="getSelectedOptionsCount(filter) > 0">
({{ getSelectedOptionsCount(filter) }} selected)
</span>
<clr-icon shape="caret down" class="dropdown-icon"></clr-icon>
</div>
<div class="multiselect-dropdown" *ngIf="isMultiselectOpen(filter, 'base')">
<div class="checkbox-group">
<div *ngFor="let option of getFilterOptions(filter); let i = index" class="checkbox-item">
<input type="checkbox"
[checked]="isOptionSelected(filter, option)"
(change)="onMultiSelectChange(filter, option, $event)"
[id]="'base-' + filter.field + '-' + i"
class="clr-checkbox">
<label [for]="'base-' + filter.field + '-' + i" class="checkbox-label">{{ option }}</label>
</div>
</div>
</div>
</div>
<!-- Date Range Filter -->
<div *ngIf="filter.type === 'date-range'" class="filter-input date-range">
<div class="date-input-group">
<input type="date"
[(ngModel)]="filter.value.start"
(ngModelChange)="onDateRangeChange(filter, { start: $event, end: filter.value.end })"
placeholder="Start Date"
class="clr-input filter-date">
<span class="date-separator">to</span>
<input type="date"
[(ngModel)]="filter.value.end"
(ngModelChange)="onDateRangeChange(filter, { start: filter.value.start, end: $event })"
placeholder="End Date"
class="clr-input filter-date">
</div>
</div>
<!-- Toggle Filter -->
<div *ngIf="filter.type === 'toggle'" class="filter-input toggle">
<input type="checkbox"
[(ngModel)]="filter.value"
(ngModelChange)="onToggleChange(filter, $event)"
clrToggle
class="clr-toggle">
<label class="toggle-label">{{ filter.field }}</label>
</div>
</div>
</div>
</div>
<!-- Drilldown Filters -->
<div class="filter-group" *ngIf="drilldownFilters && drilldownFilters.length > 0 && currentDrilldownLevel > 0">
<h4>Drilldown Filters</h4>
<div class="filter-controls">
<div *ngFor="let filter of drilldownFilters" class="filter-item">
<div class="filter-label">{{ filter.field }} ({{ filter.type || 'text' }})</div>
<!-- Text Filter -->
<div *ngIf="!filter.type || filter.type === 'text'" class="filter-input">
<input type="text"
[(ngModel)]="filter.value"
(ngModelChange)="onDrilldownFilterChange(filter)"
[placeholder]="filter.field"
class="clr-input filter-text-input">
</div>
<!-- Dropdown Filter -->
<div *ngIf="filter.type === 'dropdown'" class="filter-input">
<select [(ngModel)]="filter.value"
(ngModelChange)="onDrilldownFilterChange(filter)"
class="clr-select filter-select">
<option value="">Select {{ filter.field }}</option>
<option *ngFor="let option of getFilterOptions(filter)" [value]="option">{{ option }}</option>
</select>
</div>
<!-- Multi-Select Filter - Updated to show key first, then dropdown on click -->
<div *ngIf="filter.type === 'multiselect'" class="filter-input multiselect-container">
<div class="multiselect-display" (click)="toggleMultiselect(filter, 'drilldown')">
<span class="multiselect-label">{{ filter.field }}</span>
<span class="multiselect-value" *ngIf="getSelectedOptionsCount(filter) > 0">
({{ getSelectedOptionsCount(filter) }} selected)
</span>
<clr-icon shape="caret down" class="dropdown-icon"></clr-icon>
</div>
<div class="multiselect-dropdown" *ngIf="isMultiselectOpen(filter, 'drilldown')">
<div class="checkbox-group">
<div *ngFor="let option of getFilterOptions(filter); let i = index" class="checkbox-item">
<input type="checkbox"
[checked]="isOptionSelected(filter, option)"
(change)="onMultiSelectChange(filter, option, $event)"
[id]="'drilldown-' + filter.field + '-' + i"
class="clr-checkbox">
<label [for]="'drilldown-' + filter.field + '-' + i" class="checkbox-label">{{ option }}</label>
</div>
</div>
</div>
</div>
<!-- Date Range Filter -->
<div *ngIf="filter.type === 'date-range'" class="filter-input date-range">
<div class="date-input-group">
<input type="date"
[(ngModel)]="filter.value.start"
(ngModelChange)="onDateRangeChange(filter, { start: $event, end: filter.value.end })"
placeholder="Start Date"
class="clr-input filter-date">
<span class="date-separator">to</span>
<input type="date"
[(ngModel)]="filter.value.end"
(ngModelChange)="onDateRangeChange(filter, { start: filter.value.start, end: $event })"
placeholder="End Date"
class="clr-input filter-date">
</div>
</div>
<!-- Toggle Filter -->
<div *ngIf="filter.type === 'toggle'" class="filter-input toggle">
<input type="checkbox"
[(ngModel)]="filter.value"
(ngModelChange)="onToggleChange(filter, $event)"
clrToggle
class="clr-toggle">
<label class="toggle-label">{{ filter.field }}</label>
</div>
</div>
</div>
</div>
<!-- Layer Filters -->
<div class="filter-group" *ngIf="hasActiveLayerFilters()">
<h4>Layer Filters</h4>
<div class="filter-controls">
<div *ngFor="let filter of getActiveLayerFilters()" class="filter-item">
<div class="filter-label">{{ filter.field }} ({{ filter.type || 'text' }})</div>
<!-- Text Filter -->
<div *ngIf="!filter.type || filter.type === 'text'" class="filter-input">
<input type="text"
[(ngModel)]="filter.value"
(ngModelChange)="onLayerFilterChange(filter)"
[placeholder]="filter.field"
class="clr-input filter-text-input">
</div>
<!-- Dropdown Filter -->
<div *ngIf="filter.type === 'dropdown'" class="filter-input">
<select [(ngModel)]="filter.value"
(ngModelChange)="onLayerFilterChange(filter)"
class="clr-select filter-select">
<option value="">Select {{ filter.field }}</option>
<option *ngFor="let option of getFilterOptions(filter)" [value]="option">{{ option }}</option>
</select>
</div>
<!-- Multi-Select Filter - Updated to show key first, then dropdown on click -->
<div *ngIf="filter.type === 'multiselect'" class="filter-input multiselect-container">
<div class="multiselect-display" (click)="toggleMultiselect(filter, 'layer')">
<span class="multiselect-label">{{ filter.field }}</span>
<span class="multiselect-value" *ngIf="getSelectedOptionsCount(filter) > 0">
({{ getSelectedOptionsCount(filter) }} selected)
</span>
<clr-icon shape="caret down" class="dropdown-icon"></clr-icon>
</div>
<div class="multiselect-dropdown" *ngIf="isMultiselectOpen(filter, 'layer')">
<div class="checkbox-group">
<div *ngFor="let option of getFilterOptions(filter); let i = index" class="checkbox-item">
<input type="checkbox"
[checked]="isOptionSelected(filter, option)"
(change)="onMultiSelectChange(filter, option, $event)"
[id]="'layer-' + filter.field + '-' + i"
class="clr-checkbox">
<label [for]="'layer-' + filter.field + '-' + i" class="checkbox-label">{{ option }}</label>
</div>
</div>
</div>
</div>
<!-- Date Range Filter -->
<div *ngIf="filter.type === 'date-range'" class="filter-input date-range">
<div class="date-input-group">
<input type="date"
[(ngModel)]="filter.value.start"
(ngModelChange)="onDateRangeChange(filter, { start: $event, end: filter.value.end })"
placeholder="Start Date"
class="clr-input filter-date">
<span class="date-separator">to</span>
<input type="date"
[(ngModel)]="filter.value.end"
(ngModelChange)="onDateRangeChange(filter, { start: filter.value.start, end: $event })"
placeholder="End Date"
class="clr-input filter-date">
</div>
</div>
<!-- Toggle Filter -->
<div *ngIf="filter.type === 'toggle'" class="filter-input toggle">
<input type="checkbox"
[(ngModel)]="filter.value"
(ngModelChange)="onToggleChange(filter, $event)"
clrToggle
class="clr-toggle">
<label class="toggle-label">{{ filter.field }}</label>
</div>
</div>
</div>
</div>
<!-- Clear Filters Button -->
<div class="filter-actions">
<button class="btn btn-sm btn-outline" (click)="clearAllFilters()">Clear All Filters</button>
</div>
</div>
<div class="clr-row">
<div class="clr-col-8">
<!-- <div class="clr-col-8">
<h3>{{charttitle || 'Data Grid'}}</h3>
</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>
<!-- 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-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>
</clr-dg-placeholder>
@@ -19,7 +296,7 @@
<clr-dg-row *clrDgItems="let item of givendata" [clrDgItem]="item">
<!-- 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]}}
</clr-dg-cell>
</clr-dg-row>

View File

@@ -1,12 +1,180 @@
@import '../../../../../../../styles1.scss';
input.ng-invalid.ng-touched {
border-color: red;
.filter-section {
margin-bottom: 20px;
padding: 15px;
border: 1px solid #ddd;
border-radius: 4px;
background-color: #f9f9f9;
}
.error_mess {
color: red;
.filter-group {
margin-bottom: 15px;
h4 {
margin-top: 0;
margin-bottom: 10px;
color: #333;
font-weight: 600;
}
clr-datagrid{
height: 400px; /* Adjust the height as needed */
}
.filter-controls {
display: flex;
flex-wrap: wrap;
gap: 15px;
}
.filter-item {
flex: 1 1 300px;
min-width: 250px;
padding: 10px;
background: white;
border: 1px solid #e0e0e0;
border-radius: 4px;
}
.filter-label {
font-weight: 500;
margin-bottom: 8px;
color: #555;
font-size: 14px;
}
.filter-input {
width: 100%;
.filter-text-input,
.filter-select,
.filter-date {
width: 100%;
padding: 6px 12px;
border: 1px solid #ccc;
border-radius: 4px;
font-size: 14px;
}
.filter-select {
height: 34px;
}
}
.multiselect-container {
position: relative;
}
.multiselect-display {
display: flex;
align-items: center;
justify-content: space-between;
padding: 6px 12px;
border: 1px solid #ccc;
border-radius: 4px;
background: white;
cursor: pointer;
min-height: 34px;
.multiselect-label {
flex: 1;
font-size: 14px;
}
.multiselect-value {
color: #666;
font-size: 12px;
margin-right: 8px;
}
.dropdown-icon {
flex-shrink: 0;
transition: transform 0.2s ease;
}
&:hover {
border-color: #999;
}
}
.multiselect-dropdown {
position: absolute;
top: 100%;
left: 0;
right: 0;
z-index: 1000;
background: white;
border: 1px solid #ccc;
border-top: none;
border-radius: 0 0 4px 4px;
box-shadow: 0 2px 4px rgba(0,0,0,0.1);
max-height: 200px;
overflow-y: auto;
.checkbox-group {
padding: 8px;
.checkbox-item {
display: flex;
align-items: center;
gap: 8px;
padding: 4px 0;
.checkbox-label {
margin: 0;
font-size: 14px;
cursor: pointer;
}
}
}
}
.date-range {
.date-input-group {
display: flex;
align-items: center;
gap: 8px;
}
.date-separator {
margin: 0 5px;
color: #777;
}
}
.toggle {
display: flex;
align-items: center;
gap: 8px;
.toggle-label {
margin: 0;
font-size: 14px;
cursor: pointer;
}
}
.filter-actions {
margin-top: 15px;
padding-top: 15px;
border-top: 1px solid #eee;
.btn {
font-size: 13px;
}
}
.dg-wrapper {
padding: 15px;
}
clr-datagrid {
margin-top: 10px;
}
// Responsive design
@media (max-width: 768px) {
.filter-controls {
flex-direction: column;
}
.filter-item {
min-width: 100%;
}
}

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 { 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({
selector: 'app-grid-view',
templateUrl: './grid-view.component.html',
styleUrls: ['./grid-view.component.scss']
})
export class GridViewComponent implements OnInit, OnChanges {
export class GridViewComponent implements OnInit, OnChanges, OnDestroy {
@Input() xAxis: string;
@Input() yAxis: string | string[];
@Input() table: string;
@@ -23,6 +27,16 @@ export class GridViewComponent implements OnInit, OnChanges {
@Input() datasource: string;
@Input() fieldName: string;
@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;
givendata: any[] = [];
@@ -38,13 +52,46 @@ export class GridViewComponent implements OnInit, OnChanges {
submitted = false;
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[] = [];
// Add a flag to track if filters have been initialized
private filtersInitialized: boolean = false;
// Add properties to track open multiselect dropdowns
private openMultiselects: Map<string, string> = new Map(); // Map of filterId -> context
// Add property to track document click handler
private documentClickHandler: ((event: MouseEvent) => void) | null = null;
constructor(
private mainservice: UsergrpmaintainceService,
private dashboardService: Dashboard3Service,
// Add FilterService to constructor
private filterService: FilterService
) { }
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();
}
@@ -55,33 +102,193 @@ export class GridViewComponent implements OnInit, OnChanges {
const xAxisChanged = changes.xAxis && !changes.xAxis.firstChange;
const yAxisChanged = changes.yAxis && !changes.yAxis.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;
// Initialize filter values if they haven't been initialized yet
if (!this.filtersInitialized && (changes.baseFilters || changes.drilldownFilters || changes.drilldownLayers)) {
this.initializeFilterValues();
this.filtersInitialized = true;
}
// Respond to input changes
if (xAxisChanged || yAxisChanged || tableChanged || connectionChanged) {
console.log('X or Y axis or table or connection changed, fetching new data');
// Only fetch data if xAxis, yAxis, table, or connection has changed (and it's not the first change)
if (!this.isFetchingData && (xAxisChanged || yAxisChanged || tableChanged || connectionChanged || baseFiltersChanged ||
drilldownEnabledChanged || drilldownApiUrlChanged || drilldownXAxisChanged || drilldownYAxisChanged ||
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();
}
}
// Initialize filter values with proper default values based on type
private initializeFilterValues(): void {
console.log('Initializing filter values');
// Initialize base filters
if (this.baseFilters) {
this.baseFilters.forEach(filter => {
if (filter.value === undefined || filter.value === null) {
switch (filter.type) {
case 'multiselect':
filter.value = [];
break;
case 'date-range':
filter.value = { start: null, end: null };
break;
case 'toggle':
filter.value = false;
break;
default:
filter.value = '';
}
}
});
}
// Initialize drilldown filters
if (this.drilldownFilters) {
this.drilldownFilters.forEach(filter => {
if (filter.value === undefined || filter.value === null) {
switch (filter.type) {
case 'multiselect':
filter.value = [];
break;
case 'date-range':
filter.value = { start: null, end: null };
break;
case 'toggle':
filter.value = false;
break;
default:
filter.value = '';
}
}
});
}
// Initialize layer filters
if (this.drilldownLayers) {
this.drilldownLayers.forEach(layer => {
if (layer.filters) {
layer.filters.forEach((filter: any) => {
if (filter.value === undefined || filter.value === null) {
switch (filter.type) {
case 'multiselect':
filter.value = [];
break;
case 'date-range':
filter.value = { start: null, end: null };
break;
case 'toggle':
filter.value = false;
break;
default:
filter.value = '';
}
}
});
}
});
}
console.log('Filter values initialized:', {
baseFilters: this.baseFilters,
drilldownFilters: this.drilldownFilters,
drilldownLayers: this.drilldownLayers
});
}
// Dynamic headers for the grid
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 (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 && this.xAxis) {
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
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
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) => {
console.log('=== GRID VIEW DATA RESPONSE ===');
console.log('Received grid data:', data);
if (data === null) {
console.warn('Grid API returned null data. Check if the API endpoint is working correctly.');
this.error = "No data Available";
this.noDataAvailable = true;
// Reset flag after fetching
this.isFetchingData = false;
return;
}
@@ -90,27 +297,36 @@ export class GridViewComponent implements OnInit, OnChanges {
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 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 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 array data:', this.givendata);
} else {
console.warn('Grid received data does not have expected structure', data);
this.error = "No valid data received";
this.givendata = [];
this.noDataAvailable = true;
}
// Reset flag after fetching
this.isFetchingData = false;
}, (error) => {
console.log('Error fetching grid data:', error);
this.error = "Server Error";
this.noDataAvailable = true;
// Reset flag after fetching
this.isFetchingData = false;
});
} else if (this.table) {
console.log('Missing xAxis, falling back to default data fetching');
@@ -126,13 +342,342 @@ export class GridViewComponent implements OnInit, OnChanges {
this.givendata = Array.isArray(data) ? data : [];
this.extractDynamicHeaders(data);
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) => {
console.log(error);
this.error = "Server Error";
this.noDataAvailable = true;
// Reset flag after fetching
this.isFetchingData = false;
});
} else {
console.log('Missing required data for grid:', { table: this.table });
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 +720,261 @@ export class GridViewComponent implements OnInit, OnChanges {
.replace(/([A-Z])/g, ' $1')
.replace(/^./, str => str.toUpperCase());
}
// Check if there are active filters
hasActiveFilters(): boolean {
return (this.baseFilters && this.baseFilters.length > 0) ||
(this.drilldownFilters && this.drilldownFilters.length > 0) ||
this.hasActiveLayerFilters();
}
// Check if there are active layer filters for current drilldown level
hasActiveLayerFilters(): boolean {
if (this.currentDrilldownLevel > 1 && this.drilldownLayers && this.drilldownLayers.length > 0) {
const layerIndex = this.currentDrilldownLevel - 2; // -2 because level 1 is base drilldown
return layerIndex < this.drilldownLayers.length &&
this.drilldownLayers[layerIndex].filters &&
this.drilldownLayers[layerIndex].filters.length > 0;
}
return false;
}
// Get active layer filters for current drilldown level
getActiveLayerFilters(): any[] {
if (this.currentDrilldownLevel > 1 && this.drilldownLayers && this.drilldownLayers.length > 0) {
const layerIndex = this.currentDrilldownLevel - 2; // -2 because level 1 is base drilldown
if (layerIndex < this.drilldownLayers.length &&
this.drilldownLayers[layerIndex].filters) {
return this.drilldownLayers[layerIndex].filters;
}
}
return [];
}
// Get filter options for dropdown/multiselect filters
getFilterOptions(filter: any): string[] {
if (filter.options) {
if (Array.isArray(filter.options)) {
return filter.options;
} else if (typeof filter.options === 'string') {
return filter.options.split(',').map(opt => opt.trim()).filter(opt => opt);
}
}
return [];
}
// Check if an option is selected for multiselect filters
isOptionSelected(filter: any, option: string): boolean {
if (!filter.value) {
return false;
}
if (Array.isArray(filter.value)) {
return filter.value.includes(option);
}
return filter.value === option;
}
// Handle base filter changes
onBaseFilterChange(filter: any): void {
console.log('Base filter changed:', filter);
// Refresh data when filter changes
this.fetchGridData();
}
// Handle drilldown filter changes
onDrilldownFilterChange(filter: any): void {
console.log('Drilldown filter changed:', filter);
// Refresh data when filter changes
this.fetchGridData();
}
// Handle layer filter changes
onLayerFilterChange(filter: any): void {
console.log('Layer filter changed:', filter);
// Refresh data when filter changes
this.fetchGridData();
}
// Handle multiselect changes
onMultiSelectChange(filter: any, option: string, event: any): void {
const checked = event.target.checked;
// Initialize filter.value as array if it's not already
if (!Array.isArray(filter.value)) {
filter.value = [];
}
if (checked) {
// Add option to array if not already present
if (!filter.value.includes(option)) {
filter.value.push(option);
}
} else {
// Remove option from array
filter.value = filter.value.filter((item: string) => item !== option);
}
// Refresh data when filter changes
this.fetchGridData();
}
// Handle date range changes
onDateRangeChange(filter: any, dateRange: { start: string | null, end: string | null }): void {
filter.value = dateRange;
// Refresh data when filter changes
this.fetchGridData();
}
// Handle toggle changes
onToggleChange(filter: any, checked: boolean): void {
filter.value = checked;
// Refresh data when filter changes
this.fetchGridData();
}
// Toggle multiselect dropdown visibility
toggleMultiselect(filter: any, context: string): void {
const filterId = `${context}-${filter.field}`;
if (this.isMultiselectOpen(filter, context)) {
this.openMultiselects.delete(filterId);
} else {
// Close all other multiselects first
this.openMultiselects.clear();
this.openMultiselects.set(filterId, context);
// Add document click handler to close dropdown when clicking outside
this.addDocumentClickHandler();
}
}
// Add document click handler to close dropdowns when clicking outside
private addDocumentClickHandler(): void {
if (!this.documentClickHandler) {
this.documentClickHandler = (event: MouseEvent) => {
const target = event.target as HTMLElement;
// Check if click is outside any multiselect dropdown
if (!target.closest('.multiselect-container')) {
this.openMultiselects.clear();
this.removeDocumentClickHandler();
}
};
// Use setTimeout to ensure the click event that opened the dropdown doesn't immediately close it
setTimeout(() => {
document.addEventListener('click', this.documentClickHandler!);
}, 0);
}
}
// Remove document click handler
private removeDocumentClickHandler(): void {
if (this.documentClickHandler) {
document.removeEventListener('click', this.documentClickHandler);
this.documentClickHandler = null;
}
}
// Check if multiselect dropdown is open
isMultiselectOpen(filter: any, context: string): boolean {
const filterId = `${context}-${filter.field}`;
return this.openMultiselects.has(filterId);
}
// Get count of selected options for a multiselect filter
getSelectedOptionsCount(filter: any): number {
if (!filter.value) {
return 0;
}
if (Array.isArray(filter.value)) {
return filter.value.length;
}
return 0;
}
// Clear all filters
clearAllFilters(): void {
// Clear base filters
if (this.baseFilters) {
this.baseFilters.forEach(filter => {
if (filter.type === 'multiselect') {
filter.value = [];
} else if (filter.type === 'date-range') {
filter.value = { start: null, end: null };
} else if (filter.type === 'toggle') {
filter.value = false;
} else {
filter.value = '';
}
});
}
// Clear drilldown filters
if (this.drilldownFilters) {
this.drilldownFilters.forEach(filter => {
if (filter.type === 'multiselect') {
filter.value = [];
} else if (filter.type === 'date-range') {
filter.value = { start: null, end: null };
} else if (filter.type === 'toggle') {
filter.value = false;
} else {
filter.value = '';
}
});
}
// Clear layer filters
if (this.drilldownLayers) {
this.drilldownLayers.forEach(layer => {
if (layer.filters) {
layer.filters.forEach((filter: any) => {
if (filter.type === 'multiselect') {
filter.value = [];
} else if (filter.type === 'date-range') {
filter.value = { start: null, end: null };
} else if (filter.type === 'toggle') {
filter.value = false;
} else {
filter.value = '';
}
});
}
});
}
// Close all multiselect dropdowns
this.openMultiselects.clear();
// Refresh data
this.fetchGridData();
}
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 = [];
// Clear multiselect tracking
this.openMultiselects.clear();
// Remove document click handler
this.removeDocumentClickHandler();
console.log('GridViewComponent destroyed and cleaned up');
}
}

View File

@@ -1,14 +1,283 @@
<div style="display: block">
<!-- Drilldown mode indicator -->
<div *ngIf="currentDrilldownLevel > 0" style="background-color: #e0e0e0; padding: 5px; margin-bottom: 10px; border-radius: 4px; text-align: center;">
<span style="font-weight: bold; color: #333;">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;">
Back to Level {{currentDrilldownLevel - 1}}
</button>
<button (click)="resetToOriginalData()" style="margin-left: 10px; padding: 2px 8px; background-color: #dc3545; color: white; border: none; border-radius: 3px; cursor: pointer;">
Back to Main View
<div style="display: block; height: 100%; width: 100%;">
<!-- Filter Controls Section -->
<div class="filter-section" *ngIf="hasActiveFilters()">
<!-- Base Filters -->
<div class="filter-group" *ngIf="baseFilters && baseFilters.length > 0">
<h4>Base Filters</h4>
<div class="filter-controls">
<div *ngFor="let filter of baseFilters" class="filter-item">
<div class="filter-label">{{ filter.field }} ({{ filter.type || 'text' }})</div>
<!-- Text Filter -->
<div *ngIf="!filter.type || filter.type === 'text'" class="filter-input">
<input type="text"
[(ngModel)]="filter.value"
(ngModelChange)="onBaseFilterChange(filter)"
[placeholder]="filter.field"
class="clr-input filter-text-input">
</div>
<!-- Dropdown Filter -->
<div *ngIf="filter.type === 'dropdown'" class="filter-input">
<select [(ngModel)]="filter.value"
(ngModelChange)="onBaseFilterChange(filter)"
class="clr-select filter-select">
<option value="">Select {{ filter.field }}</option>
<option *ngFor="let option of getFilterOptions(filter)" [value]="option">{{ option }}</option>
</select>
</div>
<!-- Multi-Select Filter - Updated to show key first, then dropdown on click -->
<div *ngIf="filter.type === 'multiselect'" class="filter-input multiselect-container">
<div class="multiselect-display" (click)="toggleMultiselect(filter, 'base')">
<span class="multiselect-label">{{ filter.field }}</span>
<span class="multiselect-value" *ngIf="getSelectedOptionsCount(filter) > 0">
({{ getSelectedOptionsCount(filter) }} selected)
</span>
<clr-icon shape="caret down" class="dropdown-icon"></clr-icon>
</div>
<div class="multiselect-dropdown" *ngIf="isMultiselectOpen(filter, 'base')">
<div class="checkbox-group">
<div *ngFor="let option of getFilterOptions(filter); let i = index" class="checkbox-item">
<input type="checkbox"
[checked]="isOptionSelected(filter, option)"
(change)="onMultiSelectChange(filter, option, $event)"
[id]="'base-' + filter.field + '-' + i"
class="clr-checkbox">
<label [for]="'base-' + filter.field + '-' + i" class="checkbox-label">{{ option }}</label>
</div>
</div>
</div>
</div>
<!-- Date Range Filter -->
<div *ngIf="filter.type === 'date-range'" class="filter-input date-range">
<div class="date-input-group">
<input type="date"
[(ngModel)]="filter.value.start"
(ngModelChange)="onDateRangeChange(filter, { start: $event, end: filter.value.end })"
placeholder="Start Date"
class="clr-input filter-date">
<span class="date-separator">to</span>
<input type="date"
[(ngModel)]="filter.value.end"
(ngModelChange)="onDateRangeChange(filter, { start: filter.value.start, end: $event })"
placeholder="End Date"
class="clr-input filter-date">
</div>
</div>
<!-- Toggle Filter -->
<div *ngIf="filter.type === 'toggle'" class="filter-input toggle">
<input type="checkbox"
[(ngModel)]="filter.value"
(ngModelChange)="onToggleChange(filter, $event)"
clrToggle
class="clr-toggle">
<label class="toggle-label">{{ filter.field }}</label>
</div>
</div>
</div>
</div>
<!-- Drilldown Filters -->
<div class="filter-group" *ngIf="drilldownFilters && drilldownFilters.length > 0 && currentDrilldownLevel > 0">
<h4>Drilldown Filters</h4>
<div class="filter-controls">
<div *ngFor="let filter of drilldownFilters" class="filter-item">
<div class="filter-label">{{ filter.field }} ({{ filter.type || 'text' }})</div>
<!-- Text Filter -->
<div *ngIf="!filter.type || filter.type === 'text'" class="filter-input">
<input type="text"
[(ngModel)]="filter.value"
(ngModelChange)="onDrilldownFilterChange(filter)"
[placeholder]="filter.field"
class="clr-input filter-text-input">
</div>
<!-- Dropdown Filter -->
<div *ngIf="filter.type === 'dropdown'" class="filter-input">
<select [(ngModel)]="filter.value"
(ngModelChange)="onDrilldownFilterChange(filter)"
class="clr-select filter-select">
<option value="">Select {{ filter.field }}</option>
<option *ngFor="let option of getFilterOptions(filter)" [value]="option">{{ option }}</option>
</select>
</div>
<!-- Multi-Select Filter - Updated to show key first, then dropdown on click -->
<div *ngIf="filter.type === 'multiselect'" class="filter-input multiselect-container">
<div class="multiselect-display" (click)="toggleMultiselect(filter, 'drilldown')">
<span class="multiselect-label">{{ filter.field }}</span>
<span class="multiselect-value" *ngIf="getSelectedOptionsCount(filter) > 0">
({{ getSelectedOptionsCount(filter) }} selected)
</span>
<clr-icon shape="caret down" class="dropdown-icon"></clr-icon>
</div>
<div class="multiselect-dropdown" *ngIf="isMultiselectOpen(filter, 'drilldown')">
<div class="checkbox-group">
<div *ngFor="let option of getFilterOptions(filter); let i = index" class="checkbox-item">
<input type="checkbox"
[checked]="isOptionSelected(filter, option)"
(change)="onMultiSelectChange(filter, option, $event)"
[id]="'drilldown-' + filter.field + '-' + i"
class="clr-checkbox">
<label [for]="'drilldown-' + filter.field + '-' + i" class="checkbox-label">{{ option }}</label>
</div>
</div>
</div>
</div>
<!-- Date Range Filter -->
<div *ngIf="filter.type === 'date-range'" class="filter-input date-range">
<div class="date-input-group">
<input type="date"
[(ngModel)]="filter.value.start"
(ngModelChange)="onDateRangeChange(filter, { start: $event, end: filter.value.end })"
placeholder="Start Date"
class="clr-input filter-date">
<span class="date-separator">to</span>
<input type="date"
[(ngModel)]="filter.value.end"
(ngModelChange)="onDateRangeChange(filter, { start: filter.value.start, end: $event })"
placeholder="End Date"
class="clr-input filter-date">
</div>
</div>
<!-- Toggle Filter -->
<div *ngIf="filter.type === 'toggle'" class="filter-input toggle">
<input type="checkbox"
[(ngModel)]="filter.value"
(ngModelChange)="onToggleChange(filter, $event)"
clrToggle
class="clr-toggle">
<label class="toggle-label">{{ filter.field }}</label>
</div>
</div>
</div>
</div>
<!-- Layer Filters -->
<div class="filter-group" *ngIf="hasActiveLayerFilters()">
<h4>Layer Filters</h4>
<div class="filter-controls">
<div *ngFor="let filter of getActiveLayerFilters()" class="filter-item">
<div class="filter-label">{{ filter.field }} ({{ filter.type || 'text' }})</div>
<!-- Text Filter -->
<div *ngIf="!filter.type || filter.type === 'text'" class="filter-input">
<input type="text"
[(ngModel)]="filter.value"
(ngModelChange)="onLayerFilterChange(filter)"
[placeholder]="filter.field"
class="clr-input filter-text-input">
</div>
<!-- Dropdown Filter -->
<div *ngIf="filter.type === 'dropdown'" class="filter-input">
<select [(ngModel)]="filter.value"
(ngModelChange)="onLayerFilterChange(filter)"
class="clr-select filter-select">
<option value="">Select {{ filter.field }}</option>
<option *ngFor="let option of getFilterOptions(filter)" [value]="option">{{ option }}</option>
</select>
</div>
<!-- Multi-Select Filter - Updated to show key first, then dropdown on click -->
<div *ngIf="filter.type === 'multiselect'" class="filter-input multiselect-container">
<div class="multiselect-display" (click)="toggleMultiselect(filter, 'layer')">
<span class="multiselect-label">{{ filter.field }}</span>
<span class="multiselect-value" *ngIf="getSelectedOptionsCount(filter) > 0">
({{ getSelectedOptionsCount(filter) }} selected)
</span>
<clr-icon shape="caret down" class="dropdown-icon"></clr-icon>
</div>
<div class="multiselect-dropdown" *ngIf="isMultiselectOpen(filter, 'layer')">
<div class="checkbox-group">
<div *ngFor="let option of getFilterOptions(filter); let i = index" class="checkbox-item">
<input type="checkbox"
[checked]="isOptionSelected(filter, option)"
(change)="onMultiSelectChange(filter, option, $event)"
[id]="'layer-' + filter.field + '-' + i"
class="clr-checkbox">
<label [for]="'layer-' + filter.field + '-' + i" class="checkbox-label">{{ option }}</label>
</div>
</div>
</div>
</div>
<!-- Date Range Filter -->
<div *ngIf="filter.type === 'date-range'" class="filter-input date-range">
<div class="date-input-group">
<input type="date"
[(ngModel)]="filter.value.start"
(ngModelChange)="onDateRangeChange(filter, { start: $event, end: filter.value.end })"
placeholder="Start Date"
class="clr-input filter-date">
<span class="date-separator">to</span>
<input type="date"
[(ngModel)]="filter.value.end"
(ngModelChange)="onDateRangeChange(filter, { start: filter.value.start, end: $event })"
placeholder="End Date"
class="clr-input filter-date">
</div>
</div>
<!-- Toggle Filter -->
<div *ngIf="filter.type === 'toggle'" class="filter-input toggle">
<input type="checkbox"
[(ngModel)]="filter.value"
(ngModelChange)="onToggleChange(filter, $event)"
clrToggle
class="clr-toggle">
<label class="toggle-label">{{ filter.field }}</label>
</div>
</div>
</div>
</div>
<!-- Clear Filters Button -->
<div class="filter-actions">
<button class="btn btn-sm btn-outline" (click)="clearAllFilters()">Clear All Filters</button>
</div>
</div>
<!-- Header row with chart title and drilldown navigation -->
<div class="clr-row header-row">
<div class="clr-col-6">
<h3 class="chart-title">{{charttitle || 'Line Chart'}}</h3>
</div>
<div class="clr-col-6" style="text-align: right;">
<!-- Add drilldown navigation controls -->
<button class="btn btn-sm btn-link" *ngIf="drilldownEnabled && drilldownStack.length > 0" (click)="navigateBack()">
<cds-icon shape="arrow" direction="left"></cds-icon>
Back to {{drilldownStack.length > 1 ? 'Previous Level' : 'Main Data'}}
</button>
</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].clickedLabel}})
</span>
</span>
</div>
</div>
</div>
</div>
</div>
<!-- No data message -->
<div *ngIf="noDataAvailable" style="text-align: center; padding: 20px; color: #666; font-style: italic;">
@@ -16,12 +285,11 @@
</div>
<!-- Chart display -->
<div *ngIf="!noDataAvailable">
<div *ngIf="!noDataAvailable" style="position: relative; height: calc(100% - 50px);">
<canvas baseChart
[datasets]="lineChartData"
[labels]="lineChartLabels"
[options]="lineChartOptions"
[legend]="lineChartLegend"
[type]="lineChartType"
(chartHover)="chartHovered($event)"

View File

@@ -0,0 +1,192 @@
.filter-section {
margin-bottom: 20px;
padding: 15px;
border: 1px solid #ddd;
border-radius: 4px;
background-color: #f9f9f9;
}
.filter-group {
margin-bottom: 15px;
h4 {
margin-top: 0;
margin-bottom: 10px;
color: #333;
font-weight: 600;
}
}
.filter-controls {
display: flex;
flex-wrap: wrap;
gap: 15px;
}
.filter-item {
flex: 1 1 300px;
min-width: 250px;
padding: 10px;
background: white;
border: 1px solid #e0e0e0;
border-radius: 4px;
}
.filter-label {
font-weight: 500;
margin-bottom: 8px;
color: #555;
font-size: 14px;
}
.filter-input {
width: 100%;
.filter-text-input,
.filter-select,
.filter-date {
width: 100%;
padding: 6px 12px;
border: 1px solid #ccc;
border-radius: 4px;
font-size: 14px;
}
.filter-select {
height: 34px;
}
}
.multiselect-container {
position: relative;
}
.multiselect-display {
display: flex;
align-items: center;
justify-content: space-between;
padding: 6px 12px;
border: 1px solid #ccc;
border-radius: 4px;
background: white;
cursor: pointer;
min-height: 34px;
.multiselect-label {
flex: 1;
font-size: 14px;
}
.multiselect-value {
color: #666;
font-size: 12px;
margin-right: 8px;
}
.dropdown-icon {
flex-shrink: 0;
transition: transform 0.2s ease;
}
&:hover {
border-color: #999;
}
}
.multiselect-dropdown {
position: absolute;
top: 100%;
left: 0;
right: 0;
z-index: 1000;
background: white;
border: 1px solid #ccc;
border-top: none;
border-radius: 0 0 4px 4px;
box-shadow: 0 2px 4px rgba(0,0,0,0.1);
max-height: 200px;
overflow-y: auto;
.checkbox-group {
padding: 8px;
.checkbox-item {
display: flex;
align-items: center;
gap: 8px;
padding: 4px 0;
.checkbox-label {
margin: 0;
font-size: 14px;
cursor: pointer;
}
}
}
}
.date-range {
.date-input-group {
display: flex;
align-items: center;
gap: 8px;
}
.date-separator {
margin: 0 5px;
color: #777;
}
}
.toggle {
display: flex;
align-items: center;
gap: 8px;
.toggle-label {
margin: 0;
font-size: 14px;
cursor: pointer;
}
}
.filter-actions {
margin-top: 15px;
padding-top: 15px;
border-top: 1px solid #eee;
.btn {
font-size: 13px;
}
}
// New header row styling
.header-row {
margin-bottom: 15px;
padding-bottom: 10px;
border-bottom: 1px solid #eee;
.chart-title {
margin: 0;
font-size: 18px;
font-weight: 600;
color: #333;
}
}
// Responsive design
@media (max-width: 768px) {
.filter-controls {
flex-direction: column;
}
.filter-item {
min-width: 100%;
}
.header-row {
.chart-title {
font-size: 16px;
}
}
}

View File

@@ -1,12 +1,14 @@
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 { FilterService } from '../../common-filter/filter.service';
import { Subscription } from 'rxjs';
@Component({
selector: 'app-line-chart',
templateUrl: './line-chart.component.html',
styleUrls: ['./line-chart.component.scss']
})
export class LineChartComponent implements OnInit, OnChanges {
export class LineChartComponent implements OnInit, OnChanges, OnDestroy {
@Input() xAxis: string;
@Input() yAxis: string | string[];
@Input() table: string;
@@ -83,9 +85,28 @@ export class LineChartComponent implements OnInit, OnChanges {
// Flag to prevent infinite loops
private isFetchingData: boolean = false;
constructor(private dashboardService: Dashboard3Service) { }
// Subscriptions to unsubscribe on destroy
private subscriptions: Subscription[] = [];
// Add properties for filter functionality
private openMultiselects: Map<string, string> = new Map(); // Map of filterId -> context
private documentClickHandler: ((event: MouseEvent) => void) | null = null;
private filtersInitialized: boolean = false;
constructor(
private dashboardService: Dashboard3Service,
private filterService: FilterService
) { }
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
this.fetchChartData();
}
@@ -93,6 +114,12 @@ export class LineChartComponent implements OnInit, OnChanges {
ngOnChanges(changes: SimpleChanges): void {
console.log('LineChartComponent input changes:', changes);
// Initialize filter values if they haven't been initialized yet
if (!this.filtersInitialized && (changes.baseFilters || changes.drilldownFilters || changes.drilldownLayers)) {
this.initializeFilterValues();
this.filtersInitialized = true;
}
// Check if any of the key properties have changed
const xAxisChanged = changes.xAxis && !changes.xAxis.firstChange;
const yAxisChanged = changes.yAxis && !changes.yAxis.firstChange;
@@ -122,6 +149,327 @@ export class LineChartComponent implements OnInit, OnChanges {
}
}
ngOnDestroy(): void {
this.subscriptions.forEach(sub => sub.unsubscribe());
// Clean up document click handler
this.removeDocumentClickHandler();
}
// Initialize filter values with proper default values based on type
private initializeFilterValues(): void {
console.log('Initializing filter values');
// Initialize base filters
if (this.baseFilters) {
this.baseFilters.forEach(filter => {
if (filter.value === undefined || filter.value === null) {
switch (filter.type) {
case 'multiselect':
filter.value = [];
break;
case 'date-range':
filter.value = { start: null, end: null };
break;
case 'toggle':
filter.value = false;
break;
default:
filter.value = '';
}
}
});
}
// Initialize drilldown filters
if (this.drilldownFilters) {
this.drilldownFilters.forEach(filter => {
if (filter.value === undefined || filter.value === null) {
switch (filter.type) {
case 'multiselect':
filter.value = [];
break;
case 'date-range':
filter.value = { start: null, end: null };
break;
case 'toggle':
filter.value = false;
break;
default:
filter.value = '';
}
}
});
}
// Initialize layer filters
if (this.drilldownLayers) {
this.drilldownLayers.forEach(layer => {
if (layer.filters) {
layer.filters.forEach((filter: any) => {
if (filter.value === undefined || filter.value === null) {
switch (filter.type) {
case 'multiselect':
filter.value = [];
break;
case 'date-range':
filter.value = { start: null, end: null };
break;
case 'toggle':
filter.value = false;
break;
default:
filter.value = '';
}
}
});
}
});
}
console.log('Filter values initialized:', {
baseFilters: this.baseFilters,
drilldownFilters: this.drilldownFilters,
drilldownLayers: this.drilldownLayers
});
}
// Check if there are active filters
hasActiveFilters(): boolean {
return (this.baseFilters && this.baseFilters.length > 0) ||
(this.drilldownFilters && this.drilldownFilters.length > 0) ||
this.hasActiveLayerFilters();
}
// Check if there are active layer filters for current drilldown level
hasActiveLayerFilters(): boolean {
if (this.currentDrilldownLevel > 1 && this.drilldownLayers && this.drilldownLayers.length > 0) {
const layerIndex = this.currentDrilldownLevel - 2; // -2 because level 1 is base drilldown
return layerIndex < this.drilldownLayers.length &&
this.drilldownLayers[layerIndex].filters &&
this.drilldownLayers[layerIndex].filters.length > 0;
}
return false;
}
// Get active layer filters for current drilldown level
getActiveLayerFilters(): any[] {
if (this.currentDrilldownLevel > 1 && this.drilldownLayers && this.drilldownLayers.length > 0) {
const layerIndex = this.currentDrilldownLevel - 2; // -2 because level 1 is base drilldown
if (layerIndex < this.drilldownLayers.length &&
this.drilldownLayers[layerIndex].filters) {
return this.drilldownLayers[layerIndex].filters;
}
}
return [];
}
// Get filter options for dropdown/multiselect filters
getFilterOptions(filter: any): string[] {
if (filter.options) {
if (Array.isArray(filter.options)) {
return filter.options;
} else if (typeof filter.options === 'string') {
return filter.options.split(',').map(opt => opt.trim()).filter(opt => opt);
}
}
return [];
}
// Check if an option is selected for multiselect filters
isOptionSelected(filter: any, option: string): boolean {
if (!filter.value) {
return false;
}
if (Array.isArray(filter.value)) {
return filter.value.includes(option);
}
return filter.value === option;
}
// Handle base filter changes
onBaseFilterChange(filter: any): void {
console.log('Base filter changed:', filter);
// Refresh data when filter changes
this.fetchChartData();
}
// Handle drilldown filter changes
onDrilldownFilterChange(filter: any): void {
console.log('Drilldown filter changed:', filter);
// Refresh data when filter changes
this.fetchChartData();
}
// Handle layer filter changes
onLayerFilterChange(filter: any): void {
console.log('Layer filter changed:', filter);
// Refresh data when filter changes
this.fetchChartData();
}
// Handle multiselect changes
onMultiSelectChange(filter: any, option: string, event: any): void {
const checked = event.target.checked;
// Initialize filter.value as array if it's not already
if (!Array.isArray(filter.value)) {
filter.value = [];
}
if (checked) {
// Add option to array if not already present
if (!filter.value.includes(option)) {
filter.value.push(option);
}
} else {
// Remove option from array
filter.value = filter.value.filter((item: string) => item !== option);
}
// Refresh data when filter changes
this.fetchChartData();
}
// Handle date range changes
onDateRangeChange(filter: any, dateRange: { start: string | null, end: string | null }): void {
filter.value = dateRange;
// Refresh data when filter changes
this.fetchChartData();
}
// Handle toggle changes
onToggleChange(filter: any, checked: boolean): void {
filter.value = checked;
// Refresh data when filter changes
this.fetchChartData();
}
// Toggle multiselect dropdown visibility
toggleMultiselect(filter: any, context: string): void {
const filterId = `${context}-${filter.field}`;
if (this.isMultiselectOpen(filter, context)) {
this.openMultiselects.delete(filterId);
} else {
// Close all other multiselects first
this.openMultiselects.clear();
this.openMultiselects.set(filterId, context);
// Add document click handler to close dropdown when clicking outside
this.addDocumentClickHandler();
}
}
// Add document click handler to close dropdowns when clicking outside
private addDocumentClickHandler(): void {
if (!this.documentClickHandler) {
this.documentClickHandler = (event: MouseEvent) => {
const target = event.target as HTMLElement;
// Check if click is outside any multiselect dropdown
if (!target.closest('.multiselect-container')) {
this.openMultiselects.clear();
this.removeDocumentClickHandler();
}
};
// Use setTimeout to ensure the click event that opened the dropdown doesn't immediately close it
setTimeout(() => {
document.addEventListener('click', this.documentClickHandler!);
}, 0);
}
}
// Remove document click handler
private removeDocumentClickHandler(): void {
if (this.documentClickHandler) {
document.removeEventListener('click', this.documentClickHandler);
this.documentClickHandler = null;
}
}
// Check if multiselect dropdown is open
isMultiselectOpen(filter: any, context: string): boolean {
const filterId = `${context}-${filter.field}`;
return this.openMultiselects.has(filterId);
}
// Get count of selected options for a multiselect filter
getSelectedOptionsCount(filter: any): number {
if (!filter.value) {
return 0;
}
if (Array.isArray(filter.value)) {
return filter.value.length;
}
return 0;
}
// Clear all filters
clearAllFilters(): void {
// Clear base filters
if (this.baseFilters) {
this.baseFilters.forEach(filter => {
if (filter.type === 'multiselect') {
filter.value = [];
} else if (filter.type === 'date-range') {
filter.value = { start: null, end: null };
} else if (filter.type === 'toggle') {
filter.value = false;
} else {
filter.value = '';
}
});
}
// Clear drilldown filters
if (this.drilldownFilters) {
this.drilldownFilters.forEach(filter => {
if (filter.type === 'multiselect') {
filter.value = [];
} else if (filter.type === 'date-range') {
filter.value = { start: null, end: null };
} else if (filter.type === 'toggle') {
filter.value = false;
} else {
filter.value = '';
}
});
}
// Clear layer filters
if (this.drilldownLayers) {
this.drilldownLayers.forEach(layer => {
if (layer.filters) {
layer.filters.forEach((filter: any) => {
if (filter.type === 'multiselect') {
filter.value = [];
} else if (filter.type === 'date-range') {
filter.value = { start: null, end: null };
} else if (filter.type === 'toggle') {
filter.value = false;
} else {
filter.value = '';
}
});
}
});
}
// Close all multiselect dropdowns
this.openMultiselects.clear();
// Refresh data
this.fetchChartData();
}
// Public method to refresh data when filters change
refreshData(): void {
this.fetchChartData();
}
fetchChartData(): void {
// Set flag to prevent recursive calls
this.isFetchingData = true;
@@ -154,7 +502,49 @@ export class LineChartComponent implements OnInit, OnChanges {
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
const url = `chart/getdashjson/line?tableName=${this.table}&xAxis=${this.xAxis}&yAxes=${yAxisString}${this.connection ? `&sureId=${this.connection}` : ''}`;
@@ -313,6 +703,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
const url = `chart/getdashjson/line?tableName=${actualApiUrl}&xAxis=${drilldownConfig.xAxis}&yAxes=${drilldownConfig.yAxis}${this.connection ? `&sureId=${this.connection}` : ''}`;
console.log('Drilldown data URL:', url);

View File

@@ -1,19 +1,287 @@
<div class="pie-chart-container">
<!-- Drilldown mode indicator -->
<div *ngIf="currentDrilldownLevel > 0" style="background-color: #e0e0e0; padding: 5px; margin-bottom: 10px; border-radius: 4px; text-align: center;">
<span style="font-weight: bold; color: #333;">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;">
Back to Level {{currentDrilldownLevel - 1}}
</button>
<button (click)="resetToOriginalData()" style="margin-left: 10px; padding: 2px 8px; background-color: #dc3545; color: white; border: none; border-radius: 3px; cursor: pointer;">
Back to Main View
</button>
<!-- Filter Controls Section -->
<div class="filter-section" *ngIf="hasActiveFilters()">
<!-- Base Filters -->
<div class="filter-group" *ngIf="baseFilters && baseFilters.length > 0">
<h4>Base Filters</h4>
<div class="filter-controls">
<div *ngFor="let filter of baseFilters" class="filter-item">
<div class="filter-label">{{ filter.field }} ({{ filter.type || 'text' }})</div>
<!-- Text Filter -->
<div *ngIf="!filter.type || filter.type === 'text'" class="filter-input">
<input type="text"
[(ngModel)]="filter.value"
(ngModelChange)="onBaseFilterChange(filter)"
[placeholder]="filter.field"
class="clr-input filter-text-input">
</div>
<!-- Dropdown Filter -->
<div *ngIf="filter.type === 'dropdown'" class="filter-input">
<select [(ngModel)]="filter.value"
(ngModelChange)="onBaseFilterChange(filter)"
class="clr-select filter-select">
<option value="">Select {{ filter.field }}</option>
<option *ngFor="let option of getFilterOptions(filter)" [value]="option">{{ option }}</option>
</select>
</div>
<!-- Multi-Select Filter - Updated to show key first, then dropdown on click -->
<div *ngIf="filter.type === 'multiselect'" class="filter-input multiselect-container">
<div class="multiselect-display" (click)="toggleMultiselect(filter, 'base')">
<span class="multiselect-label">{{ filter.field }}</span>
<span class="multiselect-value" *ngIf="getSelectedOptionsCount(filter) > 0">
({{ getSelectedOptionsCount(filter) }} selected)
</span>
<clr-icon shape="caret down" class="dropdown-icon"></clr-icon>
</div>
<div class="multiselect-dropdown" *ngIf="isMultiselectOpen(filter, 'base')">
<div class="checkbox-group">
<div *ngFor="let option of getFilterOptions(filter); let i = index" class="checkbox-item">
<input type="checkbox"
[checked]="isOptionSelected(filter, option)"
(change)="onMultiSelectChange(filter, option, $event)"
[id]="'base-' + filter.field + '-' + i"
class="clr-checkbox">
<label [for]="'base-' + filter.field + '-' + i" class="checkbox-label">{{ option }}</label>
</div>
</div>
</div>
</div>
<!-- Date Range Filter -->
<div *ngIf="filter.type === 'date-range'" class="filter-input date-range">
<div class="date-input-group">
<input type="date"
[(ngModel)]="filter.value.start"
(ngModelChange)="onDateRangeChange(filter, { start: $event, end: filter.value.end })"
placeholder="Start Date"
class="clr-input filter-date">
<span class="date-separator">to</span>
<input type="date"
[(ngModel)]="filter.value.end"
(ngModelChange)="onDateRangeChange(filter, { start: filter.value.start, end: $event })"
placeholder="End Date"
class="clr-input filter-date">
</div>
</div>
<!-- Toggle Filter -->
<div *ngIf="filter.type === 'toggle'" class="filter-input toggle">
<input type="checkbox"
[(ngModel)]="filter.value"
(ngModelChange)="onToggleChange(filter, $event)"
clrToggle
class="clr-toggle">
<label class="toggle-label">{{ filter.field }}</label>
</div>
</div>
</div>
</div>
<!-- Drilldown Filters -->
<div class="filter-group" *ngIf="drilldownFilters && drilldownFilters.length > 0 && currentDrilldownLevel > 0">
<h4>Drilldown Filters</h4>
<div class="filter-controls">
<div *ngFor="let filter of drilldownFilters" class="filter-item">
<div class="filter-label">{{ filter.field }} ({{ filter.type || 'text' }})</div>
<!-- Text Filter -->
<div *ngIf="!filter.type || filter.type === 'text'" class="filter-input">
<input type="text"
[(ngModel)]="filter.value"
(ngModelChange)="onDrilldownFilterChange(filter)"
[placeholder]="filter.field"
class="clr-input filter-text-input">
</div>
<!-- Dropdown Filter -->
<div *ngIf="filter.type === 'dropdown'" class="filter-input">
<select [(ngModel)]="filter.value"
(ngModelChange)="onDrilldownFilterChange(filter)"
class="clr-select filter-select">
<option value="">Select {{ filter.field }}</option>
<option *ngFor="let option of getFilterOptions(filter)" [value]="option">{{ option }}</option>
</select>
</div>
<!-- Multi-Select Filter - Updated to show key first, then dropdown on click -->
<div *ngIf="filter.type === 'multiselect'" class="filter-input multiselect-container">
<div class="multiselect-display" (click)="toggleMultiselect(filter, 'drilldown')">
<span class="multiselect-label">{{ filter.field }}</span>
<span class="multiselect-value" *ngIf="getSelectedOptionsCount(filter) > 0">
({{ getSelectedOptionsCount(filter) }} selected)
</span>
<clr-icon shape="caret down" class="dropdown-icon"></clr-icon>
</div>
<div class="multiselect-dropdown" *ngIf="isMultiselectOpen(filter, 'drilldown')">
<div class="checkbox-group">
<div *ngFor="let option of getFilterOptions(filter); let i = index" class="checkbox-item">
<input type="checkbox"
[checked]="isOptionSelected(filter, option)"
(change)="onMultiSelectChange(filter, option, $event)"
[id]="'drilldown-' + filter.field + '-' + i"
class="clr-checkbox">
<label [for]="'drilldown-' + filter.field + '-' + i" class="checkbox-label">{{ option }}</label>
</div>
</div>
</div>
</div>
<!-- Date Range Filter -->
<div *ngIf="filter.type === 'date-range'" class="filter-input date-range">
<div class="date-input-group">
<input type="date"
[(ngModel)]="filter.value.start"
(ngModelChange)="onDateRangeChange(filter, { start: $event, end: filter.value.end })"
placeholder="Start Date"
class="clr-input filter-date">
<span class="date-separator">to</span>
<input type="date"
[(ngModel)]="filter.value.end"
(ngModelChange)="onDateRangeChange(filter, { start: filter.value.start, end: $event })"
placeholder="End Date"
class="clr-input filter-date">
</div>
</div>
<!-- Toggle Filter -->
<div *ngIf="filter.type === 'toggle'" class="filter-input toggle">
<input type="checkbox"
[(ngModel)]="filter.value"
(ngModelChange)="onToggleChange(filter, $event)"
clrToggle
class="clr-toggle">
<label class="toggle-label">{{ filter.field }}</label>
</div>
</div>
</div>
</div>
<!-- Layer Filters -->
<div class="filter-group" *ngIf="hasActiveLayerFilters()">
<h4>Layer Filters</h4>
<div class="filter-controls">
<div *ngFor="let filter of getActiveLayerFilters()" class="filter-item">
<div class="filter-label">{{ filter.field }} ({{ filter.type || 'text' }})</div>
<!-- Text Filter -->
<div *ngIf="!filter.type || filter.type === 'text'" class="filter-input">
<input type="text"
[(ngModel)]="filter.value"
(ngModelChange)="onLayerFilterChange(filter)"
[placeholder]="filter.field"
class="clr-input filter-text-input">
</div>
<!-- Dropdown Filter -->
<div *ngIf="filter.type === 'dropdown'" class="filter-input">
<select [(ngModel)]="filter.value"
(ngModelChange)="onLayerFilterChange(filter)"
class="clr-select filter-select">
<option value="">Select {{ filter.field }}</option>
<option *ngFor="let option of getFilterOptions(filter)" [value]="option">{{ option }}</option>
</select>
</div>
<!-- Multi-Select Filter - Updated to show key first, then dropdown on click -->
<div *ngIf="filter.type === 'multiselect'" class="filter-input multiselect-container">
<div class="multiselect-display" (click)="toggleMultiselect(filter, 'layer')">
<span class="multiselect-label">{{ filter.field }}</span>
<span class="multiselect-value" *ngIf="getSelectedOptionsCount(filter) > 0">
({{ getSelectedOptionsCount(filter) }} selected)
</span>
<clr-icon shape="caret down" class="dropdown-icon"></clr-icon>
</div>
<div class="multiselect-dropdown" *ngIf="isMultiselectOpen(filter, 'layer')">
<div class="checkbox-group">
<div *ngFor="let option of getFilterOptions(filter); let i = index" class="checkbox-item">
<input type="checkbox"
[checked]="isOptionSelected(filter, option)"
(change)="onMultiSelectChange(filter, option, $event)"
[id]="'layer-' + filter.field + '-' + i"
class="clr-checkbox">
<label [for]="'layer-' + filter.field + '-' + i" class="checkbox-label">{{ option }}</label>
</div>
</div>
</div>
</div>
<!-- Date Range Filter -->
<div *ngIf="filter.type === 'date-range'" class="filter-input date-range">
<div class="date-input-group">
<input type="date"
[(ngModel)]="filter.value.start"
(ngModelChange)="onDateRangeChange(filter, { start: $event, end: filter.value.end })"
placeholder="Start Date"
class="clr-input filter-date">
<span class="date-separator">to</span>
<input type="date"
[(ngModel)]="filter.value.end"
(ngModelChange)="onDateRangeChange(filter, { start: filter.value.start, end: $event })"
placeholder="End Date"
class="clr-input filter-date">
</div>
</div>
<!-- Toggle Filter -->
<div *ngIf="filter.type === 'toggle'" class="filter-input toggle">
<input type="checkbox"
[(ngModel)]="filter.value"
(ngModelChange)="onToggleChange(filter, $event)"
clrToggle
class="clr-toggle">
<label class="toggle-label">{{ filter.field }}</label>
</div>
</div>
</div>
</div>
<!-- Clear Filters Button -->
<div class="filter-actions">
<button class="btn btn-sm btn-outline" (click)="clearAllFilters()">Clear All Filters</button>
</div>
</div>
<!-- Header row with chart title and drilldown navigation -->
<div class="clr-row header-row">
<div class="clr-col-6">
<h3 class="chart-title" *ngIf="charttitle">{{ charttitle }}</h3>
</div>
<div class="clr-col-6" style="text-align: right;">
<!-- Add drilldown navigation controls -->
<button class="btn btn-sm btn-link" *ngIf="drilldownEnabled && drilldownStack.length > 0" (click)="navigateBack()">
<cds-icon shape="arrow" direction="left"></cds-icon>
Back to {{drilldownStack.length > 1 ? 'Previous Level' : 'Main Data'}}
</button>
</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].clickedLabel}})
</span>
</span>
</div>
</div>
</div>
</div>
</div>
<div class="chart-wrapper">
<!-- Show loading indicator -->
<div class="loading-indicator" *ngIf="pieChartLabels.length === 0 && pieChartData.length === 0 && !noDataAvailable">
<div class="loading-indicator" *ngIf="pieChartLabels.length === 0 && pieChartData.length === 0 && !noDataAvailable && !isFetchingData">
<div class="spinner"></div>
<p>Loading chart data...</p>
</div>
@@ -23,15 +291,15 @@
<p>No chart data available</p>
</div>
<!-- Show chart when data is available -->
<!-- Show chart when data is available or show default data -->
<canvas baseChart
*ngIf="pieChartLabels.length > 0 && pieChartData.length > 0"
[data]="pieChartData"
[datasets]="pieChartDatasets"
[labels]="pieChartLabels"
[type]="pieChartType"
[options]="pieChartOptions"
(chartHover)="chartHovered($event)"
(chartClick)="chartClicked($event)">
(chartClick)="chartClicked($event)"
[style.display]="shouldShowChart() ? 'block' : 'none'">
</canvas>
</div>
<div class="chart-legend" *ngIf="showlabel && pieChartLabels && pieChartLabels.length > 0">

View File

@@ -149,10 +149,192 @@
100% { transform: rotate(360deg); }
}
/* Responsive design */
// Filter section styles
.filter-section {
margin-bottom: 20px;
padding: 15px;
border: 1px solid #ddd;
border-radius: 4px;
background-color: #f9f9f9;
}
.filter-group {
margin-bottom: 15px;
h4 {
margin-top: 0;
margin-bottom: 10px;
color: #333;
font-weight: 600;
}
}
.filter-controls {
display: flex;
flex-wrap: wrap;
gap: 15px;
}
.filter-item {
flex: 1 1 300px;
min-width: 250px;
padding: 10px;
background: white;
border: 1px solid #e0e0e0;
border-radius: 4px;
}
.filter-label {
font-weight: 500;
margin-bottom: 8px;
color: #555;
font-size: 14px;
}
.filter-input {
width: 100%;
.filter-text-input,
.filter-select,
.filter-date {
width: 100%;
padding: 6px 12px;
border: 1px solid #ccc;
border-radius: 4px;
font-size: 14px;
}
.filter-select {
height: 34px;
}
}
.multiselect-container {
position: relative;
}
.multiselect-display {
display: flex;
align-items: center;
justify-content: space-between;
padding: 6px 12px;
border: 1px solid #ccc;
border-radius: 4px;
background: white;
cursor: pointer;
min-height: 34px;
.multiselect-label {
flex: 1;
font-size: 14px;
}
.multiselect-value {
color: #666;
font-size: 12px;
margin-right: 8px;
}
.dropdown-icon {
flex-shrink: 0;
transition: transform 0.2s ease;
}
&:hover {
border-color: #999;
}
}
.multiselect-dropdown {
position: absolute;
top: 100%;
left: 0;
right: 0;
z-index: 1000;
background: white;
border: 1px solid #ccc;
border-top: none;
border-radius: 0 0 4px 4px;
box-shadow: 0 2px 4px rgba(0,0,0,0.1);
max-height: 200px;
overflow-y: auto;
.checkbox-group {
padding: 8px;
.checkbox-item {
display: flex;
align-items: center;
gap: 8px;
padding: 4px 0;
.checkbox-label {
margin: 0;
font-size: 14px;
cursor: pointer;
}
}
}
}
.date-range {
.date-input-group {
display: flex;
align-items: center;
gap: 8px;
}
.date-separator {
margin: 0 5px;
color: #777;
}
}
.toggle {
display: flex;
align-items: center;
gap: 8px;
.toggle-label {
margin: 0;
font-size: 14px;
cursor: pointer;
}
}
.filter-actions {
margin-top: 15px;
padding-top: 15px;
border-top: 1px solid #eee;
.btn {
font-size: 13px;
}
}
// New header row styling
.header-row {
margin-bottom: 15px;
padding-bottom: 10px;
border-bottom: 1px solid #eee;
.chart-title {
margin: 0;
font-size: 18px;
font-weight: 600;
color: #333;
text-align: left;
padding-bottom: 0;
border-bottom: none;
}
}
// Responsive design
@media (max-width: 768px) {
.pie-chart-container {
padding: 15px;
height: auto;
min-height: 300px;
}
.chart-title {
@@ -179,4 +361,18 @@
font-size: 16px;
padding: 20px;
}
.filter-controls {
flex-direction: column;
}
.filter-item {
min-width: 100%;
}
.header-row {
.chart-title {
font-size: 16px;
}
}
}

View File

@@ -1,12 +1,14 @@
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 { FilterService } from '../../common-filter/filter.service';
import { Subscription } from 'rxjs';
@Component({
selector: 'app-pie-chart',
templateUrl: './pie-chart.component.html',
styleUrls: ['./pie-chart.component.scss']
})
export class PieChartComponent implements OnInit, OnChanges, AfterViewChecked {
export class PieChartComponent implements OnInit, OnChanges, AfterViewChecked, OnDestroy {
@Input() xAxis: string;
@Input() yAxis: string | string[];
@Input() table: string;
@@ -35,6 +37,12 @@ export class PieChartComponent implements OnInit, OnChanges, AfterViewChecked {
public pieChartLabels: string[] = ['Category A', 'Category B', 'Category C'];
public pieChartData: number[] = [30, 50, 20];
public pieChartDatasets: any[] = [
{
data: [30, 50, 20],
label: 'Dataset 1'
}
];
public pieChartType: string = 'pie';
public pieChartOptions: any = {
responsive: true,
@@ -94,9 +102,20 @@ export class PieChartComponent implements OnInit, OnChanges, AfterViewChecked {
noDataAvailable: boolean = false;
// Flag to prevent infinite loops
private isFetchingData: boolean = false;
isFetchingData: boolean = false;
constructor(private dashboardService: Dashboard3Service) { }
// Subscriptions to unsubscribe on destroy
private subscriptions: Subscription[] = [];
// Add properties for filter functionality
private openMultiselects: Map<string, string> = new Map(); // Map of filterId -> context
private documentClickHandler: ((event: MouseEvent) => void) | null = null;
private filtersInitialized: boolean = false;
constructor(
private dashboardService: Dashboard3Service,
private filterService: FilterService
) { }
/**
* Force chart redraw
@@ -108,15 +127,39 @@ export class PieChartComponent implements OnInit, OnChanges, AfterViewChecked {
}
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 });
// Validate initial data
this.validateChartData();
// Initialize datasets with default data
this.pieChartDatasets = [
{
data: this.pieChartData,
label: 'Dataset 1'
}
];
// Only fetch data if we have the required inputs, otherwise show default data
if (this.table && this.xAxis && this.yAxis) {
this.fetchChartData();
}
}
ngOnChanges(changes: SimpleChanges): void {
console.log('PieChartComponent input changes:', changes);
// Initialize filter values if they haven't been initialized yet
if (!this.filtersInitialized && (changes.baseFilters || changes.drilldownFilters || changes.drilldownLayers)) {
this.initializeFilterValues();
this.filtersInitialized = true;
}
// Check if any of the key properties have changed
const xAxisChanged = changes.xAxis && !changes.xAxis.firstChange;
const yAxisChanged = changes.yAxis && !changes.yAxis.firstChange;
@@ -138,6 +181,333 @@ export class PieChartComponent implements OnInit, OnChanges, AfterViewChecked {
console.log('Chart configuration changed, fetching new data');
this.fetchChartData();
}
// If we have the required inputs and haven't fetched data yet, fetch it
if ((xAxisChanged || yAxisChanged || tableChanged) && this.table && this.xAxis && this.yAxis && !this.isFetchingData) {
console.log('Required inputs available, fetching data');
this.fetchChartData();
}
}
ngOnDestroy(): void {
this.subscriptions.forEach(sub => sub.unsubscribe());
// Clean up document click handler
this.removeDocumentClickHandler();
}
// Initialize filter values with proper default values based on type
private initializeFilterValues(): void {
console.log('Initializing filter values');
// Initialize base filters
if (this.baseFilters) {
this.baseFilters.forEach(filter => {
if (filter.value === undefined || filter.value === null) {
switch (filter.type) {
case 'multiselect':
filter.value = [];
break;
case 'date-range':
filter.value = { start: null, end: null };
break;
case 'toggle':
filter.value = false;
break;
default:
filter.value = '';
}
}
});
}
// Initialize drilldown filters
if (this.drilldownFilters) {
this.drilldownFilters.forEach(filter => {
if (filter.value === undefined || filter.value === null) {
switch (filter.type) {
case 'multiselect':
filter.value = [];
break;
case 'date-range':
filter.value = { start: null, end: null };
break;
case 'toggle':
filter.value = false;
break;
default:
filter.value = '';
}
}
});
}
// Initialize layer filters
if (this.drilldownLayers) {
this.drilldownLayers.forEach(layer => {
if (layer.filters) {
layer.filters.forEach((filter: any) => {
if (filter.value === undefined || filter.value === null) {
switch (filter.type) {
case 'multiselect':
filter.value = [];
break;
case 'date-range':
filter.value = { start: null, end: null };
break;
case 'toggle':
filter.value = false;
break;
default:
filter.value = '';
}
}
});
}
});
}
console.log('Filter values initialized:', {
baseFilters: this.baseFilters,
drilldownFilters: this.drilldownFilters,
drilldownLayers: this.drilldownLayers
});
}
// Check if there are active filters
hasActiveFilters(): boolean {
return (this.baseFilters && this.baseFilters.length > 0) ||
(this.drilldownFilters && this.drilldownFilters.length > 0) ||
this.hasActiveLayerFilters();
}
// Check if there are active layer filters for current drilldown level
hasActiveLayerFilters(): boolean {
if (this.currentDrilldownLevel > 1 && this.drilldownLayers && this.drilldownLayers.length > 0) {
const layerIndex = this.currentDrilldownLevel - 2; // -2 because level 1 is base drilldown
return layerIndex < this.drilldownLayers.length &&
this.drilldownLayers[layerIndex].filters &&
this.drilldownLayers[layerIndex].filters.length > 0;
}
return false;
}
// Get active layer filters for current drilldown level
getActiveLayerFilters(): any[] {
if (this.currentDrilldownLevel > 1 && this.drilldownLayers && this.drilldownLayers.length > 0) {
const layerIndex = this.currentDrilldownLevel - 2; // -2 because level 1 is base drilldown
if (layerIndex < this.drilldownLayers.length &&
this.drilldownLayers[layerIndex].filters) {
return this.drilldownLayers[layerIndex].filters;
}
}
return [];
}
// Get filter options for dropdown/multiselect filters
getFilterOptions(filter: any): string[] {
if (filter.options) {
if (Array.isArray(filter.options)) {
return filter.options;
} else if (typeof filter.options === 'string') {
return filter.options.split(',').map(opt => opt.trim()).filter(opt => opt);
}
}
return [];
}
// Check if an option is selected for multiselect filters
isOptionSelected(filter: any, option: string): boolean {
if (!filter.value) {
return false;
}
if (Array.isArray(filter.value)) {
return filter.value.includes(option);
}
return filter.value === option;
}
// Handle base filter changes
onBaseFilterChange(filter: any): void {
console.log('Base filter changed:', filter);
// Refresh data when filter changes
this.fetchChartData();
}
// Handle drilldown filter changes
onDrilldownFilterChange(filter: any): void {
console.log('Drilldown filter changed:', filter);
// Refresh data when filter changes
this.fetchChartData();
}
// Handle layer filter changes
onLayerFilterChange(filter: any): void {
console.log('Layer filter changed:', filter);
// Refresh data when filter changes
this.fetchChartData();
}
// Handle multiselect changes
onMultiSelectChange(filter: any, option: string, event: any): void {
const checked = event.target.checked;
// Initialize filter.value as array if it's not already
if (!Array.isArray(filter.value)) {
filter.value = [];
}
if (checked) {
// Add option to array if not already present
if (!filter.value.includes(option)) {
filter.value.push(option);
}
} else {
// Remove option from array
filter.value = filter.value.filter((item: string) => item !== option);
}
// Refresh data when filter changes
this.fetchChartData();
}
// Handle date range changes
onDateRangeChange(filter: any, dateRange: { start: string | null, end: string | null }): void {
filter.value = dateRange;
// Refresh data when filter changes
this.fetchChartData();
}
// Handle toggle changes
onToggleChange(filter: any, checked: boolean): void {
filter.value = checked;
// Refresh data when filter changes
this.fetchChartData();
}
// Toggle multiselect dropdown visibility
toggleMultiselect(filter: any, context: string): void {
const filterId = `${context}-${filter.field}`;
if (this.isMultiselectOpen(filter, context)) {
this.openMultiselects.delete(filterId);
} else {
// Close all other multiselects first
this.openMultiselects.clear();
this.openMultiselects.set(filterId, context);
// Add document click handler to close dropdown when clicking outside
this.addDocumentClickHandler();
}
}
// Add document click handler to close dropdowns when clicking outside
private addDocumentClickHandler(): void {
if (!this.documentClickHandler) {
this.documentClickHandler = (event: MouseEvent) => {
const target = event.target as HTMLElement;
// Check if click is outside any multiselect dropdown
if (!target.closest('.multiselect-container')) {
this.openMultiselects.clear();
this.removeDocumentClickHandler();
}
};
// Use setTimeout to ensure the click event that opened the dropdown doesn't immediately close it
setTimeout(() => {
document.addEventListener('click', this.documentClickHandler!);
}, 0);
}
}
// Remove document click handler
private removeDocumentClickHandler(): void {
if (this.documentClickHandler) {
document.removeEventListener('click', this.documentClickHandler);
this.documentClickHandler = null;
}
}
// Check if multiselect dropdown is open
isMultiselectOpen(filter: any, context: string): boolean {
const filterId = `${context}-${filter.field}`;
return this.openMultiselects.has(filterId);
}
// Get count of selected options for a multiselect filter
getSelectedOptionsCount(filter: any): number {
if (!filter.value) {
return 0;
}
if (Array.isArray(filter.value)) {
return filter.value.length;
}
return 0;
}
// Clear all filters
clearAllFilters(): void {
// Clear base filters
if (this.baseFilters) {
this.baseFilters.forEach(filter => {
if (filter.type === 'multiselect') {
filter.value = [];
} else if (filter.type === 'date-range') {
filter.value = { start: null, end: null };
} else if (filter.type === 'toggle') {
filter.value = false;
} else {
filter.value = '';
}
});
}
// Clear drilldown filters
if (this.drilldownFilters) {
this.drilldownFilters.forEach(filter => {
if (filter.type === 'multiselect') {
filter.value = [];
} else if (filter.type === 'date-range') {
filter.value = { start: null, end: null };
} else if (filter.type === 'toggle') {
filter.value = false;
} else {
filter.value = '';
}
});
}
// Clear layer filters
if (this.drilldownLayers) {
this.drilldownLayers.forEach(layer => {
if (layer.filters) {
layer.filters.forEach((filter: any) => {
if (filter.type === 'multiselect') {
filter.value = [];
} else if (filter.type === 'date-range') {
filter.value = { start: null, end: null };
} else if (filter.type === 'toggle') {
filter.value = false;
} else {
filter.value = '';
}
});
}
});
}
// Close all multiselect dropdowns
this.openMultiselects.clear();
// Refresh data
this.fetchChartData();
}
// Public method to refresh data when filters change
refreshData(): void {
this.fetchChartData();
}
fetchChartData(): void {
@@ -172,11 +542,53 @@ export class PieChartComponent implements OnInit, OnChanges, AfterViewChecked {
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
const url = `chart/getdashjson/pie?tableName=${this.table}&xAxis=${this.xAxis}&yAxes=${yAxisString}${this.connection ? `&sureId=${this.connection}` : ''}`;
console.log('Pie chart data URL:', url);
console.log('Chart data URL:', url);
// 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
@@ -184,12 +596,8 @@ export class PieChartComponent implements OnInit, OnChanges, AfterViewChecked {
(data: any) => {
console.log('Received pie chart data:', data);
if (data === null) {
console.warn('Pie chart API returned null data. Check if the API endpoint is working correctly.');
console.warn('API returned null data. Check if the API endpoint is working correctly.');
this.noDataAvailable = true;
this.pieChartLabels = [];
this.pieChartData = [];
// Validate and sanitize data to show default data
this.validateChartData();
// Reset flag after fetching
this.isFetchingData = false;
return;
@@ -197,50 +605,57 @@ export class PieChartComponent implements OnInit, OnChanges, AfterViewChecked {
// Handle the actual data structure returned by the API
if (data && data.chartLabels && data.chartData) {
// For pie charts, we need to extract the data differently
// The first dataset's data array contains the values for the pie chart
// Backend has already filtered the data, just display it
this.noDataAvailable = data.chartLabels.length === 0;
this.pieChartLabels = data.chartLabels || [];
if (data.chartData && data.chartData.length > 0) {
this.pieChartData = data.chartData[0].data.map(value => {
// Convert to number if it's not already
const numValue = Number(value);
return isNaN(numValue) ? 0 : numValue;
});
this.pieChartLabels = data.chartLabels;
// Extract the actual data values from the chartData array
// chartData is an array with one object containing the data
if (data.chartData.length > 0 && data.chartData[0].data) {
this.pieChartData = data.chartData[0].data;
} else {
this.pieChartData = [];
}
// Ensure labels and data arrays have the same length
this.syncLabelAndDataArrays();
// Validate and sanitize data
this.validateChartData();
// Trigger change detection
this.pieChartLabels = [...this.pieChartLabels];
this.pieChartData = [...this.pieChartData];
this.pieChartDatasets = [
{
data: this.pieChartData,
label: 'Dataset 1'
}
];
console.log('Updated pie chart with data:', { labels: this.pieChartLabels, data: this.pieChartData });
} else if (data && data.labels && data.data) {
// Handle the original expected format as fallback
} else if (data && data.labels && data.datasets) {
// Backend has already filtered the data, just display it
this.noDataAvailable = data.labels.length === 0;
this.pieChartLabels = data.labels || [];
this.pieChartData = data.data.map(value => {
// Convert to number if it's not already
const numValue = Number(value);
return isNaN(numValue) ? 0 : numValue;
});
// Ensure labels and data arrays have the same length
this.syncLabelAndDataArrays();
// Validate and sanitize data
this.validateChartData();
this.pieChartLabels = data.labels;
this.pieChartData = data.datasets[0]?.data || [];
// Trigger change detection
this.pieChartLabels = [...this.pieChartLabels];
this.pieChartData = [...this.pieChartData];
this.pieChartDatasets = [
{
data: this.pieChartData,
label: 'Dataset 1'
}
];
console.log('Updated pie chart with legacy data format:', { labels: this.pieChartLabels, data: this.pieChartData });
} else {
console.warn('Pie chart received data does not have expected structure', data);
// Reset to default data
console.warn('Received data does not have expected structure', data);
this.noDataAvailable = true;
this.pieChartLabels = [];
this.pieChartData = [];
// Validate and sanitize data to show default data
this.validateChartData();
// Keep default data if no data is available
if (this.pieChartLabels.length === 0 && this.pieChartData.length === 0) {
this.pieChartLabels = ['Category A', 'Category B', 'Category C'];
this.pieChartData = [30, 50, 20];
this.pieChartDatasets = [
{
data: this.pieChartData,
label: 'Dataset 1'
}
];
}
}
// Reset flag after fetching
this.isFetchingData = false;
@@ -248,23 +663,13 @@ export class PieChartComponent implements OnInit, OnChanges, AfterViewChecked {
(error) => {
console.error('Error fetching pie chart data:', error);
this.noDataAvailable = true;
this.pieChartLabels = [];
this.pieChartData = [];
// Validate and sanitize data to show default data
this.validateChartData();
// Reset flag after fetching
this.isFetchingData = false;
}
);
} else {
console.log('Missing required data for pie chart, showing default data:', { table: this.table, xAxis: this.xAxis, yAxis: this.yAxis, connection: this.connection });
// Don't set noDataAvailable to true when there's no required data
// This allows static data to be displayed
this.noDataAvailable = false;
// Validate the chart data to ensure we have some data to display
this.validateChartData();
// Force a redraw to ensure the chart displays
this.pieChartData = [...this.pieChartData];
console.log('Missing required data for chart:', { table: this.table, xAxis: this.xAxis, yAxis: this.yAxis, connection: this.connection });
this.noDataAvailable = true;
// Reset flag after fetching
this.isFetchingData = false;
}
@@ -293,8 +698,6 @@ export class PieChartComponent implements OnInit, OnChanges, AfterViewChecked {
} else {
console.warn('Invalid drilldown layer index:', layerIndex);
this.noDataAvailable = true;
this.pieChartLabels = [];
this.pieChartData = [];
return;
}
}
@@ -305,8 +708,6 @@ export class PieChartComponent implements OnInit, OnChanges, AfterViewChecked {
if (!drilldownConfig || !drilldownConfig.apiUrl || !drilldownConfig.xAxis || !drilldownConfig.yAxis) {
console.warn('Missing drilldown configuration for level:', this.currentDrilldownLevel);
this.noDataAvailable = true;
this.pieChartLabels = [];
this.pieChartData = [];
return;
}
@@ -362,6 +763,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
const url = `chart/getdashjson/pie?tableName=${actualApiUrl}&xAxis=${drilldownConfig.xAxis}&yAxes=${drilldownConfig.yAxis}${this.connection ? `&sureId=${this.connection}` : ''}`;
console.log('Drilldown data URL:', url);
@@ -374,64 +804,57 @@ export class PieChartComponent implements OnInit, OnChanges, AfterViewChecked {
if (data === null) {
console.warn('Drilldown API returned null data. Check if the API endpoint is working correctly.');
this.noDataAvailable = true;
this.pieChartLabels = [];
this.pieChartData = [];
return;
}
// Handle the actual data structure returned by the API
if (data && data.chartLabels && data.chartData) {
// For pie charts, we need to extract the data differently
// The first dataset's data array contains the values for the pie chart
// Backend has already filtered the data, just display it
this.noDataAvailable = data.chartLabels.length === 0;
this.pieChartLabels = data.chartLabels || [];
if (data.chartData && data.chartData.length > 0) {
this.pieChartData = data.chartData[0].data.map(value => {
// Convert to number if it's not already
const numValue = Number(value);
return isNaN(numValue) ? 0 : numValue;
});
this.pieChartLabels = data.chartLabels;
// Extract the actual data values from the chartData array
// chartData is an array with one object containing the data
if (data.chartData.length > 0 && data.chartData[0].data) {
this.pieChartData = data.chartData[0].data;
} else {
this.pieChartData = [];
}
// Ensure labels and data arrays have the same length
this.syncLabelAndDataArrays();
// Validate and sanitize data
this.validateChartData();
// Trigger change detection
this.pieChartLabels = [...this.pieChartLabels];
this.pieChartData = [...this.pieChartData];
this.pieChartDatasets = [
{
data: this.pieChartData,
label: 'Dataset 1'
}
];
console.log('Updated pie chart with drilldown data:', { labels: this.pieChartLabels, data: this.pieChartData });
} else if (data && data.labels && data.data) {
// Handle the original expected format as fallback
} else if (data && data.labels && data.datasets) {
// Backend has already filtered the data, just display it
this.noDataAvailable = data.labels.length === 0;
this.pieChartLabels = data.labels || [];
this.pieChartData = data.data.map(value => {
// Convert to number if it's not already
const numValue = Number(value);
return isNaN(numValue) ? 0 : numValue;
});
// Ensure labels and data arrays have the same length
this.syncLabelAndDataArrays();
// Validate and sanitize data
this.validateChartData();
this.pieChartLabels = data.labels;
this.pieChartData = data.datasets[0]?.data || [];
// Trigger change detection
this.pieChartLabels = [...this.pieChartLabels];
this.pieChartData = [...this.pieChartData];
this.pieChartDatasets = [
{
data: this.pieChartData,
label: 'Dataset 1'
}
];
console.log('Updated pie chart with drilldown legacy data format:', { labels: this.pieChartLabels, data: this.pieChartData });
} else {
console.warn('Drilldown received data does not have expected structure', data);
this.noDataAvailable = true;
this.pieChartLabels = [];
this.pieChartData = [];
// Validate and sanitize data
this.validateChartData();
// Keep current data if no data is available
}
},
(error) => {
console.error('Error fetching drilldown data:', error);
this.noDataAvailable = true;
this.pieChartLabels = [];
this.pieChartData = [];
// Keep current data in case of error
}
);
}
@@ -492,82 +915,52 @@ export class PieChartComponent implements OnInit, OnChanges, AfterViewChecked {
}
}
/**
* Get color for legend item
* @param index Index of the legend item
*/
public getLegendColor(index: number): string {
// Validate chart data to ensure labels and data arrays have the same length
private validateChartData(): void {
if (this.pieChartLabels && this.pieChartData) {
// For pie charts, we need to ensure labels and data arrays have the same length
const labelCount = this.pieChartLabels.length;
const dataCount = this.pieChartData.length;
if (labelCount !== dataCount) {
console.warn('Pie chart labels and data arrays have different lengths:', { labels: labelCount, data: dataCount });
// Pad or truncate data array to match label count
if (dataCount < labelCount) {
// Pad with zeros
while (this.pieChartData.length < labelCount) {
this.pieChartData.push(0);
}
} else if (dataCount > labelCount) {
// Truncate data array
this.pieChartData = this.pieChartData.slice(0, labelCount);
}
}
}
}
// Get legend color for a specific index
getLegendColor(index: number): string {
return this.chartColors[index % this.chartColors.length];
}
/**
* Ensure labels and data arrays have the same length
*/
private syncLabelAndDataArrays(): void {
// Ensure we have matching arrays
if (this.pieChartLabels.length !== this.pieChartData.length) {
const maxLength = Math.max(this.pieChartLabels.length, this.pieChartData.length);
while (this.pieChartLabels.length < maxLength) {
this.pieChartLabels.push(`Label ${this.pieChartLabels.length + 1}`);
}
while (this.pieChartData.length < maxLength) {
this.pieChartData.push(0);
}
}
// Method to determine if chart should be displayed
shouldShowChart(): boolean {
// Show chart if we have data
if (this.pieChartLabels.length > 0 && this.pieChartData.length > 0) {
return true;
}
/**
* Validate and sanitize chart data
*/
private validateChartData(): void {
console.log('Validating chart data:', { labels: this.pieChartLabels, data: this.pieChartData });
// Ensure we have valid arrays
if (!Array.isArray(this.pieChartLabels)) {
this.pieChartLabels = [];
// Show chart if we're still fetching data
if (this.isFetchingData) {
return true;
}
if (!Array.isArray(this.pieChartData)) {
this.pieChartData = [];
// Show chart if we have default data
if (this.pieChartLabels.length > 0 && this.originalPieChartLabels.length > 0) {
return true;
}
// Ensure we have some data to display
if (this.pieChartLabels.length === 0 && this.pieChartData.length === 0) {
// Add default data to ensure chart visibility
this.pieChartLabels = ['Category A', 'Category B', 'Category C'];
this.pieChartData = [30, 50, 20];
console.log('Added default data for chart display');
}
// Ensure labels and data arrays have the same length
this.syncLabelAndDataArrays();
// Ensure all data values are numbers
this.pieChartData = this.pieChartData.map(value => {
const numValue = Number(value);
return isNaN(numValue) ? 0 : numValue;
});
console.log('After validation:', { labels: this.pieChartLabels, data: this.pieChartData });
}
ngAfterViewChecked() {
// Debugging: Log component state after view checks
console.log('PieChartComponent state:', {
labels: this.pieChartLabels,
data: this.pieChartData,
hasData: this.pieChartLabels.length > 0 && this.pieChartData.length > 0
});
}
/**
* Check if chart data is valid and ready to display
*/
public isChartDataValid(): boolean {
return this.pieChartLabels && this.pieChartData &&
Array.isArray(this.pieChartLabels) && Array.isArray(this.pieChartData) &&
this.pieChartLabels.length > 0 && this.pieChartData.length > 0 &&
this.pieChartLabels.length === this.pieChartData.length;
return false;
}
// events
@@ -656,6 +1049,10 @@ export class PieChartComponent implements OnInit, OnChanges, AfterViewChecked {
}
public chartHovered(e: any): void {
console.log(e);
console.log('Pie chart hovered:', e);
}
ngAfterViewChecked(): void {
// This lifecycle hook can be used if needed for post-render operations
}
}

View File

@@ -1,10 +1,292 @@
<div style="display: block; height: 100%; width: 100%;">
<!-- Filter Controls Section -->
<div class="filter-section" *ngIf="hasActiveFilters()">
<!-- Base Filters -->
<div class="filter-group" *ngIf="baseFilters && baseFilters.length > 0">
<h4>Base Filters</h4>
<div class="filter-controls">
<div *ngFor="let filter of baseFilters" class="filter-item">
<div class="filter-label">{{ filter.field }} ({{ filter.type || 'text' }})</div>
<div style="display: block">
<!-- Text Filter -->
<div *ngIf="!filter.type || filter.type === 'text'" class="filter-input">
<input type="text"
[(ngModel)]="filter.value"
(ngModelChange)="onBaseFilterChange(filter)"
[placeholder]="filter.field"
class="clr-input filter-text-input">
</div>
<!-- Dropdown Filter -->
<div *ngIf="filter.type === 'dropdown'" class="filter-input">
<select [(ngModel)]="filter.value"
(ngModelChange)="onBaseFilterChange(filter)"
class="clr-select filter-select">
<option value="">Select {{ filter.field }}</option>
<option *ngFor="let option of getFilterOptions(filter)" [value]="option">{{ option }}</option>
</select>
</div>
<!-- Multi-Select Filter - Updated to show key first, then dropdown on click -->
<div *ngIf="filter.type === 'multiselect'" class="filter-input multiselect-container">
<div class="multiselect-display" (click)="toggleMultiselect(filter, 'base')">
<span class="multiselect-label">{{ filter.field }}</span>
<span class="multiselect-value" *ngIf="getSelectedOptionsCount(filter) > 0">
({{ getSelectedOptionsCount(filter) }} selected)
</span>
<clr-icon shape="caret down" class="dropdown-icon"></clr-icon>
</div>
<div class="multiselect-dropdown" *ngIf="isMultiselectOpen(filter, 'base')">
<div class="checkbox-group">
<div *ngFor="let option of getFilterOptions(filter); let i = index" class="checkbox-item">
<input type="checkbox"
[checked]="isOptionSelected(filter, option)"
(change)="onMultiSelectChange(filter, option, $event)"
[id]="'base-' + filter.field + '-' + i"
class="clr-checkbox">
<label [for]="'base-' + filter.field + '-' + i" class="checkbox-label">{{ option }}</label>
</div>
</div>
</div>
</div>
<!-- Date Range Filter -->
<div *ngIf="filter.type === 'date-range'" class="filter-input date-range">
<div class="date-input-group">
<input type="date"
[(ngModel)]="filter.value.start"
(ngModelChange)="onDateRangeChange(filter, { start: $event, end: filter.value.end })"
placeholder="Start Date"
class="clr-input filter-date">
<span class="date-separator">to</span>
<input type="date"
[(ngModel)]="filter.value.end"
(ngModelChange)="onDateRangeChange(filter, { start: filter.value.start, end: $event })"
placeholder="End Date"
class="clr-input filter-date">
</div>
</div>
<!-- Toggle Filter -->
<div *ngIf="filter.type === 'toggle'" class="filter-input toggle">
<input type="checkbox"
[(ngModel)]="filter.value"
(ngModelChange)="onToggleChange(filter, $event)"
clrToggle
class="clr-toggle">
<label class="toggle-label">{{ filter.field }}</label>
</div>
</div>
</div>
</div>
<!-- Drilldown Filters -->
<div class="filter-group" *ngIf="drilldownFilters && drilldownFilters.length > 0 && currentDrilldownLevel > 0">
<h4>Drilldown Filters</h4>
<div class="filter-controls">
<div *ngFor="let filter of drilldownFilters" class="filter-item">
<div class="filter-label">{{ filter.field }} ({{ filter.type || 'text' }})</div>
<!-- Text Filter -->
<div *ngIf="!filter.type || filter.type === 'text'" class="filter-input">
<input type="text"
[(ngModel)]="filter.value"
(ngModelChange)="onDrilldownFilterChange(filter)"
[placeholder]="filter.field"
class="clr-input filter-text-input">
</div>
<!-- Dropdown Filter -->
<div *ngIf="filter.type === 'dropdown'" class="filter-input">
<select [(ngModel)]="filter.value"
(ngModelChange)="onDrilldownFilterChange(filter)"
class="clr-select filter-select">
<option value="">Select {{ filter.field }}</option>
<option *ngFor="let option of getFilterOptions(filter)" [value]="option">{{ option }}</option>
</select>
</div>
<!-- Multi-Select Filter - Updated to show key first, then dropdown on click -->
<div *ngIf="filter.type === 'multiselect'" class="filter-input multiselect-container">
<div class="multiselect-display" (click)="toggleMultiselect(filter, 'drilldown')">
<span class="multiselect-label">{{ filter.field }}</span>
<span class="multiselect-value" *ngIf="getSelectedOptionsCount(filter) > 0">
({{ getSelectedOptionsCount(filter) }} selected)
</span>
<clr-icon shape="caret down" class="dropdown-icon"></clr-icon>
</div>
<div class="multiselect-dropdown" *ngIf="isMultiselectOpen(filter, 'drilldown')">
<div class="checkbox-group">
<div *ngFor="let option of getFilterOptions(filter); let i = index" class="checkbox-item">
<input type="checkbox"
[checked]="isOptionSelected(filter, option)"
(change)="onMultiSelectChange(filter, option, $event)"
[id]="'drilldown-' + filter.field + '-' + i"
class="clr-checkbox">
<label [for]="'drilldown-' + filter.field + '-' + i" class="checkbox-label">{{ option }}</label>
</div>
</div>
</div>
</div>
<!-- Date Range Filter -->
<div *ngIf="filter.type === 'date-range'" class="filter-input date-range">
<div class="date-input-group">
<input type="date"
[(ngModel)]="filter.value.start"
(ngModelChange)="onDateRangeChange(filter, { start: $event, end: filter.value.end })"
placeholder="Start Date"
class="clr-input filter-date">
<span class="date-separator">to</span>
<input type="date"
[(ngModel)]="filter.value.end"
(ngModelChange)="onDateRangeChange(filter, { start: filter.value.start, end: $event })"
placeholder="End Date"
class="clr-input filter-date">
</div>
</div>
<!-- Toggle Filter -->
<div *ngIf="filter.type === 'toggle'" class="filter-input toggle">
<input type="checkbox"
[(ngModel)]="filter.value"
(ngModelChange)="onToggleChange(filter, $event)"
clrToggle
class="clr-toggle">
<label class="toggle-label">{{ filter.field }}</label>
</div>
</div>
</div>
</div>
<!-- Layer Filters -->
<div class="filter-group" *ngIf="hasActiveLayerFilters()">
<h4>Layer Filters</h4>
<div class="filter-controls">
<div *ngFor="let filter of getActiveLayerFilters()" class="filter-item">
<div class="filter-label">{{ filter.field }} ({{ filter.type || 'text' }})</div>
<!-- Text Filter -->
<div *ngIf="!filter.type || filter.type === 'text'" class="filter-input">
<input type="text"
[(ngModel)]="filter.value"
(ngModelChange)="onLayerFilterChange(filter)"
[placeholder]="filter.field"
class="clr-input filter-text-input">
</div>
<!-- Dropdown Filter -->
<div *ngIf="filter.type === 'dropdown'" class="filter-input">
<select [(ngModel)]="filter.value"
(ngModelChange)="onLayerFilterChange(filter)"
class="clr-select filter-select">
<option value="">Select {{ filter.field }}</option>
<option *ngFor="let option of getFilterOptions(filter)" [value]="option">{{ option }}</option>
</select>
</div>
<!-- Multi-Select Filter - Updated to show key first, then dropdown on click -->
<div *ngIf="filter.type === 'multiselect'" class="filter-input multiselect-container">
<div class="multiselect-display" (click)="toggleMultiselect(filter, 'layer')">
<span class="multiselect-label">{{ filter.field }}</span>
<span class="multiselect-value" *ngIf="getSelectedOptionsCount(filter) > 0">
({{ getSelectedOptionsCount(filter) }} selected)
</span>
<clr-icon shape="caret down" class="dropdown-icon"></clr-icon>
</div>
<div class="multiselect-dropdown" *ngIf="isMultiselectOpen(filter, 'layer')">
<div class="checkbox-group">
<div *ngFor="let option of getFilterOptions(filter); let i = index" class="checkbox-item">
<input type="checkbox"
[checked]="isOptionSelected(filter, option)"
(change)="onMultiSelectChange(filter, option, $event)"
[id]="'layer-' + filter.field + '-' + i"
class="clr-checkbox">
<label [for]="'layer-' + filter.field + '-' + i" class="checkbox-label">{{ option }}</label>
</div>
</div>
</div>
</div>
<!-- Date Range Filter -->
<div *ngIf="filter.type === 'date-range'" class="filter-input date-range">
<div class="date-input-group">
<input type="date"
[(ngModel)]="filter.value.start"
(ngModelChange)="onDateRangeChange(filter, { start: $event, end: filter.value.end })"
placeholder="Start Date"
class="clr-input filter-date">
<span class="date-separator">to</span>
<input type="date"
[(ngModel)]="filter.value.end"
(ngModelChange)="onDateRangeChange(filter, { start: filter.value.start, end: $event })"
placeholder="End Date"
class="clr-input filter-date">
</div>
</div>
<!-- Toggle Filter -->
<div *ngIf="filter.type === 'toggle'" class="filter-input toggle">
<input type="checkbox"
[(ngModel)]="filter.value"
(ngModelChange)="onToggleChange(filter, $event)"
clrToggle
class="clr-toggle">
<label class="toggle-label">{{ filter.field }}</label>
</div>
</div>
</div>
</div>
<!-- Clear Filters Button -->
<div class="filter-actions">
<button class="btn btn-sm btn-outline" (click)="clearAllFilters()">Clear All Filters</button>
</div>
</div>
<!-- Header row with chart title and drilldown navigation -->
<div class="clr-row header-row">
<div class="clr-col-6">
<h3 class="chart-title">{{charttitle || 'Polar Chart'}}</h3>
</div>
<div class="clr-col-6" style="text-align: right;">
<!-- Add drilldown navigation controls -->
<button class="btn btn-sm btn-link" *ngIf="drilldownEnabled && drilldownStack.length > 0" (click)="navigateBack()">
<cds-icon shape="arrow" direction="left"></cds-icon>
Back to {{drilldownStack.length > 1 ? 'Previous Level' : 'Main Data'}}
</button>
</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].clickedLabel}})
</span>
</span>
</div>
</div>
</div>
</div>
</div>
<div style="position: relative; height: calc(100% - 80px); padding: 0 10px 30px 10px;">
<canvas baseChart
[datasets]="polarAreaChartData"
[labels]="polarAreaChartLabels"
[options]="polarAreaChartOptions"
[type]="polarAreaChartType"
(chartHover)="chartHovered($event)"
(chartClick)="chartClicked($event)">
</canvas>
</div>
</div>

View File

@@ -1,18 +1,192 @@
// Polar Chart Component Styles
div[style*="display: block"] {
.filter-section {
margin-bottom: 20px;
padding: 15px;
border: 1px solid #ddd;
border-radius: 4px;
background-color: #f9f9f9;
}
.filter-group {
margin-bottom: 15px;
h4 {
margin-top: 0;
margin-bottom: 10px;
color: #333;
font-weight: 600;
}
}
.filter-controls {
display: flex;
flex-wrap: wrap;
gap: 15px;
}
.filter-item {
flex: 1 1 300px;
min-width: 250px;
padding: 10px;
background: white;
border: 1px solid #e0e0e0;
border-radius: 4px;
}
.filter-label {
font-weight: 500;
margin-bottom: 8px;
color: #555;
font-size: 14px;
}
.filter-input {
width: 100%;
.filter-text-input,
.filter-select,
.filter-date {
width: 100%;
padding: 6px 12px;
border: 1px solid #ccc;
border-radius: 4px;
font-size: 14px;
}
.filter-select {
height: 34px;
}
}
.multiselect-container {
position: relative;
width: 100%;
height: 100%;
}
canvas {
max-width: 100%;
max-height: 100%;
.multiselect-display {
display: flex;
align-items: center;
justify-content: space-between;
padding: 6px 12px;
border: 1px solid #ccc;
border-radius: 4px;
background: white;
cursor: pointer;
min-height: 34px;
.multiselect-label {
flex: 1;
font-size: 14px;
}
// Ensure the chart container has proper sizing
:host {
display: block;
width: 100%;
height: 100%;
.multiselect-value {
color: #666;
font-size: 12px;
margin-right: 8px;
}
.dropdown-icon {
flex-shrink: 0;
transition: transform 0.2s ease;
}
&:hover {
border-color: #999;
}
}
.multiselect-dropdown {
position: absolute;
top: 100%;
left: 0;
right: 0;
z-index: 1000;
background: white;
border: 1px solid #ccc;
border-top: none;
border-radius: 0 0 4px 4px;
box-shadow: 0 2px 4px rgba(0,0,0,0.1);
max-height: 200px;
overflow-y: auto;
.checkbox-group {
padding: 8px;
.checkbox-item {
display: flex;
align-items: center;
gap: 8px;
padding: 4px 0;
.checkbox-label {
margin: 0;
font-size: 14px;
cursor: pointer;
}
}
}
}
.date-range {
.date-input-group {
display: flex;
align-items: center;
gap: 8px;
}
.date-separator {
margin: 0 5px;
color: #777;
}
}
.toggle {
display: flex;
align-items: center;
gap: 8px;
.toggle-label {
margin: 0;
font-size: 14px;
cursor: pointer;
}
}
.filter-actions {
margin-top: 15px;
padding-top: 15px;
border-top: 1px solid #eee;
.btn {
font-size: 13px;
}
}
// New header row styling
.header-row {
margin-bottom: 15px;
padding-bottom: 10px;
border-bottom: 1px solid #eee;
.chart-title {
margin: 0;
font-size: 18px;
font-weight: 600;
color: #333;
}
}
// Responsive design
@media (max-width: 768px) {
.filter-controls {
flex-direction: column;
}
.filter-item {
min-width: 100%;
}
.header-row {
.chart-title {
font-size: 16px;
}
}
}

View File

@@ -1,5 +1,7 @@
import { Component, OnInit, Input, OnChanges, SimpleChanges } from '@angular/core';
import { Dashboard3Service } from 'src/app/services/builder/dashboard3.service';
import { FilterService } from '../../common-filter/filter.service';
import { Subscription } from 'rxjs';
@Component({
selector: 'app-polar-chart',
@@ -33,9 +35,20 @@ export class PolarChartComponent implements OnInit, OnChanges {
// Multi-layer drilldown configuration inputs
@Input() drilldownLayers: any[] = []; // Array of drilldown layer configurations
constructor(private dashboardService: Dashboard3Service) { }
constructor(
private dashboardService: Dashboard3Service,
private filterService: FilterService
) { }
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
this.fetchChartData();
}
@@ -43,6 +56,12 @@ export class PolarChartComponent implements OnInit, OnChanges {
ngOnChanges(changes: SimpleChanges): void {
console.log('PolarChartComponent input changes:', changes);
// Initialize filter values if they haven't been initialized yet
if (!this.filtersInitialized && (changes.baseFilters || changes.drilldownFilters || changes.drilldownLayers)) {
this.initializeFilterValues();
this.filtersInitialized = true;
}
// Check if any of the key properties have changed
const xAxisChanged = changes.xAxis && !changes.xAxis.firstChange;
const yAxisChanged = changes.yAxis && !changes.yAxis.firstChange;
@@ -71,6 +90,32 @@ export class PolarChartComponent implements OnInit, OnChanges {
{ data: [ 300, 500, 100, 40, 120 ], label: 'Series 1'}
];
public polarAreaChartOptions: any = {
responsive: true,
maintainAspectRatio: false,
layout: {
padding: {
left: 10,
right: 10,
top: 10,
bottom: 30
}
},
plugins: {
legend: {
display: true,
position: 'top'
}
},
scales: {
r: {
ticks: {
backdropColor: 'rgba(0, 0, 0, 0)'
}
}
}
};
public polarAreaChartType: string = 'polarArea';
// Multi-layer drilldown state tracking
@@ -85,6 +130,324 @@ export class PolarChartComponent implements OnInit, OnChanges {
// Flag to prevent infinite loops
private isFetchingData: boolean = false;
// Subscriptions to unsubscribe on destroy
private subscriptions: Subscription[] = [];
// Add properties for filter functionality
private openMultiselects: Map<string, string> = new Map(); // Map of filterId -> context
private documentClickHandler: ((event: MouseEvent) => void) | null = null;
private filtersInitialized: boolean = false;
// Initialize filter values with proper default values based on type
private initializeFilterValues(): void {
console.log('Initializing filter values');
// Initialize base filters
if (this.baseFilters) {
this.baseFilters.forEach(filter => {
if (filter.value === undefined || filter.value === null) {
switch (filter.type) {
case 'multiselect':
filter.value = [];
break;
case 'date-range':
filter.value = { start: null, end: null };
break;
case 'toggle':
filter.value = false;
break;
default:
filter.value = '';
}
}
});
}
// Initialize drilldown filters
if (this.drilldownFilters) {
this.drilldownFilters.forEach(filter => {
if (filter.value === undefined || filter.value === null) {
switch (filter.type) {
case 'multiselect':
filter.value = [];
break;
case 'date-range':
filter.value = { start: null, end: null };
break;
case 'toggle':
filter.value = false;
break;
default:
filter.value = '';
}
}
});
}
// Initialize layer filters
if (this.drilldownLayers) {
this.drilldownLayers.forEach(layer => {
if (layer.filters) {
layer.filters.forEach((filter: any) => {
if (filter.value === undefined || filter.value === null) {
switch (filter.type) {
case 'multiselect':
filter.value = [];
break;
case 'date-range':
filter.value = { start: null, end: null };
break;
case 'toggle':
filter.value = false;
break;
default:
filter.value = '';
}
}
});
}
});
}
console.log('Filter values initialized:', {
baseFilters: this.baseFilters,
drilldownFilters: this.drilldownFilters,
drilldownLayers: this.drilldownLayers
});
}
// Check if there are active filters
hasActiveFilters(): boolean {
return (this.baseFilters && this.baseFilters.length > 0) ||
(this.drilldownFilters && this.drilldownFilters.length > 0) ||
this.hasActiveLayerFilters();
}
// Check if there are active layer filters for current drilldown level
hasActiveLayerFilters(): boolean {
if (this.currentDrilldownLevel > 1 && this.drilldownLayers && this.drilldownLayers.length > 0) {
const layerIndex = this.currentDrilldownLevel - 2; // -2 because level 1 is base drilldown
return layerIndex < this.drilldownLayers.length &&
this.drilldownLayers[layerIndex].filters &&
this.drilldownLayers[layerIndex].filters.length > 0;
}
return false;
}
// Get active layer filters for current drilldown level
getActiveLayerFilters(): any[] {
if (this.currentDrilldownLevel > 1 && this.drilldownLayers && this.drilldownLayers.length > 0) {
const layerIndex = this.currentDrilldownLevel - 2; // -2 because level 1 is base drilldown
if (layerIndex < this.drilldownLayers.length &&
this.drilldownLayers[layerIndex].filters) {
return this.drilldownLayers[layerIndex].filters;
}
}
return [];
}
// Get filter options for dropdown/multiselect filters
getFilterOptions(filter: any): string[] {
if (filter.options) {
if (Array.isArray(filter.options)) {
return filter.options;
} else if (typeof filter.options === 'string') {
return filter.options.split(',').map(opt => opt.trim()).filter(opt => opt);
}
}
return [];
}
// Check if an option is selected for multiselect filters
isOptionSelected(filter: any, option: string): boolean {
if (!filter.value) {
return false;
}
if (Array.isArray(filter.value)) {
return filter.value.includes(option);
}
return filter.value === option;
}
// Handle base filter changes
onBaseFilterChange(filter: any): void {
console.log('Base filter changed:', filter);
// Refresh data when filter changes
this.fetchChartData();
}
// Handle drilldown filter changes
onDrilldownFilterChange(filter: any): void {
console.log('Drilldown filter changed:', filter);
// Refresh data when filter changes
this.fetchChartData();
}
// Handle layer filter changes
onLayerFilterChange(filter: any): void {
console.log('Layer filter changed:', filter);
// Refresh data when filter changes
this.fetchChartData();
}
// Handle multiselect changes
onMultiSelectChange(filter: any, option: string, event: any): void {
const checked = event.target.checked;
// Initialize filter.value as array if it's not already
if (!Array.isArray(filter.value)) {
filter.value = [];
}
if (checked) {
// Add option to array if not already present
if (!filter.value.includes(option)) {
filter.value.push(option);
}
} else {
// Remove option from array
filter.value = filter.value.filter((item: string) => item !== option);
}
// Refresh data when filter changes
this.fetchChartData();
}
// Handle date range changes
onDateRangeChange(filter: any, dateRange: { start: string | null, end: string | null }): void {
filter.value = dateRange;
// Refresh data when filter changes
this.fetchChartData();
}
// Handle toggle changes
onToggleChange(filter: any, checked: boolean): void {
filter.value = checked;
// Refresh data when filter changes
this.fetchChartData();
}
// Toggle multiselect dropdown visibility
toggleMultiselect(filter: any, context: string): void {
const filterId = `${context}-${filter.field}`;
if (this.isMultiselectOpen(filter, context)) {
this.openMultiselects.delete(filterId);
} else {
// Close all other multiselects first
this.openMultiselects.clear();
this.openMultiselects.set(filterId, context);
// Add document click handler to close dropdown when clicking outside
this.addDocumentClickHandler();
}
}
// Add document click handler to close dropdowns when clicking outside
private addDocumentClickHandler(): void {
if (!this.documentClickHandler) {
this.documentClickHandler = (event: MouseEvent) => {
const target = event.target as HTMLElement;
// Check if click is outside any multiselect dropdown
if (!target.closest('.multiselect-container')) {
this.openMultiselects.clear();
this.removeDocumentClickHandler();
}
};
// Use setTimeout to ensure the click event that opened the dropdown doesn't immediately close it
setTimeout(() => {
document.addEventListener('click', this.documentClickHandler!);
}, 0);
}
}
// Remove document click handler
private removeDocumentClickHandler(): void {
if (this.documentClickHandler) {
document.removeEventListener('click', this.documentClickHandler);
this.documentClickHandler = null;
}
}
// Check if multiselect dropdown is open
isMultiselectOpen(filter: any, context: string): boolean {
const filterId = `${context}-${filter.field}`;
return this.openMultiselects.has(filterId);
}
// Get count of selected options for a multiselect filter
getSelectedOptionsCount(filter: any): number {
if (!filter.value) {
return 0;
}
if (Array.isArray(filter.value)) {
return filter.value.length;
}
return 0;
}
// Clear all filters
clearAllFilters(): void {
// Clear base filters
if (this.baseFilters) {
this.baseFilters.forEach(filter => {
if (filter.type === 'multiselect') {
filter.value = [];
} else if (filter.type === 'date-range') {
filter.value = { start: null, end: null };
} else if (filter.type === 'toggle') {
filter.value = false;
} else {
filter.value = '';
}
});
}
// Clear drilldown filters
if (this.drilldownFilters) {
this.drilldownFilters.forEach(filter => {
if (filter.type === 'multiselect') {
filter.value = [];
} else if (filter.type === 'date-range') {
filter.value = { start: null, end: null };
} else if (filter.type === 'toggle') {
filter.value = false;
} else {
filter.value = '';
}
});
}
// Clear layer filters
if (this.drilldownLayers) {
this.drilldownLayers.forEach(layer => {
if (layer.filters) {
layer.filters.forEach((filter: any) => {
if (filter.type === 'multiselect') {
filter.value = [];
} else if (filter.type === 'date-range') {
filter.value = { start: null, end: null };
} else if (filter.type === 'toggle') {
filter.value = false;
} else {
filter.value = '';
}
});
}
});
}
// Close all multiselect dropdowns
this.openMultiselects.clear();
// Refresh data
this.fetchChartData();
}
fetchChartData(): void {
// Set flag to prevent recursive calls
this.isFetchingData = true;
@@ -117,7 +480,49 @@ export class PolarChartComponent implements OnInit, OnChanges {
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
const url = `chart/getdashjson/polar?tableName=${this.table}&xAxis=${this.xAxis}&yAxes=${yAxisString}${this.connection ? `&sureId=${this.connection}` : ''}`;
@@ -132,7 +537,12 @@ export class PolarChartComponent implements OnInit, OnChanges {
console.warn('Polar chart API returned null data. Check if the API endpoint is working correctly.');
this.noDataAvailable = true;
this.polarAreaChartLabels = [];
this.polarAreaChartData = [];
this.polarAreaChartData = [
{
data: [],
label: 'Dataset 1'
}
];
// Reset flag after fetching
this.isFetchingData = false;
return;
@@ -145,32 +555,54 @@ export class PolarChartComponent implements OnInit, OnChanges {
this.noDataAvailable = data.chartLabels.length === 0;
this.polarAreaChartLabels = data.chartLabels;
if (data.chartData && data.chartData.length > 0) {
this.polarAreaChartData = data.chartData[0].data.map(value => {
// Convert the data to the expected format for polar area charts
const chartValues = data.chartData[0].data.map(value => {
// Convert to number if it's not already
return isNaN(Number(value)) ? 0 : Number(value);
});
} else {
this.polarAreaChartData = [];
// Assign data in the correct format (array of objects with data property)
this.polarAreaChartData = [
{
data: chartValues,
label: data.chartData[0].label || 'Dataset 1'
}
];
} else {
this.polarAreaChartData = [
{
data: [],
label: 'Dataset 1'
}
];
}
// Trigger change detection
this.polarAreaChartData = [...this.polarAreaChartData];
console.log('Updated polar chart with data:', { labels: this.polarAreaChartLabels, data: this.polarAreaChartData });
} else if (data && data.labels && data.data) {
// Handle the original expected format as fallback
this.noDataAvailable = data.labels.length === 0;
this.polarAreaChartLabels = data.labels;
this.polarAreaChartData = data.data.map(value => {
// Convert the data to the expected format for polar area charts
const chartValues = data.data.map(value => {
// Convert to number if it's not already
return isNaN(Number(value)) ? 0 : Number(value);
});
// Trigger change detection
this.polarAreaChartData = [...this.polarAreaChartData];
// Assign data in the correct format (array of objects with data property)
this.polarAreaChartData = [
{
data: chartValues,
label: 'Dataset 1'
}
];
console.log('Updated polar chart with legacy data format:', { labels: this.polarAreaChartLabels, data: this.polarAreaChartData });
} else {
console.warn('Polar chart received data does not have expected structure', data);
this.noDataAvailable = true;
this.polarAreaChartLabels = [];
this.polarAreaChartData = [];
this.polarAreaChartData = [
{
data: [],
label: 'Dataset 1'
}
];
}
// Reset flag after fetching
this.isFetchingData = false;
@@ -179,7 +611,12 @@ export class PolarChartComponent implements OnInit, OnChanges {
console.error('Error fetching polar chart data:', error);
this.noDataAvailable = true;
this.polarAreaChartLabels = [];
this.polarAreaChartData = [];
this.polarAreaChartData = [
{
data: [],
label: 'Dataset 1'
}
];
// Reset flag after fetching
this.isFetchingData = false;
// Keep default data in case of error
@@ -189,7 +626,12 @@ export class PolarChartComponent implements OnInit, OnChanges {
console.log('Missing required data for polar chart:', { table: this.table, xAxis: this.xAxis, yAxis: this.yAxis, connection: this.connection });
this.noDataAvailable = true;
this.polarAreaChartLabels = [];
this.polarAreaChartData = [];
this.polarAreaChartData = [
{
data: [],
label: 'Dataset 1'
}
];
// Reset flag after fetching
this.isFetchingData = false;
}
@@ -219,7 +661,12 @@ export class PolarChartComponent implements OnInit, OnChanges {
console.warn('Invalid drilldown layer index:', layerIndex);
this.noDataAvailable = true;
this.polarAreaChartLabels = [];
this.polarAreaChartData = [];
this.polarAreaChartData = [
{
data: [],
label: 'Dataset 1'
}
];
return;
}
}
@@ -231,7 +678,12 @@ export class PolarChartComponent implements OnInit, OnChanges {
console.warn('Missing drilldown configuration for level:', this.currentDrilldownLevel);
this.noDataAvailable = true;
this.polarAreaChartLabels = [];
this.polarAreaChartData = [];
this.polarAreaChartData = [
{
data: [],
label: 'Dataset 1'
}
];
return;
}
@@ -287,6 +739,35 @@ export class PolarChartComponent 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
const url = `chart/getdashjson/polar?tableName=${actualApiUrl}&xAxis=${drilldownConfig.xAxis}&yAxes=${drilldownConfig.yAxis}${this.connection ? `&sureId=${this.connection}` : ''}`;
console.log('Drilldown data URL:', url);
@@ -300,23 +781,40 @@ export class PolarChartComponent implements OnInit, OnChanges {
console.warn('Drilldown API returned null data. Check if the API endpoint is working correctly.');
this.noDataAvailable = true;
this.polarAreaChartLabels = [];
this.polarAreaChartData = [];
this.polarAreaChartData = [
{
data: [],
label: 'Dataset 1'
}
];
return;
}
// Handle the actual data structure returned by the API
if (data && data.chartLabels && data.chartData) {
// For polar charts, we need to extract the data differently
// The first dataset's data array contains the values for the polar chart
this.noDataAvailable = data.chartLabels.length === 0;
this.polarAreaChartLabels = data.chartLabels;
if (data.chartData && data.chartData.length > 0) {
this.polarAreaChartData = data.chartData[0].data.map(value => {
// Convert the data to the expected format for polar area charts
const chartValues = data.chartData[0].data.map(value => {
// Convert to number if it's not already
return isNaN(Number(value)) ? 0 : Number(value);
});
// Assign data in the correct format (array of objects with data property)
this.polarAreaChartData = [
{
data: chartValues,
label: data.chartData[0].label || 'Dataset 1'
}
];
} else {
this.polarAreaChartData = [];
this.polarAreaChartData = [
{
data: [],
label: 'Dataset 1'
}
];
}
// Trigger change detection
this.polarAreaChartData = [...this.polarAreaChartData];
@@ -325,10 +823,18 @@ export class PolarChartComponent implements OnInit, OnChanges {
// Handle the original expected format as fallback
this.noDataAvailable = data.labels.length === 0;
this.polarAreaChartLabels = data.labels;
this.polarAreaChartData = data.data.map(value => {
// Convert the data to the expected format for polar area charts
const chartValues = data.data.map(value => {
// Convert to number if it's not already
return isNaN(Number(value)) ? 0 : Number(value);
});
// Assign data in the correct format (array of objects with data property)
this.polarAreaChartData = [
{
data: chartValues,
label: 'Dataset 1'
}
];
// Trigger change detection
this.polarAreaChartData = [...this.polarAreaChartData];
console.log('Updated polar chart with drilldown legacy data format:', { labels: this.polarAreaChartLabels, data: this.polarAreaChartData });
@@ -336,14 +842,24 @@ export class PolarChartComponent implements OnInit, OnChanges {
console.warn('Drilldown received data does not have expected structure', data);
this.noDataAvailable = true;
this.polarAreaChartLabels = [];
this.polarAreaChartData = [];
this.polarAreaChartData = [
{
data: [],
label: 'Dataset 1'
}
];
}
},
(error) => {
console.error('Error fetching drilldown data:', error);
this.noDataAvailable = true;
this.polarAreaChartLabels = [];
this.polarAreaChartData = [];
this.polarAreaChartData = [
{
data: [],
label: 'Dataset 1'
}
];
// Keep current data in case of error
}
);
@@ -417,13 +933,13 @@ export class PolarChartComponent implements OnInit, OnChanges {
// Get the label of the clicked element
const clickedLabel = this.polarAreaChartLabels[clickedIndex];
console.log('Clicked on polar area:', { index: clickedIndex, label: clickedLabel });
console.log('Clicked on polar point:', { index: clickedIndex, label: clickedLabel });
// If we're not at the base level, store original data
if (this.currentDrilldownLevel === 0) {
// Store original data before entering drilldown mode
this.originalPolarAreaChartLabels = [...this.polarAreaChartLabels];
this.originalPolarAreaChartData = [...this.polarAreaChartData];
this.originalPolarAreaChartData = JSON.parse(JSON.stringify(this.polarAreaChartData));
console.log('Stored original data for drilldown');
}
@@ -491,6 +1007,12 @@ export class PolarChartComponent implements OnInit, OnChanges {
}
public chartHovered(e: any): void {
console.log(e);
console.log('Polar chart hovered:', e);
}
ngOnDestroy(): void {
this.subscriptions.forEach(sub => sub.unsubscribe());
// Clean up document click handler
this.removeDocumentClickHandler();
}
}

View File

@@ -1,14 +1,283 @@
<div style="display: block">
<!-- Drilldown mode indicator -->
<div *ngIf="currentDrilldownLevel > 0" style="background-color: #e0e0e0; padding: 5px; margin-bottom: 10px; border-radius: 4px; text-align: center;">
<span style="font-weight: bold; color: #333;">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;">
Back to Level {{currentDrilldownLevel - 1}}
</button>
<button (click)="resetToOriginalData()" style="margin-left: 10px; padding: 2px 8px; background-color: #dc3545; color: white; border: none; border-radius: 3px; cursor: pointer;">
Back to Main View
<div style="display: block; height: 100%; width: 100%;">
<!-- Filter Controls Section -->
<div class="filter-section" *ngIf="hasActiveFilters()">
<!-- Base Filters -->
<div class="filter-group" *ngIf="baseFilters && baseFilters.length > 0">
<h4>Base Filters</h4>
<div class="filter-controls">
<div *ngFor="let filter of baseFilters" class="filter-item">
<div class="filter-label">{{ filter.field }} ({{ filter.type || 'text' }})</div>
<!-- Text Filter -->
<div *ngIf="!filter.type || filter.type === 'text'" class="filter-input">
<input type="text"
[(ngModel)]="filter.value"
(ngModelChange)="onBaseFilterChange(filter)"
[placeholder]="filter.field"
class="clr-input filter-text-input">
</div>
<!-- Dropdown Filter -->
<div *ngIf="filter.type === 'dropdown'" class="filter-input">
<select [(ngModel)]="filter.value"
(ngModelChange)="onBaseFilterChange(filter)"
class="clr-select filter-select">
<option value="">Select {{ filter.field }}</option>
<option *ngFor="let option of getFilterOptions(filter)" [value]="option">{{ option }}</option>
</select>
</div>
<!-- Multi-Select Filter - Updated to show key first, then dropdown on click -->
<div *ngIf="filter.type === 'multiselect'" class="filter-input multiselect-container">
<div class="multiselect-display" (click)="toggleMultiselect(filter, 'base')">
<span class="multiselect-label">{{ filter.field }}</span>
<span class="multiselect-value" *ngIf="getSelectedOptionsCount(filter) > 0">
({{ getSelectedOptionsCount(filter) }} selected)
</span>
<clr-icon shape="caret down" class="dropdown-icon"></clr-icon>
</div>
<div class="multiselect-dropdown" *ngIf="isMultiselectOpen(filter, 'base')">
<div class="checkbox-group">
<div *ngFor="let option of getFilterOptions(filter); let i = index" class="checkbox-item">
<input type="checkbox"
[checked]="isOptionSelected(filter, option)"
(change)="onMultiSelectChange(filter, option, $event)"
[id]="'base-' + filter.field + '-' + i"
class="clr-checkbox">
<label [for]="'base-' + filter.field + '-' + i" class="checkbox-label">{{ option }}</label>
</div>
</div>
</div>
</div>
<!-- Date Range Filter -->
<div *ngIf="filter.type === 'date-range'" class="filter-input date-range">
<div class="date-input-group">
<input type="date"
[(ngModel)]="filter.value.start"
(ngModelChange)="onDateRangeChange(filter, { start: $event, end: filter.value.end })"
placeholder="Start Date"
class="clr-input filter-date">
<span class="date-separator">to</span>
<input type="date"
[(ngModel)]="filter.value.end"
(ngModelChange)="onDateRangeChange(filter, { start: filter.value.start, end: $event })"
placeholder="End Date"
class="clr-input filter-date">
</div>
</div>
<!-- Toggle Filter -->
<div *ngIf="filter.type === 'toggle'" class="filter-input toggle">
<input type="checkbox"
[(ngModel)]="filter.value"
(ngModelChange)="onToggleChange(filter, $event)"
clrToggle
class="clr-toggle">
<label class="toggle-label">{{ filter.field }}</label>
</div>
</div>
</div>
</div>
<!-- Drilldown Filters -->
<div class="filter-group" *ngIf="drilldownFilters && drilldownFilters.length > 0 && currentDrilldownLevel > 0">
<h4>Drilldown Filters</h4>
<div class="filter-controls">
<div *ngFor="let filter of drilldownFilters" class="filter-item">
<div class="filter-label">{{ filter.field }} ({{ filter.type || 'text' }})</div>
<!-- Text Filter -->
<div *ngIf="!filter.type || filter.type === 'text'" class="filter-input">
<input type="text"
[(ngModel)]="filter.value"
(ngModelChange)="onDrilldownFilterChange(filter)"
[placeholder]="filter.field"
class="clr-input filter-text-input">
</div>
<!-- Dropdown Filter -->
<div *ngIf="filter.type === 'dropdown'" class="filter-input">
<select [(ngModel)]="filter.value"
(ngModelChange)="onDrilldownFilterChange(filter)"
class="clr-select filter-select">
<option value="">Select {{ filter.field }}</option>
<option *ngFor="let option of getFilterOptions(filter)" [value]="option">{{ option }}</option>
</select>
</div>
<!-- Multi-Select Filter - Updated to show key first, then dropdown on click -->
<div *ngIf="filter.type === 'multiselect'" class="filter-input multiselect-container">
<div class="multiselect-display" (click)="toggleMultiselect(filter, 'drilldown')">
<span class="multiselect-label">{{ filter.field }}</span>
<span class="multiselect-value" *ngIf="getSelectedOptionsCount(filter) > 0">
({{ getSelectedOptionsCount(filter) }} selected)
</span>
<clr-icon shape="caret down" class="dropdown-icon"></clr-icon>
</div>
<div class="multiselect-dropdown" *ngIf="isMultiselectOpen(filter, 'drilldown')">
<div class="checkbox-group">
<div *ngFor="let option of getFilterOptions(filter); let i = index" class="checkbox-item">
<input type="checkbox"
[checked]="isOptionSelected(filter, option)"
(change)="onMultiSelectChange(filter, option, $event)"
[id]="'drilldown-' + filter.field + '-' + i"
class="clr-checkbox">
<label [for]="'drilldown-' + filter.field + '-' + i" class="checkbox-label">{{ option }}</label>
</div>
</div>
</div>
</div>
<!-- Date Range Filter -->
<div *ngIf="filter.type === 'date-range'" class="filter-input date-range">
<div class="date-input-group">
<input type="date"
[(ngModel)]="filter.value.start"
(ngModelChange)="onDateRangeChange(filter, { start: $event, end: filter.value.end })"
placeholder="Start Date"
class="clr-input filter-date">
<span class="date-separator">to</span>
<input type="date"
[(ngModel)]="filter.value.end"
(ngModelChange)="onDateRangeChange(filter, { start: filter.value.start, end: $event })"
placeholder="End Date"
class="clr-input filter-date">
</div>
</div>
<!-- Toggle Filter -->
<div *ngIf="filter.type === 'toggle'" class="filter-input toggle">
<input type="checkbox"
[(ngModel)]="filter.value"
(ngModelChange)="onToggleChange(filter, $event)"
clrToggle
class="clr-toggle">
<label class="toggle-label">{{ filter.field }}</label>
</div>
</div>
</div>
</div>
<!-- Layer Filters -->
<div class="filter-group" *ngIf="hasActiveLayerFilters()">
<h4>Layer Filters</h4>
<div class="filter-controls">
<div *ngFor="let filter of getActiveLayerFilters()" class="filter-item">
<div class="filter-label">{{ filter.field }} ({{ filter.type || 'text' }})</div>
<!-- Text Filter -->
<div *ngIf="!filter.type || filter.type === 'text'" class="filter-input">
<input type="text"
[(ngModel)]="filter.value"
(ngModelChange)="onLayerFilterChange(filter)"
[placeholder]="filter.field"
class="clr-input filter-text-input">
</div>
<!-- Dropdown Filter -->
<div *ngIf="filter.type === 'dropdown'" class="filter-input">
<select [(ngModel)]="filter.value"
(ngModelChange)="onLayerFilterChange(filter)"
class="clr-select filter-select">
<option value="">Select {{ filter.field }}</option>
<option *ngFor="let option of getFilterOptions(filter)" [value]="option">{{ option }}</option>
</select>
</div>
<!-- Multi-Select Filter - Updated to show key first, then dropdown on click -->
<div *ngIf="filter.type === 'multiselect'" class="filter-input multiselect-container">
<div class="multiselect-display" (click)="toggleMultiselect(filter, 'layer')">
<span class="multiselect-label">{{ filter.field }}</span>
<span class="multiselect-value" *ngIf="getSelectedOptionsCount(filter) > 0">
({{ getSelectedOptionsCount(filter) }} selected)
</span>
<clr-icon shape="caret down" class="dropdown-icon"></clr-icon>
</div>
<div class="multiselect-dropdown" *ngIf="isMultiselectOpen(filter, 'layer')">
<div class="checkbox-group">
<div *ngFor="let option of getFilterOptions(filter); let i = index" class="checkbox-item">
<input type="checkbox"
[checked]="isOptionSelected(filter, option)"
(change)="onMultiSelectChange(filter, option, $event)"
[id]="'layer-' + filter.field + '-' + i"
class="clr-checkbox">
<label [for]="'layer-' + filter.field + '-' + i" class="checkbox-label">{{ option }}</label>
</div>
</div>
</div>
</div>
<!-- Date Range Filter -->
<div *ngIf="filter.type === 'date-range'" class="filter-input date-range">
<div class="date-input-group">
<input type="date"
[(ngModel)]="filter.value.start"
(ngModelChange)="onDateRangeChange(filter, { start: $event, end: filter.value.end })"
placeholder="Start Date"
class="clr-input filter-date">
<span class="date-separator">to</span>
<input type="date"
[(ngModel)]="filter.value.end"
(ngModelChange)="onDateRangeChange(filter, { start: filter.value.start, end: $event })"
placeholder="End Date"
class="clr-input filter-date">
</div>
</div>
<!-- Toggle Filter -->
<div *ngIf="filter.type === 'toggle'" class="filter-input toggle">
<input type="checkbox"
[(ngModel)]="filter.value"
(ngModelChange)="onToggleChange(filter, $event)"
clrToggle
class="clr-toggle">
<label class="toggle-label">{{ filter.field }}</label>
</div>
</div>
</div>
</div>
<!-- Clear Filters Button -->
<div class="filter-actions">
<button class="btn btn-sm btn-outline" (click)="clearAllFilters()">Clear All Filters</button>
</div>
</div>
<!-- Header row with chart title and drilldown navigation -->
<div class="clr-row header-row">
<div class="clr-col-6">
<h3 class="chart-title">{{charttitle || 'Radar Chart'}}</h3>
</div>
<div class="clr-col-6" style="text-align: right;">
<!-- Add drilldown navigation controls -->
<button class="btn btn-sm btn-link" *ngIf="drilldownEnabled && drilldownStack.length > 0" (click)="navigateBack()">
<cds-icon shape="arrow" direction="left"></cds-icon>
Back to {{drilldownStack.length > 1 ? 'Previous Level' : 'Main Data'}}
</button>
</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].clickedLabel}})
</span>
</span>
</div>
</div>
</div>
</div>
</div>
<!-- No data message -->
<div *ngIf="noDataAvailable" style="text-align: center; padding: 20px; color: #666; font-style: italic;">
@@ -16,7 +285,7 @@
</div>
<!-- Chart display -->
<div *ngIf="!noDataAvailable">
<div *ngIf="!noDataAvailable" style="position: relative; height: calc(100% - 50px);">
<canvas baseChart
[datasets]="radarChartData"
[labels]="radarChartLabels"

View File

@@ -0,0 +1,192 @@
.filter-section {
margin-bottom: 20px;
padding: 15px;
border: 1px solid #ddd;
border-radius: 4px;
background-color: #f9f9f9;
}
.filter-group {
margin-bottom: 15px;
h4 {
margin-top: 0;
margin-bottom: 10px;
color: #333;
font-weight: 600;
}
}
.filter-controls {
display: flex;
flex-wrap: wrap;
gap: 15px;
}
.filter-item {
flex: 1 1 300px;
min-width: 250px;
padding: 10px;
background: white;
border: 1px solid #e0e0e0;
border-radius: 4px;
}
.filter-label {
font-weight: 500;
margin-bottom: 8px;
color: #555;
font-size: 14px;
}
.filter-input {
width: 100%;
.filter-text-input,
.filter-select,
.filter-date {
width: 100%;
padding: 6px 12px;
border: 1px solid #ccc;
border-radius: 4px;
font-size: 14px;
}
.filter-select {
height: 34px;
}
}
.multiselect-container {
position: relative;
}
.multiselect-display {
display: flex;
align-items: center;
justify-content: space-between;
padding: 6px 12px;
border: 1px solid #ccc;
border-radius: 4px;
background: white;
cursor: pointer;
min-height: 34px;
.multiselect-label {
flex: 1;
font-size: 14px;
}
.multiselect-value {
color: #666;
font-size: 12px;
margin-right: 8px;
}
.dropdown-icon {
flex-shrink: 0;
transition: transform 0.2s ease;
}
&:hover {
border-color: #999;
}
}
.multiselect-dropdown {
position: absolute;
top: 100%;
left: 0;
right: 0;
z-index: 1000;
background: white;
border: 1px solid #ccc;
border-top: none;
border-radius: 0 0 4px 4px;
box-shadow: 0 2px 4px rgba(0,0,0,0.1);
max-height: 200px;
overflow-y: auto;
.checkbox-group {
padding: 8px;
.checkbox-item {
display: flex;
align-items: center;
gap: 8px;
padding: 4px 0;
.checkbox-label {
margin: 0;
font-size: 14px;
cursor: pointer;
}
}
}
}
.date-range {
.date-input-group {
display: flex;
align-items: center;
gap: 8px;
}
.date-separator {
margin: 0 5px;
color: #777;
}
}
.toggle {
display: flex;
align-items: center;
gap: 8px;
.toggle-label {
margin: 0;
font-size: 14px;
cursor: pointer;
}
}
.filter-actions {
margin-top: 15px;
padding-top: 15px;
border-top: 1px solid #eee;
.btn {
font-size: 13px;
}
}
// New header row styling
.header-row {
margin-bottom: 15px;
padding-bottom: 10px;
border-bottom: 1px solid #eee;
.chart-title {
margin: 0;
font-size: 18px;
font-weight: 600;
color: #333;
}
}
// Responsive design
@media (max-width: 768px) {
.filter-controls {
flex-direction: column;
}
.filter-item {
min-width: 100%;
}
.header-row {
.chart-title {
font-size: 16px;
}
}
}

View File

@@ -1,5 +1,7 @@
import { Component, OnInit, Input, OnChanges, SimpleChanges } from '@angular/core';
import { Dashboard3Service } from 'src/app/services/builder/dashboard3.service';
import { FilterService } from '../../common-filter/filter.service';
import { Subscription } from 'rxjs';
@Component({
selector: 'app-radar-chart',
@@ -62,15 +64,40 @@ export class RadarChartComponent implements OnInit, OnChanges {
// Flag to prevent infinite loops
private isFetchingData: boolean = false;
constructor(private dashboardService: Dashboard3Service) { }
// Subscriptions to unsubscribe on destroy
private subscriptions: Subscription[] = [];
// Add properties for filter functionality
private openMultiselects: Map<string, string> = new Map(); // Map of filterId -> context
private documentClickHandler: ((event: MouseEvent) => void) | null = null;
private filtersInitialized: boolean = false;
constructor(
private dashboardService: Dashboard3Service,
private filterService: FilterService
) { }
ngOnInit(): void {
// Subscribe to filter changes
this.subscriptions.push(
this.filterService.filterState$.subscribe(filters => {
// When filters change, refresh the chart data
this.fetchChartData();
})
);
this.fetchChartData();
}
ngOnChanges(changes: SimpleChanges): void {
console.log('RadarChartComponent input changes:', changes);
// Initialize filter values if they haven't been initialized yet
if (!this.filtersInitialized && (changes.baseFilters || changes.drilldownFilters || changes.drilldownLayers)) {
this.initializeFilterValues();
this.filtersInitialized = true;
}
// Check if any of the key properties have changed
const xAxisChanged = changes.xAxis && !changes.xAxis.firstChange;
const yAxisChanged = changes.yAxis && !changes.yAxis.firstChange;
@@ -94,6 +121,316 @@ export class RadarChartComponent implements OnInit, OnChanges {
}
}
// Initialize filter values with proper default values based on type
private initializeFilterValues(): void {
console.log('Initializing filter values');
// Initialize base filters
if (this.baseFilters) {
this.baseFilters.forEach(filter => {
if (filter.value === undefined || filter.value === null) {
switch (filter.type) {
case 'multiselect':
filter.value = [];
break;
case 'date-range':
filter.value = { start: null, end: null };
break;
case 'toggle':
filter.value = false;
break;
default:
filter.value = '';
}
}
});
}
// Initialize drilldown filters
if (this.drilldownFilters) {
this.drilldownFilters.forEach(filter => {
if (filter.value === undefined || filter.value === null) {
switch (filter.type) {
case 'multiselect':
filter.value = [];
break;
case 'date-range':
filter.value = { start: null, end: null };
break;
case 'toggle':
filter.value = false;
break;
default:
filter.value = '';
}
}
});
}
// Initialize layer filters
if (this.drilldownLayers) {
this.drilldownLayers.forEach(layer => {
if (layer.filters) {
layer.filters.forEach((filter: any) => {
if (filter.value === undefined || filter.value === null) {
switch (filter.type) {
case 'multiselect':
filter.value = [];
break;
case 'date-range':
filter.value = { start: null, end: null };
break;
case 'toggle':
filter.value = false;
break;
default:
filter.value = '';
}
}
});
}
});
}
console.log('Filter values initialized:', {
baseFilters: this.baseFilters,
drilldownFilters: this.drilldownFilters,
drilldownLayers: this.drilldownLayers
});
}
// Check if there are active filters
hasActiveFilters(): boolean {
return (this.baseFilters && this.baseFilters.length > 0) ||
(this.drilldownFilters && this.drilldownFilters.length > 0) ||
this.hasActiveLayerFilters();
}
// Check if there are active layer filters for current drilldown level
hasActiveLayerFilters(): boolean {
if (this.currentDrilldownLevel > 1 && this.drilldownLayers && this.drilldownLayers.length > 0) {
const layerIndex = this.currentDrilldownLevel - 2; // -2 because level 1 is base drilldown
return layerIndex < this.drilldownLayers.length &&
this.drilldownLayers[layerIndex].filters &&
this.drilldownLayers[layerIndex].filters.length > 0;
}
return false;
}
// Get active layer filters for current drilldown level
getActiveLayerFilters(): any[] {
if (this.currentDrilldownLevel > 1 && this.drilldownLayers && this.drilldownLayers.length > 0) {
const layerIndex = this.currentDrilldownLevel - 2; // -2 because level 1 is base drilldown
if (layerIndex < this.drilldownLayers.length &&
this.drilldownLayers[layerIndex].filters) {
return this.drilldownLayers[layerIndex].filters;
}
}
return [];
}
// Get filter options for dropdown/multiselect filters
getFilterOptions(filter: any): string[] {
if (filter.options) {
if (Array.isArray(filter.options)) {
return filter.options;
} else if (typeof filter.options === 'string') {
return filter.options.split(',').map(opt => opt.trim()).filter(opt => opt);
}
}
return [];
}
// Check if an option is selected for multiselect filters
isOptionSelected(filter: any, option: string): boolean {
if (!filter.value) {
return false;
}
if (Array.isArray(filter.value)) {
return filter.value.includes(option);
}
return filter.value === option;
}
// Handle base filter changes
onBaseFilterChange(filter: any): void {
console.log('Base filter changed:', filter);
// Refresh data when filter changes
this.fetchChartData();
}
// Handle drilldown filter changes
onDrilldownFilterChange(filter: any): void {
console.log('Drilldown filter changed:', filter);
// Refresh data when filter changes
this.fetchChartData();
}
// Handle layer filter changes
onLayerFilterChange(filter: any): void {
console.log('Layer filter changed:', filter);
// Refresh data when filter changes
this.fetchChartData();
}
// Handle multiselect changes
onMultiSelectChange(filter: any, option: string, event: any): void {
const checked = event.target.checked;
// Initialize filter.value as array if it's not already
if (!Array.isArray(filter.value)) {
filter.value = [];
}
if (checked) {
// Add option to array if not already present
if (!filter.value.includes(option)) {
filter.value.push(option);
}
} else {
// Remove option from array
filter.value = filter.value.filter((item: string) => item !== option);
}
// Refresh data when filter changes
this.fetchChartData();
}
// Handle date range changes
onDateRangeChange(filter: any, dateRange: { start: string | null, end: string | null }): void {
filter.value = dateRange;
// Refresh data when filter changes
this.fetchChartData();
}
// Handle toggle changes
onToggleChange(filter: any, checked: boolean): void {
filter.value = checked;
// Refresh data when filter changes
this.fetchChartData();
}
// Toggle multiselect dropdown visibility
toggleMultiselect(filter: any, context: string): void {
const filterId = `${context}-${filter.field}`;
if (this.isMultiselectOpen(filter, context)) {
this.openMultiselects.delete(filterId);
} else {
// Close all other multiselects first
this.openMultiselects.clear();
this.openMultiselects.set(filterId, context);
// Add document click handler to close dropdown when clicking outside
this.addDocumentClickHandler();
}
}
// Add document click handler to close dropdowns when clicking outside
private addDocumentClickHandler(): void {
if (!this.documentClickHandler) {
this.documentClickHandler = (event: MouseEvent) => {
const target = event.target as HTMLElement;
// Check if click is outside any multiselect dropdown
if (!target.closest('.multiselect-container')) {
this.openMultiselects.clear();
this.removeDocumentClickHandler();
}
};
// Use setTimeout to ensure the click event that opened the dropdown doesn't immediately close it
setTimeout(() => {
document.addEventListener('click', this.documentClickHandler!);
}, 0);
}
}
// Remove document click handler
private removeDocumentClickHandler(): void {
if (this.documentClickHandler) {
document.removeEventListener('click', this.documentClickHandler);
this.documentClickHandler = null;
}
}
// Check if multiselect dropdown is open
isMultiselectOpen(filter: any, context: string): boolean {
const filterId = `${context}-${filter.field}`;
return this.openMultiselects.has(filterId);
}
// Get count of selected options for a multiselect filter
getSelectedOptionsCount(filter: any): number {
if (!filter.value) {
return 0;
}
if (Array.isArray(filter.value)) {
return filter.value.length;
}
return 0;
}
// Clear all filters
clearAllFilters(): void {
// Clear base filters
if (this.baseFilters) {
this.baseFilters.forEach(filter => {
if (filter.type === 'multiselect') {
filter.value = [];
} else if (filter.type === 'date-range') {
filter.value = { start: null, end: null };
} else if (filter.type === 'toggle') {
filter.value = false;
} else {
filter.value = '';
}
});
}
// Clear drilldown filters
if (this.drilldownFilters) {
this.drilldownFilters.forEach(filter => {
if (filter.type === 'multiselect') {
filter.value = [];
} else if (filter.type === 'date-range') {
filter.value = { start: null, end: null };
} else if (filter.type === 'toggle') {
filter.value = false;
} else {
filter.value = '';
}
});
}
// Clear layer filters
if (this.drilldownLayers) {
this.drilldownLayers.forEach(layer => {
if (layer.filters) {
layer.filters.forEach((filter: any) => {
if (filter.type === 'multiselect') {
filter.value = [];
} else if (filter.type === 'date-range') {
filter.value = { start: null, end: null };
} else if (filter.type === 'toggle') {
filter.value = false;
} else {
filter.value = '';
}
});
}
});
}
// Close all multiselect dropdowns
this.openMultiselects.clear();
// Refresh data
this.fetchChartData();
}
fetchChartData(): void {
// Set flag to prevent recursive calls
this.isFetchingData = true;
@@ -126,7 +463,49 @@ export class RadarChartComponent implements OnInit, OnChanges {
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
const url = `chart/getdashjson/radar?tableName=${this.table}&xAxis=${this.xAxis}&yAxes=${yAxisString}${this.connection ? `&sureId=${this.connection}` : ''}`;
@@ -298,6 +677,35 @@ export class RadarChartComponent 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
const url = `chart/getdashjson/radar?tableName=${actualApiUrl}&xAxis=${drilldownConfig.xAxis}&yAxes=${drilldownConfig.yAxis}${this.connection ? `&sureId=${this.connection}` : ''}`;
console.log('Drilldown data URL:', url);
@@ -321,7 +729,6 @@ export class RadarChartComponent implements OnInit, OnChanges {
this.noDataAvailable = data.chartLabels.length === 0;
this.radarChartLabels = data.chartLabels;
// For radar charts, we need to ensure the data is properly formatted
// Each dataset should have a data array with numeric values
this.radarChartData = data.chartData.map(dataset => ({
...dataset,
data: dataset.data ? dataset.data.map(value => {
@@ -358,6 +765,7 @@ export class RadarChartComponent implements OnInit, OnChanges {
this.noDataAvailable = true;
this.radarChartLabels = [];
this.radarChartData = [];
// Keep current data in case of error
}
);
}
@@ -436,7 +844,7 @@ export class RadarChartComponent implements OnInit, OnChanges {
if (this.currentDrilldownLevel === 0) {
// Store original data before entering drilldown mode
this.originalRadarChartLabels = [...this.radarChartLabels];
this.originalRadarChartData = [...this.radarChartData];
this.originalRadarChartData = JSON.parse(JSON.stringify(this.radarChartData));
console.log('Stored original data for drilldown');
}
@@ -504,6 +912,12 @@ export class RadarChartComponent implements OnInit, OnChanges {
}
public chartHovered(e: any): void {
console.log(e);
console.log('Radar chart hovered:', e);
}
ngOnDestroy(): void {
this.subscriptions.forEach(sub => sub.unsubscribe());
// Clean up document click handler
this.removeDocumentClickHandler();
}
}

View File

@@ -1,14 +1,283 @@
<div style="display: block">
<!-- Drilldown mode indicator -->
<div *ngIf="currentDrilldownLevel > 0" style="background-color: #e0e0e0; padding: 5px; margin-bottom: 10px; border-radius: 4px; text-align: center;">
<span style="font-weight: bold; color: #333;">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;">
Back to Level {{currentDrilldownLevel - 1}}
</button>
<button (click)="resetToOriginalData()" style="margin-left: 10px; padding: 2px 8px; background-color: #dc3545; color: white; border: none; border-radius: 3px; cursor: pointer;">
Back to Main View
<div style="display: block; height: 100%; width: 100%;">
<!-- Filter Controls Section -->
<div class="filter-section" *ngIf="hasActiveFilters()">
<!-- Base Filters -->
<div class="filter-group" *ngIf="baseFilters && baseFilters.length > 0">
<h4>Base Filters</h4>
<div class="filter-controls">
<div *ngFor="let filter of baseFilters" class="filter-item">
<div class="filter-label">{{ filter.field }} ({{ filter.type || 'text' }})</div>
<!-- Text Filter -->
<div *ngIf="!filter.type || filter.type === 'text'" class="filter-input">
<input type="text"
[(ngModel)]="filter.value"
(ngModelChange)="onBaseFilterChange(filter)"
[placeholder]="filter.field"
class="clr-input filter-text-input">
</div>
<!-- Dropdown Filter -->
<div *ngIf="filter.type === 'dropdown'" class="filter-input">
<select [(ngModel)]="filter.value"
(ngModelChange)="onBaseFilterChange(filter)"
class="clr-select filter-select">
<option value="">Select {{ filter.field }}</option>
<option *ngFor="let option of getFilterOptions(filter)" [value]="option">{{ option }}</option>
</select>
</div>
<!-- Multi-Select Filter - Updated to show key first, then dropdown on click -->
<div *ngIf="filter.type === 'multiselect'" class="filter-input multiselect-container">
<div class="multiselect-display" (click)="toggleMultiselect(filter, 'base')">
<span class="multiselect-label">{{ filter.field }}</span>
<span class="multiselect-value" *ngIf="getSelectedOptionsCount(filter) > 0">
({{ getSelectedOptionsCount(filter) }} selected)
</span>
<clr-icon shape="caret down" class="dropdown-icon"></clr-icon>
</div>
<div class="multiselect-dropdown" *ngIf="isMultiselectOpen(filter, 'base')">
<div class="checkbox-group">
<div *ngFor="let option of getFilterOptions(filter); let i = index" class="checkbox-item">
<input type="checkbox"
[checked]="isOptionSelected(filter, option)"
(change)="onMultiSelectChange(filter, option, $event)"
[id]="'base-' + filter.field + '-' + i"
class="clr-checkbox">
<label [for]="'base-' + filter.field + '-' + i" class="checkbox-label">{{ option }}</label>
</div>
</div>
</div>
</div>
<!-- Date Range Filter -->
<div *ngIf="filter.type === 'date-range'" class="filter-input date-range">
<div class="date-input-group">
<input type="date"
[(ngModel)]="filter.value.start"
(ngModelChange)="onDateRangeChange(filter, { start: $event, end: filter.value.end })"
placeholder="Start Date"
class="clr-input filter-date">
<span class="date-separator">to</span>
<input type="date"
[(ngModel)]="filter.value.end"
(ngModelChange)="onDateRangeChange(filter, { start: filter.value.start, end: $event })"
placeholder="End Date"
class="clr-input filter-date">
</div>
</div>
<!-- Toggle Filter -->
<div *ngIf="filter.type === 'toggle'" class="filter-input toggle">
<input type="checkbox"
[(ngModel)]="filter.value"
(ngModelChange)="onToggleChange(filter, $event)"
clrToggle
class="clr-toggle">
<label class="toggle-label">{{ filter.field }}</label>
</div>
</div>
</div>
</div>
<!-- Drilldown Filters -->
<div class="filter-group" *ngIf="drilldownFilters && drilldownFilters.length > 0 && currentDrilldownLevel > 0">
<h4>Drilldown Filters</h4>
<div class="filter-controls">
<div *ngFor="let filter of drilldownFilters" class="filter-item">
<div class="filter-label">{{ filter.field }} ({{ filter.type || 'text' }})</div>
<!-- Text Filter -->
<div *ngIf="!filter.type || filter.type === 'text'" class="filter-input">
<input type="text"
[(ngModel)]="filter.value"
(ngModelChange)="onDrilldownFilterChange(filter)"
[placeholder]="filter.field"
class="clr-input filter-text-input">
</div>
<!-- Dropdown Filter -->
<div *ngIf="filter.type === 'dropdown'" class="filter-input">
<select [(ngModel)]="filter.value"
(ngModelChange)="onDrilldownFilterChange(filter)"
class="clr-select filter-select">
<option value="">Select {{ filter.field }}</option>
<option *ngFor="let option of getFilterOptions(filter)" [value]="option">{{ option }}</option>
</select>
</div>
<!-- Multi-Select Filter - Updated to show key first, then dropdown on click -->
<div *ngIf="filter.type === 'multiselect'" class="filter-input multiselect-container">
<div class="multiselect-display" (click)="toggleMultiselect(filter, 'drilldown')">
<span class="multiselect-label">{{ filter.field }}</span>
<span class="multiselect-value" *ngIf="getSelectedOptionsCount(filter) > 0">
({{ getSelectedOptionsCount(filter) }} selected)
</span>
<clr-icon shape="caret down" class="dropdown-icon"></clr-icon>
</div>
<div class="multiselect-dropdown" *ngIf="isMultiselectOpen(filter, 'drilldown')">
<div class="checkbox-group">
<div *ngFor="let option of getFilterOptions(filter); let i = index" class="checkbox-item">
<input type="checkbox"
[checked]="isOptionSelected(filter, option)"
(change)="onMultiSelectChange(filter, option, $event)"
[id]="'drilldown-' + filter.field + '-' + i"
class="clr-checkbox">
<label [for]="'drilldown-' + filter.field + '-' + i" class="checkbox-label">{{ option }}</label>
</div>
</div>
</div>
</div>
<!-- Date Range Filter -->
<div *ngIf="filter.type === 'date-range'" class="filter-input date-range">
<div class="date-input-group">
<input type="date"
[(ngModel)]="filter.value.start"
(ngModelChange)="onDateRangeChange(filter, { start: $event, end: filter.value.end })"
placeholder="Start Date"
class="clr-input filter-date">
<span class="date-separator">to</span>
<input type="date"
[(ngModel)]="filter.value.end"
(ngModelChange)="onDateRangeChange(filter, { start: filter.value.start, end: $event })"
placeholder="End Date"
class="clr-input filter-date">
</div>
</div>
<!-- Toggle Filter -->
<div *ngIf="filter.type === 'toggle'" class="filter-input toggle">
<input type="checkbox"
[(ngModel)]="filter.value"
(ngModelChange)="onToggleChange(filter, $event)"
clrToggle
class="clr-toggle">
<label class="toggle-label">{{ filter.field }}</label>
</div>
</div>
</div>
</div>
<!-- Layer Filters -->
<div class="filter-group" *ngIf="hasActiveLayerFilters()">
<h4>Layer Filters</h4>
<div class="filter-controls">
<div *ngFor="let filter of getActiveLayerFilters()" class="filter-item">
<div class="filter-label">{{ filter.field }} ({{ filter.type || 'text' }})</div>
<!-- Text Filter -->
<div *ngIf="!filter.type || filter.type === 'text'" class="filter-input">
<input type="text"
[(ngModel)]="filter.value"
(ngModelChange)="onLayerFilterChange(filter)"
[placeholder]="filter.field"
class="clr-input filter-text-input">
</div>
<!-- Dropdown Filter -->
<div *ngIf="filter.type === 'dropdown'" class="filter-input">
<select [(ngModel)]="filter.value"
(ngModelChange)="onLayerFilterChange(filter)"
class="clr-select filter-select">
<option value="">Select {{ filter.field }}</option>
<option *ngFor="let option of getFilterOptions(filter)" [value]="option">{{ option }}</option>
</select>
</div>
<!-- Multi-Select Filter - Updated to show key first, then dropdown on click -->
<div *ngIf="filter.type === 'multiselect'" class="filter-input multiselect-container">
<div class="multiselect-display" (click)="toggleMultiselect(filter, 'layer')">
<span class="multiselect-label">{{ filter.field }}</span>
<span class="multiselect-value" *ngIf="getSelectedOptionsCount(filter) > 0">
({{ getSelectedOptionsCount(filter) }} selected)
</span>
<clr-icon shape="caret down" class="dropdown-icon"></clr-icon>
</div>
<div class="multiselect-dropdown" *ngIf="isMultiselectOpen(filter, 'layer')">
<div class="checkbox-group">
<div *ngFor="let option of getFilterOptions(filter); let i = index" class="checkbox-item">
<input type="checkbox"
[checked]="isOptionSelected(filter, option)"
(change)="onMultiSelectChange(filter, option, $event)"
[id]="'layer-' + filter.field + '-' + i"
class="clr-checkbox">
<label [for]="'layer-' + filter.field + '-' + i" class="checkbox-label">{{ option }}</label>
</div>
</div>
</div>
</div>
<!-- Date Range Filter -->
<div *ngIf="filter.type === 'date-range'" class="filter-input date-range">
<div class="date-input-group">
<input type="date"
[(ngModel)]="filter.value.start"
(ngModelChange)="onDateRangeChange(filter, { start: $event, end: filter.value.end })"
placeholder="Start Date"
class="clr-input filter-date">
<span class="date-separator">to</span>
<input type="date"
[(ngModel)]="filter.value.end"
(ngModelChange)="onDateRangeChange(filter, { start: filter.value.start, end: $event })"
placeholder="End Date"
class="clr-input filter-date">
</div>
</div>
<!-- Toggle Filter -->
<div *ngIf="filter.type === 'toggle'" class="filter-input toggle">
<input type="checkbox"
[(ngModel)]="filter.value"
(ngModelChange)="onToggleChange(filter, $event)"
clrToggle
class="clr-toggle">
<label class="toggle-label">{{ filter.field }}</label>
</div>
</div>
</div>
</div>
<!-- Clear Filters Button -->
<div class="filter-actions">
<button class="btn btn-sm btn-outline" (click)="clearAllFilters()">Clear All Filters</button>
</div>
</div>
<!-- Header row with chart title and drilldown navigation -->
<div class="clr-row header-row">
<div class="clr-col-6">
<h3 class="chart-title">{{charttitle || 'Scatter Chart'}}</h3>
</div>
<div class="clr-col-6" style="text-align: right;">
<!-- Add drilldown navigation controls -->
<button class="btn btn-sm btn-link" *ngIf="drilldownEnabled && drilldownStack.length > 0" (click)="navigateBack()">
<cds-icon shape="arrow" direction="left"></cds-icon>
Back to {{drilldownStack.length > 1 ? 'Previous Level' : 'Main Data'}}
</button>
</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].clickedLabel}})
</span>
</span>
</div>
</div>
</div>
</div>
</div>
<!-- No data message -->
<div *ngIf="noDataAvailable" style="text-align: center; padding: 20px; color: #666; font-style: italic;">
@@ -16,9 +285,10 @@
</div>
<!-- Chart display -->
<div *ngIf="!noDataAvailable">
<div *ngIf="!noDataAvailable" style="position: relative; height: calc(100% - 70px); min-height: 300px;">
<canvas baseChart
[datasets]="scatterChartData"
[options]="scatterChartOptions"
[type]="scatterChartType"
(chartHover)="chartHovered($event)"
(chartClick)="chartClicked($event)">

View File

@@ -0,0 +1,192 @@
.filter-section {
margin-bottom: 20px;
padding: 15px;
border: 1px solid #ddd;
border-radius: 4px;
background-color: #f9f9f9;
}
.filter-group {
margin-bottom: 15px;
h4 {
margin-top: 0;
margin-bottom: 10px;
color: #333;
font-weight: 600;
}
}
.filter-controls {
display: flex;
flex-wrap: wrap;
gap: 15px;
}
.filter-item {
flex: 1 1 300px;
min-width: 250px;
padding: 10px;
background: white;
border: 1px solid #e0e0e0;
border-radius: 4px;
}
.filter-label {
font-weight: 500;
margin-bottom: 8px;
color: #555;
font-size: 14px;
}
.filter-input {
width: 100%;
.filter-text-input,
.filter-select,
.filter-date {
width: 100%;
padding: 6px 12px;
border: 1px solid #ccc;
border-radius: 4px;
font-size: 14px;
}
.filter-select {
height: 34px;
}
}
.multiselect-container {
position: relative;
}
.multiselect-display {
display: flex;
align-items: center;
justify-content: space-between;
padding: 6px 12px;
border: 1px solid #ccc;
border-radius: 4px;
background: white;
cursor: pointer;
min-height: 34px;
.multiselect-label {
flex: 1;
font-size: 14px;
}
.multiselect-value {
color: #666;
font-size: 12px;
margin-right: 8px;
}
.dropdown-icon {
flex-shrink: 0;
transition: transform 0.2s ease;
}
&:hover {
border-color: #999;
}
}
.multiselect-dropdown {
position: absolute;
top: 100%;
left: 0;
right: 0;
z-index: 1000;
background: white;
border: 1px solid #ccc;
border-top: none;
border-radius: 0 0 4px 4px;
box-shadow: 0 2px 4px rgba(0,0,0,0.1);
max-height: 200px;
overflow-y: auto;
.checkbox-group {
padding: 8px;
.checkbox-item {
display: flex;
align-items: center;
gap: 8px;
padding: 4px 0;
.checkbox-label {
margin: 0;
font-size: 14px;
cursor: pointer;
}
}
}
}
.date-range {
.date-input-group {
display: flex;
align-items: center;
gap: 8px;
}
.date-separator {
margin: 0 5px;
color: #777;
}
}
.toggle {
display: flex;
align-items: center;
gap: 8px;
.toggle-label {
margin: 0;
font-size: 14px;
cursor: pointer;
}
}
.filter-actions {
margin-top: 15px;
padding-top: 15px;
border-top: 1px solid #eee;
.btn {
font-size: 13px;
}
}
// New header row styling
.header-row {
margin-bottom: 15px;
padding-bottom: 10px;
border-bottom: 1px solid #eee;
.chart-title {
margin: 0;
font-size: 18px;
font-weight: 600;
color: #333;
}
}
// Responsive design
@media (max-width: 768px) {
.filter-controls {
flex-direction: column;
}
.filter-item {
min-width: 100%;
}
.header-row {
.chart-title {
font-size: 16px;
}
}
}

View File

@@ -1,6 +1,8 @@
import { Component, OnInit, Input, OnChanges, SimpleChanges } from '@angular/core';
import { ChartData,ChartDataset } from 'chart.js';
import { ChartData,ChartDataset,ChartOptions } from 'chart.js';
import { Dashboard3Service } from 'src/app/services/builder/dashboard3.service';
import { FilterService } from '../../common-filter/filter.service';
import { Subscription } from 'rxjs';
@Component({
selector: 'app-scatter-chart',
@@ -34,9 +36,20 @@ export class ScatterChartComponent implements OnInit, OnChanges {
// Multi-layer drilldown configuration inputs
@Input() drilldownLayers: any[] = []; // Array of drilldown layer configurations
constructor(private dashboardService: Dashboard3Service) { }
constructor(
private dashboardService: Dashboard3Service,
private filterService: FilterService
) { }
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
this.fetchChartData();
}
@@ -44,6 +57,12 @@ export class ScatterChartComponent implements OnInit, OnChanges {
ngOnChanges(changes: SimpleChanges): void {
console.log('ScatterChartComponent input changes:', changes);
// Initialize filter values if they haven't been initialized yet
if (!this.filtersInitialized && (changes.baseFilters || changes.drilldownFilters || changes.drilldownLayers)) {
this.initializeFilterValues();
this.filtersInitialized = true;
}
// Check if any of the key properties have changed
const xAxisChanged = changes.xAxis && !changes.xAxis.firstChange;
const yAxisChanged = changes.yAxis && !changes.yAxis.firstChange;
@@ -94,6 +113,89 @@ export class ScatterChartComponent implements OnInit, OnChanges {
],
},
];
public scatterChartOptions: any = {
responsive: true,
maintainAspectRatio: false,
scales: {
x: {
type: 'linear',
position: 'bottom',
title: {
display: true,
text: 'X Axis'
},
ticks: {
autoSkip: true,
maxTicksLimit: 10,
callback: function(value: any) {
if (typeof value === 'number') {
// Format large numbers for better readability
if (Math.abs(value) >= 1000000) {
return (value / 1000000).toFixed(1) + 'M';
} else if (Math.abs(value) >= 1000) {
return (value / 1000).toFixed(1) + 'K';
}
return value.toString();
}
return value;
}
},
grid: {
display: true,
color: 'rgba(0, 0, 0, 0.1)'
}
},
y: {
title: {
display: true,
text: 'Y Axis'
},
ticks: {
autoSkip: true,
maxTicksLimit: 10,
callback: function(value: any) {
if (typeof value === 'number') {
// Format large numbers for better readability
if (Math.abs(value) >= 1000000) {
return (value / 1000000).toFixed(1) + 'M';
} else if (Math.abs(value) >= 1000) {
return (value / 1000).toFixed(1) + 'K';
}
return value.toString();
}
return value;
}
},
grid: {
display: true,
color: 'rgba(0, 0, 0, 0.1)'
}
}
},
plugins: {
legend: {
display: true,
position: 'top',
},
tooltip: {
callbacks: {
label: function(context: any) {
return `(${context.parsed.x}, ${context.parsed.y})`;
}
}
}
},
layout: {
padding: {
left: 15,
right: 15,
top: 15,
bottom: 60 // Add padding at the bottom to ensure X-axis visibility
}
}
};
public scatterChartType: string = 'scatter';
// Multi-layer drilldown state tracking
@@ -107,6 +209,417 @@ export class ScatterChartComponent implements OnInit, OnChanges {
// Flag to prevent infinite loops
private isFetchingData: boolean = false;
// Subscriptions to unsubscribe on destroy
private subscriptions: Subscription[] = [];
// Add properties for filter functionality
private openMultiselects: Map<string, string> = new Map(); // Map of filterId -> context
private documentClickHandler: ((event: MouseEvent) => void) | null = null;
private filtersInitialized: boolean = false;
// Initialize filter values with proper default values based on type
private initializeFilterValues(): void {
console.log('Initializing filter values');
// Initialize base filters
if (this.baseFilters) {
this.baseFilters.forEach(filter => {
if (filter.value === undefined || filter.value === null) {
switch (filter.type) {
case 'multiselect':
filter.value = [];
break;
case 'date-range':
filter.value = { start: null, end: null };
break;
case 'toggle':
filter.value = false;
break;
default:
filter.value = '';
}
}
});
}
// Initialize drilldown filters
if (this.drilldownFilters) {
this.drilldownFilters.forEach(filter => {
if (filter.value === undefined || filter.value === null) {
switch (filter.type) {
case 'multiselect':
filter.value = [];
break;
case 'date-range':
filter.value = { start: null, end: null };
break;
case 'toggle':
filter.value = false;
break;
default:
filter.value = '';
}
}
});
}
// Initialize layer filters
if (this.drilldownLayers) {
this.drilldownLayers.forEach(layer => {
if (layer.filters) {
layer.filters.forEach((filter: any) => {
if (filter.value === undefined || filter.value === null) {
switch (filter.type) {
case 'multiselect':
filter.value = [];
break;
case 'date-range':
filter.value = { start: null, end: null };
break;
case 'toggle':
filter.value = false;
break;
default:
filter.value = '';
}
}
});
}
});
}
console.log('Filter values initialized:', {
baseFilters: this.baseFilters,
drilldownFilters: this.drilldownFilters,
drilldownLayers: this.drilldownLayers
});
}
// Check if there are active filters
hasActiveFilters(): boolean {
return (this.baseFilters && this.baseFilters.length > 0) ||
(this.drilldownFilters && this.drilldownFilters.length > 0) ||
this.hasActiveLayerFilters();
}
// Check if there are active layer filters for current drilldown level
hasActiveLayerFilters(): boolean {
if (this.currentDrilldownLevel > 1 && this.drilldownLayers && this.drilldownLayers.length > 0) {
const layerIndex = this.currentDrilldownLevel - 2; // -2 because level 1 is base drilldown
return layerIndex < this.drilldownLayers.length &&
this.drilldownLayers[layerIndex].filters &&
this.drilldownLayers[layerIndex].filters.length > 0;
}
return false;
}
// Get active layer filters for current drilldown level
getActiveLayerFilters(): any[] {
if (this.currentDrilldownLevel > 1 && this.drilldownLayers && this.drilldownLayers.length > 0) {
const layerIndex = this.currentDrilldownLevel - 2; // -2 because level 1 is base drilldown
if (layerIndex < this.drilldownLayers.length &&
this.drilldownLayers[layerIndex].filters) {
return this.drilldownLayers[layerIndex].filters;
}
}
return [];
}
// Get filter options for dropdown/multiselect filters
getFilterOptions(filter: any): string[] {
if (filter.options) {
if (Array.isArray(filter.options)) {
return filter.options;
} else if (typeof filter.options === 'string') {
return filter.options.split(',').map(opt => opt.trim()).filter(opt => opt);
}
}
return [];
}
// Check if an option is selected for multiselect filters
isOptionSelected(filter: any, option: string): boolean {
if (!filter.value) {
return false;
}
if (Array.isArray(filter.value)) {
return filter.value.includes(option);
}
return filter.value === option;
}
// Handle base filter changes
onBaseFilterChange(filter: any): void {
console.log('Base filter changed:', filter);
// Refresh data when filter changes
this.fetchChartData();
}
// Handle drilldown filter changes
onDrilldownFilterChange(filter: any): void {
console.log('Drilldown filter changed:', filter);
// Refresh data when filter changes
this.fetchChartData();
}
// Handle layer filter changes
onLayerFilterChange(filter: any): void {
console.log('Layer filter changed:', filter);
// Refresh data when filter changes
this.fetchChartData();
}
// Handle multiselect changes
onMultiSelectChange(filter: any, option: string, event: any): void {
const checked = event.target.checked;
// Initialize filter.value as array if it's not already
if (!Array.isArray(filter.value)) {
filter.value = [];
}
if (checked) {
// Add option to array if not already present
if (!filter.value.includes(option)) {
filter.value.push(option);
}
} else {
// Remove option from array
filter.value = filter.value.filter((item: string) => item !== option);
}
// Refresh data when filter changes
this.fetchChartData();
}
// Handle date range changes
onDateRangeChange(filter: any, dateRange: { start: string | null, end: string | null }): void {
filter.value = dateRange;
// Refresh data when filter changes
this.fetchChartData();
}
// Handle toggle changes
onToggleChange(filter: any, checked: boolean): void {
filter.value = checked;
// Refresh data when filter changes
this.fetchChartData();
}
// Toggle multiselect dropdown visibility
toggleMultiselect(filter: any, context: string): void {
const filterId = `${context}-${filter.field}`;
if (this.isMultiselectOpen(filter, context)) {
this.openMultiselects.delete(filterId);
} else {
// Close all other multiselects first
this.openMultiselects.clear();
this.openMultiselects.set(filterId, context);
// Add document click handler to close dropdown when clicking outside
this.addDocumentClickHandler();
}
}
// Add document click handler to close dropdowns when clicking outside
private addDocumentClickHandler(): void {
if (!this.documentClickHandler) {
this.documentClickHandler = (event: MouseEvent) => {
const target = event.target as HTMLElement;
// Check if click is outside any multiselect dropdown
if (!target.closest('.multiselect-container')) {
this.openMultiselects.clear();
this.removeDocumentClickHandler();
}
};
// Use setTimeout to ensure the click event that opened the dropdown doesn't immediately close it
setTimeout(() => {
document.addEventListener('click', this.documentClickHandler!);
}, 0);
}
}
// Remove document click handler
private removeDocumentClickHandler(): void {
if (this.documentClickHandler) {
document.removeEventListener('click', this.documentClickHandler);
this.documentClickHandler = null;
}
}
// Check if multiselect dropdown is open
isMultiselectOpen(filter: any, context: string): boolean {
const filterId = `${context}-${filter.field}`;
return this.openMultiselects.has(filterId);
}
// Get count of selected options for a multiselect filter
getSelectedOptionsCount(filter: any): number {
if (!filter.value) {
return 0;
}
if (Array.isArray(filter.value)) {
return filter.value.length;
}
return 0;
}
// Clear all filters
clearAllFilters(): void {
// Clear base filters
if (this.baseFilters) {
this.baseFilters.forEach(filter => {
if (filter.type === 'multiselect') {
filter.value = [];
} else if (filter.type === 'date-range') {
filter.value = { start: null, end: null };
} else if (filter.type === 'toggle') {
filter.value = false;
} else {
filter.value = '';
}
});
}
// Clear drilldown filters
if (this.drilldownFilters) {
this.drilldownFilters.forEach(filter => {
if (filter.type === 'multiselect') {
filter.value = [];
} else if (filter.type === 'date-range') {
filter.value = { start: null, end: null };
} else if (filter.type === 'toggle') {
filter.value = false;
} else {
filter.value = '';
}
});
}
// Clear layer filters
if (this.drilldownLayers) {
this.drilldownLayers.forEach(layer => {
if (layer.filters) {
layer.filters.forEach((filter: any) => {
if (filter.type === 'multiselect') {
filter.value = [];
} else if (filter.type === 'date-range') {
filter.value = { start: null, end: null };
} else if (filter.type === 'toggle') {
filter.value = false;
} else {
filter.value = '';
}
});
}
});
}
// Close all multiselect dropdowns
this.openMultiselects.clear();
// Refresh data
this.fetchChartData();
}
// Transform data to scatter chart format
private transformToScatterData(labels: any[], data: any[]): ChartDataset[] {
// For scatter charts, we need to transform the data into scatter format
// Scatter charts expect data in the format: {x: number, y: number}
console.log('Transforming data to scatter format:', { labels, data });
// If we have the expected scatter data format, return it as is
if (data && data.length > 0 && data[0].data && data[0].data.length > 0 &&
typeof data[0].data[0] === 'object' && data[0].data[0].hasOwnProperty('x') &&
data[0].data[0].hasOwnProperty('y')) {
return data;
}
// Transform the data properly for scatter chart
// Assuming labels are x-values and data[0].data are y-values
if (labels && data && data.length > 0 && data[0].data) {
const yValues = data[0].data;
const label = data[0].label || 'Dataset 1';
// Create scatter points from labels (x) and data (y)
const scatterPoints = [];
const minLength = Math.min(labels.length, yValues.length);
for (let i = 0; i < minLength; i++) {
// Convert to numbers if they're strings
const x = typeof labels[i] === 'string' ? parseFloat(labels[i]) : labels[i];
const y = typeof yValues[i] === 'string' ? parseFloat(yValues[i]) : yValues[i];
// Only add valid points
if (!isNaN(x) && !isNaN(y)) {
scatterPoints.push({ x, y });
}
}
// Generate different colors for each point to avoid all points showing the same color
const backgroundColors = [];
const borderColors = [];
for (let i = 0; i < scatterPoints.length; i++) {
// Generate a color based on the point index for variety
const hue = (i * 137.508) % 360; // Use golden angle to spread colors
backgroundColors.push(`hsla(${hue}, 70%, 50%, 0.6)`);
borderColors.push(`hsla(${hue}, 70%, 40%, 1)`);
}
// Create a single dataset with all scatter points
const scatterDatasets: ChartDataset[] = [
{
data: scatterPoints,
label: label,
pointRadius: 8,
pointHoverRadius: 10,
backgroundColor: backgroundColors,
borderColor: borderColors,
borderWidth: 1,
pointHoverBackgroundColor: 'rgba(255, 99, 132, 1)',
}
];
console.log('Transformed scatter data:', scatterDatasets);
return scatterDatasets;
}
// Otherwise, create a default scatter dataset
const scatterDatasets: ChartDataset[] = [
{
data: [
{ x: 1, y: 1 },
{ x: 2, y: 3 },
{ x: 3, y: -2 },
{ x: 4, y: 4 },
{ x: 5, y: -3 },
],
label: 'Dataset 1',
pointRadius: 10,
backgroundColor: [
'red',
'green',
'blue',
'purple',
'yellow',
'brown',
'magenta',
'cyan',
'orange',
'pink'
],
}
];
return scatterDatasets;
}
fetchChartData(): void {
// Set flag to prevent recursive calls
this.isFetchingData = true;
@@ -139,7 +652,49 @@ export class ScatterChartComponent implements OnInit, OnChanges {
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
const url = `chart/getdashjson/scatter?tableName=${this.table}&xAxis=${this.xAxis}&yAxes=${yAxisString}${this.connection ? `&sureId=${this.connection}` : ''}`;
@@ -165,11 +720,19 @@ export class ScatterChartComponent implements OnInit, OnChanges {
// Scatter charts expect data in the format: {x: number, y: number}
this.noDataAvailable = data.chartLabels.length === 0;
this.scatterChartData = this.transformToScatterData(data.chartLabels, data.chartData);
// Update chart options with axis titles
this.updateChartOptionsWithAxisTitles();
console.log('Updated scatter chart with data:', this.scatterChartData);
} else if (data && data.labels && data.datasets) {
// Handle the original expected format as fallback
this.noDataAvailable = data.labels.length === 0;
this.scatterChartData = data.datasets;
// Update chart options with axis titles
this.updateChartOptionsWithAxisTitles();
console.log('Updated scatter chart with legacy data format:', this.scatterChartData);
} else {
console.warn('Scatter chart received data does not have expected structure', data);
@@ -197,6 +760,20 @@ export class ScatterChartComponent implements OnInit, OnChanges {
}
}
// Update chart options with axis titles
private updateChartOptionsWithAxisTitles(): void {
// Update X axis title
if (this.scatterChartOptions.scales && this.scatterChartOptions.scales.x) {
this.scatterChartOptions.scales.x.title.text = this.xAxis || 'X Axis';
}
// Update Y axis title
if (this.scatterChartOptions.scales && this.scatterChartOptions.scales.y) {
const yAxisLabel = Array.isArray(this.yAxis) ? this.yAxis[0] : this.yAxis;
this.scatterChartOptions.scales.y.title.text = yAxisLabel || 'Y Axis';
}
}
// Fetch drilldown data based on current drilldown level
fetchDrilldownData(): void {
console.log('Fetching drilldown data, current level:', this.currentDrilldownLevel);
@@ -287,24 +864,34 @@ export class ScatterChartComponent implements OnInit, OnChanges {
}
}
// Convert drilldownFilters to filter parameters for drilldown level
let drilldownFilterParams = '';
if (this.drilldownFilters && this.drilldownFilters.length > 0) {
const filterObj = {};
this.drilldownFilters.forEach(filter => {
if (filter.field && filter.value) {
filterObj[filter.field] = filter.value;
// 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(filterObj).length > 0) {
drilldownFilterParams = JSON.stringify(filterObj);
}
}
console.log('Drilldown filter parameters:', drilldownFilterParams);
// Use drilldown filters if available, otherwise use layer filters
const finalFilterParams = drilldownFilterParams || filterParams;
console.log('Final filter parameters:', finalFilterParams);
if (Object.keys(mergedFilterObj).length > 0) {
filterParams = JSON.stringify(mergedFilterObj);
}
}
// Log the URL that will be called
const url = `chart/getdashjson/scatter?tableName=${actualApiUrl}&xAxis=${drilldownConfig.xAxis}&yAxes=${drilldownConfig.yAxis}${this.connection ? `&sureId=${this.connection}` : ''}`;
@@ -312,7 +899,7 @@ export class ScatterChartComponent implements OnInit, OnChanges {
// Fetch data from the dashboard service with parameter field and value
// Backend handles filtering, we just pass the parameter field and value
this.dashboardService.getChartData(actualApiUrl, 'scatter', drilldownConfig.xAxis, drilldownConfig.yAxis, this.connection, parameterField, parameterValue, finalFilterParams).subscribe(
this.dashboardService.getChartData(actualApiUrl, 'scatter', drilldownConfig.xAxis, drilldownConfig.yAxis, this.connection, parameterField, parameterValue, filterParams).subscribe(
(data: any) => {
console.log('Received drilldown data:', data);
if (data === null) {
@@ -325,7 +912,6 @@ export class ScatterChartComponent implements OnInit, OnChanges {
// Handle the actual data structure returned by the API
if (data && data.chartLabels && data.chartData) {
// For scatter charts, we need to transform the data into scatter format
// Scatter charts expect data in the format: {x: number, y: number}
this.noDataAvailable = data.chartLabels.length === 0;
this.scatterChartData = this.transformToScatterData(data.chartLabels, data.chartData);
console.log('Updated scatter chart with drilldown data:', this.scatterChartData);
@@ -349,33 +935,6 @@ export class ScatterChartComponent implements OnInit, OnChanges {
);
}
// Transform chart data to scatter chart format
private transformToScatterData(labels: string[], datasets: any[]): ChartDataset[] {
// For scatter charts, we need to transform the data into scatter format
// Scatter charts expect data in the format: {x: number, y: number}
// This is a simple transformation - in a real implementation, you might want to
// create a more sophisticated mapping based on your data structure
return datasets.map((dataset, index) => {
// Create scatter data points
const scatterData = labels.map((label, i) => {
// Use x-axis data as x coordinate, y-axis data as y coordinate
const xValue = dataset.data[i] || 0;
const yValue = i < datasets.length ? (datasets[i].data[index] || 0) : 0;
return { x: xValue, y: yValue };
});
return {
data: scatterData,
label: dataset.label || `Dataset ${index + 1}`,
backgroundColor: dataset.backgroundColor || `rgba(${Math.floor(Math.random() * 255)}, ${Math.floor(Math.random() * 255)}, ${Math.floor(Math.random() * 255)}, 0.6)`,
borderColor: dataset.borderColor || 'rgba(0, 0, 0, 1)',
pointRadius: dataset.pointRadius || 5
};
});
}
// Reset to original data (go back to base level)
resetToOriginalData(): void {
console.log('Resetting to original data');
@@ -436,16 +995,18 @@ export class ScatterChartComponent implements OnInit, OnChanges {
// Get the index of the clicked element
const clickedIndex = e.active[0].index;
// Get the label of the clicked element
// For scatter charts, we might not have labels in the same way as other charts
const clickedLabel = `Point ${clickedIndex}`;
// Get the dataset index
const datasetIndex = e.active[0].datasetIndex;
console.log('Clicked on scatter point:', { index: clickedIndex, label: clickedLabel });
// Get the data point
const dataPoint = this.scatterChartData[datasetIndex].data[clickedIndex];
console.log('Clicked on scatter point:', { datasetIndex, clickedIndex, dataPoint });
// If we're not at the base level, store original data
if (this.currentDrilldownLevel === 0) {
// Store original data before entering drilldown mode
this.originalScatterChartData = [...this.scatterChartData];
this.originalScatterChartData = JSON.parse(JSON.stringify(this.scatterChartData));
console.log('Stored original data for drilldown');
}
@@ -487,9 +1048,10 @@ export class ScatterChartComponent implements OnInit, OnChanges {
// Add this click to the drilldown stack
const stackEntry = {
level: nextDrilldownLevel,
datasetIndex: datasetIndex,
clickedIndex: clickedIndex,
clickedLabel: clickedLabel,
clickedValue: clickedLabel // Using label as value for now
dataPoint: dataPoint,
clickedValue: dataPoint // Using data point as value for now
};
this.drilldownStack.push(stackEntry);
@@ -513,6 +1075,12 @@ export class ScatterChartComponent implements OnInit, OnChanges {
}
public chartHovered(e: any): void {
console.log(e);
console.log('Scatter chart hovered:', e);
}
ngOnDestroy(): void {
this.subscriptions.forEach(sub => sub.unsubscribe());
// Clean up document click handler
this.removeDocumentClickHandler();
}
}

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

@@ -1,4 +1,287 @@
<table class="table">
<div class="to-do-chart-container">
<!-- Filter Controls Section -->
<div class="filter-section" *ngIf="hasActiveFilters()">
<!-- Base Filters -->
<div class="filter-group" *ngIf="baseFilters && baseFilters.length > 0">
<h4>Base Filters</h4>
<div class="filter-controls">
<div *ngFor="let filter of baseFilters" class="filter-item">
<div class="filter-label">{{ filter.field }} ({{ filter.type || 'text' }})</div>
<!-- Text Filter -->
<div *ngIf="!filter.type || filter.type === 'text'" class="filter-input">
<input type="text"
[(ngModel)]="filter.value"
(ngModelChange)="onBaseFilterChange(filter)"
[placeholder]="filter.field"
class="clr-input filter-text-input">
</div>
<!-- Dropdown Filter -->
<div *ngIf="filter.type === 'dropdown'" class="filter-input">
<select [(ngModel)]="filter.value"
(ngModelChange)="onBaseFilterChange(filter)"
class="clr-select filter-select">
<option value="">Select {{ filter.field }}</option>
<option *ngFor="let option of getFilterOptions(filter)" [value]="option">{{ option }}</option>
</select>
</div>
<!-- Multi-Select Filter - Updated to show key first, then dropdown on click -->
<div *ngIf="filter.type === 'multiselect'" class="filter-input multiselect-container">
<div class="multiselect-display" (click)="toggleMultiselect(filter, 'base')">
<span class="multiselect-label">{{ filter.field }}</span>
<span class="multiselect-value" *ngIf="getSelectedOptionsCount(filter) > 0">
({{ getSelectedOptionsCount(filter) }} selected)
</span>
<clr-icon shape="caret down" class="dropdown-icon"></clr-icon>
</div>
<div class="multiselect-dropdown" *ngIf="isMultiselectOpen(filter, 'base')">
<div class="checkbox-group">
<div *ngFor="let option of getFilterOptions(filter); let i = index" class="checkbox-item">
<input type="checkbox"
[checked]="isOptionSelected(filter, option)"
(change)="onMultiSelectChange(filter, option, $event)"
[id]="'base-' + filter.field + '-' + i"
class="clr-checkbox">
<label [for]="'base-' + filter.field + '-' + i" class="checkbox-label">{{ option }}</label>
</div>
</div>
</div>
</div>
<!-- Date Range Filter -->
<div *ngIf="filter.type === 'date-range'" class="filter-input date-range">
<div class="date-input-group">
<input type="date"
[(ngModel)]="filter.value.start"
(ngModelChange)="onDateRangeChange(filter, { start: $event, end: filter.value.end })"
placeholder="Start Date"
class="clr-input filter-date">
<span class="date-separator">to</span>
<input type="date"
[(ngModel)]="filter.value.end"
(ngModelChange)="onDateRangeChange(filter, { start: filter.value.start, end: $event })"
placeholder="End Date"
class="clr-input filter-date">
</div>
</div>
<!-- Toggle Filter -->
<div *ngIf="filter.type === 'toggle'" class="filter-input toggle">
<input type="checkbox"
[(ngModel)]="filter.value"
(ngModelChange)="onToggleChange(filter, $event)"
clrToggle
class="clr-toggle">
<label class="toggle-label">{{ filter.field }}</label>
</div>
</div>
</div>
</div>
<!-- Drilldown Filters -->
<div class="filter-group" *ngIf="drilldownFilters && drilldownFilters.length > 0 && currentDrilldownLevel > 0">
<h4>Drilldown Filters</h4>
<div class="filter-controls">
<div *ngFor="let filter of drilldownFilters" class="filter-item">
<div class="filter-label">{{ filter.field }} ({{ filter.type || 'text' }})</div>
<!-- Text Filter -->
<div *ngIf="!filter.type || filter.type === 'text'" class="filter-input">
<input type="text"
[(ngModel)]="filter.value"
(ngModelChange)="onDrilldownFilterChange(filter)"
[placeholder]="filter.field"
class="clr-input filter-text-input">
</div>
<!-- Dropdown Filter -->
<div *ngIf="filter.type === 'dropdown'" class="filter-input">
<select [(ngModel)]="filter.value"
(ngModelChange)="onDrilldownFilterChange(filter)"
class="clr-select filter-select">
<option value="">Select {{ filter.field }}</option>
<option *ngFor="let option of getFilterOptions(filter)" [value]="option">{{ option }}</option>
</select>
</div>
<!-- Multi-Select Filter - Updated to show key first, then dropdown on click -->
<div *ngIf="filter.type === 'multiselect'" class="filter-input multiselect-container">
<div class="multiselect-display" (click)="toggleMultiselect(filter, 'drilldown')">
<span class="multiselect-label">{{ filter.field }}</span>
<span class="multiselect-value" *ngIf="getSelectedOptionsCount(filter) > 0">
({{ getSelectedOptionsCount(filter) }} selected)
</span>
<clr-icon shape="caret down" class="dropdown-icon"></clr-icon>
</div>
<div class="multiselect-dropdown" *ngIf="isMultiselectOpen(filter, 'drilldown')">
<div class="checkbox-group">
<div *ngFor="let option of getFilterOptions(filter); let i = index" class="checkbox-item">
<input type="checkbox"
[checked]="isOptionSelected(filter, option)"
(change)="onMultiSelectChange(filter, option, $event)"
[id]="'drilldown-' + filter.field + '-' + i"
class="clr-checkbox">
<label [for]="'drilldown-' + filter.field + '-' + i" class="checkbox-label">{{ option }}</label>
</div>
</div>
</div>
</div>
<!-- Date Range Filter -->
<div *ngIf="filter.type === 'date-range'" class="filter-input date-range">
<div class="date-input-group">
<input type="date"
[(ngModel)]="filter.value.start"
(ngModelChange)="onDateRangeChange(filter, { start: $event, end: filter.value.end })"
placeholder="Start Date"
class="clr-input filter-date">
<span class="date-separator">to</span>
<input type="date"
[(ngModel)]="filter.value.end"
(ngModelChange)="onDateRangeChange(filter, { start: filter.value.start, end: $event })"
placeholder="End Date"
class="clr-input filter-date">
</div>
</div>
<!-- Toggle Filter -->
<div *ngIf="filter.type === 'toggle'" class="filter-input toggle">
<input type="checkbox"
[(ngModel)]="filter.value"
(ngModelChange)="onToggleChange(filter, $event)"
clrToggle
class="clr-toggle">
<label class="toggle-label">{{ filter.field }}</label>
</div>
</div>
</div>
</div>
<!-- Layer Filters -->
<div class="filter-group" *ngIf="hasActiveLayerFilters()">
<h4>Layer Filters</h4>
<div class="filter-controls">
<div *ngFor="let filter of getActiveLayerFilters()" class="filter-item">
<div class="filter-label">{{ filter.field }} ({{ filter.type || 'text' }})</div>
<!-- Text Filter -->
<div *ngIf="!filter.type || filter.type === 'text'" class="filter-input">
<input type="text"
[(ngModel)]="filter.value"
(ngModelChange)="onLayerFilterChange(filter)"
[placeholder]="filter.field"
class="clr-input filter-text-input">
</div>
<!-- Dropdown Filter -->
<div *ngIf="filter.type === 'dropdown'" class="filter-input">
<select [(ngModel)]="filter.value"
(ngModelChange)="onLayerFilterChange(filter)"
class="clr-select filter-select">
<option value="">Select {{ filter.field }}</option>
<option *ngFor="let option of getFilterOptions(filter)" [value]="option">{{ option }}</option>
</select>
</div>
<!-- Multi-Select Filter - Updated to show key first, then dropdown on click -->
<div *ngIf="filter.type === 'multiselect'" class="filter-input multiselect-container">
<div class="multiselect-display" (click)="toggleMultiselect(filter, 'layer')">
<span class="multiselect-label">{{ filter.field }}</span>
<span class="multiselect-value" *ngIf="getSelectedOptionsCount(filter) > 0">
({{ getSelectedOptionsCount(filter) }} selected)
</span>
<clr-icon shape="caret down" class="dropdown-icon"></clr-icon>
</div>
<div class="multiselect-dropdown" *ngIf="isMultiselectOpen(filter, 'layer')">
<div class="checkbox-group">
<div *ngFor="let option of getFilterOptions(filter); let i = index" class="checkbox-item">
<input type="checkbox"
[checked]="isOptionSelected(filter, option)"
(change)="onMultiSelectChange(filter, option, $event)"
[id]="'layer-' + filter.field + '-' + i"
class="clr-checkbox">
<label [for]="'layer-' + filter.field + '-' + i" class="checkbox-label">{{ option }}</label>
</div>
</div>
</div>
</div>
<!-- Date Range Filter -->
<div *ngIf="filter.type === 'date-range'" class="filter-input date-range">
<div class="date-input-group">
<input type="date"
[(ngModel)]="filter.value.start"
(ngModelChange)="onDateRangeChange(filter, { start: $event, end: filter.value.end })"
placeholder="Start Date"
class="clr-input filter-date">
<span class="date-separator">to</span>
<input type="date"
[(ngModel)]="filter.value.end"
(ngModelChange)="onDateRangeChange(filter, { start: filter.value.start, end: $event })"
placeholder="End Date"
class="clr-input filter-date">
</div>
</div>
<!-- Toggle Filter -->
<div *ngIf="filter.type === 'toggle'" class="filter-input toggle">
<input type="checkbox"
[(ngModel)]="filter.value"
(ngModelChange)="onToggleChange(filter, $event)"
clrToggle
class="clr-toggle">
<label class="toggle-label">{{ filter.field }}</label>
</div>
</div>
</div>
</div>
<!-- Clear Filters Button -->
<div class="filter-actions">
<button class="btn btn-sm btn-outline" (click)="clearAllFilters()">Clear All Filters</button>
</div>
</div>
<!-- Header row with chart title and drilldown navigation -->
<div class="clr-row header-row">
<div class="clr-col-6">
<h3 class="chart-title">{{charttitle || 'To Do Chart'}}</h3>
</div>
<div class="clr-col-6" style="text-align: right;">
<!-- Add drilldown navigation controls -->
<button class="btn btn-sm btn-link" *ngIf="drilldownEnabled && drilldownStack.length > 0" (click)="navigateBack()">
<cds-icon shape="arrow" direction="left"></cds-icon>
Back to {{drilldownStack.length > 1 ? 'Previous Level' : 'Main Data'}}
</button>
</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].clickedLabel}})
</span>
</span>
</div>
</div>
</div>
</div>
</div>
<!-- Existing content -->
<div class="todo-table-container">
<table class="table todo-table">
<thead>
<th class="c-col">#</th>
<th>Item</th>
@@ -8,20 +291,17 @@
<td class="c-col">{{i + 1}}</td>
<td>{{todo}}</td>
<td style="text-align:right">
<a routerLink="." (click)="removeTodo(i)">
<a (click)="removeTodo(i)" class="remove-button">
<clr-icon shape="times"></clr-icon>
</a>
</td>
</tr>
<tr>
<td></td>
<td>
<input [(ngModel)]="todo" placeholder="Add Todo" class="clr-input">
</td>
<td style="text-align:right">
<a routerLink="." color='primary' (click)="addTodo(todo)">
<clr-icon shape="plus"></clr-icon>
</a>
</td>
</tr>
</table>
</div>
<div class="add-todo-section">
<input [(ngModel)]="todo" (keyup.enter)="addTodo(todo)" placeholder="Add Todo" class="clr-input todo-input">
<button (click)="addTodo(todo)" class="btn btn-primary add-button">
<clr-icon shape="plus"></clr-icon> Add
</button>
</div>
</div>

View File

@@ -0,0 +1,289 @@
// To Do Chart specific styles
.to-do-chart-container {
padding: 20px;
height: 100%;
display: flex;
flex-direction: column;
}
.todo-table-container {
flex: 1;
overflow-y: auto;
max-height: 400px;
border: 1px solid #ddd;
border-radius: 4px;
margin-bottom: 20px; // Add margin at the bottom for spacing
.todo-table {
width: 100%;
border-collapse: collapse;
margin: 0;
th, td {
padding: 12px;
text-align: left;
border-bottom: 1px solid #ddd;
}
th {
background-color: #f2f2f2;
font-weight: bold;
position: sticky;
top: 0;
z-index: 10;
}
tr:hover {
background-color: #f5f5f5;
}
.c-col {
width: 50px;
}
// Add padding at the bottom of the table body
tbody {
tr:last-child td {
padding-bottom: 20px; // Extra padding for the last row
}
}
}
}
.add-todo-section {
display: flex;
gap: 10px;
margin-top: 15px;
padding: 20px; // Increased padding all around
border-top: 1px solid #eee;
border: 1px solid #ddd;
border-radius: 4px;
background-color: #f9f9f9;
.todo-input {
flex: 1;
padding: 12px; // Increased padding for better touch targets
border: 1px solid #ccc;
border-radius: 4px;
font-size: 14px;
}
.add-button {
white-space: nowrap;
padding: 12px 20px; // Increased padding for better touch targets
}
}
.remove-button {
background: none;
border: none;
cursor: pointer;
padding: 8px; // Increased padding for better touch targets
border-radius: 3px;
color: #dc3545;
&:hover {
background-color: #e0e0e0;
}
}
.filter-section {
margin-bottom: 20px;
padding: 15px;
border: 1px solid #ddd;
border-radius: 4px;
background-color: #f9f9f9;
}
.filter-group {
margin-bottom: 15px;
h4 {
margin-top: 0;
margin-bottom: 10px;
color: #333;
font-weight: 600;
}
}
.filter-controls {
display: flex;
flex-wrap: wrap;
gap: 15px;
}
.filter-item {
flex: 1 1 300px;
min-width: 250px;
padding: 10px;
background: white;
border: 1px solid #e0e0e0;
border-radius: 4px;
}
.filter-label {
font-weight: 500;
margin-bottom: 8px;
color: #555;
font-size: 14px;
}
.filter-input {
width: 100%;
.filter-text-input,
.filter-select,
.filter-date {
width: 100%;
padding: 6px 12px;
border: 1px solid #ccc;
border-radius: 4px;
font-size: 14px;
}
.filter-select {
height: 34px;
}
}
.multiselect-container {
position: relative;
}
.multiselect-display {
display: flex;
align-items: center;
justify-content: space-between;
padding: 6px 12px;
border: 1px solid #ccc;
border-radius: 4px;
background: white;
cursor: pointer;
min-height: 34px;
.multiselect-label {
flex: 1;
font-size: 14px;
}
.multiselect-value {
color: #666;
font-size: 12px;
margin-right: 8px;
}
.dropdown-icon {
flex-shrink: 0;
transition: transform 0.2s ease;
}
&:hover {
border-color: #999;
}
}
.multiselect-dropdown {
position: absolute;
top: 100%;
left: 0;
right: 0;
z-index: 1000;
background: white;
border: 1px solid #ccc;
border-top: none;
border-radius: 0 0 4px 4px;
box-shadow: 0 2px 4px rgba(0,0,0,0.1);
max-height: 200px;
overflow-y: auto;
.checkbox-group {
padding: 8px;
.checkbox-item {
display: flex;
align-items: center;
gap: 8px;
padding: 4px 0;
.checkbox-label {
margin: 0;
font-size: 14px;
cursor: pointer;
}
}
}
}
.date-range {
.date-input-group {
display: flex;
align-items: center;
gap: 8px;
}
.date-separator {
margin: 0 5px;
color: #777;
}
}
.toggle {
display: flex;
align-items: center;
gap: 8px;
.toggle-label {
margin: 0;
font-size: 14px;
cursor: pointer;
}
}
.filter-actions {
margin-top: 15px;
padding-top: 15px;
border-top: 1px solid #eee;
.btn {
font-size: 13px;
}
}
// New header row styling
.header-row {
margin-bottom: 15px;
padding-bottom: 10px;
border-bottom: 1px solid #eee;
.chart-title {
margin: 0;
font-size: 18px;
font-weight: 600;
color: #333;
}
}
// Responsive design
@media (max-width: 768px) {
.filter-controls {
flex-direction: column;
}
.filter-item {
min-width: 100%;
}
.header-row {
.chart-title {
font-size: 16px;
}
}
.add-todo-section {
flex-direction: column;
}
.to-do-chart-container {
padding: 15px; // Adjust padding for mobile
}
}

View File

@@ -1,4 +1,7 @@
import { Component, OnInit, Input, OnChanges, SimpleChanges } from '@angular/core';
import { Dashboard3Service } from 'src/app/services/builder/dashboard3.service';
import { FilterService } from '../../common-filter/filter.service';
import { Subscription } from 'rxjs';
@Component({
selector: 'app-to-do-chart',
@@ -21,15 +24,49 @@ export class ToDoChartComponent implements OnInit, OnChanges {
@Input() datasource: string;
@Input() fieldName: string;
@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
constructor() { }
// Multi-layer drilldown state tracking
drilldownStack: any[] = []; // Stack to track drilldown navigation history
currentDrilldownLevel: number = 0; // Current drilldown level (0 = base level)
originalTodoList: string[] = [];
constructor(
private dashboardService: Dashboard3Service,
private filterService: FilterService
) { }
ngOnInit(): void {
// Subscribe to filter changes
this.subscriptions.push(
this.filterService.filterState$.subscribe(filters => {
// When filters change, refresh the chart data
this.fetchToDoData();
})
);
// Initialize with default data
this.fetchToDoData();
}
ngOnChanges(changes: SimpleChanges): void {
console.log('ToDoChartComponent input changes:', changes);
// Initialize filter values if they haven't been initialized yet
if (!this.filtersInitialized && (changes.baseFilters || changes.drilldownFilters || changes.drilldownLayers)) {
this.initializeFilterValues();
this.filtersInitialized = true;
}
// Check if any of the key properties have changed
const xAxisChanged = changes.xAxis && !changes.xAxis.firstChange;
const yAxisChanged = changes.yAxis && !changes.yAxis.firstChange;
@@ -46,26 +83,119 @@ export class ToDoChartComponent implements OnInit, OnChanges {
data: any;
todo: string;
todoList = ['todo 1'];
todoList: string[] = [];
// Add properties for filter functionality
private openMultiselects: Map<string, string> = new Map(); // Map of filterId -> context
private documentClickHandler: ((event: MouseEvent) => void) | null = null;
private filtersInitialized: boolean = false;
// Subscriptions to unsubscribe on destroy
private subscriptions: Subscription[] = [];
fetchToDoData(): void {
// If we have the necessary data, fetch to-do data from the service
if (this.table) {
console.log('Fetching to-do data for:', { table: this.table });
if (this.table && this.xAxis) {
console.log('Fetching to-do data for:', { table: this.table, xAxis: this.xAxis, connection: this.connection });
// For to-do chart, we might want to fetch data differently
// This is a placeholder implementation - you may need to adjust based on your API
console.log('To-do chart would fetch data from table:', this.table);
// Convert baseFilters to filter parameters
let filterParams = '';
if (this.baseFilters && this.baseFilters.length > 0) {
const filterObj = {};
this.baseFilters.forEach(filter => {
if (filter.field && filter.value) {
filterObj[filter.field] = filter.value;
}
});
if (Object.keys(filterObj).length > 0) {
filterParams = JSON.stringify(filterObj);
}
}
// In a real implementation, you would connect to your service here
// For now, we'll just keep the default to-do list
// 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 {
console.log('Missing required data for to-do chart:', { table: this.table });
// 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);
// Fetch data from the dashboard service
this.dashboardService.getChartData(this.table, 'todo', this.xAxis, '', this.connection, '', '', filterParams).subscribe(
(data: any) => {
console.log('Received to-do chart data:', data);
if (data === null) {
console.warn('To-do chart API returned null data. Check if the API endpoint is working correctly.');
this.todoList = [];
return;
}
// Handle the actual data structure returned by the API
if (data && data.chartLabels) {
// Use chartLabels as the todo items
this.todoList = data.chartLabels;
} else if (data && data.labels) {
// Fallback to labels if chartLabels is not available
this.todoList = data.labels;
} else {
console.warn('To-do chart received data does not have expected structure', data);
this.todoList = [];
}
},
(error) => {
console.error('Error fetching to-do chart data:', error);
this.todoList = [];
}
);
} else {
console.log('Missing required data for to-do chart:', { table: this.table, xAxis: this.xAxis });
// Initialize with default data if no table is specified
if (this.todoList.length === 0) {
this.todoList = ['Sample Task 1', 'Sample Task 2', 'Sample Task 3'];
}
}
}
public addTodo(todo: string) {
this.todoList.push(todo);
if (todo && todo.trim() !== '') {
this.todoList.push(todo.trim());
this.todo = ''; // Clear the input field
}
}
public removeTodo(todoIx: number) {
@@ -73,4 +203,353 @@ export class ToDoChartComponent implements OnInit, OnChanges {
this.todoList.splice(todoIx, 1);
}
}
// 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.fetchToDoData();
} else {
// Back to base level
console.log('Back to base level, resetting to original data');
this.todoList = [...this.originalTodoList];
}
} else {
// Already at base level, reset to original data
console.log('Already at base level, resetting to original data');
this.todoList = [...this.originalTodoList];
}
}
// Initialize filter values with proper default values based on type
private initializeFilterValues(): void {
console.log('Initializing filter values');
// Initialize base filters
if (this.baseFilters) {
this.baseFilters.forEach(filter => {
if (filter.value === undefined || filter.value === null) {
switch (filter.type) {
case 'multiselect':
filter.value = [];
break;
case 'date-range':
filter.value = { start: null, end: null };
break;
case 'toggle':
filter.value = false;
break;
default:
filter.value = '';
}
}
});
}
// Initialize drilldown filters
if (this.drilldownFilters) {
this.drilldownFilters.forEach(filter => {
if (filter.value === undefined || filter.value === null) {
switch (filter.type) {
case 'multiselect':
filter.value = [];
break;
case 'date-range':
filter.value = { start: null, end: null };
break;
case 'toggle':
filter.value = false;
break;
default:
filter.value = '';
}
}
});
}
// Initialize layer filters
if (this.drilldownLayers) {
this.drilldownLayers.forEach(layer => {
if (layer.filters) {
layer.filters.forEach((filter: any) => {
if (filter.value === undefined || filter.value === null) {
switch (filter.type) {
case 'multiselect':
filter.value = [];
break;
case 'date-range':
filter.value = { start: null, end: null };
break;
case 'toggle':
filter.value = false;
break;
default:
filter.value = '';
}
}
});
}
});
}
console.log('Filter values initialized:', {
baseFilters: this.baseFilters,
drilldownFilters: this.drilldownFilters,
drilldownLayers: this.drilldownLayers
});
}
// Check if there are active filters
hasActiveFilters(): boolean {
return (this.baseFilters && this.baseFilters.length > 0) ||
(this.drilldownFilters && this.drilldownFilters.length > 0) ||
this.hasActiveLayerFilters();
}
// Check if there are active layer filters for current drilldown level
hasActiveLayerFilters(): boolean {
if (this.currentDrilldownLevel > 1 && this.drilldownLayers && this.drilldownLayers.length > 0) {
const layerIndex = this.currentDrilldownLevel - 2; // -2 because level 1 is base drilldown
return layerIndex < this.drilldownLayers.length &&
this.drilldownLayers[layerIndex].filters &&
this.drilldownLayers[layerIndex].filters.length > 0;
}
return false;
}
// Get active layer filters for current drilldown level
getActiveLayerFilters(): any[] {
if (this.currentDrilldownLevel > 1 && this.drilldownLayers && this.drilldownLayers.length > 0) {
const layerIndex = this.currentDrilldownLevel - 2; // -2 because level 1 is base drilldown
if (layerIndex < this.drilldownLayers.length &&
this.drilldownLayers[layerIndex].filters) {
return this.drilldownLayers[layerIndex].filters;
}
}
return [];
}
// Get filter options for dropdown/multiselect filters
getFilterOptions(filter: any): string[] {
if (filter.options) {
if (Array.isArray(filter.options)) {
return filter.options;
} else if (typeof filter.options === 'string') {
return filter.options.split(',').map(opt => opt.trim()).filter(opt => opt);
}
}
return [];
}
// Check if an option is selected for multiselect filters
isOptionSelected(filter: any, option: string): boolean {
if (!filter.value) {
return false;
}
if (Array.isArray(filter.value)) {
return filter.value.includes(option);
}
return filter.value === option;
}
// Handle base filter changes
onBaseFilterChange(filter: any): void {
console.log('Base filter changed:', filter);
// Refresh data when filter changes
this.fetchToDoData();
}
// Handle drilldown filter changes
onDrilldownFilterChange(filter: any): void {
console.log('Drilldown filter changed:', filter);
// Refresh data when filter changes
this.fetchToDoData();
}
// Handle layer filter changes
onLayerFilterChange(filter: any): void {
console.log('Layer filter changed:', filter);
// Refresh data when filter changes
this.fetchToDoData();
}
// Handle multiselect changes
onMultiSelectChange(filter: any, option: string, event: any): void {
const checked = event.target.checked;
// Initialize filter.value as array if it's not already
if (!Array.isArray(filter.value)) {
filter.value = [];
}
if (checked) {
// Add option to array if not already present
if (!filter.value.includes(option)) {
filter.value.push(option);
}
} else {
// Remove option from array
filter.value = filter.value.filter((item: string) => item !== option);
}
// Refresh data when filter changes
this.fetchToDoData();
}
// Handle date range changes
onDateRangeChange(filter: any, dateRange: { start: string | null, end: string | null }): void {
filter.value = dateRange;
// Refresh data when filter changes
this.fetchToDoData();
}
// Handle toggle changes
onToggleChange(filter: any, checked: boolean): void {
filter.value = checked;
// Refresh data when filter changes
this.fetchToDoData();
}
// Toggle multiselect dropdown visibility
toggleMultiselect(filter: any, context: string): void {
const filterId = `${context}-${filter.field}`;
if (this.isMultiselectOpen(filter, context)) {
this.openMultiselects.delete(filterId);
} else {
// Close all other multiselects first
this.openMultiselects.clear();
this.openMultiselects.set(filterId, context);
// Add document click handler to close dropdown when clicking outside
this.addDocumentClickHandler();
}
}
// Add document click handler to close dropdowns when clicking outside
private addDocumentClickHandler(): void {
if (!this.documentClickHandler) {
this.documentClickHandler = (event: MouseEvent) => {
const target = event.target as HTMLElement;
// Check if click is outside any multiselect dropdown
if (!target.closest('.multiselect-container')) {
this.openMultiselects.clear();
this.removeDocumentClickHandler();
}
};
// Use setTimeout to ensure the click event that opened the dropdown doesn't immediately close it
setTimeout(() => {
document.addEventListener('click', this.documentClickHandler!);
}, 0);
}
}
// Remove document click handler
private removeDocumentClickHandler(): void {
if (this.documentClickHandler) {
document.removeEventListener('click', this.documentClickHandler);
this.documentClickHandler = null;
}
}
// Check if multiselect dropdown is open
isMultiselectOpen(filter: any, context: string): boolean {
const filterId = `${context}-${filter.field}`;
return this.openMultiselects.has(filterId);
}
// Get count of selected options for a multiselect filter
getSelectedOptionsCount(filter: any): number {
if (!filter.value) {
return 0;
}
if (Array.isArray(filter.value)) {
return filter.value.length;
}
return 0;
}
// Clear all filters
clearAllFilters(): void {
// Clear base filters
if (this.baseFilters) {
this.baseFilters.forEach(filter => {
if (filter.type === 'multiselect') {
filter.value = [];
} else if (filter.type === 'date-range') {
filter.value = { start: null, end: null };
} else if (filter.type === 'toggle') {
filter.value = false;
} else {
filter.value = '';
}
});
}
// Clear drilldown filters
if (this.drilldownFilters) {
this.drilldownFilters.forEach(filter => {
if (filter.type === 'multiselect') {
filter.value = [];
} else if (filter.type === 'date-range') {
filter.value = { start: null, end: null };
} else if (filter.type === 'toggle') {
filter.value = false;
} else {
filter.value = '';
}
});
}
// Clear layer filters
if (this.drilldownLayers) {
this.drilldownLayers.forEach(layer => {
if (layer.filters) {
layer.filters.forEach((filter: any) => {
if (filter.type === 'multiselect') {
filter.value = [];
} else if (filter.type === 'date-range') {
filter.value = { start: null, end: null };
} else if (filter.type === 'toggle') {
filter.value = false;
} else {
filter.value = '';
}
});
}
});
}
// Close all multiselect dropdowns
this.openMultiselects.clear();
// Refresh data
this.fetchToDoData();
}
ngOnDestroy(): void {
// Unsubscribe from all subscriptions to prevent memory leaks
this.subscriptions.forEach(subscription => subscription.unsubscribe());
// Remove document click handler if it exists
this.removeDocumentClickHandler();
}
}

View File

@@ -0,0 +1 @@
export * from './unified-chart.component';

View File

@@ -0,0 +1,387 @@
<div class="chart-container" *ngIf="!noDataAvailable && !isLoading">
<!-- Back button for drilldown navigation -->
<div class="drilldown-back" *ngIf="currentDrilldownLevel > 0">
<button class="btn btn-sm btn-secondary" (click)="navigateBack()">
<clr-icon shape="arrow" dir="left"></clr-icon>
Back
</button>
<span class="drilldown-level">Level: {{ currentDrilldownLevel }}</span>
</div>
<!-- Chart title -->
<div class="chart-title" *ngIf="charttitle">
<h4>{{ charttitle }}</h4>
</div>
<!-- Filter toggle icon -->
<div class="filter-toggle-icon" *ngIf="baseFilters && baseFilters.length > 0"
(click)="toggleFilters()"
style="cursor: pointer; text-align: right; padding: 5px;">
<clr-icon shape="filter" size="24"
[style.color]="showFilters ? '#007cba' : '#666'"
title="Toggle Filters">
</clr-icon>
<span style="margin-left: 5px; font-size: 12px; color: #666;">
{{ showFilters ? 'Hide Filters' : 'Show Filters' }}
</span>
</div>
<!-- Render different chart types based on chartType input -->
<div class="chart-wrapper">
<!-- Bar Chart -->
<div *ngIf="chartType === 'bar'" class="chart-canvas-container">
<canvas baseChart
[datasets]="chartData"
[labels]="chartLabels"
[options]="chartOptions"
[legend]="chartLegend"
[chartType]="'bar'"
(chartClick)="chartClicked($event)"
(chartHover)="chartHovered($event)">
</canvas>
</div>
<!-- Line Chart -->
<div *ngIf="chartType === 'line'" class="chart-canvas-container">
<canvas baseChart
[datasets]="chartData"
[labels]="chartLabels"
[options]="chartOptions"
[legend]="chartLegend"
[chartType]="'line'"
(chartClick)="chartClicked($event)"
(chartHover)="chartHovered($event)">
</canvas>
</div>
<!-- Pie Chart -->
<div *ngIf="chartType === 'pie'" class="chart-canvas-container">
<canvas baseChart
[datasets]="chartData"
[labels]="chartLabels"
[options]="chartOptions"
[legend]="chartLegend"
[chartType]="'pie'"
(chartClick)="chartClicked($event)"
(chartHover)="chartHovered($event)">
</canvas>
</div>
<!-- Doughnut Chart -->
<div *ngIf="chartType === 'doughnut'" class="chart-canvas-container">
<canvas baseChart
[datasets]="chartData"
[labels]="chartLabels"
[options]="chartOptions"
[legend]="chartLegend"
[chartType]="'doughnut'"
(chartClick)="chartClicked($event)"
(chartHover)="chartHovered($event)">
</canvas>
</div>
<!-- Bubble Chart -->
<div *ngIf="chartType === 'bubble'" class="chart-canvas-container">
<canvas baseChart
[datasets]="bubbleChartData"
[options]="chartOptions"
[legend]="chartLegend"
[chartType]="'bubble'"
(chartClick)="chartClicked($event)"
(chartHover)="chartHovered($event)">
</canvas>
</div>
<!-- Radar Chart -->
<div *ngIf="chartType === 'radar'" class="chart-canvas-container">
<canvas baseChart
[datasets]="chartData"
[labels]="chartLabels"
[options]="chartOptions"
[legend]="chartLegend"
[chartType]="'radar'"
(chartClick)="chartClicked($event)"
(chartHover)="chartHovered($event)">
</canvas>
</div>
<!-- Polar Area Chart -->
<div *ngIf="chartType === 'polar'" class="chart-canvas-container">
<canvas baseChart
[datasets]="chartData"
[labels]="chartLabels"
[options]="chartOptions"
[legend]="chartLegend"
[chartType]="'polarArea'"
(chartClick)="chartClicked($event)"
(chartHover)="chartHovered($event)">
</canvas>
</div>
<!-- Scatter Chart -->
<div *ngIf="chartType === 'scatter'" class="chart-canvas-container">
<canvas baseChart
[datasets]="chartData"
[options]="chartOptions"
[legend]="chartLegend"
[chartType]="'scatter'"
(chartClick)="chartClicked($event)"
(chartHover)="chartHovered($event)">
</canvas>
</div>
<!-- Default/Unknown Chart Type -->
<div *ngIf="!['bar', 'line', 'pie', 'doughnut', 'bubble', 'radar', 'polar', 'scatter'].includes(chartType)" class="chart-canvas-container">
<canvas baseChart
[datasets]="chartData"
[labels]="chartLabels"
[options]="chartOptions"
[legend]="chartLegend"
[chartType]="'bar'"
(chartClick)="chartClicked($event)"
(chartHover)="chartHovered($event)">
</canvas>
</div>
</div>
<!-- Collapsible Base Filters -->
<div class="filters-section" *ngIf="baseFilters && baseFilters.length > 0 && showFilters">
<h5>Filters</h5>
<div class="filters-container">
<div class="filter-item" *ngFor="let filter of baseFilters; let i = index">
<!-- Text Filter -->
<div *ngIf="!filter.type || filter.type === 'text'" class="filter-text">
<label>{{ filter.field || 'Filter ' + (i + 1) }}</label>
<input type="text" [(ngModel)]="filter.value" (ngModelChange)="onBaseFilterChange(filter)"
class="form-control" placeholder="Enter {{ filter.field || 'value' }}">
</div>
<!-- Dropdown Filter -->
<div *ngIf="filter.type === 'dropdown'" class="filter-dropdown">
<label>{{ filter.field || 'Filter ' + (i + 1) }}</label>
<select [(ngModel)]="filter.value" (ngModelChange)="onBaseFilterChange(filter)" class="form-control">
<option value="">Select {{ filter.field || 'value' }}</option>
<option *ngFor="let option of getFilterOptions(filter)" [value]="option">
{{ option }}
</option>
</select>
</div>
<!-- Multiselect Filter -->
<div *ngIf="filter.type === 'multiselect'" class="filter-multiselect">
<label>{{ filter.field || 'Filter ' + (i + 1) }}</label>
<div class="multiselect-container">
<div class="multiselect-display" (click)="toggleMultiselect(filter, 'base')">
<span *ngIf="!filter.value || (Array.isArray(filter.value) && filter.value.length === 0)">Select {{ filter.field || 'options' }}</span>
<span *ngIf="filter.value && !Array.isArray(filter.value)">
{{ filter.value }}
</span>
<span *ngIf="filter.value && Array.isArray(filter.value) && filter.value.length > 0">
{{ filter.value.length }} selected
</span>
</div>
<div class="multiselect-dropdown" *ngIf="isMultiselectOpen(filter, 'base')">
<div class="multiselect-option" *ngFor="let option of getFilterOptions(filter)">
<input type="checkbox"
[checked]="isOptionSelected(filter, option)"
(change)="onMultiSelectChange(filter, option, $event)"
id="base-{{ filter.field }}-{{ option }}">
<label [for]="'base-' + filter.field + '-' + option">{{ option }}</label>
</div>
</div>
</div>
</div>
<!-- Date Range Filter -->
<div *ngIf="filter.type === 'date-range'" class="filter-date-range">
<label>{{ filter.field || 'Filter ' + (i + 1) }}</label>
<div class="date-range-inputs">
<input type="date" [(ngModel)]="filter.value.start" (ngModelChange)="onDateRangeInputChange(filter, 'start', $event)"
class="form-control" placeholder="Start Date">
<input type="date" [(ngModel)]="filter.value.end" (ngModelChange)="onDateRangeInputChange(filter, 'end', $event)"
class="form-control" placeholder="End Date">
</div>
</div>
<!-- Toggle Filter -->
<div *ngIf="filter.type === 'toggle'" class="filter-toggle">
<label>{{ filter.field || 'Filter ' + (i + 1) }}</label>
<div class="toggle-switch">
<input type="checkbox" [(ngModel)]="filter.value" (ngModelChange)="onToggleChange(filter, $event.target.checked)"
id="toggle-{{ filter.field }}">
<label [for]="'toggle-' + filter.field" class="toggle-label">
<span class="toggle-slider"></span>
</label>
</div>
</div>
</div>
</div>
</div>
<!-- Drilldown Filters -->
<div class="filters-section" *ngIf="drilldownFilters && drilldownFilters.length > 0 && currentDrilldownLevel > 0 && showFilters">
<h5>Drilldown Filters</h5>
<div class="filters-container">
<div class="filter-item" *ngFor="let filter of drilldownFilters; let i = index">
<!-- Text Filter -->
<div *ngIf="!filter.type || filter.type === 'text'" class="filter-text">
<label>{{ filter.field || 'Filter ' + (i + 1) }}</label>
<input type="text" [(ngModel)]="filter.value" (ngModelChange)="onDrilldownFilterChange(filter)"
class="form-control" placeholder="Enter {{ filter.field || 'value' }}">
</div>
<!-- Dropdown Filter -->
<div *ngIf="filter.type === 'dropdown'" class="filter-dropdown">
<label>{{ filter.field || 'Filter ' + (i + 1) }}</label>
<select [(ngModel)]="filter.value" (ngModelChange)="onDrilldownFilterChange(filter)" class="form-control">
<option value="">Select {{ filter.field || 'value' }}</option>
<option *ngFor="let option of getFilterOptions(filter)" [value]="option">
{{ option }}
</option>
</select>
</div>
<!-- Multiselect Filter -->
<div *ngIf="filter.type === 'multiselect'" class="filter-multiselect">
<label>{{ filter.field || 'Filter ' + (i + 1) }}</label>
<div class="multiselect-container">
<div class="multiselect-display" (click)="toggleMultiselect(filter, 'drilldown')">
<span *ngIf="!filter.value || (Array.isArray(filter.value) && filter.value.length === 0)">Select {{ filter.field || 'options' }}</span>
<span *ngIf="filter.value && !Array.isArray(filter.value)">
{{ filter.value }}
</span>
<span *ngIf="filter.value && Array.isArray(filter.value) && filter.value.length > 0">
{{ filter.value.length }} selected
</span>
</div>
<div class="multiselect-dropdown" *ngIf="isMultiselectOpen(filter, 'drilldown')">
<div class="multiselect-option" *ngFor="let option of getFilterOptions(filter)">
<input type="checkbox"
[checked]="isOptionSelected(filter, option)"
(change)="onMultiSelectChange(filter, option, $event)"
id="drilldown-{{ filter.field }}-{{ option }}">
<label [for]="'drilldown-' + filter.field + '-' + option">{{ option }}</label>
</div>
</div>
</div>
</div>
<!-- Date Range Filter -->
<div *ngIf="filter.type === 'date-range'" class="filter-date-range">
<label>{{ filter.field || 'Filter ' + (i + 1) }}</label>
<div class="date-range-inputs">
<input type="date" [(ngModel)]="filter.value.start" (ngModelChange)="onDateRangeInputChange(filter, 'start', $event)"
class="form-control" placeholder="Start Date">
<input type="date" [(ngModel)]="filter.value.end" (ngModelChange)="onDateRangeInputChange(filter, 'end', $event)"
class="form-control" placeholder="End Date">
</div>
</div>
<!-- Toggle Filter -->
<div *ngIf="filter.type === 'toggle'" class="filter-toggle">
<label>{{ filter.field || 'Filter ' + (i + 1) }}</label>
<div class="toggle-switch">
<input type="checkbox" [(ngModel)]="filter.value" (ngModelChange)="onToggleChange(filter, $event.target.checked)"
id="drilldown-toggle-{{ filter.field }}">
<label [for]="'drilldown-toggle-' + filter.field" class="toggle-label">
<span class="toggle-slider"></span>
</label>
</div>
</div>
</div>
</div>
</div>
<!-- Layer Filters -->
<div class="filters-section" *ngIf="hasActiveLayerFilters() && showFilters">
<h5>Layer Filters</h5>
<div class="filters-container">
<div class="filter-item" *ngFor="let filter of getActiveLayerFilters(); let i = index">
<!-- Text Filter -->
<div *ngIf="!filter.type || filter.type === 'text'" class="filter-text">
<label>{{ filter.field || 'Filter ' + (i + 1) }}</label>
<input type="text" [(ngModel)]="filter.value" (ngModelChange)="onLayerFilterChange(filter)"
class="form-control" placeholder="Enter {{ filter.field || 'value' }}">
</div>
<!-- Dropdown Filter -->
<div *ngIf="filter.type === 'dropdown'" class="filter-dropdown">
<label>{{ filter.field || 'Filter ' + (i + 1) }}</label>
<select [(ngModel)]="filter.value" (ngModelChange)="onLayerFilterChange(filter)" class="form-control">
<option value="">Select {{ filter.field || 'value' }}</option>
<option *ngFor="let option of getFilterOptions(filter)" [value]="option">
{{ option }}
</option>
</select>
</div>
<!-- Multiselect Filter -->
<div *ngIf="filter.type === 'multiselect'" class="filter-multiselect">
<label>{{ filter.field || 'Filter ' + (i + 1) }}</label>
<div class="multiselect-container">
<div class="multiselect-display" (click)="toggleMultiselect(filter, 'layer')">
<span *ngIf="!filter.value || (Array.isArray(filter.value) && filter.value.length === 0)">Select {{ filter.field || 'options' }}</span>
<span *ngIf="filter.value && !Array.isArray(filter.value)">
{{ filter.value }}
</span>
<span *ngIf="filter.value && Array.isArray(filter.value) && filter.value.length > 0">
{{ filter.value.length }} selected
</span>
</div>
<div class="multiselect-dropdown" *ngIf="isMultiselectOpen(filter, 'layer')">
<div class="multiselect-option" *ngFor="let option of getFilterOptions(filter)">
<input type="checkbox"
[checked]="isOptionSelected(filter, option)"
(change)="onMultiSelectChange(filter, option, $event)"
id="layer-{{ filter.field }}-{{ option }}">
<label [for]="'layer-' + filter.field + '-' + option">{{ option }}</label>
</div>
</div>
</div>
</div>
<!-- Date Range Filter -->
<div *ngIf="filter.type === 'date-range'" class="filter-date-range">
<label>{{ filter.field || 'Filter ' + (i + 1) }}</label>
<div class="date-range-inputs">
<input type="date" [(ngModel)]="filter.value.start" (ngModelChange)="onDateRangeInputChange(filter, 'start', $event)"
class="form-control" placeholder="Start Date">
<input type="date" [(ngModel)]="filter.value.end" (ngModelChange)="onDateRangeInputChange(filter, 'end', $event)"
class="form-control" placeholder="End Date">
</div>
</div>
<!-- Toggle Filter -->
<div *ngIf="filter.type === 'toggle'" class="filter-toggle">
<label>{{ filter.field || 'Filter ' + (i + 1) }}</label>
<div class="toggle-switch">
<input type="checkbox" [(ngModel)]="filter.value" (ngModelChange)="onToggleChange(filter, $event.target.checked)"
id="layer-toggle-{{ filter.field }}">
<label [for]="'layer-toggle-' + filter.field" class="toggle-label">
<span class="toggle-slider"></span>
</label>
</div>
</div>
</div>
</div>
</div>
<!-- Clear Filters Button -->
<div class="clear-filters" *ngIf="hasActiveFilters() && showFilters">
<button class="btn btn-sm btn-outline" (click)="clearAllFilters()">
Clear All Filters
</button>
</div>
</div>
<!-- No Data Available Message -->
<div class="no-data-message" *ngIf="noDataAvailable && !isLoading">
<p>No data available for the selected filters.</p>
<button class="btn btn-sm btn-primary" (click)="fetchChartData()">Retry</button>
</div>
<!-- Loading Indicator -->
<div class="loading-indicator" *ngIf="isLoading">
<div class="spinner"></div>
<p>Loading chart data...</p>
</div>

View File

@@ -0,0 +1,307 @@
.chart-container {
position: relative;
height: 100%;
width: 100%;
padding: 10px;
}
.drilldown-back {
display: flex;
align-items: center;
margin-bottom: 10px;
.drilldown-level {
margin-left: 10px;
font-size: 14px;
color: #666;
}
}
.chart-title {
text-align: center;
margin-bottom: 15px;
h4 {
margin: 0;
color: #333;
}
}
.chart-wrapper {
position: relative;
height: calc(100% - 100px);
min-height: 400px;
padding: 10px;
}
.chart-canvas-container {
position: relative;
height: 100%;
width: 100%;
padding: 15px;
box-sizing: border-box;
canvas {
display: block;
max-width: 100%;
max-height: 100%;
}
}
.filter-toggle-icon {
display: flex;
align-items: center;
justify-content: flex-end;
padding: 5px;
cursor: pointer;
clr-icon {
transition: color 0.3s ease;
&:hover {
color: #007cba !important;
}
}
span {
margin-left: 5px;
font-size: 12px;
color: #666;
transition: color 0.3s ease;
}
&:hover span {
color: #007cba;
}
}
.filters-section {
margin-top: 20px;
padding: 15px;
border: 1px solid #e0e0e0;
border-radius: 4px;
background-color: #f9f9f9;
h5 {
margin-top: 0;
margin-bottom: 10px;
color: #333;
}
}
.filters-container {
display: flex;
flex-wrap: wrap;
gap: 15px;
}
.filter-item {
flex: 1 1 200px;
min-width: 150px;
label {
display: block;
margin-bottom: 5px;
font-weight: 500;
color: #555;
}
.form-control {
width: 100%;
padding: 8px;
border: 1px solid #ccc;
border-radius: 4px;
font-size: 14px;
}
}
.filter-text,
.filter-dropdown,
.filter-date-range {
.form-control {
height: 36px;
}
}
.date-range-inputs {
display: flex;
gap: 10px;
.form-control {
flex: 1;
}
}
.filter-multiselect {
position: relative;
.multiselect-container {
position: relative;
}
.multiselect-display {
width: 100%;
padding: 8px;
border: 1px solid #ccc;
border-radius: 4px;
background-color: white;
cursor: pointer;
min-height: 36px;
display: flex;
align-items: center;
}
.multiselect-dropdown {
position: absolute;
top: 100%;
left: 0;
right: 0;
background-color: white;
border: 1px solid #ccc;
border-radius: 4px;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.15);
z-index: 1000;
max-height: 200px;
overflow-y: auto;
}
.multiselect-option {
padding: 8px 12px;
border-bottom: 1px solid #eee;
&:last-child {
border-bottom: none;
}
&:hover {
background-color: #f5f5f5;
}
input[type="checkbox"] {
margin-right: 8px;
}
label {
margin: 0;
font-weight: normal;
cursor: pointer;
}
}
}
.filter-toggle {
display: flex;
align-items: center;
gap: 10px;
.toggle-switch {
position: relative;
display: inline-block;
width: 50px;
height: 24px;
}
.toggle-switch input {
opacity: 0;
width: 0;
height: 0;
}
.toggle-label {
position: absolute;
cursor: pointer;
top: 0;
left: 0;
right: 0;
bottom: 0;
background-color: #ccc;
transition: .4s;
border-radius: 24px;
}
.toggle-slider {
position: absolute;
content: "";
height: 16px;
width: 16px;
left: 4px;
bottom: 4px;
background-color: white;
transition: .4s;
border-radius: 50%;
}
input:checked + .toggle-label {
background-color: #2196F3;
}
input:checked + .toggle-label .toggle-slider {
transform: translateX(26px);
}
}
.clear-filters {
margin-top: 15px;
text-align: center;
}
.no-data-message {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
height: 300px;
text-align: center;
color: #666;
p {
margin-bottom: 15px;
font-size: 16px;
}
}
.loading-indicator {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
height: 300px;
.spinner {
width: 40px;
height: 40px;
border: 4px solid #f3f3f3;
border-top: 4px solid #3498db;
border-radius: 50%;
animation: spin 1s linear infinite;
margin-bottom: 15px;
}
p {
margin: 0;
color: #666;
}
}
@keyframes spin {
0% { transform: rotate(0deg); }
100% { transform: rotate(360deg); }
}
@media (max-width: 768px) {
.filters-container {
flex-direction: column;
}
.filter-item {
min-width: 100%;
}
.chart-wrapper {
min-height: 250px;
}
.chart-canvas-container {
padding: 10px;
}
}

View File

@@ -0,0 +1,21 @@
import { ComponentFixture, TestBed } from '@angular/core/testing';
import { UnifiedChartComponent } from './unified-chart.component';
describe('UnifiedChartComponent', () => {
let component: UnifiedChartComponent;
let fixture: ComponentFixture<UnifiedChartComponent>;
beforeEach(() => {
TestBed.configureTestingModule({
declarations: [UnifiedChartComponent]
});
fixture = TestBed.createComponent(UnifiedChartComponent);
component = fixture.componentInstance;
fixture.detectChanges();
});
it('should create', () => {
expect(component).toBeTruthy();
});
});

View File

@@ -11,6 +11,9 @@
<h3>{{ 'all_dashboard' | translate }}</h3>
</div>
<div class="clr-col-4" style="text-align: right;">
<button class="btn btn-success" [routerLink]="['/cns-portal/shield-dashboard']">
<clr-icon shape="shield"></clr-icon>Shield Dashboard
</button>
<button id="add" class="btn btn-primary" (click)="gotoadd()">
<clr-icon shape="plus"></clr-icon>{{ 'dashboard_builder' | translate }}
</button>
@@ -112,6 +115,3 @@
</div>
</div>
</clr-modal>

View File

@@ -51,8 +51,7 @@ export class DashrunnerallComponent implements OnInit {
}
getdashboard()
{
getdashboard() {
this.dashboardService.getAllDash().subscribe((data) => {
this.data = data;
this.rows = this.data;
@@ -62,17 +61,23 @@ export class DashrunnerallComponent implements OnInit {
});
}
openModal()
{
openModal() {
this.addModall = true;
}
gotoadd()
{
gotoadd() {
this.router.navigate(['../../dashboardbuilder'], { relativeTo: this.route });
}
goToEdit(id:number)
{
this.router.navigate(['../dashrunner/'+id],{relativeTo:this.route});
// for runner line navigation
// goToEditData(id: number){
// this.router.navigate(['../editdata/'+id],{relativeTo:this.route});
// }
goToEdit(id: number) {
// Navigate to editnewdash component instead of dashrunnerline
// Pass a query parameter to indicate this is from dashboard runner
this.router.navigate(['../../dashboardbuilder/editdashn/' + id], {
relativeTo: this.route,
queryParams: { fromRunner: true }
});
}
goToEditData(id: number) {
@@ -96,8 +101,7 @@ export class DashrunnerallComponent implements OnInit {
console.log(this.rowSelected);
this.modalDelete = true;
}
delete(id)
{
delete(id) {
this.modalDelete = false;
console.log("in delete " + id);
this.dashboardService.deleteField(id).subscribe((data) => {

Some files were not shown because too many files have changed in this diff Show More