filter
This commit is contained in:
parent
0e3aa9b903
commit
f60657ca64
@ -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.
|
||||||
@ -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
|
||||||
@ -0,0 +1,23 @@
|
|||||||
|
.chart-wrapper {
|
||||||
|
height: 100%;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
|
||||||
|
.chart-header {
|
||||||
|
padding: 10px 15px;
|
||||||
|
background: #f8f8f8;
|
||||||
|
border-bottom: 1px solid #eee;
|
||||||
|
|
||||||
|
h5 {
|
||||||
|
margin: 0;
|
||||||
|
color: #333;
|
||||||
|
font-size: 16px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.chart-container {
|
||||||
|
flex: 1;
|
||||||
|
padding: 15px;
|
||||||
|
overflow: auto;
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -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();
|
||||||
|
});
|
||||||
|
});
|
||||||
@ -0,0 +1,90 @@
|
|||||||
|
import { Component, Input, OnInit, OnDestroy, ComponentRef, ViewChild, ViewContainerRef } 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();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
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();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -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>
|
||||||
@ -0,0 +1,103 @@
|
|||||||
|
.common-filter-container {
|
||||||
|
padding: 15px;
|
||||||
|
background: #f5f5f5;
|
||||||
|
border: 1px solid #ddd;
|
||||||
|
border-radius: 4px;
|
||||||
|
margin-bottom: 20px;
|
||||||
|
|
||||||
|
.filter-header {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
margin-bottom: 15px;
|
||||||
|
|
||||||
|
h4 {
|
||||||
|
margin: 0;
|
||||||
|
color: #333;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.presets-section {
|
||||||
|
margin-bottom: 15px;
|
||||||
|
|
||||||
|
.preset-controls {
|
||||||
|
display: flex;
|
||||||
|
gap: 10px;
|
||||||
|
align-items: center;
|
||||||
|
|
||||||
|
select {
|
||||||
|
flex: 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.save-preset-section {
|
||||||
|
margin-bottom: 15px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.filters-form {
|
||||||
|
.filters-grid {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: repeat(auto-fill, minmax(300px, 1fr));
|
||||||
|
gap: 15px;
|
||||||
|
|
||||||
|
.filter-item {
|
||||||
|
background: white;
|
||||||
|
border: 1px solid #e0e0e0;
|
||||||
|
border-radius: 4px;
|
||||||
|
padding: 15px;
|
||||||
|
|
||||||
|
.filter-header {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
margin-bottom: 10px;
|
||||||
|
|
||||||
|
.filter-label {
|
||||||
|
font-weight: bold;
|
||||||
|
color: #333;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.filter-type-select {
|
||||||
|
width: 100%;
|
||||||
|
margin-bottom: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.clr-form-control {
|
||||||
|
margin-bottom: 10px;
|
||||||
|
|
||||||
|
input, select, textarea {
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.date-range-controls {
|
||||||
|
display: flex;
|
||||||
|
gap: 10px;
|
||||||
|
|
||||||
|
.clr-form-control {
|
||||||
|
flex: 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.toggle-control {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.multiselect {
|
||||||
|
height: 100px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.no-filters {
|
||||||
|
text-align: center;
|
||||||
|
padding: 20px;
|
||||||
|
color: #666;
|
||||||
|
font-style: italic;
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -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');
|
||||||
|
});
|
||||||
|
});
|
||||||
@ -0,0 +1,173 @@
|
|||||||
|
import { Component, OnInit, OnDestroy } 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 {
|
||||||
|
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());
|
||||||
|
}
|
||||||
|
|
||||||
|
// 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 {
|
||||||
|
this.filterService.updateFilterValue(filterId, value);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -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');
|
||||||
|
});
|
||||||
|
});
|
||||||
@ -0,0 +1,191 @@
|
|||||||
|
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 {
|
||||||
|
const currentState = this.filterStateSubject.value;
|
||||||
|
this.filterStateSubject.next({
|
||||||
|
...currentState,
|
||||||
|
[filterId]: value
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// 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);
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -0,0 +1,3 @@
|
|||||||
|
export * from './filter.service';
|
||||||
|
export * from './common-filter.component';
|
||||||
|
export * from './chart-wrapper.component';
|
||||||
@ -22,6 +22,8 @@ import { AlertsService } from 'src/app/services/fnd/alerts.service';
|
|||||||
import { isArray } from 'highcharts';
|
import { isArray } from 'highcharts';
|
||||||
// Add the SureconnectService import
|
// Add the SureconnectService import
|
||||||
import { SureconnectService } from '../sureconnect/sureconnect.service';
|
import { SureconnectService } from '../sureconnect/sureconnect.service';
|
||||||
|
// Add the CommonFilterComponent import
|
||||||
|
import { CommonFilterComponent } from '../common-filter/common-filter.component';
|
||||||
|
|
||||||
function isNullArray(arr) {
|
function isNullArray(arr) {
|
||||||
return !Array.isArray(arr) || arr.length === 0;
|
return !Array.isArray(arr) || arr.length === 0;
|
||||||
@ -45,6 +47,10 @@ export class EditnewdashComponent implements OnInit {
|
|||||||
public commonFilterForm: FormGroup; // Add common filter form
|
public commonFilterForm: FormGroup; // Add common filter form
|
||||||
|
|
||||||
WidgetsMock: WidgetModel[] = [
|
WidgetsMock: WidgetModel[] = [
|
||||||
|
{
|
||||||
|
name: 'Common Filter',
|
||||||
|
identifier: 'common_filter'
|
||||||
|
},
|
||||||
{
|
{
|
||||||
name: 'Radar Chart',
|
name: 'Radar Chart',
|
||||||
identifier: 'radar_chart'
|
identifier: 'radar_chart'
|
||||||
@ -104,6 +110,7 @@ export class EditnewdashComponent implements OnInit {
|
|||||||
public dashArr: [];
|
public dashArr: [];
|
||||||
|
|
||||||
protected componentCollection = [
|
protected componentCollection = [
|
||||||
|
{ name: "Common Filter", componentInstance: CommonFilterComponent },
|
||||||
{ name: "Line Chart", componentInstance: LineChartComponent },
|
{ name: "Line Chart", componentInstance: LineChartComponent },
|
||||||
{ name: "Doughnut Chart", componentInstance: DoughnutChartComponent },
|
{ name: "Doughnut Chart", componentInstance: DoughnutChartComponent },
|
||||||
{ name: "Radar Chart", componentInstance: RadarChartComponent },
|
{ name: "Radar Chart", componentInstance: RadarChartComponent },
|
||||||
@ -500,6 +507,16 @@ export class EditnewdashComponent implements OnInit {
|
|||||||
component: ToDoChartComponent,
|
component: ToDoChartComponent,
|
||||||
name: "To Do Chart"
|
name: "To Do Chart"
|
||||||
});
|
});
|
||||||
|
case "common_filter":
|
||||||
|
return this.dashboardArray.push({
|
||||||
|
cols: 10,
|
||||||
|
rows: 3,
|
||||||
|
x: 0,
|
||||||
|
y: 0,
|
||||||
|
chartid: maxChartId + 1,
|
||||||
|
component: CommonFilterComponent,
|
||||||
|
name: "Common Filter"
|
||||||
|
});
|
||||||
case "grid_view":
|
case "grid_view":
|
||||||
return this.dashboardArray.push({
|
return this.dashboardArray.push({
|
||||||
cols: 5,
|
cols: 5,
|
||||||
|
|||||||
@ -1,6 +1,7 @@
|
|||||||
import { Component, Input, OnInit, OnChanges, OnDestroy, SimpleChanges } from '@angular/core';
|
import { Component, Input, OnInit, OnChanges, OnDestroy, SimpleChanges } from '@angular/core';
|
||||||
import { Dashboard3Service } from '../../../../../../services/builder/dashboard3.service';
|
import { Dashboard3Service } from '../../../../../../services/builder/dashboard3.service';
|
||||||
import { Subscription } from 'rxjs';
|
import { Subscription } from 'rxjs';
|
||||||
|
import { FilterService } from '../../common-filter/filter.service';
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
selector: 'app-bar-chart',
|
selector: 'app-bar-chart',
|
||||||
@ -57,9 +58,20 @@ export class BarChartComponent implements OnInit, OnChanges, OnDestroy {
|
|||||||
// Subscriptions to unsubscribe on destroy
|
// Subscriptions to unsubscribe on destroy
|
||||||
private subscriptions: Subscription[] = [];
|
private subscriptions: Subscription[] = [];
|
||||||
|
|
||||||
constructor(private dashboardService: Dashboard3Service) { }
|
constructor(
|
||||||
|
private dashboardService: Dashboard3Service,
|
||||||
|
private filterService: FilterService
|
||||||
|
) { }
|
||||||
|
|
||||||
ngOnInit(): void {
|
ngOnInit(): void {
|
||||||
|
// Subscribe to filter changes
|
||||||
|
this.subscriptions.push(
|
||||||
|
this.filterService.filterState$.subscribe(filters => {
|
||||||
|
// When filters change, refresh the chart data
|
||||||
|
this.fetchChartData();
|
||||||
|
})
|
||||||
|
);
|
||||||
|
|
||||||
// Initialize with default data
|
// Initialize with default data
|
||||||
this.fetchChartData();
|
this.fetchChartData();
|
||||||
}
|
}
|
||||||
@ -135,7 +147,49 @@ export class BarChartComponent implements OnInit, OnChanges, OnDestroy {
|
|||||||
filterParams = JSON.stringify(filterObj);
|
filterParams = JSON.stringify(filterObj);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
console.log('Base filter parameters:', filterParams);
|
|
||||||
|
// Add common filters to filter parameters
|
||||||
|
const commonFilters = this.filterService.getFilterValues();
|
||||||
|
console.log('Common filters from service:', commonFilters);
|
||||||
|
|
||||||
|
if (Object.keys(commonFilters).length > 0) {
|
||||||
|
// Merge common filters with base filters
|
||||||
|
const mergedFilterObj = {};
|
||||||
|
|
||||||
|
// Add base filters first
|
||||||
|
if (filterParams) {
|
||||||
|
try {
|
||||||
|
const baseFilterObj = JSON.parse(filterParams);
|
||||||
|
Object.assign(mergedFilterObj, baseFilterObj);
|
||||||
|
} catch (e) {
|
||||||
|
console.warn('Failed to parse base filter parameters:', e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add common filters using the field name as the key, not the filter id
|
||||||
|
Object.keys(commonFilters).forEach(filterId => {
|
||||||
|
const filterValue = commonFilters[filterId];
|
||||||
|
// Find the filter definition to get the field name
|
||||||
|
const filterDef = this.filterService.getFilters().find(f => f.id === filterId);
|
||||||
|
if (filterDef && filterDef.field) {
|
||||||
|
const fieldName = filterDef.field;
|
||||||
|
if (filterValue !== undefined && filterValue !== null && filterValue !== '') {
|
||||||
|
mergedFilterObj[fieldName] = filterValue;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// Fallback to using filterId as field name if no field is defined
|
||||||
|
if (filterValue !== undefined && filterValue !== null && filterValue !== '') {
|
||||||
|
mergedFilterObj[filterId] = filterValue;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
if (Object.keys(mergedFilterObj).length > 0) {
|
||||||
|
filterParams = JSON.stringify(mergedFilterObj);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log('Final filter parameters:', filterParams);
|
||||||
// Fetch data from the dashboard service with parameter field and value
|
// Fetch data from the dashboard service with parameter field and value
|
||||||
// For base level, we pass empty parameter and value, but now also pass filters
|
// For base level, we pass empty parameter and value, but now also pass filters
|
||||||
const subscription = this.dashboardService.getChartData(this.table, 'bar', this.xAxis, yAxisString, this.connection, '', '', filterParams).subscribe(
|
const subscription = this.dashboardService.getChartData(this.table, 'bar', this.xAxis, yAxisString, this.connection, '', '', filterParams).subscribe(
|
||||||
@ -313,6 +367,36 @@ export class BarChartComponent implements OnInit, OnChanges, OnDestroy {
|
|||||||
drilldownFilterParams = JSON.stringify(filterObj);
|
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);
|
console.log('Drilldown filter parameters:', drilldownFilterParams);
|
||||||
|
|
||||||
// For drilldown level, we pass the parameter value from the drilldown stack and drilldown filters
|
// For drilldown level, we pass the parameter value from the drilldown stack and drilldown filters
|
||||||
@ -426,6 +510,11 @@ export class BarChartComponent implements OnInit, OnChanges, OnDestroy {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Public method to refresh data when filters change
|
||||||
|
refreshData(): void {
|
||||||
|
this.fetchChartData();
|
||||||
|
}
|
||||||
|
|
||||||
// Ensure labels and data arrays have the same length
|
// Ensure labels and data arrays have the same length
|
||||||
private syncLabelAndDataArrays(): void {
|
private syncLabelAndDataArrays(): void {
|
||||||
// For bar charts, we need to ensure all datasets have the same number of data points
|
// For bar charts, we need to ensure all datasets have the same number of data points
|
||||||
|
|||||||
@ -1,5 +1,7 @@
|
|||||||
import { Component, OnInit, Input, OnChanges, SimpleChanges, AfterViewChecked } from '@angular/core';
|
import { Component, OnInit, Input, OnChanges, SimpleChanges, AfterViewChecked, OnDestroy } from '@angular/core';
|
||||||
import { Dashboard3Service } from 'src/app/services/builder/dashboard3.service';
|
import { Dashboard3Service } from 'src/app/services/builder/dashboard3.service';
|
||||||
|
import { FilterService } from '../../common-filter/filter.service';
|
||||||
|
import { Subscription } from 'rxjs';
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
selector: 'app-doughnut-chart',
|
selector: 'app-doughnut-chart',
|
||||||
@ -97,9 +99,23 @@ export class DoughnutChartComponent implements OnInit, OnChanges, AfterViewCheck
|
|||||||
// Flag to prevent infinite loops
|
// Flag to prevent infinite loops
|
||||||
private isFetchingData: boolean = false;
|
private isFetchingData: boolean = false;
|
||||||
|
|
||||||
constructor(private dashboardService: Dashboard3Service) { }
|
// Subscriptions to unsubscribe on destroy
|
||||||
|
private subscriptions: Subscription[] = [];
|
||||||
|
|
||||||
|
constructor(
|
||||||
|
private dashboardService: Dashboard3Service,
|
||||||
|
private filterService: FilterService
|
||||||
|
) { }
|
||||||
|
|
||||||
ngOnInit(): void {
|
ngOnInit(): void {
|
||||||
|
// Subscribe to filter changes
|
||||||
|
this.subscriptions.push(
|
||||||
|
this.filterService.filterState$.subscribe(filters => {
|
||||||
|
// When filters change, refresh the chart data
|
||||||
|
this.fetchChartData();
|
||||||
|
})
|
||||||
|
);
|
||||||
|
|
||||||
// Validate initial data
|
// Validate initial data
|
||||||
this.validateChartData();
|
this.validateChartData();
|
||||||
this.fetchChartData();
|
this.fetchChartData();
|
||||||
@ -180,6 +196,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 {
|
fetchChartData(): void {
|
||||||
// Set flag to prevent recursive calls
|
// Set flag to prevent recursive calls
|
||||||
this.isFetchingData = true;
|
this.isFetchingData = true;
|
||||||
@ -212,7 +237,49 @@ export class DoughnutChartComponent implements OnInit, OnChanges, AfterViewCheck
|
|||||||
filterParams = JSON.stringify(filterObj);
|
filterParams = JSON.stringify(filterObj);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
console.log('Base filter parameters:', filterParams);
|
|
||||||
|
// Add common filters to filter parameters
|
||||||
|
const commonFilters = this.filterService.getFilterValues();
|
||||||
|
console.log('Common filters from service:', commonFilters);
|
||||||
|
|
||||||
|
if (Object.keys(commonFilters).length > 0) {
|
||||||
|
// Merge common filters with base filters
|
||||||
|
const mergedFilterObj = {};
|
||||||
|
|
||||||
|
// Add base filters first
|
||||||
|
if (filterParams) {
|
||||||
|
try {
|
||||||
|
const baseFilterObj = JSON.parse(filterParams);
|
||||||
|
Object.assign(mergedFilterObj, baseFilterObj);
|
||||||
|
} catch (e) {
|
||||||
|
console.warn('Failed to parse base filter parameters:', e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add common filters using the field name as the key, not the filter id
|
||||||
|
Object.keys(commonFilters).forEach(filterId => {
|
||||||
|
const filterValue = commonFilters[filterId];
|
||||||
|
// Find the filter definition to get the field name
|
||||||
|
const filterDef = this.filterService.getFilters().find(f => f.id === filterId);
|
||||||
|
if (filterDef && filterDef.field) {
|
||||||
|
const fieldName = filterDef.field;
|
||||||
|
if (filterValue !== undefined && filterValue !== null && filterValue !== '') {
|
||||||
|
mergedFilterObj[fieldName] = filterValue;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// Fallback to using filterId as field name if no field is defined
|
||||||
|
if (filterValue !== undefined && filterValue !== null && filterValue !== '') {
|
||||||
|
mergedFilterObj[filterId] = filterValue;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
if (Object.keys(mergedFilterObj).length > 0) {
|
||||||
|
filterParams = JSON.stringify(mergedFilterObj);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log('Final filter parameters:', filterParams);
|
||||||
|
|
||||||
// Log the URL that will be called
|
// Log the URL that will be called
|
||||||
const url = `chart/getdashjson/doughnut?tableName=${this.table}&xAxis=${this.xAxis}&yAxes=${yAxisString}${this.connection ? `&sureId=${this.connection}` : ''}`;
|
const url = `chart/getdashjson/doughnut?tableName=${this.table}&xAxis=${this.xAxis}&yAxes=${yAxisString}${this.connection ? `&sureId=${this.connection}` : ''}`;
|
||||||
|
|||||||
@ -1,5 +1,7 @@
|
|||||||
import { Component, OnInit, Input, OnChanges, SimpleChanges } from '@angular/core';
|
import { Component, OnInit, Input, OnChanges, SimpleChanges, OnDestroy } from '@angular/core';
|
||||||
import { Dashboard3Service } from 'src/app/services/builder/dashboard3.service';
|
import { Dashboard3Service } from 'src/app/services/builder/dashboard3.service';
|
||||||
|
import { FilterService } from '../../common-filter/filter.service';
|
||||||
|
import { Subscription } from 'rxjs';
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
selector: 'app-line-chart',
|
selector: 'app-line-chart',
|
||||||
@ -83,9 +85,23 @@ export class LineChartComponent implements OnInit, OnChanges {
|
|||||||
// Flag to prevent infinite loops
|
// Flag to prevent infinite loops
|
||||||
private isFetchingData: boolean = false;
|
private isFetchingData: boolean = false;
|
||||||
|
|
||||||
constructor(private dashboardService: Dashboard3Service) { }
|
// Subscriptions to unsubscribe on destroy
|
||||||
|
private subscriptions: Subscription[] = [];
|
||||||
|
|
||||||
|
constructor(
|
||||||
|
private dashboardService: Dashboard3Service,
|
||||||
|
private filterService: FilterService
|
||||||
|
) { }
|
||||||
|
|
||||||
ngOnInit(): void {
|
ngOnInit(): void {
|
||||||
|
// Subscribe to filter changes
|
||||||
|
this.subscriptions.push(
|
||||||
|
this.filterService.filterState$.subscribe(filters => {
|
||||||
|
// When filters change, refresh the chart data
|
||||||
|
this.fetchChartData();
|
||||||
|
})
|
||||||
|
);
|
||||||
|
|
||||||
// Initialize with default data
|
// Initialize with default data
|
||||||
this.fetchChartData();
|
this.fetchChartData();
|
||||||
}
|
}
|
||||||
@ -122,6 +138,15 @@ export class LineChartComponent implements OnInit, OnChanges {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
ngOnDestroy(): void {
|
||||||
|
this.subscriptions.forEach(sub => sub.unsubscribe());
|
||||||
|
}
|
||||||
|
|
||||||
|
// Public method to refresh data when filters change
|
||||||
|
refreshData(): void {
|
||||||
|
this.fetchChartData();
|
||||||
|
}
|
||||||
|
|
||||||
fetchChartData(): void {
|
fetchChartData(): void {
|
||||||
// Set flag to prevent recursive calls
|
// Set flag to prevent recursive calls
|
||||||
this.isFetchingData = true;
|
this.isFetchingData = true;
|
||||||
@ -154,7 +179,49 @@ export class LineChartComponent implements OnInit, OnChanges {
|
|||||||
filterParams = JSON.stringify(filterObj);
|
filterParams = JSON.stringify(filterObj);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
console.log('Base filter parameters:', filterParams);
|
|
||||||
|
// Add common filters to filter parameters
|
||||||
|
const commonFilters = this.filterService.getFilterValues();
|
||||||
|
console.log('Common filters from service:', commonFilters);
|
||||||
|
|
||||||
|
if (Object.keys(commonFilters).length > 0) {
|
||||||
|
// Merge common filters with base filters
|
||||||
|
const mergedFilterObj = {};
|
||||||
|
|
||||||
|
// Add base filters first
|
||||||
|
if (filterParams) {
|
||||||
|
try {
|
||||||
|
const baseFilterObj = JSON.parse(filterParams);
|
||||||
|
Object.assign(mergedFilterObj, baseFilterObj);
|
||||||
|
} catch (e) {
|
||||||
|
console.warn('Failed to parse base filter parameters:', e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add common filters using the field name as the key, not the filter id
|
||||||
|
Object.keys(commonFilters).forEach(filterId => {
|
||||||
|
const filterValue = commonFilters[filterId];
|
||||||
|
// Find the filter definition to get the field name
|
||||||
|
const filterDef = this.filterService.getFilters().find(f => f.id === filterId);
|
||||||
|
if (filterDef && filterDef.field) {
|
||||||
|
const fieldName = filterDef.field;
|
||||||
|
if (filterValue !== undefined && filterValue !== null && filterValue !== '') {
|
||||||
|
mergedFilterObj[fieldName] = filterValue;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// Fallback to using filterId as field name if no field is defined
|
||||||
|
if (filterValue !== undefined && filterValue !== null && filterValue !== '') {
|
||||||
|
mergedFilterObj[filterId] = filterValue;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
if (Object.keys(mergedFilterObj).length > 0) {
|
||||||
|
filterParams = JSON.stringify(mergedFilterObj);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log('Final filter parameters:', filterParams);
|
||||||
|
|
||||||
// Log the URL that will be called
|
// Log the URL that will be called
|
||||||
const url = `chart/getdashjson/line?tableName=${this.table}&xAxis=${this.xAxis}&yAxes=${yAxisString}${this.connection ? `&sureId=${this.connection}` : ''}`;
|
const url = `chart/getdashjson/line?tableName=${this.table}&xAxis=${this.xAxis}&yAxes=${yAxisString}${this.connection ? `&sureId=${this.connection}` : ''}`;
|
||||||
@ -313,6 +380,35 @@ export class LineChartComponent implements OnInit, OnChanges {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Add common filters to drilldown filter parameters
|
||||||
|
const commonFilters = this.filterService.getFilterValues();
|
||||||
|
if (Object.keys(commonFilters).length > 0) {
|
||||||
|
// Merge common filters with drilldown filters
|
||||||
|
const mergedFilterObj = {};
|
||||||
|
|
||||||
|
// Add drilldown filters first
|
||||||
|
if (filterParams) {
|
||||||
|
try {
|
||||||
|
const drilldownFilterObj = JSON.parse(filterParams);
|
||||||
|
Object.assign(mergedFilterObj, drilldownFilterObj);
|
||||||
|
} catch (e) {
|
||||||
|
console.warn('Failed to parse drilldown filter parameters:', e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add common filters
|
||||||
|
Object.keys(commonFilters).forEach(key => {
|
||||||
|
const value = commonFilters[key];
|
||||||
|
if (value !== undefined && value !== null && value !== '') {
|
||||||
|
mergedFilterObj[key] = value;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
if (Object.keys(mergedFilterObj).length > 0) {
|
||||||
|
filterParams = JSON.stringify(mergedFilterObj);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Log the URL that will be called
|
// Log the URL that will be called
|
||||||
const url = `chart/getdashjson/line?tableName=${actualApiUrl}&xAxis=${drilldownConfig.xAxis}&yAxes=${drilldownConfig.yAxis}${this.connection ? `&sureId=${this.connection}` : ''}`;
|
const url = `chart/getdashjson/line?tableName=${actualApiUrl}&xAxis=${drilldownConfig.xAxis}&yAxes=${drilldownConfig.yAxis}${this.connection ? `&sureId=${this.connection}` : ''}`;
|
||||||
console.log('Drilldown data URL:', url);
|
console.log('Drilldown data URL:', url);
|
||||||
|
|||||||
@ -1,5 +1,7 @@
|
|||||||
import { Component, OnInit, Input, OnChanges, SimpleChanges, AfterViewChecked } from '@angular/core';
|
import { Component, OnInit, Input, OnChanges, SimpleChanges, AfterViewChecked, OnDestroy } from '@angular/core';
|
||||||
import { Dashboard3Service } from 'src/app/services/builder/dashboard3.service';
|
import { Dashboard3Service } from 'src/app/services/builder/dashboard3.service';
|
||||||
|
import { FilterService } from '../../common-filter/filter.service';
|
||||||
|
import { Subscription } from 'rxjs';
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
selector: 'app-pie-chart',
|
selector: 'app-pie-chart',
|
||||||
@ -96,7 +98,13 @@ export class PieChartComponent implements OnInit, OnChanges, AfterViewChecked {
|
|||||||
// Flag to prevent infinite loops
|
// Flag to prevent infinite loops
|
||||||
private isFetchingData: boolean = false;
|
private isFetchingData: boolean = false;
|
||||||
|
|
||||||
constructor(private dashboardService: Dashboard3Service) { }
|
// Subscriptions to unsubscribe on destroy
|
||||||
|
private subscriptions: Subscription[] = [];
|
||||||
|
|
||||||
|
constructor(
|
||||||
|
private dashboardService: Dashboard3Service,
|
||||||
|
private filterService: FilterService
|
||||||
|
) { }
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Force chart redraw
|
* Force chart redraw
|
||||||
@ -108,6 +116,14 @@ export class PieChartComponent implements OnInit, OnChanges, AfterViewChecked {
|
|||||||
}
|
}
|
||||||
|
|
||||||
ngOnInit(): void {
|
ngOnInit(): void {
|
||||||
|
// Subscribe to filter changes
|
||||||
|
this.subscriptions.push(
|
||||||
|
this.filterService.filterState$.subscribe(filters => {
|
||||||
|
// When filters change, refresh the chart data
|
||||||
|
this.fetchChartData();
|
||||||
|
})
|
||||||
|
);
|
||||||
|
|
||||||
console.log('PieChartComponent initialized with default data:', { labels: this.pieChartLabels, data: this.pieChartData });
|
console.log('PieChartComponent initialized with default data:', { labels: this.pieChartLabels, data: this.pieChartData });
|
||||||
// Validate initial data
|
// Validate initial data
|
||||||
this.validateChartData();
|
this.validateChartData();
|
||||||
@ -140,6 +156,15 @@ export class PieChartComponent implements OnInit, OnChanges, AfterViewChecked {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
ngOnDestroy(): void {
|
||||||
|
this.subscriptions.forEach(sub => sub.unsubscribe());
|
||||||
|
}
|
||||||
|
|
||||||
|
// Public method to refresh data when filters change
|
||||||
|
refreshData(): void {
|
||||||
|
this.fetchChartData();
|
||||||
|
}
|
||||||
|
|
||||||
fetchChartData(): void {
|
fetchChartData(): void {
|
||||||
// Set flag to prevent recursive calls
|
// Set flag to prevent recursive calls
|
||||||
this.isFetchingData = true;
|
this.isFetchingData = true;
|
||||||
@ -172,7 +197,49 @@ export class PieChartComponent implements OnInit, OnChanges, AfterViewChecked {
|
|||||||
filterParams = JSON.stringify(filterObj);
|
filterParams = JSON.stringify(filterObj);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
console.log('Base filter parameters:', filterParams);
|
|
||||||
|
// Add common filters to filter parameters
|
||||||
|
const commonFilters = this.filterService.getFilterValues();
|
||||||
|
console.log('Common filters from service:', commonFilters);
|
||||||
|
|
||||||
|
if (Object.keys(commonFilters).length > 0) {
|
||||||
|
// Merge common filters with base filters
|
||||||
|
const mergedFilterObj = {};
|
||||||
|
|
||||||
|
// Add base filters first
|
||||||
|
if (filterParams) {
|
||||||
|
try {
|
||||||
|
const baseFilterObj = JSON.parse(filterParams);
|
||||||
|
Object.assign(mergedFilterObj, baseFilterObj);
|
||||||
|
} catch (e) {
|
||||||
|
console.warn('Failed to parse base filter parameters:', e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add common filters using the field name as the key, not the filter id
|
||||||
|
Object.keys(commonFilters).forEach(filterId => {
|
||||||
|
const filterValue = commonFilters[filterId];
|
||||||
|
// Find the filter definition to get the field name
|
||||||
|
const filterDef = this.filterService.getFilters().find(f => f.id === filterId);
|
||||||
|
if (filterDef && filterDef.field) {
|
||||||
|
const fieldName = filterDef.field;
|
||||||
|
if (filterValue !== undefined && filterValue !== null && filterValue !== '') {
|
||||||
|
mergedFilterObj[fieldName] = filterValue;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// Fallback to using filterId as field name if no field is defined
|
||||||
|
if (filterValue !== undefined && filterValue !== null && filterValue !== '') {
|
||||||
|
mergedFilterObj[filterId] = filterValue;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
if (Object.keys(mergedFilterObj).length > 0) {
|
||||||
|
filterParams = JSON.stringify(mergedFilterObj);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log('Final filter parameters:', filterParams);
|
||||||
|
|
||||||
// Log the URL that will be called
|
// Log the URL that will be called
|
||||||
const url = `chart/getdashjson/pie?tableName=${this.table}&xAxis=${this.xAxis}&yAxes=${yAxisString}${this.connection ? `&sureId=${this.connection}` : ''}`;
|
const url = `chart/getdashjson/pie?tableName=${this.table}&xAxis=${this.xAxis}&yAxes=${yAxisString}${this.connection ? `&sureId=${this.connection}` : ''}`;
|
||||||
@ -362,6 +429,35 @@ export class PieChartComponent implements OnInit, OnChanges, AfterViewChecked {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Add common filters to drilldown filter parameters
|
||||||
|
const commonFilters = this.filterService.getFilterValues();
|
||||||
|
if (Object.keys(commonFilters).length > 0) {
|
||||||
|
// Merge common filters with drilldown filters
|
||||||
|
const mergedFilterObj = {};
|
||||||
|
|
||||||
|
// Add drilldown filters first
|
||||||
|
if (filterParams) {
|
||||||
|
try {
|
||||||
|
const drilldownFilterObj = JSON.parse(filterParams);
|
||||||
|
Object.assign(mergedFilterObj, drilldownFilterObj);
|
||||||
|
} catch (e) {
|
||||||
|
console.warn('Failed to parse drilldown filter parameters:', e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add common filters
|
||||||
|
Object.keys(commonFilters).forEach(key => {
|
||||||
|
const value = commonFilters[key];
|
||||||
|
if (value !== undefined && value !== null && value !== '') {
|
||||||
|
mergedFilterObj[key] = value;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
if (Object.keys(mergedFilterObj).length > 0) {
|
||||||
|
filterParams = JSON.stringify(mergedFilterObj);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Log the URL that will be called
|
// Log the URL that will be called
|
||||||
const url = `chart/getdashjson/pie?tableName=${actualApiUrl}&xAxis=${drilldownConfig.xAxis}&yAxes=${drilldownConfig.yAxis}${this.connection ? `&sureId=${this.connection}` : ''}`;
|
const url = `chart/getdashjson/pie?tableName=${actualApiUrl}&xAxis=${drilldownConfig.xAxis}&yAxes=${drilldownConfig.yAxis}${this.connection ? `&sureId=${this.connection}` : ''}`;
|
||||||
console.log('Drilldown data URL:', url);
|
console.log('Drilldown data URL:', url);
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user