Compare commits

...

3 Commits
main ... build2

Author SHA1 Message Date
aade12d6ff filter 2025-10-25 15:02:26 +05:30
6c01e71d04 dashboard 2025-10-25 14:43:42 +05:30
f60657ca64 filter 2025-10-25 10:33:16 +05:30
27 changed files with 2671 additions and 96 deletions

View File

@ -23,6 +23,20 @@ export interface DashboardContentModel {
component?: any;
name: string;
type?:string;
// Common properties
table?: string;
connection?: string;
baseFilters?: any[];
// Common filter properties
commonFilterEnabled?: boolean;
// Drilldown properties
drilldownEnabled?: boolean;
drilldownApiUrl?: string;
drilldownXAxis?: string;
drilldownYAxis?: string;
drilldownParameter?: string;
drilldownFilters?: any[];
drilldownLayers?: any[];
}
export interface DashboardModel {
@ -67,6 +81,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

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

View File

@ -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,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,3 @@
export * from './filter.service';
export * from './common-filter.component';
export * from './chart-wrapper.component';

View File

@ -22,6 +22,8 @@ import { AlertsService } from 'src/app/services/fnd/alerts.service';
import { isArray } from 'highcharts';
// Add the SureconnectService import
import { SureconnectService } from '../sureconnect/sureconnect.service';
// Add the CommonFilterComponent import
import { CommonFilterComponent } from '../common-filter/common-filter.component';
function isNullArray(arr) {
return !Array.isArray(arr) || arr.length === 0;
@ -45,6 +47,10 @@ export class EditnewdashComponent implements OnInit {
public commonFilterForm: FormGroup; // Add common filter form
WidgetsMock: WidgetModel[] = [
{
name: 'Common Filter',
identifier: 'common_filter'
},
{
name: 'Radar Chart',
identifier: 'radar_chart'
@ -104,6 +110,7 @@ export class EditnewdashComponent implements OnInit {
public dashArr: [];
protected componentCollection = [
{ name: "Common Filter", componentInstance: CommonFilterComponent },
{ name: "Line Chart", componentInstance: LineChartComponent },
{ name: "Doughnut Chart", componentInstance: DoughnutChartComponent },
{ name: "Radar Chart", componentInstance: RadarChartComponent },
@ -201,7 +208,9 @@ export class EditnewdashComponent implements OnInit {
},
displayGrid: "always",
minCols: 10,
minRows: 10
minRows: 10,
// Add resize callback to handle chart resizing
itemResizeCallback: this.itemResize.bind(this)
};
this.editId = this.route.snapshot.params.id;
@ -500,6 +509,16 @@ export class EditnewdashComponent implements OnInit {
component: ToDoChartComponent,
name: "To Do Chart"
});
case "common_filter":
return this.dashboardArray.push({
cols: 10,
rows: 3,
x: 0,
y: 0,
chartid: maxChartId + 1,
component: CommonFilterComponent,
name: "Common Filter"
});
case "grid_view":
return this.dashboardArray.push({
cols: 5,
@ -781,6 +800,15 @@ export class EditnewdashComponent implements OnInit {
drilldownLayers: item['drilldownLayers'] || []
};
// For CommonFilterComponent, also pass baseFilters, drilldownFilters, drilldownLayers, fieldName, and connection
if (item.component && item.component.name === 'CommonFilterComponent') {
chartInputs['baseFilters'] = item['baseFilters'] || [];
chartInputs['drilldownFilters'] = item['drilldownFilters'] || [];
chartInputs['drilldownLayers'] = item['drilldownLayers'] || [];
chartInputs['fieldName'] = item['name'] || '';
chartInputs['connection'] = item['connection'] || undefined;
}
// Remove undefined properties to avoid passing unnecessary data
Object.keys(chartInputs).forEach(key => {
if (chartInputs[key] === undefined) {
@ -1247,4 +1275,17 @@ export class EditnewdashComponent implements OnInit {
// When disabling, the user can edit the filters normally
}
}
// Add method to handle item resize events
itemResize(item: any, itemComponent: any) {
console.log('Item resized:', item);
// Trigger a window resize event to notify charts to resize
window.dispatchEvent(new Event('resize'));
// Also try to directly notify the chart component if possible
if (itemComponent && itemComponent.item && itemComponent.item.component) {
// If the resized item contains a chart, we could try to call its resize method directly
// This would require the chart component to have a public resize method
}
}
}

View File

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

View File

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

View File

@ -1,6 +1,9 @@
import { Component, Input, OnInit, OnChanges, OnDestroy, SimpleChanges } from '@angular/core';
import { Component, Input, OnInit, OnChanges, OnDestroy, SimpleChanges, ViewChild } from '@angular/core';
import { Dashboard3Service } from '../../../../../../services/builder/dashboard3.service';
import { 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,33 @@ 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
}
},
y: {
beginAtZero: true
}
},
plugins: {
legend: {
display: true,
position: 'top',
},
tooltip: {
enabled: true
}
}
};
// Multi-layer drilldown state tracking
drilldownStack: any[] = []; // Stack to track drilldown navigation history
currentDrilldownLevel: number = 0; // Current drilldown level (0 = base level)
@ -57,9 +90,20 @@ export class BarChartComponent implements OnInit, OnChanges, OnDestroy {
// Subscriptions to unsubscribe on destroy
private subscriptions: Subscription[] = [];
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();
}
@ -92,6 +136,7 @@ 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);
}
}
@ -110,10 +155,15 @@ export class BarChartComponent implements OnInit, OnChanges, OnDestroy {
// 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 = '';
@ -135,11 +185,66 @@ export class BarChartComponent implements OnInit, OnChanges, OnDestroy {
filterParams = JSON.stringify(filterObj);
}
}
console.log('Base filter parameters:', filterParams);
console.log('Base filters:', this.baseFilters);
console.log('Base filter params:', filterParams);
// Add common filters to filter parameters
const commonFilters = this.filterService.getFilterValues();
const filterDefinitions = this.filterService.getFilters();
console.log('Common filters from service:', commonFilters);
console.log('Filter definitions:', filterDefinitions);
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];
console.log(`Processing filter ID: ${filterId}, Value:`, filterValue);
// Find the filter definition to get the field name
const filterDef = this.filterService.getFilters().find(f => f.id === filterId);
console.log(`Filter definition for ${filterId}:`, filterDef);
if (filterDef && filterDef.field) {
const fieldName = filterDef.field;
console.log(`Mapping filter ID ${filterId} to field name: ${fieldName}`);
if (filterValue !== undefined && filterValue !== null && filterValue !== '') {
mergedFilterObj[fieldName] = filterValue;
console.log(`Added to merged filters: ${fieldName} =`, filterValue);
}
} else {
// Fallback to using filterId as field name if no field is defined
console.log(`No field name found for filter ID ${filterId}, using ID as field name`);
if (filterValue !== undefined && filterValue !== null && filterValue !== '') {
mergedFilterObj[filterId] = filterValue;
console.log(`Added to merged filters: ${filterId} =`, filterValue);
}
}
});
if (Object.keys(mergedFilterObj).length > 0) {
filterParams = JSON.stringify(mergedFilterObj);
}
}
console.log('Final merged filter object:', filterParams);
// 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.');
@ -160,6 +265,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,6 +274,7 @@ 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;
@ -178,6 +285,7 @@ export class BarChartComponent implements OnInit, OnChanges, OnDestroy {
this.isFetchingData = false;
},
(error) => {
console.error('=== BAR CHART ERROR ===');
console.error('Error fetching bar chart data:', error);
this.noDataAvailable = true;
this.barChartLabels = [];
@ -313,6 +421,36 @@ export class BarChartComponent implements OnInit, OnChanges, OnDestroy {
drilldownFilterParams = JSON.stringify(filterObj);
}
}
// 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 (drilldownFilterParams) {
try {
const drilldownFilterObj = JSON.parse(drilldownFilterParams);
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) {
drilldownFilterParams = JSON.stringify(mergedFilterObj);
}
}
console.log('Drilldown filter parameters:', drilldownFilterParams);
// For drilldown level, we pass the parameter value from the drilldown stack and drilldown filters
@ -426,6 +564,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

View File

@ -1,5 +1,7 @@
import { Component, OnInit, Input, OnChanges, SimpleChanges, AfterViewChecked } from '@angular/core';
import { Component, OnInit, Input, OnChanges, SimpleChanges, AfterViewChecked, OnDestroy } from '@angular/core';
import { Dashboard3Service } from 'src/app/services/builder/dashboard3.service';
import { FilterService } from '../../common-filter/filter.service';
import { Subscription } from 'rxjs';
@Component({
selector: 'app-doughnut-chart',
@ -97,9 +99,23 @@ export class DoughnutChartComponent implements OnInit, OnChanges, AfterViewCheck
// Flag to prevent infinite loops
private isFetchingData: boolean = false;
constructor(private dashboardService: Dashboard3Service) { }
// Subscriptions to unsubscribe on destroy
private subscriptions: Subscription[] = [];
constructor(
private dashboardService: Dashboard3Service,
private filterService: FilterService
) { }
ngOnInit(): void {
// 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();
this.fetchChartData();
@ -180,6 +196,15 @@ export class DoughnutChartComponent implements OnInit, OnChanges, AfterViewCheck
});
}
ngOnDestroy(): void {
this.subscriptions.forEach(sub => sub.unsubscribe());
}
// Public method to refresh data when filters change
refreshData(): void {
this.fetchChartData();
}
fetchChartData(): void {
// Set flag to prevent recursive calls
this.isFetchingData = true;
@ -212,7 +237,49 @@ 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}` : ''}`;

View File

@ -1,5 +1,7 @@
import { Component, OnInit, Input, OnChanges, SimpleChanges } from '@angular/core';
import { Component, OnInit, Input, OnChanges, SimpleChanges, OnDestroy } from '@angular/core';
import { Dashboard3Service } from 'src/app/services/builder/dashboard3.service';
import { FilterService } from '../../common-filter/filter.service';
import { Subscription } from 'rxjs';
@Component({
selector: 'app-line-chart',
@ -83,9 +85,23 @@ 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[] = [];
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();
}
@ -122,6 +138,15 @@ export class LineChartComponent implements OnInit, OnChanges {
}
}
ngOnDestroy(): void {
this.subscriptions.forEach(sub => sub.unsubscribe());
}
// Public method to refresh data when filters change
refreshData(): void {
this.fetchChartData();
}
fetchChartData(): void {
// Set flag to prevent recursive calls
this.isFetchingData = true;
@ -154,7 +179,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 +380,35 @@ export class LineChartComponent implements OnInit, OnChanges {
}
}
// Add common filters to drilldown filter parameters
const commonFilters = this.filterService.getFilterValues();
if (Object.keys(commonFilters).length > 0) {
// Merge common filters with drilldown filters
const mergedFilterObj = {};
// Add drilldown filters first
if (filterParams) {
try {
const drilldownFilterObj = JSON.parse(filterParams);
Object.assign(mergedFilterObj, drilldownFilterObj);
} catch (e) {
console.warn('Failed to parse drilldown filter parameters:', e);
}
}
// Add common filters
Object.keys(commonFilters).forEach(key => {
const value = commonFilters[key];
if (value !== undefined && value !== null && value !== '') {
mergedFilterObj[key] = value;
}
});
if (Object.keys(mergedFilterObj).length > 0) {
filterParams = JSON.stringify(mergedFilterObj);
}
}
// Log the URL that will be called
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,5 +1,7 @@
import { Component, OnInit, Input, OnChanges, SimpleChanges, AfterViewChecked } from '@angular/core';
import { Component, OnInit, Input, OnChanges, SimpleChanges, AfterViewChecked, OnDestroy } from '@angular/core';
import { Dashboard3Service } from 'src/app/services/builder/dashboard3.service';
import { FilterService } from '../../common-filter/filter.service';
import { Subscription } from 'rxjs';
@Component({
selector: 'app-pie-chart',
@ -96,7 +98,13 @@ export class PieChartComponent implements OnInit, OnChanges, AfterViewChecked {
// Flag to prevent infinite loops
private isFetchingData: boolean = false;
constructor(private dashboardService: Dashboard3Service) { }
// Subscriptions to unsubscribe on destroy
private subscriptions: Subscription[] = [];
constructor(
private dashboardService: Dashboard3Service,
private filterService: FilterService
) { }
/**
* Force chart redraw
@ -108,6 +116,14 @@ 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();
@ -140,6 +156,15 @@ export class PieChartComponent implements OnInit, OnChanges, AfterViewChecked {
}
}
ngOnDestroy(): void {
this.subscriptions.forEach(sub => sub.unsubscribe());
}
// Public method to refresh data when filters change
refreshData(): void {
this.fetchChartData();
}
fetchChartData(): void {
// Set flag to prevent recursive calls
this.isFetchingData = true;
@ -172,7 +197,49 @@ 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}` : ''}`;
@ -362,6 +429,35 @@ export class PieChartComponent implements OnInit, OnChanges, AfterViewChecked {
}
}
// Add common filters to drilldown filter parameters
const commonFilters = this.filterService.getFilterValues();
if (Object.keys(commonFilters).length > 0) {
// Merge common filters with drilldown filters
const mergedFilterObj = {};
// Add drilldown filters first
if (filterParams) {
try {
const drilldownFilterObj = JSON.parse(filterParams);
Object.assign(mergedFilterObj, drilldownFilterObj);
} catch (e) {
console.warn('Failed to parse drilldown filter parameters:', e);
}
}
// Add common filters
Object.keys(commonFilters).forEach(key => {
const value = commonFilters[key];
if (value !== undefined && value !== null && value !== '') {
mergedFilterObj[key] = value;
}
});
if (Object.keys(mergedFilterObj).length > 0) {
filterParams = JSON.stringify(mergedFilterObj);
}
}
// Log the URL that will be called
const url = `chart/getdashjson/pie?tableName=${actualApiUrl}&xAxis=${drilldownConfig.xAxis}&yAxes=${drilldownConfig.yAxis}${this.connection ? `&sureId=${this.connection}` : ''}`;
console.log('Drilldown data URL:', url);

View File

@ -1,7 +1,7 @@
import { Injectable } from '@angular/core';
import { Router } from '@angular/router';
import { Observable, BehaviorSubject } from 'rxjs';
import { Observable, BehaviorSubject, Subject } from 'rxjs';
import { UserInfoService, LoginInfoInStorage} from '../user-info.service';
import { ApiRequestService } from './api-request.service';
import { HttpClient } from '@angular/common/http';
@ -43,63 +43,70 @@ export class LoginService {
*/
let loginDataSubject:BehaviorSubject<any> = new BehaviorSubject<any>([]); // Will use this BehaviorSubject to emit data that we want after ajax login attempt
// let loginDataSubject:BehaviorSubject<any> = new BehaviorSubject<any>([]); // Will use this BehaviorSubject to emit data that we want after ajax login attempt
let loginDataSubject: Subject<any> = new Subject<any>();
let loginInfoReturn:LoginInfoInStorage; // Object that we want to send back to Login Page
this.apiRequest.loginAuthentication('token/session', bodyData)
.subscribe(jsonResp => {
console.log('login response in service : ', jsonResp);
if (jsonResp.operationMessage=='Login Failed') {
this.toastr.warning('Not Login Getting Error check your Username and password');
}
if (jsonResp !== undefined && jsonResp !== null && jsonResp.operationStatus === "SUCCESS"){
//Create a success object that we want to send back to login page
////"displayName": jsonResp.item.fullname,
//"username" : jsonResp.item.username,
loginInfoReturn = {
"success" : true,
"message" : jsonResp.operationMessage,
"landingPage": this.landingPage,
"user" : {
"userId" : jsonResp.item.userId,
"email" : jsonResp.item.email,
"displayName": jsonResp.item.firstName,
"username" : jsonResp.item.username,
"roles" : jsonResp.item.roles,
"token" : jsonResp.item.token,
},
};
console.log(loginInfoReturn.user);
if(jsonResp !== undefined && jsonResp !== null && jsonResp.operationStatus === "SUCCESS"){
this.toastr.success(`Welcome To home Page!! your Role is ${jsonResp.item.roles}`);
.subscribe({
next: (jsonResp) => {
console.log('login response in service : ', jsonResp);
if (jsonResp.operationMessage=='Login Failed') {
this.toastr.warning('Not Login Getting Error check your Username and password');
}
// store username and jwt token in session storage to keep user logged in between page refreshes
this.userInfoService.storeUserInfo(JSON.stringify(loginInfoReturn.user));
}
else {
//Create a faliure object that we want to send back to login page
if (jsonResp !== undefined && jsonResp !== null && jsonResp.operationStatus === "SUCCESS"){
//Create a success object that we want to send back to login page
////"displayName": jsonResp.item.fullname,
//"username" : jsonResp.item.username,
loginInfoReturn = {
"success" : true,
"message" : jsonResp.operationMessage,
"landingPage": this.landingPage,
"user" : {
"userId" : jsonResp.item.userId,
"email" : jsonResp.item.email,
"displayName": jsonResp.item.firstName,
"username" : jsonResp.item.username,
"roles" : jsonResp.item.roles,
"token" : jsonResp.item.token,
},
};
console.log(loginInfoReturn.user);
if(jsonResp !== undefined && jsonResp !== null && jsonResp.operationStatus === "SUCCESS"){
this.toastr.success(`Welcome To home Page!! your Role is ${jsonResp.item.roles}`);
}
// store username and jwt token in session storage to keep user logged in between page refreshes
this.userInfoService.storeUserInfo(JSON.stringify(loginInfoReturn.user));
}
else {
//Create a failure object that we want to send back to login page
loginInfoReturn = {
"success":false,
"message":jsonResp.operationMessage,
"landingPage":"/login"
};
}
loginDataSubject.next(loginInfoReturn);
loginDataSubject.complete(); // Complete the subject
},
error: (err) => {
console.log('login error ', err);
loginInfoReturn = {
"success":false,
"message":jsonResp.operationMessage,
"landingPage":"/login"
"success": false,
"message": err.url + " >>> " + err.statusText + "[" + err.status +"]",
"landingPage": "/login"
};
if (err) {
this.toastr.error('Getting Server Error');
}
loginDataSubject.next(loginInfoReturn); // Send the error response
loginDataSubject.complete(); // Complete the subject
}
loginDataSubject.next(loginInfoReturn);
},
err => {
console.log('login error ', err);
loginInfoReturn = {
"success": false,
"message": err.url + " >>> " + err.statusText + "[" + err.status +"]",
"landingPage": "/login"
};
if (err) {
this.toastr.error('Getting Server Error');
}
});
return loginDataSubject;
return loginDataSubject;
}
logout(navigatetoLogout=true): void {

View File

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

View File

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

View File

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