Compare commits
41 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
7c1a487114 | ||
|
|
47e9fb92e3 | ||
|
|
7f735dcada | ||
|
|
bd315f42a3 | ||
|
|
e8c1f46430 | ||
|
|
fa96ca81bd | ||
|
|
c384f44c0c | ||
|
|
50df914ca9 | ||
|
|
e0bd888c45 | ||
|
|
02b82fcaf8 | ||
|
|
557afc348f | ||
|
|
1dec787062 | ||
|
|
8853cf75cf | ||
|
|
ad57f11f8a | ||
|
|
9b775a8c63 | ||
|
|
cf4fc1be93 | ||
|
|
4c135c4901 | ||
|
|
0b738ca7ca | ||
|
|
1b17bb706d | ||
|
|
f740076d60 | ||
|
|
bedcc0822d | ||
|
|
87810acc9e | ||
|
|
96b90e5dbd | ||
|
|
ced99e0940 | ||
|
|
4f82ae8698 | ||
|
|
e6779e8f34 | ||
|
|
82425d5377 | ||
|
|
2995328ec1 | ||
|
|
afc2c1f8a1 | ||
|
|
418b02acd7 | ||
|
|
cdd752469c | ||
|
|
cdcf1e07c7 | ||
|
|
c6ad8b5c2f | ||
|
|
98e0908920 | ||
|
|
8266bfdc01 | ||
|
|
f2b6a4d145 | ||
|
|
02f37a1bc5 | ||
|
|
f24138cfbd | ||
|
|
aade12d6ff | ||
|
|
6c01e71d04 | ||
|
|
f60657ca64 |
@@ -23,6 +23,20 @@ export interface DashboardContentModel {
|
||||
component?: any;
|
||||
name: string;
|
||||
type?:string;
|
||||
// Common properties
|
||||
table?: string;
|
||||
connection?: string;
|
||||
baseFilters?: any[];
|
||||
// Common filter properties
|
||||
commonFilterEnabled?: boolean;
|
||||
// Drilldown properties
|
||||
drilldownEnabled?: boolean;
|
||||
drilldownApiUrl?: string;
|
||||
drilldownXAxis?: string;
|
||||
drilldownYAxis?: string;
|
||||
drilldownParameter?: string;
|
||||
drilldownFilters?: any[];
|
||||
drilldownLayers?: any[];
|
||||
}
|
||||
|
||||
export interface DashboardModel {
|
||||
@@ -67,6 +81,10 @@ export class value1{
|
||||
|
||||
|
||||
export const WidgetsMock: WidgetModel[] = [
|
||||
{
|
||||
name: 'Common Filter',
|
||||
identifier: 'common_filter'
|
||||
},
|
||||
{
|
||||
name: 'Radar Chart',
|
||||
identifier: 'radar_chart'
|
||||
|
||||
@@ -97,6 +97,7 @@ export class LoginPageComponent implements OnInit {
|
||||
.subscribe(
|
||||
resp => {
|
||||
console.log('API Response received:', resp);
|
||||
// Always reset loading state when response is received
|
||||
this.isLoading = false;
|
||||
|
||||
// Handle different response formats
|
||||
@@ -106,7 +107,7 @@ export class LoginPageComponent implements OnInit {
|
||||
return;
|
||||
}
|
||||
// Handle different response formats
|
||||
if (resp.success === 'false') {
|
||||
if (resp.success === 'false' || resp.success === false) {
|
||||
this.isError = true;
|
||||
this.errMsg = resp.message || 'Login failed';
|
||||
return;
|
||||
@@ -123,6 +124,7 @@ export class LoginPageComponent implements OnInit {
|
||||
},
|
||||
(errResponse: HttpErrorResponse) => {
|
||||
console.log('API Error received:', errResponse);
|
||||
// Always reset loading state when error occurs
|
||||
this.isLoading = false;
|
||||
this.isError = true;
|
||||
|
||||
|
||||
Binary file not shown.
@@ -11,6 +11,9 @@
|
||||
<h3>{{ 'Dashboard_builder' | translate }}</h3>
|
||||
</div>
|
||||
<div class="clr-col-4" style="text-align: right;">
|
||||
<button class="btn btn-success" [routerLink]="['/cns-portal/shield-dashboard']">
|
||||
<clr-icon shape="shield"></clr-icon>Shield Dashboard
|
||||
</button>
|
||||
<button id="add" class="btn btn-primary" (click)="gotorunner()">
|
||||
<clr-icon shape="grid-view"></clr-icon>{{ 'Dashboard_runner' | translate }}
|
||||
</button>
|
||||
@@ -140,6 +143,3 @@
|
||||
</div>
|
||||
</div>
|
||||
</clr-modal>
|
||||
|
||||
|
||||
|
||||
@@ -0,0 +1,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,68 @@
|
||||
.chart-wrapper {
|
||||
height: 100%;
|
||||
width: 100%;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
box-sizing: border-box;
|
||||
|
||||
.chart-header {
|
||||
padding: 10px 15px;
|
||||
background: #f8f8f8;
|
||||
border-bottom: 1px solid #eee;
|
||||
|
||||
h5 {
|
||||
margin: 0;
|
||||
color: #333;
|
||||
font-size: 16px;
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
}
|
||||
|
||||
.chart-container {
|
||||
flex: 1;
|
||||
padding: 15px;
|
||||
overflow: auto;
|
||||
position: relative;
|
||||
|
||||
// Ensure chart containers fill available space
|
||||
::ng-deep canvas {
|
||||
max-width: 100%;
|
||||
max-height: 100%;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Responsive adjustments
|
||||
@media (max-width: 768px) {
|
||||
.chart-wrapper {
|
||||
.chart-header {
|
||||
padding: 8px 12px;
|
||||
|
||||
h5 {
|
||||
font-size: 14px;
|
||||
}
|
||||
}
|
||||
|
||||
.chart-container {
|
||||
padding: 10px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 480px) {
|
||||
.chart-wrapper {
|
||||
.chart-header {
|
||||
padding: 6px 10px;
|
||||
|
||||
h5 {
|
||||
font-size: 13px;
|
||||
}
|
||||
}
|
||||
|
||||
.chart-container {
|
||||
padding: 8px;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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,111 @@
|
||||
import { Component, Input, OnInit, OnDestroy, ComponentRef, ViewChild, ViewContainerRef, HostListener } from '@angular/core';
|
||||
import { Subscription } from 'rxjs';
|
||||
import { FilterService } from './filter.service';
|
||||
|
||||
@Component({
|
||||
selector: 'app-chart-wrapper',
|
||||
template: `
|
||||
<div class="chart-wrapper">
|
||||
<div class="chart-header" *ngIf="chartTitle">
|
||||
<h5>{{ chartTitle }}</h5>
|
||||
</div>
|
||||
<div class="chart-container">
|
||||
<ng-container #chartContainer></ng-container>
|
||||
</div>
|
||||
</div>
|
||||
`,
|
||||
styleUrls: ['./chart-wrapper.component.scss']
|
||||
})
|
||||
export class ChartWrapperComponent implements OnInit, OnDestroy {
|
||||
@Input() chartComponent: any;
|
||||
@Input() chartInputs: any = {};
|
||||
@Input() chartTitle: string = '';
|
||||
|
||||
@ViewChild('chartContainer', { read: ViewContainerRef }) chartContainer!: ViewContainerRef;
|
||||
|
||||
private componentRef: ComponentRef<any> | null = null;
|
||||
private filterSubscription: Subscription | null = null;
|
||||
|
||||
constructor(private filterService: FilterService) { }
|
||||
|
||||
ngOnInit(): void {
|
||||
this.loadChartComponent();
|
||||
this.subscribeToFilters();
|
||||
}
|
||||
|
||||
ngOnDestroy(): void {
|
||||
if (this.filterSubscription) {
|
||||
this.filterSubscription.unsubscribe();
|
||||
}
|
||||
if (this.componentRef) {
|
||||
this.componentRef.destroy();
|
||||
}
|
||||
}
|
||||
|
||||
// Handle window resize events
|
||||
@HostListener('window:resize', ['$event'])
|
||||
onResize(event: any) {
|
||||
// Notify the chart component to resize if it has a resize method
|
||||
if (this.componentRef && this.componentRef.instance) {
|
||||
const chartInstance = this.componentRef.instance;
|
||||
|
||||
// If it's a chart component with an onResize method, call it
|
||||
if (chartInstance.onResize && typeof chartInstance.onResize === 'function') {
|
||||
chartInstance.onResize();
|
||||
}
|
||||
|
||||
// If it's a chart component with a chart property (from BaseChartDirective), resize it
|
||||
if (chartInstance.chart && typeof chartInstance.chart.resize === 'function') {
|
||||
setTimeout(() => {
|
||||
chartInstance.chart.resize();
|
||||
}, 100);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private loadChartComponent(): void {
|
||||
if (this.chartContainer && this.chartComponent) {
|
||||
this.chartContainer.clear();
|
||||
const factory = this.chartContainer.createComponent(this.chartComponent);
|
||||
this.componentRef = factory;
|
||||
|
||||
// Set initial inputs
|
||||
Object.keys(this.chartInputs).forEach(key => {
|
||||
factory.instance[key] = this.chartInputs[key];
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
private subscribeToFilters(): void {
|
||||
this.filterSubscription = this.filterService.filterState$.subscribe(filterValues => {
|
||||
this.updateChartWithFilters(filterValues);
|
||||
});
|
||||
}
|
||||
|
||||
private updateChartWithFilters(filterValues: any): void {
|
||||
if (this.componentRef) {
|
||||
// Add filter values to chart inputs
|
||||
const updatedInputs = {
|
||||
...this.chartInputs,
|
||||
filterValues: filterValues,
|
||||
// Pass the query params string for easy API integration
|
||||
filterQueryParams: this.filterService.buildQueryParams()
|
||||
};
|
||||
|
||||
// Update chart component inputs
|
||||
Object.keys(updatedInputs).forEach(key => {
|
||||
this.componentRef!.instance[key] = updatedInputs[key];
|
||||
});
|
||||
|
||||
// Trigger change detection if the component has a method for it
|
||||
if (this.componentRef!.instance.ngOnChanges) {
|
||||
// We can't easily trigger ngOnChanges manually, but the input update should trigger it
|
||||
}
|
||||
|
||||
// If the chart component has a method to refresh data, call it
|
||||
if (this.componentRef!.instance.refreshData) {
|
||||
this.componentRef!.instance.refreshData();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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,192 @@
|
||||
.common-filter-container {
|
||||
padding: 15px;
|
||||
background: #f5f5f5;
|
||||
border: 1px solid #ddd;
|
||||
border-radius: 4px;
|
||||
margin-bottom: 20px;
|
||||
height: 100%;
|
||||
width: 100%;
|
||||
box-sizing: border-box;
|
||||
|
||||
.filter-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
margin-bottom: 15px;
|
||||
flex-wrap: wrap;
|
||||
gap: 10px;
|
||||
|
||||
h4 {
|
||||
margin: 0;
|
||||
color: #333;
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.btn {
|
||||
white-space: nowrap;
|
||||
}
|
||||
}
|
||||
|
||||
.presets-section {
|
||||
margin-bottom: 15px;
|
||||
|
||||
.preset-controls {
|
||||
display: flex;
|
||||
gap: 10px;
|
||||
align-items: center;
|
||||
flex-wrap: wrap;
|
||||
|
||||
select {
|
||||
flex: 1;
|
||||
min-width: 150px;
|
||||
}
|
||||
|
||||
.btn {
|
||||
white-space: nowrap;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.save-preset-section {
|
||||
margin-bottom: 15px;
|
||||
|
||||
.clr-input-group {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 10px;
|
||||
|
||||
.clr-input {
|
||||
flex: 1;
|
||||
min-width: 150px;
|
||||
}
|
||||
|
||||
.clr-input-group-btn {
|
||||
.btn {
|
||||
white-space: nowrap;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.filters-form {
|
||||
.filters-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fill, minmax(250px, 1fr));
|
||||
gap: 15px;
|
||||
|
||||
.filter-item {
|
||||
background: white;
|
||||
border: 1px solid #e0e0e0;
|
||||
border-radius: 4px;
|
||||
padding: 15px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
|
||||
.filter-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
margin-bottom: 10px;
|
||||
|
||||
.filter-label {
|
||||
font-weight: bold;
|
||||
color: #333;
|
||||
flex: 1;
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
}
|
||||
|
||||
.filter-type-select {
|
||||
width: 100%;
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
|
||||
.clr-form-control {
|
||||
margin-bottom: 10px;
|
||||
|
||||
input, select, textarea {
|
||||
width: 100%;
|
||||
min-width: 0; // Allow flexbox to shrink items
|
||||
}
|
||||
}
|
||||
|
||||
.date-range-controls {
|
||||
display: flex;
|
||||
gap: 10px;
|
||||
flex-direction: column;
|
||||
|
||||
.clr-form-control {
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
@media (min-width: 480px) {
|
||||
flex-direction: row;
|
||||
}
|
||||
}
|
||||
|
||||
.toggle-control {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.multiselect {
|
||||
height: 100px;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.no-filters {
|
||||
text-align: center;
|
||||
padding: 20px;
|
||||
color: #666;
|
||||
font-style: italic;
|
||||
}
|
||||
}
|
||||
|
||||
// Responsive design for smaller screens
|
||||
@media (max-width: 768px) {
|
||||
.common-filter-container {
|
||||
padding: 10px;
|
||||
|
||||
.filters-form {
|
||||
.filters-grid {
|
||||
grid-template-columns: 1fr;
|
||||
gap: 10px;
|
||||
}
|
||||
}
|
||||
|
||||
.filter-header {
|
||||
flex-direction: column;
|
||||
align-items: stretch;
|
||||
}
|
||||
|
||||
.presets-section {
|
||||
.preset-controls {
|
||||
flex-direction: column;
|
||||
}
|
||||
}
|
||||
|
||||
.save-preset-section {
|
||||
.clr-input-group {
|
||||
flex-direction: column;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 480px) {
|
||||
.common-filter-container {
|
||||
.date-range-controls {
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.filter-item {
|
||||
padding: 10px;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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,197 @@
|
||||
import { Component, OnInit, OnDestroy, Input, HostListener } from '@angular/core';
|
||||
import { FormBuilder, FormGroup } from '@angular/forms';
|
||||
import { Subscription } from 'rxjs';
|
||||
import { Filter, FilterService, FilterType } from './filter.service';
|
||||
|
||||
@Component({
|
||||
selector: 'app-common-filter',
|
||||
templateUrl: './common-filter.component.html',
|
||||
styleUrls: ['./common-filter.component.scss']
|
||||
})
|
||||
export class CommonFilterComponent implements OnInit, OnDestroy {
|
||||
@Input() baseFilters: any[] = [];
|
||||
@Input() drilldownFilters: any[] = [];
|
||||
@Input() drilldownLayers: any[] = [];
|
||||
@Input() fieldName: string;
|
||||
@Input() connection: number;
|
||||
|
||||
filters: Filter[] = [];
|
||||
filterForm: FormGroup;
|
||||
presets: string[] = [];
|
||||
activePreset: string | null = null;
|
||||
newPresetName: string = '';
|
||||
|
||||
private subscriptions: Subscription[] = [];
|
||||
|
||||
constructor(
|
||||
private filterService: FilterService,
|
||||
private fb: FormBuilder
|
||||
) {
|
||||
this.filterForm = this.fb.group({});
|
||||
}
|
||||
|
||||
ngOnInit(): void {
|
||||
// Subscribe to filter definitions
|
||||
this.subscriptions.push(
|
||||
this.filterService.filters$.subscribe(filters => {
|
||||
this.filters = filters;
|
||||
this.buildForm();
|
||||
})
|
||||
);
|
||||
|
||||
// Subscribe to filter state changes
|
||||
this.subscriptions.push(
|
||||
this.filterService.filterState$.subscribe(state => {
|
||||
this.updateFormValues(state);
|
||||
})
|
||||
);
|
||||
|
||||
// Subscribe to preset changes
|
||||
this.subscriptions.push(
|
||||
this.filterService.activePreset$.subscribe(preset => {
|
||||
this.activePreset = preset;
|
||||
})
|
||||
);
|
||||
|
||||
// Get initial presets
|
||||
this.presets = this.filterService.getPresets();
|
||||
}
|
||||
|
||||
ngOnDestroy(): void {
|
||||
this.subscriptions.forEach(sub => sub.unsubscribe());
|
||||
}
|
||||
|
||||
// Handle window resize events
|
||||
@HostListener('window:resize', ['$event'])
|
||||
onResize(event: any) {
|
||||
// Trigger change detection to reflow the layout
|
||||
setTimeout(() => {
|
||||
// This will cause the grid to recalculate its layout
|
||||
this.filters = [...this.filters];
|
||||
}, 100);
|
||||
}
|
||||
|
||||
// Build the form based on current filters
|
||||
private buildForm(): void {
|
||||
// Clear existing form controls
|
||||
Object.keys(this.filterForm.controls).forEach(key => {
|
||||
this.filterForm.removeControl(key);
|
||||
});
|
||||
|
||||
// Add controls for each filter
|
||||
this.filters.forEach(filter => {
|
||||
let initialValue: any;
|
||||
|
||||
switch (filter.type) {
|
||||
case 'multiselect':
|
||||
initialValue = filter.value || [];
|
||||
break;
|
||||
case 'date-range':
|
||||
initialValue = filter.value || { start: null, end: null };
|
||||
break;
|
||||
case 'toggle':
|
||||
initialValue = filter.value || false;
|
||||
break;
|
||||
default:
|
||||
initialValue = filter.value || '';
|
||||
}
|
||||
|
||||
const control = this.fb.control(initialValue);
|
||||
|
||||
// Subscribe to value changes for this control
|
||||
control.valueChanges.subscribe(value => {
|
||||
this.onFilterChange(filter.id, value);
|
||||
});
|
||||
|
||||
this.filterForm.addControl(filter.id, control);
|
||||
});
|
||||
}
|
||||
|
||||
// Update form values based on filter state
|
||||
private updateFormValues(state: any): void {
|
||||
Object.keys(state).forEach(key => {
|
||||
if (this.filterForm.contains(key)) {
|
||||
this.filterForm.get(key)?.setValue(state[key], { emitEvent: false });
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// Handle filter value changes
|
||||
onFilterChange(filterId: string, value: any): void {
|
||||
console.log('=== COMMON FILTER DEBUG INFO ===');
|
||||
console.log('Filter value changed for ID:', filterId);
|
||||
console.log('New value:', value);
|
||||
|
||||
const filterDef = this.filters.find(f => f.id === filterId);
|
||||
console.log('Filter definition:', filterDef);
|
||||
|
||||
this.filterService.updateFilterValue(filterId, value);
|
||||
console.log('=== END COMMON FILTER DEBUG ===');
|
||||
}
|
||||
|
||||
// Handle multiselect changes
|
||||
onMultiselectChange(filterId: string, selectedValues: string[]): void {
|
||||
this.filterService.updateFilterValue(filterId, selectedValues);
|
||||
}
|
||||
|
||||
// Handle date range changes
|
||||
onDateRangeChange(filterId: string, dateRange: { start: string | null, end: string | null }): void {
|
||||
this.filterService.updateFilterValue(filterId, dateRange);
|
||||
}
|
||||
|
||||
// Handle toggle changes
|
||||
onToggleChange(filterId: string, checked: boolean): void {
|
||||
this.filterService.updateFilterValue(filterId, checked);
|
||||
}
|
||||
|
||||
// Reset all filters
|
||||
resetFilters(): void {
|
||||
this.filterService.resetFilters();
|
||||
}
|
||||
|
||||
// Save current filter state as preset
|
||||
savePreset(): void {
|
||||
if (this.newPresetName.trim()) {
|
||||
this.filterService.savePreset(this.newPresetName.trim());
|
||||
this.presets = this.filterService.getPresets();
|
||||
this.newPresetName = '';
|
||||
}
|
||||
}
|
||||
|
||||
// Load a preset
|
||||
loadPreset(presetName: string): void {
|
||||
this.filterService.loadPreset(presetName);
|
||||
}
|
||||
|
||||
// Delete a preset
|
||||
deletePreset(presetName: string): void {
|
||||
this.filterService.deletePreset(presetName);
|
||||
this.presets = this.filterService.getPresets();
|
||||
}
|
||||
|
||||
// Add a new filter
|
||||
addFilter(): void {
|
||||
const newFilter: Filter = {
|
||||
id: `filter_${Date.now()}`,
|
||||
field: '',
|
||||
label: 'New Filter',
|
||||
type: 'text'
|
||||
};
|
||||
this.filterService.addFilter(newFilter);
|
||||
}
|
||||
|
||||
// Remove a filter
|
||||
removeFilter(filterId: string): void {
|
||||
this.filterService.removeFilter(filterId);
|
||||
}
|
||||
|
||||
// Update filter properties
|
||||
updateFilter(filterId: string, property: string, value: any): void {
|
||||
const filterIndex = this.filters.findIndex(f => f.id === filterId);
|
||||
if (filterIndex !== -1) {
|
||||
const updatedFilters = [...this.filters];
|
||||
(updatedFilters[filterIndex] as any)[property] = value;
|
||||
this.filterService.setFilters(updatedFilters);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,122 @@
|
||||
<!-- Configuration Mode -->
|
||||
<div class="compact-filter-config" *ngIf="isConfigMode">
|
||||
<div class="config-header">
|
||||
<h5>Compact Filter Configuration</h5>
|
||||
<button class="btn btn-sm btn-link" (click)="cancelConfiguration()">
|
||||
<clr-icon shape="close"></clr-icon>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div class="config-form">
|
||||
<div class="clr-form-control">
|
||||
<label class="clr-control-label">API URL</label>
|
||||
<input type="text" [(ngModel)]="configApiUrl" (ngModelChange)="onApiUrlChange($event)" placeholder="Enter API URL" class="clr-input">
|
||||
</div>
|
||||
|
||||
<div class="clr-form-control">
|
||||
<label class="clr-control-label">Filter Key</label>
|
||||
<select [(ngModel)]="configFilterKey" (ngModelChange)="onFilterKeyChange($event)" class="clr-select">
|
||||
<option value="">Select a key</option>
|
||||
<option *ngFor="let key of availableKeys" [value]="key">{{ key }}</option>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<div class="clr-form-control">
|
||||
<label class="clr-control-label">Filter Type</label>
|
||||
<select [(ngModel)]="configFilterType" (ngModelChange)="onFilterTypeChange($event)" class="clr-select">
|
||||
<option value="text">Text</option>
|
||||
<option value="dropdown">Dropdown</option>
|
||||
<option value="multiselect">Multi-Select</option>
|
||||
<option value="date-range">Date Range</option>
|
||||
<option value="toggle">Toggle</option>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<!-- Options will be automatically populated for dropdown/multiselect based on API data -->
|
||||
<div class="clr-form-control" *ngIf="configFilterType === 'dropdown' || configFilterType === 'multiselect'">
|
||||
<label class="clr-control-label">Available Values (comma separated)</label>
|
||||
<div class="available-values">
|
||||
{{ availableValues.join(', ') }}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="config-actions">
|
||||
<button class="btn btn-sm btn-outline" (click)="cancelConfiguration()">Cancel</button>
|
||||
<button class="btn btn-sm btn-primary" (click)="saveConfiguration()">Save</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Display Mode -->
|
||||
<div class="compact-filter" *ngIf="!isConfigMode">
|
||||
<div class="filter-header">
|
||||
<span class="filter-label" *ngIf="filterLabel">{{ filterLabel }}</span>
|
||||
<span class="filter-key" *ngIf="!filterLabel && filterKey">{{ filterKey }}</span>
|
||||
<span class="filter-type">({{ filterType }})</span>
|
||||
<button class="btn btn-icon btn-sm" (click)="toggleConfigMode()">
|
||||
<clr-icon shape="cog"></clr-icon>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<!-- Text Filter -->
|
||||
<div class="filter-control" *ngIf="filterType === 'text'">
|
||||
<input type="text"
|
||||
[(ngModel)]="filterValue"
|
||||
(ngModelChange)="onFilterValueChange($event)"
|
||||
[placeholder]="filterLabel || filterKey"
|
||||
class="clr-input compact-input">
|
||||
</div>
|
||||
|
||||
<!-- Dropdown Filter -->
|
||||
<div class="filter-control" *ngIf="filterType === 'dropdown'">
|
||||
<select [(ngModel)]="filterValue"
|
||||
(ngModelChange)="onFilterValueChange($event)"
|
||||
class="clr-select compact-select">
|
||||
<option value="">{{ filterLabel || filterKey }}</option>
|
||||
<option *ngFor="let option of filterOptions" [value]="option">{{ option }}</option>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<!-- Multi-Select Filter -->
|
||||
<div class="filter-control" *ngIf="filterType === 'multiselect'">
|
||||
<div class="compact-multiselect-display" (click)="toggleMultiselectDropdown()" style="padding: 5px; border: 1px solid #ddd; cursor: pointer; background-color: #f8f8f8;">
|
||||
<span *ngIf="filterValue && filterValue.length > 0">{{ filterValue.length }} selected</span>
|
||||
<span *ngIf="!filterValue || filterValue.length === 0">{{ filterLabel || filterKey || 'Select options' }}</span>
|
||||
<clr-icon shape="caret down" style="float: right; margin-top: 3px;"></clr-icon>
|
||||
</div>
|
||||
<div class="compact-multiselect-dropdown" *ngIf="showMultiselectDropdown" style="max-height: 200px; overflow-y: auto; border: 1px solid #ddd; border-top: none; padding: 10px; background-color: white; position: absolute; z-index: 1000; width: calc(100% - 2px); box-shadow: 0 2px 4px rgba(0,0,0,0.1);">
|
||||
<div *ngFor="let option of filterOptions" class="clr-checkbox-wrapper" style="margin-bottom: 5px;">
|
||||
<input type="checkbox"
|
||||
[id]="'multiselect-' + option"
|
||||
[value]="option"
|
||||
[checked]="isOptionSelected(option)"
|
||||
(change)="onMultiselectOptionChange($event, option)">
|
||||
<label [for]="'multiselect-' + option" class="clr-control-label">{{ option }}</label>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Date Range Filter -->
|
||||
<div class="filter-control date-range" *ngIf="filterType === 'date-range'">
|
||||
<input type="date"
|
||||
[(ngModel)]="filterValue.start"
|
||||
(ngModelChange)="onDateRangeChange({ start: $event, end: filterValue.end })"
|
||||
placeholder="Start Date"
|
||||
class="clr-input compact-date">
|
||||
<input type="date"
|
||||
[(ngModel)]="filterValue.end"
|
||||
(ngModelChange)="onDateRangeChange({ start: filterValue.start, end: $event })"
|
||||
placeholder="End Date"
|
||||
class="clr-input compact-date">
|
||||
</div>
|
||||
|
||||
<!-- Toggle Filter -->
|
||||
<div class="filter-control toggle" *ngIf="filterType === 'toggle'">
|
||||
<input type="checkbox"
|
||||
[(ngModel)]="filterValue"
|
||||
(ngModelChange)="onToggleChange($event)"
|
||||
clrToggle
|
||||
class="clr-toggle">
|
||||
<label class="toggle-label">{{ filterLabel || filterKey }}</label>
|
||||
</div>
|
||||
</div>
|
||||
@@ -0,0 +1,224 @@
|
||||
.compact-filter {
|
||||
display: inline-block;
|
||||
min-width: 120px;
|
||||
max-width: 200px;
|
||||
margin: 5px;
|
||||
padding: 5px;
|
||||
background: #f8f9fa;
|
||||
border: 1px solid #e9ecef;
|
||||
border-radius: 4px;
|
||||
font-size: 12px;
|
||||
|
||||
.filter-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
margin-bottom: 5px;
|
||||
|
||||
.filter-label, .filter-key {
|
||||
font-weight: bold;
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
flex-grow: 1;
|
||||
}
|
||||
|
||||
.filter-type {
|
||||
font-size: 10px;
|
||||
color: #6c757d;
|
||||
margin-left: 5px;
|
||||
}
|
||||
|
||||
.btn-icon {
|
||||
padding: 2px;
|
||||
min-width: auto;
|
||||
}
|
||||
}
|
||||
|
||||
.filter-control {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 5px;
|
||||
|
||||
&.date-range {
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
&.toggle {
|
||||
justify-content: center;
|
||||
}
|
||||
}
|
||||
|
||||
.compact-input,
|
||||
.compact-select,
|
||||
.compact-multiselect,
|
||||
.compact-date {
|
||||
width: 100%;
|
||||
padding: 4px 6px;
|
||||
font-size: 12px;
|
||||
border-radius: 3px;
|
||||
min-height: 24px;
|
||||
}
|
||||
|
||||
.compact-select,
|
||||
.compact-multiselect {
|
||||
height: 24px;
|
||||
}
|
||||
|
||||
.compact-multiselect {
|
||||
height: auto;
|
||||
min-height: 24px;
|
||||
}
|
||||
|
||||
.multiselect-container {
|
||||
max-height: 150px;
|
||||
overflow-y: auto;
|
||||
border: 1px solid #ddd;
|
||||
border-radius: 4px;
|
||||
padding: 5px;
|
||||
background: white;
|
||||
}
|
||||
|
||||
.checkbox-group {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 3px;
|
||||
}
|
||||
|
||||
.checkbox-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 5px;
|
||||
}
|
||||
|
||||
.checkbox-label {
|
||||
font-size: 12px;
|
||||
margin: 0;
|
||||
cursor: pointer;
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
|
||||
.clr-checkbox {
|
||||
margin: 0;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.toggle-label {
|
||||
margin: 0;
|
||||
font-size: 12px;
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
max-width: 100px;
|
||||
}
|
||||
|
||||
.clr-toggle {
|
||||
margin: 0 5px 0 0;
|
||||
}
|
||||
|
||||
/* Responsive design */
|
||||
@media (max-width: 768px) {
|
||||
min-width: 100px;
|
||||
max-width: 150px;
|
||||
|
||||
.compact-input,
|
||||
.compact-select,
|
||||
.compact-multiselect,
|
||||
.compact-date {
|
||||
font-size: 11px;
|
||||
padding: 3px 5px;
|
||||
}
|
||||
|
||||
.toggle-label {
|
||||
font-size: 11px;
|
||||
max-width: 80px;
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 480px) {
|
||||
min-width: 80px;
|
||||
max-width: 120px;
|
||||
|
||||
.compact-input,
|
||||
.compact-select,
|
||||
.compact-multiselect,
|
||||
.compact-date {
|
||||
font-size: 10px;
|
||||
padding: 2px 4px;
|
||||
}
|
||||
|
||||
.toggle-label {
|
||||
font-size: 10px;
|
||||
max-width: 60px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.compact-filter-config {
|
||||
min-width: 200px;
|
||||
max-width: 300px;
|
||||
padding: 10px;
|
||||
background: #f8f9fa;
|
||||
border: 1px solid #e9ecef;
|
||||
border-radius: 4px;
|
||||
|
||||
.config-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
margin-bottom: 10px;
|
||||
|
||||
h5 {
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.btn-link {
|
||||
padding: 2px;
|
||||
min-width: auto;
|
||||
}
|
||||
}
|
||||
|
||||
.config-form {
|
||||
.clr-form-control {
|
||||
margin-bottom: 8px;
|
||||
|
||||
.clr-control-label {
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
.clr-input,
|
||||
.clr-select,
|
||||
.clr-textarea {
|
||||
font-size: 12px;
|
||||
padding: 4px 6px;
|
||||
}
|
||||
|
||||
.clr-textarea {
|
||||
min-height: 60px;
|
||||
}
|
||||
|
||||
.available-values {
|
||||
background: #e9ecef;
|
||||
border-radius: 3px;
|
||||
padding: 6px;
|
||||
font-size: 11px;
|
||||
margin-top: 4px;
|
||||
word-break: break-all;
|
||||
}
|
||||
}
|
||||
|
||||
.config-actions {
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
gap: 5px;
|
||||
margin-top: 10px;
|
||||
|
||||
.btn {
|
||||
font-size: 12px;
|
||||
padding: 4px 8px;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,441 @@
|
||||
import { Component, OnInit, Input, Output, EventEmitter, OnChanges, SimpleChanges, OnDestroy } from '@angular/core';
|
||||
import { FilterService, Filter } from './filter.service';
|
||||
import { AlertsService } from 'src/app/services/fnd/alerts.service';
|
||||
|
||||
@Component({
|
||||
selector: 'app-compact-filter',
|
||||
templateUrl: './compact-filter.component.html',
|
||||
styleUrls: ['./compact-filter.component.scss']
|
||||
})
|
||||
export class CompactFilterComponent implements OnInit, OnChanges, OnDestroy {
|
||||
@Input() filterKey: string = '';
|
||||
@Input() filterType: string = 'text';
|
||||
@Input() filterOptions: string[] = [];
|
||||
@Input() filterLabel: string = '';
|
||||
@Input() apiUrl: string = '';
|
||||
@Input() connectionId: number | undefined;
|
||||
@Output() filterChange = new EventEmitter<any>();
|
||||
@Output() configChange = new EventEmitter<any>();
|
||||
|
||||
selectedFilter: Filter | null = null;
|
||||
filterValue: any = '';
|
||||
availableFilters: Filter[] = [];
|
||||
availableKeys: string[] = [];
|
||||
availableValues: string[] = [];
|
||||
|
||||
// Multiselect dropdown state
|
||||
showMultiselectDropdown: boolean = false;
|
||||
|
||||
// Configuration properties
|
||||
isConfigMode: boolean = false;
|
||||
configFilterKey: string = '';
|
||||
configFilterType: string = 'text';
|
||||
configFilterOptions: string = '';
|
||||
configFilterLabel: string = '';
|
||||
configApiUrl: string = '';
|
||||
configConnectionId: number | undefined;
|
||||
|
||||
constructor(
|
||||
private filterService: FilterService,
|
||||
private alertService: AlertsService
|
||||
) { }
|
||||
|
||||
ngOnInit(): void {
|
||||
// Initialize configuration from inputs
|
||||
this.configFilterKey = this.filterKey;
|
||||
this.configFilterType = this.filterType;
|
||||
this.configFilterLabel = this.filterLabel;
|
||||
this.configFilterOptions = this.filterOptions.join(',');
|
||||
this.configApiUrl = this.apiUrl;
|
||||
this.configConnectionId = this.connectionId;
|
||||
|
||||
// Load available keys and values if API URL and filter key are provided
|
||||
if (this.apiUrl) {
|
||||
this.loadAvailableKeys();
|
||||
// Load available values for the current filter key if it's a dropdown or multiselect
|
||||
if ((this.filterType === 'dropdown' || this.filterType === 'multiselect') && this.filterKey) {
|
||||
this.loadAvailableValues(this.filterKey);
|
||||
}
|
||||
}
|
||||
|
||||
// Register this filter with the filter service
|
||||
this.registerFilter();
|
||||
|
||||
// Subscribe to filter definitions to get available filters
|
||||
this.filterService.filters$.subscribe(filters => {
|
||||
this.availableFilters = filters;
|
||||
this.updateSelectedFilter();
|
||||
});
|
||||
|
||||
// Subscribe to filter state changes
|
||||
this.filterService.filterState$.subscribe(state => {
|
||||
if (this.selectedFilter && state.hasOwnProperty(this.selectedFilter.id)) {
|
||||
this.filterValue = state[this.selectedFilter.id];
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
ngOnChanges(changes: SimpleChanges): void {
|
||||
// If filterKey changes, clear the previous filter value and remove old filter from service
|
||||
if (changes.filterKey) {
|
||||
// Clear the previous filter value
|
||||
this.filterValue = '';
|
||||
|
||||
// Clear filter options
|
||||
this.filterOptions = [];
|
||||
|
||||
// Clear available values
|
||||
this.availableValues = [];
|
||||
|
||||
// If we had a previous selected filter, clear its value in the service
|
||||
if (this.selectedFilter && changes.filterKey.previousValue) {
|
||||
const oldFilterId = changes.filterKey.previousValue;
|
||||
this.filterService.updateFilterValue(oldFilterId, '');
|
||||
}
|
||||
}
|
||||
|
||||
// If filterKey or filterType changes, re-register the filter
|
||||
if (changes.filterKey || changes.filterType) {
|
||||
// Load available values for the current filter key if it's a dropdown or multiselect
|
||||
if ((this.filterType === 'dropdown' || this.filterType === 'multiselect') && this.filterKey) {
|
||||
this.loadAvailableValues(this.filterKey);
|
||||
}
|
||||
this.registerFilter();
|
||||
}
|
||||
|
||||
// Handle API URL changes
|
||||
if (changes.apiUrl && !changes.apiUrl.firstChange) {
|
||||
if (this.apiUrl) {
|
||||
this.loadAvailableKeys();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Register this filter with the filter service
|
||||
registerFilter(): void {
|
||||
if (this.filterKey) {
|
||||
// Get current filter values from the service
|
||||
const currentFilterValues = this.filterService.getFilterValues();
|
||||
|
||||
// Create a filter definition for this compact filter
|
||||
const filterDef: Filter = {
|
||||
id: `${this.filterKey}`,
|
||||
field: this.filterKey,
|
||||
label: this.filterLabel || this.filterKey,
|
||||
type: this.filterType as any,
|
||||
options: this.filterOptions,
|
||||
value: this.filterValue // Use the current filter value
|
||||
};
|
||||
|
||||
// Get current filters
|
||||
const currentFilters = this.filterService.getFilters();
|
||||
|
||||
// Check if this filter is already registered
|
||||
const existingFilterIndex = currentFilters.findIndex(f => f.id === filterDef.id);
|
||||
|
||||
if (existingFilterIndex >= 0) {
|
||||
// Preserve the existing filter configuration
|
||||
const existingFilter = currentFilters[existingFilterIndex];
|
||||
|
||||
// Preserve the existing filter value if it exists in the service
|
||||
if (currentFilterValues.hasOwnProperty(existingFilter.id)) {
|
||||
filterDef.value = currentFilterValues[existingFilter.id];
|
||||
this.filterValue = filterDef.value; // Update local value
|
||||
} else if (existingFilter.value !== undefined) {
|
||||
// Fallback to existing filter's value if no service value
|
||||
filterDef.value = existingFilter.value;
|
||||
this.filterValue = filterDef.value;
|
||||
}
|
||||
|
||||
// Preserve other configuration properties
|
||||
filterDef.label = existingFilter.label;
|
||||
filterDef.options = existingFilter.options || this.filterOptions;
|
||||
|
||||
// Update existing filter
|
||||
currentFilters[existingFilterIndex] = filterDef;
|
||||
} else {
|
||||
// For new filters, check if there's already a value in the service
|
||||
if (currentFilterValues.hasOwnProperty(filterDef.id)) {
|
||||
filterDef.value = currentFilterValues[filterDef.id];
|
||||
this.filterValue = filterDef.value; // Update local value
|
||||
}
|
||||
|
||||
// Add new filter
|
||||
currentFilters.push(filterDef);
|
||||
}
|
||||
|
||||
// Update the filter service with the new filter list
|
||||
this.filterService.setFilters(currentFilters);
|
||||
|
||||
// Update the selected filter reference
|
||||
this.selectedFilter = filterDef;
|
||||
}
|
||||
}
|
||||
|
||||
updateSelectedFilter(): void {
|
||||
if (this.filterKey && this.availableFilters.length > 0) {
|
||||
this.selectedFilter = this.availableFilters.find(f => f.field === this.filterKey) || null;
|
||||
if (this.selectedFilter) {
|
||||
// Get current value for this filter from the service
|
||||
const currentState = this.filterService.getFilterValues();
|
||||
const filterValue = currentState[this.selectedFilter.id];
|
||||
if (filterValue !== undefined) {
|
||||
this.filterValue = filterValue;
|
||||
} else if (this.selectedFilter.value !== undefined) {
|
||||
// Use the filter's default value if no service value
|
||||
this.filterValue = this.selectedFilter.value;
|
||||
} else {
|
||||
// Use the current filter value as fallback
|
||||
this.filterValue = this.filterValue || '';
|
||||
}
|
||||
|
||||
// Also update configuration properties from the selected filter
|
||||
this.configFilterKey = this.selectedFilter.field;
|
||||
this.configFilterType = this.selectedFilter.type;
|
||||
this.configFilterLabel = this.selectedFilter.label;
|
||||
this.configFilterOptions = (this.selectedFilter.options || []).join(',');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
onFilterValueChange(value: any): void {
|
||||
if (this.selectedFilter) {
|
||||
this.filterValue = value;
|
||||
this.filterService.updateFilterValue(this.selectedFilter.id, value);
|
||||
this.filterChange.emit({ filterId: this.selectedFilter.id, value: value });
|
||||
|
||||
// Update the filter definition in the service to reflect the new value
|
||||
const currentFilters = this.filterService.getFilters();
|
||||
const filterIndex = currentFilters.findIndex(f => f.id === this.selectedFilter.id);
|
||||
if (filterIndex >= 0) {
|
||||
currentFilters[filterIndex].value = value;
|
||||
this.filterService.setFilters(currentFilters);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
onToggleChange(checked: boolean): void {
|
||||
this.onFilterValueChange(checked);
|
||||
}
|
||||
|
||||
onDateRangeChange(dateRange: { start: string | null, end: string | null }): void {
|
||||
this.onFilterValueChange(dateRange);
|
||||
}
|
||||
|
||||
ngOnDestroy(): void {
|
||||
// Component cleanup - remove this filter from the filter service
|
||||
if (this.selectedFilter) {
|
||||
// Use the proper removeFilter method which handles both filter definition and state
|
||||
this.filterService.removeFilter(this.selectedFilter.id);
|
||||
}
|
||||
}
|
||||
|
||||
// Load available keys from API
|
||||
loadAvailableKeys(): void {
|
||||
if (this.apiUrl) {
|
||||
this.alertService.getColumnfromurl(this.apiUrl, this.connectionId).subscribe(
|
||||
(keys: string[]) => {
|
||||
this.availableKeys = keys;
|
||||
},
|
||||
(error) => {
|
||||
console.error('Error loading available keys:', error);
|
||||
this.availableKeys = [];
|
||||
}
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// Load available values for a specific key
|
||||
loadAvailableValues(key: string): void {
|
||||
if (this.apiUrl && key) {
|
||||
this.alertService.getValuesFromUrl(this.apiUrl, this.connectionId, key).subscribe(
|
||||
(values: string[]) => {
|
||||
this.availableValues = values;
|
||||
// Update filter options if this is a dropdown or multiselect
|
||||
if (this.filterType === 'dropdown' || this.filterType === 'multiselect') {
|
||||
this.filterOptions = values;
|
||||
}
|
||||
},
|
||||
(error) => {
|
||||
console.error('Error loading available values:', error);
|
||||
this.availableValues = [];
|
||||
}
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// Configuration methods
|
||||
toggleConfigMode(): void {
|
||||
this.isConfigMode = !this.isConfigMode;
|
||||
if (this.isConfigMode) {
|
||||
// Initialize config values from current filter if available
|
||||
if (this.selectedFilter) {
|
||||
this.configFilterKey = this.selectedFilter.field;
|
||||
this.configFilterType = this.selectedFilter.type;
|
||||
this.configFilterLabel = this.selectedFilter.label;
|
||||
this.configFilterOptions = (this.selectedFilter.options || []).join(',');
|
||||
} else {
|
||||
// Fallback to current properties
|
||||
this.configFilterKey = this.filterKey;
|
||||
this.configFilterType = this.filterType;
|
||||
this.configFilterLabel = this.filterLabel;
|
||||
this.configFilterOptions = this.filterOptions.join(',');
|
||||
}
|
||||
this.configApiUrl = this.apiUrl;
|
||||
this.configConnectionId = this.connectionId;
|
||||
}
|
||||
}
|
||||
|
||||
saveConfiguration(): void {
|
||||
const config = {
|
||||
filterKey: this.configFilterKey,
|
||||
filterType: this.configFilterType,
|
||||
filterLabel: this.configFilterLabel,
|
||||
filterOptions: this.configFilterOptions.split(',').map(opt => opt.trim()).filter(opt => opt),
|
||||
apiUrl: this.configApiUrl,
|
||||
connectionId: this.configConnectionId
|
||||
};
|
||||
|
||||
// Emit configuration change
|
||||
this.configChange.emit(config);
|
||||
|
||||
// Update local properties
|
||||
this.filterKey = config.filterKey;
|
||||
this.filterType = config.filterType;
|
||||
this.filterLabel = config.filterLabel;
|
||||
this.filterOptions = config.filterOptions;
|
||||
this.apiUrl = config.apiUrl;
|
||||
this.connectionId = config.connectionId;
|
||||
|
||||
// Clear filter value when changing configuration
|
||||
this.filterValue = '';
|
||||
|
||||
// Load available keys if API URL is provided
|
||||
if (this.apiUrl) {
|
||||
this.loadAvailableKeys();
|
||||
}
|
||||
|
||||
// Load available values for the selected key if it's a dropdown or multiselect
|
||||
if ((this.configFilterType === 'dropdown' || this.configFilterType === 'multiselect') && this.configFilterKey) {
|
||||
this.loadAvailableValues(this.configFilterKey);
|
||||
}
|
||||
|
||||
// Register the updated filter with the filter service
|
||||
this.registerFilter();
|
||||
|
||||
// Update selected filter
|
||||
this.updateSelectedFilter();
|
||||
|
||||
// Exit config mode
|
||||
this.isConfigMode = false;
|
||||
}
|
||||
|
||||
cancelConfiguration(): void {
|
||||
this.isConfigMode = false;
|
||||
}
|
||||
|
||||
// Handle filter key change in configuration
|
||||
onFilterKeyChange(key: string): void {
|
||||
// Clear the previous filter value when changing keys
|
||||
this.filterValue = '';
|
||||
|
||||
// Clear filter options until new values are loaded
|
||||
this.filterOptions = [];
|
||||
|
||||
this.configFilterKey = key;
|
||||
|
||||
// Load available values for the selected key if it's a dropdown or multiselect
|
||||
if ((this.configFilterType === 'dropdown' || this.configFilterType === 'multiselect') && key) {
|
||||
this.loadAvailableValues(key);
|
||||
}
|
||||
|
||||
// Clear the filter service value for the previous filter key
|
||||
if (this.selectedFilter) {
|
||||
this.filterService.updateFilterValue(this.selectedFilter.id, '');
|
||||
}
|
||||
}
|
||||
|
||||
// Handle API URL change in configuration
|
||||
onApiUrlChange(url: string): void {
|
||||
this.configApiUrl = url;
|
||||
// Load available keys when API URL changes
|
||||
if (url) {
|
||||
this.loadAvailableKeys();
|
||||
// Also clear available values since the API has changed
|
||||
this.availableValues = [];
|
||||
this.filterOptions = [];
|
||||
}
|
||||
}
|
||||
|
||||
// Handle filter type change in configuration
|
||||
onFilterTypeChange(type: string): void {
|
||||
this.configFilterType = type;
|
||||
// If changing to dropdown or multiselect and we have a key selected, load values
|
||||
if ((type === 'dropdown' || type === 'multiselect') && this.configFilterKey) {
|
||||
this.loadAvailableValues(this.configFilterKey);
|
||||
}
|
||||
}
|
||||
|
||||
// Add method to check if an option is selected for checkboxes
|
||||
isOptionSelected(option: string): boolean {
|
||||
if (!this.filterValue) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Ensure filterValue is an array for multiselect
|
||||
if (!Array.isArray(this.filterValue)) {
|
||||
this.filterValue = [];
|
||||
return false;
|
||||
}
|
||||
|
||||
return this.filterValue.includes(option);
|
||||
}
|
||||
// need to check this
|
||||
// Add method to handle multiselect option change
|
||||
onMultiselectOptionChange(event: any, option: string): void {
|
||||
// Initialize filterValue array if it doesn't exist
|
||||
if (!this.filterValue) {
|
||||
this.filterValue = [];
|
||||
}
|
||||
|
||||
// Ensure filterValue is an array
|
||||
if (!Array.isArray(this.filterValue)) {
|
||||
this.filterValue = [];
|
||||
}
|
||||
|
||||
if (event.target.checked) {
|
||||
// Add option if not already in array
|
||||
if (!this.filterValue.includes(option)) {
|
||||
this.filterValue.push(option);
|
||||
}
|
||||
} else {
|
||||
// Remove option from array
|
||||
const index = this.filterValue.indexOf(option);
|
||||
if (index > -1) {
|
||||
this.filterValue.splice(index, 1);
|
||||
}
|
||||
}
|
||||
|
||||
// Emit the change event
|
||||
this.onFilterValueChange(this.filterValue);
|
||||
}
|
||||
|
||||
// Add method to toggle multiselect dropdown visibility
|
||||
toggleMultiselectDropdown(): void {
|
||||
this.showMultiselectDropdown = !this.showMultiselectDropdown;
|
||||
|
||||
// Add document click handler to close dropdown when clicking outside
|
||||
if (this.showMultiselectDropdown) {
|
||||
setTimeout(() => {
|
||||
const handleClick = (event: MouseEvent) => {
|
||||
const target = event.target as HTMLElement;
|
||||
if (!target.closest('.compact-multiselect-display') && !target.closest('.compact-multiselect-dropdown')) {
|
||||
this.showMultiselectDropdown = false;
|
||||
document.removeEventListener('click', handleClick);
|
||||
}
|
||||
};
|
||||
document.addEventListener('click', handleClick);
|
||||
}, 0);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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,199 @@
|
||||
import { Injectable } from '@angular/core';
|
||||
import { BehaviorSubject, Observable } from 'rxjs';
|
||||
|
||||
// Define the filter types
|
||||
export type FilterType = 'dropdown' | 'multiselect' | 'date-range' | 'text' | 'toggle';
|
||||
|
||||
// Define the filter interface
|
||||
export interface Filter {
|
||||
id: string;
|
||||
field: string;
|
||||
label: string;
|
||||
type: FilterType;
|
||||
options?: string[]; // For dropdown and multiselect
|
||||
value?: any; // Current value
|
||||
placeholder?: string;
|
||||
}
|
||||
|
||||
// Define the filter state
|
||||
export interface FilterState {
|
||||
[key: string]: any;
|
||||
}
|
||||
|
||||
@Injectable({
|
||||
providedIn: 'root'
|
||||
})
|
||||
export class FilterService {
|
||||
// Store the filter definitions
|
||||
private filtersSubject = new BehaviorSubject<Filter[]>([]);
|
||||
public filters$ = this.filtersSubject.asObservable();
|
||||
|
||||
// Store the current filter values
|
||||
private filterStateSubject = new BehaviorSubject<FilterState>({});
|
||||
public filterState$ = this.filterStateSubject.asObservable();
|
||||
|
||||
// Store the active filter presets
|
||||
private activePresetSubject = new BehaviorSubject<string | null>(null);
|
||||
public activePreset$ = this.activePresetSubject.asObservable();
|
||||
|
||||
// Store filter presets
|
||||
private presets: { [key: string]: FilterState } = {};
|
||||
|
||||
constructor() { }
|
||||
|
||||
// Add a new filter
|
||||
addFilter(filter: Filter): void {
|
||||
const currentFilters = this.filtersSubject.value;
|
||||
this.filtersSubject.next([...currentFilters, filter]);
|
||||
}
|
||||
|
||||
// Remove a filter
|
||||
removeFilter(filterId: string): void {
|
||||
const currentFilters = this.filtersSubject.value;
|
||||
const updatedFilters = currentFilters.filter(f => f.id !== filterId);
|
||||
this.filtersSubject.next(updatedFilters);
|
||||
|
||||
// Also remove the filter value from state
|
||||
const currentState = this.filterStateSubject.value;
|
||||
const newState = { ...currentState };
|
||||
delete newState[filterId];
|
||||
this.filterStateSubject.next(newState);
|
||||
}
|
||||
|
||||
// Update filter value
|
||||
updateFilterValue(filterId: string, value: any): void {
|
||||
console.log('=== FILTER SERVICE DEBUG INFO ===');
|
||||
console.log('Updating filter value for ID:', filterId);
|
||||
console.log('New value:', value);
|
||||
|
||||
const currentState = this.filterStateSubject.value;
|
||||
const newState = {
|
||||
...currentState,
|
||||
[filterId]: value
|
||||
};
|
||||
|
||||
console.log('New filter state:', newState);
|
||||
this.filterStateSubject.next(newState);
|
||||
console.log('=== END FILTER SERVICE DEBUG ===');
|
||||
}
|
||||
|
||||
// Get current filter values
|
||||
getFilterValues(): FilterState {
|
||||
return this.filterStateSubject.value;
|
||||
}
|
||||
|
||||
// Reset all filters
|
||||
resetFilters(): void {
|
||||
const currentFilters = this.filtersSubject.value;
|
||||
const resetState: FilterState = {};
|
||||
|
||||
// Initialize all filters with empty/default values
|
||||
currentFilters.forEach(filter => {
|
||||
switch (filter.type) {
|
||||
case 'multiselect':
|
||||
resetState[filter.id] = [];
|
||||
break;
|
||||
case 'date-range':
|
||||
resetState[filter.id] = { start: null, end: null };
|
||||
break;
|
||||
case 'toggle':
|
||||
resetState[filter.id] = false;
|
||||
break;
|
||||
default:
|
||||
resetState[filter.id] = '';
|
||||
}
|
||||
});
|
||||
|
||||
this.filterStateSubject.next(resetState);
|
||||
}
|
||||
|
||||
// Save current filter state as a preset
|
||||
savePreset(name: string): void {
|
||||
this.presets[name] = this.filterStateSubject.value;
|
||||
}
|
||||
|
||||
// Load a preset
|
||||
loadPreset(name: string): void {
|
||||
if (this.presets[name]) {
|
||||
this.filterStateSubject.next(this.presets[name]);
|
||||
this.activePresetSubject.next(name);
|
||||
}
|
||||
}
|
||||
|
||||
// Get all presets
|
||||
getPresets(): string[] {
|
||||
return Object.keys(this.presets);
|
||||
}
|
||||
|
||||
// Delete a preset
|
||||
deletePreset(name: string): void {
|
||||
delete this.presets[name];
|
||||
if (this.activePresetSubject.value === name) {
|
||||
this.activePresetSubject.next(null);
|
||||
}
|
||||
}
|
||||
|
||||
// Clear all presets
|
||||
clearPresets(): void {
|
||||
this.presets = {};
|
||||
this.activePresetSubject.next(null);
|
||||
}
|
||||
|
||||
// Build query parameters for API calls
|
||||
buildQueryParams(): string {
|
||||
const filterValues = this.getFilterValues();
|
||||
const params = new URLSearchParams();
|
||||
|
||||
Object.keys(filterValues).forEach(key => {
|
||||
const value = filterValues[key];
|
||||
if (value !== undefined && value !== null && value !== '') {
|
||||
if (typeof value === 'object') {
|
||||
// Handle date ranges and other objects
|
||||
if (value.hasOwnProperty('start') && value.hasOwnProperty('end')) {
|
||||
// Date range
|
||||
if (value.start) params.append(`${key}_start`, value.start);
|
||||
if (value.end) params.append(`${key}_end`, value.end);
|
||||
} else {
|
||||
// Other objects as JSON
|
||||
params.append(key, JSON.stringify(value));
|
||||
}
|
||||
} else {
|
||||
// Simple values
|
||||
params.append(key, value.toString());
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
return params.toString();
|
||||
}
|
||||
|
||||
// Get filter definitions
|
||||
getFilters(): Filter[] {
|
||||
return this.filtersSubject.value;
|
||||
}
|
||||
|
||||
// Update filter definitions
|
||||
setFilters(filters: Filter[]): void {
|
||||
this.filtersSubject.next(filters);
|
||||
|
||||
// Initialize filter state with default values
|
||||
const initialState: FilterState = {};
|
||||
filters.forEach(filter => {
|
||||
switch (filter.type) {
|
||||
case 'multiselect':
|
||||
initialState[filter.id] = filter.value || [];
|
||||
break;
|
||||
case 'date-range':
|
||||
initialState[filter.id] = filter.value || { start: null, end: null };
|
||||
break;
|
||||
case 'toggle':
|
||||
initialState[filter.id] = filter.value || false;
|
||||
break;
|
||||
default:
|
||||
initialState[filter.id] = filter.value || '';
|
||||
}
|
||||
});
|
||||
|
||||
this.filterStateSubject.next(initialState);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,4 @@
|
||||
export * from './filter.service';
|
||||
export * from './common-filter.component';
|
||||
export * from './chart-wrapper.component';
|
||||
export * from './compact-filter.component';
|
||||
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
Binary file not shown.
@@ -1,31 +1,334 @@
|
||||
<div style="display: block">
|
||||
<!-- No filter controls needed with the new simplified approach -->
|
||||
<!-- Filters are now configured at the drilldown level -->
|
||||
<div class="chart-container">
|
||||
<!-- Filter Controls Section -->
|
||||
<div class="filter-section" *ngIf="hasActiveFilters()">
|
||||
<!-- Base Filters -->
|
||||
<div class="filter-group" *ngIf="baseFilters && baseFilters.length > 0">
|
||||
<h4>Base Filters</h4>
|
||||
<div class="filter-controls">
|
||||
<div *ngFor="let filter of baseFilters" class="filter-item">
|
||||
<div class="filter-label">{{ filter.field }} ({{ filter.type || 'text' }})</div>
|
||||
|
||||
<!-- Drilldown mode indicator -->
|
||||
<div *ngIf="currentDrilldownLevel > 0" style="background-color: #e0e0e0; padding: 5px; margin-bottom: 10px; border-radius: 4px; text-align: center;">
|
||||
<span style="font-weight: bold; color: #333;">Drilldown Level: {{currentDrilldownLevel}}</span>
|
||||
<button (click)="navigateBack()" style="margin-left: 10px; padding: 2px 8px; background-color: #007cba; color: white; border: none; border-radius: 3px; cursor: pointer;">
|
||||
Back to Level {{currentDrilldownLevel - 1}}
|
||||
</button>
|
||||
<button (click)="resetToOriginalData()" style="margin-left: 10px; padding: 2px 8px; background-color: #dc3545; color: white; border: none; border-radius: 3px; cursor: pointer;">
|
||||
Back to Main View
|
||||
</button>
|
||||
<!-- Text Filter -->
|
||||
<div *ngIf="!filter.type || filter.type === 'text'" class="filter-input">
|
||||
<input type="text"
|
||||
[(ngModel)]="filter.value"
|
||||
(ngModelChange)="onBaseFilterChange(filter)"
|
||||
[placeholder]="filter.field"
|
||||
class="clr-input filter-text-input">
|
||||
</div>
|
||||
|
||||
<!-- Dropdown Filter -->
|
||||
<div *ngIf="filter.type === 'dropdown'" class="filter-input">
|
||||
<select [(ngModel)]="filter.value"
|
||||
(ngModelChange)="onBaseFilterChange(filter)"
|
||||
class="clr-select filter-select">
|
||||
<option value="">Select {{ filter.field }}</option>
|
||||
<option *ngFor="let option of getFilterOptions(filter)" [value]="option">{{ option }}</option>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<!-- Multi-Select Filter - Updated to show key first, then dropdown on click -->
|
||||
<div *ngIf="filter.type === 'multiselect'" class="filter-input multiselect-container">
|
||||
<div class="multiselect-display" (click)="toggleMultiselect(filter, 'base')">
|
||||
<span class="multiselect-label">{{ filter.field }}</span>
|
||||
<span class="multiselect-value" *ngIf="getSelectedOptionsCount(filter) > 0">
|
||||
({{ getSelectedOptionsCount(filter) }} selected)
|
||||
</span>
|
||||
<clr-icon shape="caret down" class="dropdown-icon"></clr-icon>
|
||||
</div>
|
||||
<div class="multiselect-dropdown" *ngIf="isMultiselectOpen(filter, 'base')">
|
||||
<div class="checkbox-group">
|
||||
<div *ngFor="let option of getFilterOptions(filter); let i = index" class="checkbox-item">
|
||||
<input type="checkbox"
|
||||
[checked]="isOptionSelected(filter, option)"
|
||||
(change)="onMultiSelectChange(filter, option, $event)"
|
||||
[id]="'base-' + filter.field + '-' + i"
|
||||
class="clr-checkbox">
|
||||
<label [for]="'base-' + filter.field + '-' + i" class="checkbox-label">{{ option }}</label>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Date Range Filter -->
|
||||
<div *ngIf="filter.type === 'date-range'" class="filter-input date-range">
|
||||
<div class="date-input-group">
|
||||
<input type="date"
|
||||
[(ngModel)]="filter.value.start"
|
||||
(ngModelChange)="onDateRangeChange(filter, { start: $event, end: filter.value.end })"
|
||||
placeholder="Start Date"
|
||||
class="clr-input filter-date">
|
||||
<span class="date-separator">to</span>
|
||||
<input type="date"
|
||||
[(ngModel)]="filter.value.end"
|
||||
(ngModelChange)="onDateRangeChange(filter, { start: filter.value.start, end: $event })"
|
||||
placeholder="End Date"
|
||||
class="clr-input filter-date">
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Toggle Filter -->
|
||||
<div *ngIf="filter.type === 'toggle'" class="filter-input toggle">
|
||||
<input type="checkbox"
|
||||
[(ngModel)]="filter.value"
|
||||
(ngModelChange)="onToggleChange(filter, $event)"
|
||||
clrToggle
|
||||
class="clr-toggle">
|
||||
<label class="toggle-label">{{ filter.field }}</label>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Drilldown Filters -->
|
||||
<div class="filter-group" *ngIf="drilldownFilters && drilldownFilters.length > 0 && currentDrilldownLevel > 0">
|
||||
<h4>Drilldown Filters</h4>
|
||||
<div class="filter-controls">
|
||||
<div *ngFor="let filter of drilldownFilters" class="filter-item">
|
||||
<div class="filter-label">{{ filter.field }} ({{ filter.type || 'text' }})</div>
|
||||
|
||||
<!-- Text Filter -->
|
||||
<div *ngIf="!filter.type || filter.type === 'text'" class="filter-input">
|
||||
<input type="text"
|
||||
[(ngModel)]="filter.value"
|
||||
(ngModelChange)="onDrilldownFilterChange(filter)"
|
||||
[placeholder]="filter.field"
|
||||
class="clr-input filter-text-input">
|
||||
</div>
|
||||
|
||||
<!-- Dropdown Filter -->
|
||||
<div *ngIf="filter.type === 'dropdown'" class="filter-input">
|
||||
<select [(ngModel)]="filter.value"
|
||||
(ngModelChange)="onDrilldownFilterChange(filter)"
|
||||
class="clr-select filter-select">
|
||||
<option value="">Select {{ filter.field }}</option>
|
||||
<option *ngFor="let option of getFilterOptions(filter)" [value]="option">{{ option }}</option>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<!-- Multi-Select Filter - Updated to show key first, then dropdown on click -->
|
||||
<div *ngIf="filter.type === 'multiselect'" class="filter-input multiselect-container">
|
||||
<div class="multiselect-display" (click)="toggleMultiselect(filter, 'drilldown')">
|
||||
<span class="multiselect-label">{{ filter.field }}</span>
|
||||
<span class="multiselect-value" *ngIf="getSelectedOptionsCount(filter) > 0">
|
||||
({{ getSelectedOptionsCount(filter) }} selected)
|
||||
</span>
|
||||
<clr-icon shape="caret down" class="dropdown-icon"></clr-icon>
|
||||
</div>
|
||||
<div class="multiselect-dropdown" *ngIf="isMultiselectOpen(filter, 'drilldown')">
|
||||
<div class="checkbox-group">
|
||||
<div *ngFor="let option of getFilterOptions(filter); let i = index" class="checkbox-item">
|
||||
<input type="checkbox"
|
||||
[checked]="isOptionSelected(filter, option)"
|
||||
(change)="onMultiSelectChange(filter, option, $event)"
|
||||
[id]="'drilldown-' + filter.field + '-' + i"
|
||||
class="clr-checkbox">
|
||||
<label [for]="'drilldown-' + filter.field + '-' + i" class="checkbox-label">{{ option }}</label>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Date Range Filter -->
|
||||
<div *ngIf="filter.type === 'date-range'" class="filter-input date-range">
|
||||
<div class="date-input-group">
|
||||
<input type="date"
|
||||
[(ngModel)]="filter.value.start"
|
||||
(ngModelChange)="onDateRangeChange(filter, { start: $event, end: filter.value.end })"
|
||||
placeholder="Start Date"
|
||||
class="clr-input filter-date">
|
||||
<span class="date-separator">to</span>
|
||||
<input type="date"
|
||||
[(ngModel)]="filter.value.end"
|
||||
(ngModelChange)="onDateRangeChange(filter, { start: filter.value.start, end: $event })"
|
||||
placeholder="End Date"
|
||||
class="clr-input filter-date">
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Toggle Filter -->
|
||||
<div *ngIf="filter.type === 'toggle'" class="filter-input toggle">
|
||||
<input type="checkbox"
|
||||
[(ngModel)]="filter.value"
|
||||
(ngModelChange)="onToggleChange(filter, $event)"
|
||||
clrToggle
|
||||
class="clr-toggle">
|
||||
<label class="toggle-label">{{ filter.field }}</label>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Layer Filters -->
|
||||
<div class="filter-group" *ngIf="hasActiveLayerFilters()">
|
||||
<h4>Layer Filters</h4>
|
||||
<div class="filter-controls">
|
||||
<div *ngFor="let filter of getActiveLayerFilters()" class="filter-item">
|
||||
<div class="filter-label">{{ filter.field }} ({{ filter.type || 'text' }})</div>
|
||||
|
||||
<!-- Text Filter -->
|
||||
<div *ngIf="!filter.type || filter.type === 'text'" class="filter-input">
|
||||
<input type="text"
|
||||
[(ngModel)]="filter.value"
|
||||
(ngModelChange)="onLayerFilterChange(filter)"
|
||||
[placeholder]="filter.field"
|
||||
class="clr-input filter-text-input">
|
||||
</div>
|
||||
|
||||
<!-- Dropdown Filter -->
|
||||
<div *ngIf="filter.type === 'dropdown'" class="filter-input">
|
||||
<select [(ngModel)]="filter.value"
|
||||
(ngModelChange)="onLayerFilterChange(filter)"
|
||||
class="clr-select filter-select">
|
||||
<option value="">Select {{ filter.field }}</option>
|
||||
<option *ngFor="let option of getFilterOptions(filter)" [value]="option">{{ option }}</option>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<!-- Multi-Select Filter - Updated to show key first, then dropdown on click -->
|
||||
<div *ngIf="filter.type === 'multiselect'" class="filter-input multiselect-container">
|
||||
<div class="multiselect-display" (click)="toggleMultiselect(filter, 'layer')">
|
||||
<span class="multiselect-label">{{ filter.field }}</span>
|
||||
<span class="multiselect-value" *ngIf="getSelectedOptionsCount(filter) > 0">
|
||||
({{ getSelectedOptionsCount(filter) }} selected)
|
||||
</span>
|
||||
<clr-icon shape="caret down" class="dropdown-icon"></clr-icon>
|
||||
</div>
|
||||
<div class="multiselect-dropdown" *ngIf="isMultiselectOpen(filter, 'layer')">
|
||||
<div class="checkbox-group">
|
||||
<div *ngFor="let option of getFilterOptions(filter); let i = index" class="checkbox-item">
|
||||
<input type="checkbox"
|
||||
[checked]="isOptionSelected(filter, option)"
|
||||
(change)="onMultiSelectChange(filter, option, $event)"
|
||||
[id]="'layer-' + filter.field + '-' + i"
|
||||
class="clr-checkbox">
|
||||
<label [for]="'layer-' + filter.field + '-' + i" class="checkbox-label">{{ option }}</label>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Date Range Filter -->
|
||||
<div *ngIf="filter.type === 'date-range'" class="filter-input date-range">
|
||||
<div class="date-input-group">
|
||||
<input type="date"
|
||||
[(ngModel)]="filter.value.start"
|
||||
(ngModelChange)="onDateRangeChange(filter, { start: $event, end: filter.value.end })"
|
||||
placeholder="Start Date"
|
||||
class="clr-input filter-date">
|
||||
<span class="date-separator">to</span>
|
||||
<input type="date"
|
||||
[(ngModel)]="filter.value.end"
|
||||
(ngModelChange)="onDateRangeChange(filter, { start: filter.value.start, end: $event })"
|
||||
placeholder="End Date"
|
||||
class="clr-input filter-date">
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Toggle Filter -->
|
||||
<div *ngIf="filter.type === 'toggle'" class="filter-input toggle">
|
||||
<input type="checkbox"
|
||||
[(ngModel)]="filter.value"
|
||||
(ngModelChange)="onToggleChange(filter, $event)"
|
||||
clrToggle
|
||||
class="clr-toggle">
|
||||
<label class="toggle-label">{{ filter.field }}</label>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Clear Filters Button -->
|
||||
<div class="filter-actions">
|
||||
<button class="btn btn-sm btn-outline" (click)="clearAllFilters()">Clear All Filters</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- No data message -->
|
||||
<div *ngIf="noDataAvailable" style="text-align: center; padding: 20px; color: #666; font-style: italic;">
|
||||
No data available
|
||||
<!-- Header row with chart title and drilldown navigation -->
|
||||
<div class="chart-header">
|
||||
<div class="clr-row header-row">
|
||||
<div class="clr-col-6">
|
||||
<h3 class="chart-title">{{charttitle || 'Bar Chart'}}</h3>
|
||||
</div>
|
||||
<div class="clr-col-6" style="text-align: right;">
|
||||
<!-- Add drilldown navigation controls -->
|
||||
<button class="btn btn-sm btn-link" *ngIf="drilldownEnabled && drilldownStack.length > 0" (click)="navigateBack()">
|
||||
<cds-icon shape="arrow" direction="left"></cds-icon>
|
||||
Back to {{drilldownStack.length > 1 ? 'Previous Level' : 'Main Data'}}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Show current drilldown level -->
|
||||
<div class="clr-row" *ngIf="drilldownEnabled && drilldownStack.length > 0">
|
||||
<div class="clr-col-12">
|
||||
<div class="alert alert-info" style="padding: 8px 12px; margin-bottom: 12px;">
|
||||
<div class="alert-items">
|
||||
<div class="alert-item static">
|
||||
<div class="alert-icon-wrapper">
|
||||
<cds-icon class="alert-icon" shape="info-circle"></cds-icon>
|
||||
</div>
|
||||
<span class="alert-text">
|
||||
Drilldown Level: {{currentDrilldownLevel}}
|
||||
<span *ngIf="drilldownStack.length > 0">
|
||||
(Clicked on: {{drilldownStack[drilldownStack.length - 1].clickedLabel}})
|
||||
</span>
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Chart display -->
|
||||
<div *ngIf="!noDataAvailable">
|
||||
<canvas baseChart
|
||||
[datasets]="barChartData"
|
||||
[labels]="barChartLabels"
|
||||
[type]="barChartType"
|
||||
(chartHover)="chartHovered($event)"
|
||||
(chartClick)="chartClicked($event)">
|
||||
</canvas>
|
||||
<div class="chart-wrapper">
|
||||
<div class="chart-content" [class.loading]="isLoading">
|
||||
|
||||
<div *ngIf="noDataAvailable" class="no-data-message">
|
||||
No data available
|
||||
</div>
|
||||
|
||||
|
||||
<div *ngIf="!noDataAvailable" class="chart-display">
|
||||
<canvas baseChart
|
||||
[datasets]="barChartData"
|
||||
[labels]="barChartLabels"
|
||||
[type]="barChartType"
|
||||
[options]="barChartOptions"
|
||||
(chartHover)="chartHovered($event)"
|
||||
(chartClick)="chartClicked($event)">
|
||||
</canvas>
|
||||
</div>
|
||||
|
||||
<div class="loading-overlay" *ngIf="isLoading">
|
||||
<div class="shimmer-bar"></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
<!-- sheield dashboard -->
|
||||
<!--
|
||||
<div class="chart-container">
|
||||
<div class="chart-header">
|
||||
<h3>Deal Stage Wise Progress</h3>
|
||||
</div>
|
||||
<div class="chart-wrapper">
|
||||
<div class="chart-content" [class.loading]="isLoading">
|
||||
<canvas
|
||||
baseChart
|
||||
[data]="barChartData"
|
||||
[options]="barChartOptions"
|
||||
[type]="barChartType"
|
||||
(chartClick)="chartClicked($event)"
|
||||
(chartHover)="chartHovered($event)">
|
||||
</canvas>
|
||||
<div class="loading-overlay" *ngIf="isLoading">
|
||||
<div class="shimmer-bar"></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div> -->
|
||||
|
||||
|
||||
</div>
|
||||
@@ -0,0 +1,278 @@
|
||||
// Chart container structure
|
||||
.chart-container {
|
||||
height: 100%;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
|
||||
// Filter section styling
|
||||
.filter-section {
|
||||
margin-bottom: 20px;
|
||||
padding: 15px;
|
||||
border: 1px solid #ddd;
|
||||
border-radius: 4px;
|
||||
background-color: #f9f9f9;
|
||||
}
|
||||
|
||||
.filter-group {
|
||||
margin-bottom: 15px;
|
||||
|
||||
h4 {
|
||||
margin-top: 0;
|
||||
margin-bottom: 10px;
|
||||
color: #333;
|
||||
font-weight: 600;
|
||||
}
|
||||
}
|
||||
|
||||
.filter-controls {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 15px;
|
||||
}
|
||||
|
||||
.filter-item {
|
||||
flex: 1 1 300px;
|
||||
min-width: 250px;
|
||||
padding: 10px;
|
||||
background: white;
|
||||
border: 1px solid #e0e0e0;
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
.filter-label {
|
||||
font-weight: 500;
|
||||
margin-bottom: 8px;
|
||||
color: #555;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.filter-input {
|
||||
width: 100%;
|
||||
|
||||
.filter-text-input,
|
||||
.filter-select,
|
||||
.filter-date {
|
||||
width: 100%;
|
||||
padding: 6px 12px;
|
||||
border: 1px solid #ccc;
|
||||
border-radius: 4px;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.filter-select {
|
||||
height: 34px;
|
||||
}
|
||||
}
|
||||
|
||||
.multiselect-container {
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.multiselect-display {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
padding: 6px 12px;
|
||||
border: 1px solid #ccc;
|
||||
border-radius: 4px;
|
||||
background: white;
|
||||
cursor: pointer;
|
||||
min-height: 34px;
|
||||
|
||||
.multiselect-label {
|
||||
flex: 1;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.multiselect-value {
|
||||
color: #666;
|
||||
font-size: 12px;
|
||||
margin-right: 8px;
|
||||
}
|
||||
|
||||
.dropdown-icon {
|
||||
flex-shrink: 0;
|
||||
transition: transform 0.2s ease;
|
||||
}
|
||||
|
||||
&:hover {
|
||||
border-color: #999;
|
||||
}
|
||||
}
|
||||
|
||||
.multiselect-dropdown {
|
||||
position: absolute;
|
||||
top: 100%;
|
||||
left: 0;
|
||||
right: 0;
|
||||
z-index: 1000;
|
||||
background: white;
|
||||
border: 1px solid #ccc;
|
||||
border-top: none;
|
||||
border-radius: 0 0 4px 4px;
|
||||
box-shadow: 0 2px 4px rgba(0,0,0,0.1);
|
||||
max-height: 200px;
|
||||
overflow-y: auto;
|
||||
|
||||
.checkbox-group {
|
||||
padding: 8px;
|
||||
|
||||
.checkbox-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
padding: 4px 0;
|
||||
|
||||
.checkbox-label {
|
||||
margin: 0;
|
||||
font-size: 14px;
|
||||
cursor: pointer;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.date-range {
|
||||
.date-input-group {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.date-separator {
|
||||
margin: 0 5px;
|
||||
color: #777;
|
||||
}
|
||||
}
|
||||
|
||||
.toggle {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
|
||||
.toggle-label {
|
||||
margin: 0;
|
||||
font-size: 14px;
|
||||
cursor: pointer;
|
||||
}
|
||||
}
|
||||
|
||||
.filter-actions {
|
||||
margin-top: 15px;
|
||||
padding-top: 15px;
|
||||
border-top: 1px solid #eee;
|
||||
|
||||
.btn {
|
||||
font-size: 13px;
|
||||
}
|
||||
}
|
||||
|
||||
// Chart header styling
|
||||
.chart-header {
|
||||
margin-bottom: 20px;
|
||||
|
||||
.header-row {
|
||||
margin-bottom: 15px;
|
||||
padding-bottom: 10px;
|
||||
border-bottom: 1px solid #eee;
|
||||
|
||||
.chart-title {
|
||||
margin: 0;
|
||||
font-size: 18px;
|
||||
font-weight: 600;
|
||||
color: #333;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Chart wrapper and content
|
||||
.chart-wrapper {
|
||||
flex: 1;
|
||||
position: relative;
|
||||
|
||||
.chart-content {
|
||||
position: relative;
|
||||
height: 100%;
|
||||
min-height: 300px; // Ensure minimum height for chart
|
||||
|
||||
&.loading {
|
||||
opacity: 0.7;
|
||||
|
||||
.chart-display {
|
||||
filter: blur(2px);
|
||||
}
|
||||
}
|
||||
|
||||
.no-data-message {
|
||||
text-align: center;
|
||||
padding: 20px;
|
||||
color: #666;
|
||||
font-style: italic;
|
||||
}
|
||||
|
||||
.chart-display {
|
||||
position: relative;
|
||||
height: 100%;
|
||||
width: 100%;
|
||||
max-width: 100%;
|
||||
max-height: 100%;
|
||||
transition: filter 0.3s ease;
|
||||
}
|
||||
|
||||
.loading-overlay {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
background: rgba(255, 255, 255, 0.8);
|
||||
|
||||
.shimmer-bar {
|
||||
width: 80%;
|
||||
height: 20px;
|
||||
background: linear-gradient(90deg, #f0f0f0 25%, #e0e0e0 50%, #f0f0f0 75%);
|
||||
background-size: 200% 100%;
|
||||
animation: shimmer 1.5s infinite;
|
||||
border-radius: 4px;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes shimmer {
|
||||
0% {
|
||||
background-position: -200% 0;
|
||||
}
|
||||
100% {
|
||||
background-position: 200% 0;
|
||||
}
|
||||
}
|
||||
|
||||
// Responsive design
|
||||
@media (max-width: 768px) {
|
||||
.chart-container {
|
||||
.filter-controls {
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.filter-item {
|
||||
min-width: 100%;
|
||||
}
|
||||
|
||||
.chart-header {
|
||||
.header-row {
|
||||
.chart-title {
|
||||
font-size: 16px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.chart-content {
|
||||
min-height: 250px; // Adjust for mobile
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,6 +1,9 @@
|
||||
import { Component, Input, OnInit, OnChanges, OnDestroy, SimpleChanges } from '@angular/core';
|
||||
import { Component, Input, OnInit, OnChanges, OnDestroy, SimpleChanges, ViewChild } from '@angular/core';
|
||||
import { Dashboard3Service } from '../../../../../../services/builder/dashboard3.service';
|
||||
import { Subscription } from 'rxjs';
|
||||
import { FilterService } from '../../common-filter/filter.service';
|
||||
// Add BaseChartDirective import for chart resizing
|
||||
import { BaseChartDirective } from 'ng2-charts';
|
||||
|
||||
@Component({
|
||||
selector: 'app-bar-chart',
|
||||
@@ -34,6 +37,9 @@ export class BarChartComponent implements OnInit, OnChanges, OnDestroy {
|
||||
// Multi-layer drilldown configuration inputs
|
||||
@Input() drilldownLayers: any[] = []; // Array of drilldown layer configurations
|
||||
|
||||
// Add ViewChild to access the chart directive
|
||||
@ViewChild(BaseChartDirective) chart?: BaseChartDirective;
|
||||
|
||||
barChartLabels: string[] = ['Apple', 'Banana', 'Kiwifruit', 'Blueberry', 'Orange', 'Grapes'];
|
||||
barChartType: string = 'bar';
|
||||
barChartPlugins = [];
|
||||
@@ -42,6 +48,58 @@ export class BarChartComponent implements OnInit, OnChanges, OnDestroy {
|
||||
];
|
||||
barChartLegend: boolean = true;
|
||||
|
||||
// Add responsive chart options
|
||||
barChartOptions: any = {
|
||||
responsive: true,
|
||||
maintainAspectRatio: false,
|
||||
scales: {
|
||||
x: {
|
||||
ticks: {
|
||||
autoSkip: false,
|
||||
maxRotation: 45,
|
||||
minRotation: 45,
|
||||
padding: 15,
|
||||
font: {
|
||||
size: 12
|
||||
}
|
||||
},
|
||||
grid: {
|
||||
display: false
|
||||
}
|
||||
},
|
||||
y: {
|
||||
beginAtZero: true,
|
||||
ticks: {
|
||||
font: {
|
||||
size: 12
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
plugins: {
|
||||
legend: {
|
||||
display: true,
|
||||
position: 'top',
|
||||
labels: {
|
||||
font: {
|
||||
size: 12
|
||||
}
|
||||
}
|
||||
},
|
||||
tooltip: {
|
||||
enabled: true
|
||||
}
|
||||
},
|
||||
layout: {
|
||||
padding: {
|
||||
bottom: 60,
|
||||
left: 15,
|
||||
right: 15,
|
||||
top: 15
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
// Multi-layer drilldown state tracking
|
||||
drilldownStack: any[] = []; // Stack to track drilldown navigation history
|
||||
currentDrilldownLevel: number = 0; // Current drilldown level (0 = base level)
|
||||
@@ -51,15 +109,34 @@ export class BarChartComponent implements OnInit, OnChanges, OnDestroy {
|
||||
// No data state
|
||||
noDataAvailable: boolean = false;
|
||||
|
||||
// Loading state
|
||||
isLoading: boolean = false;
|
||||
|
||||
// Flag to prevent infinite loops
|
||||
private isFetchingData: boolean = false;
|
||||
|
||||
// Subscriptions to unsubscribe on destroy
|
||||
private subscriptions: Subscription[] = [];
|
||||
|
||||
constructor(private dashboardService: Dashboard3Service) { }
|
||||
// Add properties for filter functionality
|
||||
private openMultiselects: Map<string, string> = new Map(); // Map of filterId -> context
|
||||
private documentClickHandler: ((event: MouseEvent) => void) | null = null;
|
||||
private filtersInitialized: boolean = false;
|
||||
|
||||
constructor(
|
||||
private dashboardService: Dashboard3Service,
|
||||
private filterService: FilterService
|
||||
) { }
|
||||
|
||||
ngOnInit(): void {
|
||||
// Subscribe to filter changes
|
||||
this.subscriptions.push(
|
||||
this.filterService.filterState$.subscribe(filters => {
|
||||
// When filters change, refresh the chart data
|
||||
this.fetchChartData();
|
||||
})
|
||||
);
|
||||
|
||||
// Initialize with default data
|
||||
this.fetchChartData();
|
||||
}
|
||||
@@ -67,6 +144,12 @@ export class BarChartComponent implements OnInit, OnChanges, OnDestroy {
|
||||
ngOnChanges(changes: SimpleChanges): void {
|
||||
console.log('BarChartComponent input changes:', changes);
|
||||
|
||||
// Initialize filter values if they haven't been initialized yet
|
||||
if (!this.filtersInitialized && (changes.baseFilters || changes.drilldownFilters || changes.drilldownLayers)) {
|
||||
this.initializeFilterValues();
|
||||
this.filtersInitialized = true;
|
||||
}
|
||||
|
||||
// Check if any of the key properties have changed
|
||||
const xAxisChanged = changes.xAxis && !changes.xAxis.firstChange;
|
||||
const yAxisChanged = changes.yAxis && !changes.yAxis.firstChange;
|
||||
@@ -92,28 +175,348 @@ export class BarChartComponent implements OnInit, OnChanges, OnDestroy {
|
||||
// Update legend visibility if it changed
|
||||
if (changes.chartlegend !== undefined) {
|
||||
this.barChartLegend = changes.chartlegend.currentValue;
|
||||
this.barChartOptions.plugins.legend.display = this.barChartLegend;
|
||||
console.log('Chart legend changed to:', this.barChartLegend);
|
||||
}
|
||||
}
|
||||
|
||||
// Initialize filter values with proper default values based on type
|
||||
private initializeFilterValues(): void {
|
||||
console.log('Initializing filter values');
|
||||
|
||||
// Initialize base filters
|
||||
if (this.baseFilters) {
|
||||
this.baseFilters.forEach(filter => {
|
||||
if (filter.value === undefined || filter.value === null) {
|
||||
switch (filter.type) {
|
||||
case 'multiselect':
|
||||
filter.value = [];
|
||||
break;
|
||||
case 'date-range':
|
||||
filter.value = { start: null, end: null };
|
||||
break;
|
||||
case 'toggle':
|
||||
filter.value = false;
|
||||
break;
|
||||
default:
|
||||
filter.value = '';
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// Initialize drilldown filters
|
||||
if (this.drilldownFilters) {
|
||||
this.drilldownFilters.forEach(filter => {
|
||||
if (filter.value === undefined || filter.value === null) {
|
||||
switch (filter.type) {
|
||||
case 'multiselect':
|
||||
filter.value = [];
|
||||
break;
|
||||
case 'date-range':
|
||||
filter.value = { start: null, end: null };
|
||||
break;
|
||||
case 'toggle':
|
||||
filter.value = false;
|
||||
break;
|
||||
default:
|
||||
filter.value = '';
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// Initialize layer filters
|
||||
if (this.drilldownLayers) {
|
||||
this.drilldownLayers.forEach(layer => {
|
||||
if (layer.filters) {
|
||||
layer.filters.forEach((filter: any) => {
|
||||
if (filter.value === undefined || filter.value === null) {
|
||||
switch (filter.type) {
|
||||
case 'multiselect':
|
||||
filter.value = [];
|
||||
break;
|
||||
case 'date-range':
|
||||
filter.value = { start: null, end: null };
|
||||
break;
|
||||
case 'toggle':
|
||||
filter.value = false;
|
||||
break;
|
||||
default:
|
||||
filter.value = '';
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
console.log('Filter values initialized:', {
|
||||
baseFilters: this.baseFilters,
|
||||
drilldownFilters: this.drilldownFilters,
|
||||
drilldownLayers: this.drilldownLayers
|
||||
});
|
||||
}
|
||||
|
||||
// Check if there are active filters
|
||||
hasActiveFilters(): boolean {
|
||||
return (this.baseFilters && this.baseFilters.length > 0) ||
|
||||
(this.drilldownFilters && this.drilldownFilters.length > 0) ||
|
||||
this.hasActiveLayerFilters();
|
||||
}
|
||||
|
||||
// Check if there are active layer filters for current drilldown level
|
||||
hasActiveLayerFilters(): boolean {
|
||||
if (this.currentDrilldownLevel > 1 && this.drilldownLayers && this.drilldownLayers.length > 0) {
|
||||
const layerIndex = this.currentDrilldownLevel - 2; // -2 because level 1 is base drilldown
|
||||
return layerIndex < this.drilldownLayers.length &&
|
||||
this.drilldownLayers[layerIndex].filters &&
|
||||
this.drilldownLayers[layerIndex].filters.length > 0;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
// Get active layer filters for current drilldown level
|
||||
getActiveLayerFilters(): any[] {
|
||||
if (this.currentDrilldownLevel > 1 && this.drilldownLayers && this.drilldownLayers.length > 0) {
|
||||
const layerIndex = this.currentDrilldownLevel - 2; // -2 because level 1 is base drilldown
|
||||
if (layerIndex < this.drilldownLayers.length &&
|
||||
this.drilldownLayers[layerIndex].filters) {
|
||||
return this.drilldownLayers[layerIndex].filters;
|
||||
}
|
||||
}
|
||||
return [];
|
||||
}
|
||||
|
||||
// Get filter options for dropdown/multiselect filters
|
||||
getFilterOptions(filter: any): string[] {
|
||||
if (filter.options) {
|
||||
if (Array.isArray(filter.options)) {
|
||||
return filter.options;
|
||||
} else if (typeof filter.options === 'string') {
|
||||
return filter.options.split(',').map(opt => opt.trim()).filter(opt => opt);
|
||||
}
|
||||
}
|
||||
return [];
|
||||
}
|
||||
|
||||
// Check if an option is selected for multiselect filters
|
||||
isOptionSelected(filter: any, option: string): boolean {
|
||||
if (!filter.value) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (Array.isArray(filter.value)) {
|
||||
return filter.value.includes(option);
|
||||
}
|
||||
|
||||
return filter.value === option;
|
||||
}
|
||||
|
||||
// Handle base filter changes
|
||||
onBaseFilterChange(filter: any): void {
|
||||
console.log('Base filter changed:', filter);
|
||||
// Refresh data when filter changes
|
||||
this.fetchChartData();
|
||||
}
|
||||
|
||||
// Handle drilldown filter changes
|
||||
onDrilldownFilterChange(filter: any): void {
|
||||
console.log('Drilldown filter changed:', filter);
|
||||
// Refresh data when filter changes
|
||||
this.fetchChartData();
|
||||
}
|
||||
|
||||
// Handle layer filter changes
|
||||
onLayerFilterChange(filter: any): void {
|
||||
console.log('Layer filter changed:', filter);
|
||||
// Refresh data when filter changes
|
||||
this.fetchChartData();
|
||||
}
|
||||
|
||||
// Handle multiselect changes
|
||||
onMultiSelectChange(filter: any, option: string, event: any): void {
|
||||
const checked = event.target.checked;
|
||||
|
||||
// Initialize filter.value as array if it's not already
|
||||
if (!Array.isArray(filter.value)) {
|
||||
filter.value = [];
|
||||
}
|
||||
|
||||
if (checked) {
|
||||
// Add option to array if not already present
|
||||
if (!filter.value.includes(option)) {
|
||||
filter.value.push(option);
|
||||
}
|
||||
} else {
|
||||
// Remove option from array
|
||||
filter.value = filter.value.filter((item: string) => item !== option);
|
||||
}
|
||||
|
||||
// Refresh data when filter changes
|
||||
this.fetchChartData();
|
||||
}
|
||||
|
||||
// Handle date range changes
|
||||
onDateRangeChange(filter: any, dateRange: { start: string | null, end: string | null }): void {
|
||||
filter.value = dateRange;
|
||||
// Refresh data when filter changes
|
||||
this.fetchChartData();
|
||||
}
|
||||
|
||||
// Handle toggle changes
|
||||
onToggleChange(filter: any, checked: boolean): void {
|
||||
filter.value = checked;
|
||||
// Refresh data when filter changes
|
||||
this.fetchChartData();
|
||||
}
|
||||
|
||||
// Toggle multiselect dropdown visibility
|
||||
toggleMultiselect(filter: any, context: string): void {
|
||||
const filterId = `${context}-${filter.field}`;
|
||||
if (this.isMultiselectOpen(filter, context)) {
|
||||
this.openMultiselects.delete(filterId);
|
||||
} else {
|
||||
// Close all other multiselects first
|
||||
this.openMultiselects.clear();
|
||||
this.openMultiselects.set(filterId, context);
|
||||
|
||||
// Add document click handler to close dropdown when clicking outside
|
||||
this.addDocumentClickHandler();
|
||||
}
|
||||
}
|
||||
|
||||
// Add document click handler to close dropdowns when clicking outside
|
||||
private addDocumentClickHandler(): void {
|
||||
if (!this.documentClickHandler) {
|
||||
this.documentClickHandler = (event: MouseEvent) => {
|
||||
const target = event.target as HTMLElement;
|
||||
// Check if click is outside any multiselect dropdown
|
||||
if (!target.closest('.multiselect-container')) {
|
||||
this.openMultiselects.clear();
|
||||
this.removeDocumentClickHandler();
|
||||
}
|
||||
};
|
||||
|
||||
// Use setTimeout to ensure the click event that opened the dropdown doesn't immediately close it
|
||||
setTimeout(() => {
|
||||
document.addEventListener('click', this.documentClickHandler!);
|
||||
}, 0);
|
||||
}
|
||||
}
|
||||
|
||||
// Remove document click handler
|
||||
private removeDocumentClickHandler(): void {
|
||||
if (this.documentClickHandler) {
|
||||
document.removeEventListener('click', this.documentClickHandler);
|
||||
this.documentClickHandler = null;
|
||||
}
|
||||
}
|
||||
|
||||
// Check if multiselect dropdown is open
|
||||
isMultiselectOpen(filter: any, context: string): boolean {
|
||||
const filterId = `${context}-${filter.field}`;
|
||||
return this.openMultiselects.has(filterId);
|
||||
}
|
||||
|
||||
// Get count of selected options for a multiselect filter
|
||||
getSelectedOptionsCount(filter: any): number {
|
||||
if (!filter.value) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
if (Array.isArray(filter.value)) {
|
||||
return filter.value.length;
|
||||
}
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
// Clear all filters
|
||||
clearAllFilters(): void {
|
||||
// Clear base filters
|
||||
if (this.baseFilters) {
|
||||
this.baseFilters.forEach(filter => {
|
||||
if (filter.type === 'multiselect') {
|
||||
filter.value = [];
|
||||
} else if (filter.type === 'date-range') {
|
||||
filter.value = { start: null, end: null };
|
||||
} else if (filter.type === 'toggle') {
|
||||
filter.value = false;
|
||||
} else {
|
||||
filter.value = '';
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// Clear drilldown filters
|
||||
if (this.drilldownFilters) {
|
||||
this.drilldownFilters.forEach(filter => {
|
||||
if (filter.type === 'multiselect') {
|
||||
filter.value = [];
|
||||
} else if (filter.type === 'date-range') {
|
||||
filter.value = { start: null, end: null };
|
||||
} else if (filter.type === 'toggle') {
|
||||
filter.value = false;
|
||||
} else {
|
||||
filter.value = '';
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// Clear layer filters
|
||||
if (this.drilldownLayers) {
|
||||
this.drilldownLayers.forEach(layer => {
|
||||
if (layer.filters) {
|
||||
layer.filters.forEach((filter: any) => {
|
||||
if (filter.type === 'multiselect') {
|
||||
filter.value = [];
|
||||
} else if (filter.type === 'date-range') {
|
||||
filter.value = { start: null, end: null };
|
||||
} else if (filter.type === 'toggle') {
|
||||
filter.value = false;
|
||||
} else {
|
||||
filter.value = '';
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// Close all multiselect dropdowns
|
||||
this.openMultiselects.clear();
|
||||
|
||||
// Refresh data
|
||||
this.fetchChartData();
|
||||
}
|
||||
|
||||
fetchChartData(): void {
|
||||
// Set loading state
|
||||
this.isLoading = true;
|
||||
|
||||
// Set flag to prevent recursive calls
|
||||
this.isFetchingData = true;
|
||||
|
||||
// If we're in drilldown mode, fetch the appropriate drilldown data
|
||||
if (this.currentDrilldownLevel > 0 && this.drilldownStack.length > 0) {
|
||||
this.fetchDrilldownData();
|
||||
// Reset flag after fetching
|
||||
// Reset flags after fetching
|
||||
this.isFetchingData = false;
|
||||
this.isLoading = false;
|
||||
return;
|
||||
}
|
||||
|
||||
// If we have the necessary data, fetch chart data from the service
|
||||
if (this.table && this.xAxis && this.yAxis) {
|
||||
console.log('Fetching bar chart data for:', { table: this.table, xAxis: this.xAxis, yAxis: this.yAxis, connection: this.connection });
|
||||
console.log('=== BAR CHART DEBUG INFO ===');
|
||||
console.log('Table:', this.table);
|
||||
console.log('X-Axis:', this.xAxis);
|
||||
console.log('Y-Axis:', this.yAxis);
|
||||
console.log('Connection:', this.connection);
|
||||
|
||||
// Convert yAxis to string if it's an array
|
||||
const yAxisString = Array.isArray(this.yAxis) ? this.yAxis.join(',') : this.yAxis;
|
||||
console.log('Y-Axis String:', yAxisString);
|
||||
|
||||
// Get the parameter value from the drilldown stack for base level (should be empty)
|
||||
let parameterValue = '';
|
||||
@@ -123,31 +526,55 @@ export class BarChartComponent implements OnInit, OnChanges, OnDestroy {
|
||||
console.log('Bar chart data URL:', url);
|
||||
|
||||
// Convert baseFilters to filter parameters
|
||||
let filterParams = '';
|
||||
const filterObj = {};
|
||||
|
||||
// Add base filters
|
||||
if (this.baseFilters && this.baseFilters.length > 0) {
|
||||
const filterObj = {};
|
||||
this.baseFilters.forEach(filter => {
|
||||
if (filter.field && filter.value) {
|
||||
filterObj[filter.field] = filter.value;
|
||||
}
|
||||
});
|
||||
if (Object.keys(filterObj).length > 0) {
|
||||
filterParams = JSON.stringify(filterObj);
|
||||
}
|
||||
}
|
||||
console.log('Base filter parameters:', filterParams);
|
||||
|
||||
// Add common filters
|
||||
const commonFilters = this.filterService.getFilterValues();
|
||||
const filterDefinitions = this.filterService.getFilters();
|
||||
Object.keys(commonFilters).forEach(filterId => {
|
||||
const filterValue = commonFilters[filterId];
|
||||
|
||||
// Find the filter definition to get the field name
|
||||
const filterDef = this.filterService.getFilters().find(f => f.id === filterId);
|
||||
|
||||
if (filterDef && filterDef.field) {
|
||||
const fieldName = filterDef.field;
|
||||
if (filterValue !== undefined && filterValue !== null && filterValue !== '') {
|
||||
filterObj[fieldName] = filterValue;
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
// Convert to JSON string for API call
|
||||
let filterParams = '';
|
||||
if (Object.keys(filterObj).length > 0) {
|
||||
filterParams = JSON.stringify(filterObj);
|
||||
}
|
||||
|
||||
console.log('Final filter object:', filterObj);
|
||||
// Fetch data from the dashboard service with parameter field and value
|
||||
// For base level, we pass empty parameter and value, but now also pass filters
|
||||
const subscription = this.dashboardService.getChartData(this.table, 'bar', this.xAxis, yAxisString, this.connection, '', '', filterParams).subscribe(
|
||||
(data: any) => {
|
||||
console.log('=== BAR CHART DATA RESPONSE ===');
|
||||
console.log('Received bar chart data:', data);
|
||||
if (data === null) {
|
||||
console.warn('Bar chart API returned null data. Check if the API endpoint is working correctly.');
|
||||
this.noDataAvailable = true;
|
||||
this.barChartLabels = [];
|
||||
this.barChartData = [];
|
||||
// Reset flag after fetching
|
||||
// Reset flags after fetching
|
||||
this.isFetchingData = false;
|
||||
this.isLoading = false;
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -160,6 +587,7 @@ export class BarChartComponent implements OnInit, OnChanges, OnDestroy {
|
||||
// Trigger change detection
|
||||
// this.barChartData = [...this.barChartData];
|
||||
console.log('Updated bar chart with data:', { labels: this.barChartLabels, data: this.barChartData });
|
||||
console.log('=== CHART UPDATED SUCCESSFULLY ===');
|
||||
} else if (data && data.labels && data.datasets) {
|
||||
// Backend has already filtered the data, just display it
|
||||
this.noDataAvailable = data.labels.length === 0;
|
||||
@@ -168,22 +596,26 @@ export class BarChartComponent implements OnInit, OnChanges, OnDestroy {
|
||||
// Trigger change detection
|
||||
// this.barChartData = [...this.barChartData];
|
||||
console.log('Updated bar chart with legacy data format:', { labels: this.barChartLabels, data: this.barChartData });
|
||||
console.log('=== CHART UPDATED SUCCESSFULLY (LEGACY) ===');
|
||||
} else {
|
||||
console.warn('Bar chart received data does not have expected structure', data);
|
||||
this.noDataAvailable = true;
|
||||
this.barChartLabels = [];
|
||||
this.barChartData = [];
|
||||
}
|
||||
// Reset flag after fetching
|
||||
// Reset flags after fetching
|
||||
this.isFetchingData = false;
|
||||
this.isLoading = false;
|
||||
},
|
||||
(error) => {
|
||||
console.error('=== BAR CHART ERROR ===');
|
||||
console.error('Error fetching bar chart data:', error);
|
||||
this.noDataAvailable = true;
|
||||
this.barChartLabels = [];
|
||||
this.barChartData = [];
|
||||
// Reset flag after fetching
|
||||
// Reset flags after fetching
|
||||
this.isFetchingData = false;
|
||||
this.isLoading = false;
|
||||
// Keep default data in case of error
|
||||
}
|
||||
);
|
||||
@@ -195,8 +627,9 @@ export class BarChartComponent implements OnInit, OnChanges, OnDestroy {
|
||||
this.noDataAvailable = true;
|
||||
this.barChartLabels = [];
|
||||
this.barChartData = [];
|
||||
// Reset flag after fetching
|
||||
// Reset flags after fetching
|
||||
this.isFetchingData = false;
|
||||
this.isLoading = false;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -286,33 +719,49 @@ export class BarChartComponent implements OnInit, OnChanges, OnDestroy {
|
||||
console.log('Drilldown data URL:', url);
|
||||
|
||||
// Convert drilldown layer filters to filter parameters (if applicable)
|
||||
let filterParams = '';
|
||||
const filterObj = {};
|
||||
|
||||
// Add drilldown layer filters
|
||||
if (drilldownConfig.filters && drilldownConfig.filters.length > 0) {
|
||||
const filterObj = {};
|
||||
drilldownConfig.filters.forEach((filter: any) => {
|
||||
if (filter.field && filter.value) {
|
||||
filterObj[filter.field] = filter.value;
|
||||
}
|
||||
});
|
||||
if (Object.keys(filterObj).length > 0) {
|
||||
filterParams = JSON.stringify(filterObj);
|
||||
}
|
||||
}
|
||||
console.log('Drilldown layer filter parameters:', filterParams);
|
||||
|
||||
// Convert drilldownFilters to filter parameters for drilldown level
|
||||
let drilldownFilterParams = '';
|
||||
// Add drilldownFilters
|
||||
if (this.drilldownFilters && this.drilldownFilters.length > 0) {
|
||||
const filterObj = {};
|
||||
this.drilldownFilters.forEach(filter => {
|
||||
if (filter.field && filter.value) {
|
||||
filterObj[filter.field] = filter.value;
|
||||
}
|
||||
});
|
||||
if (Object.keys(filterObj).length > 0) {
|
||||
drilldownFilterParams = JSON.stringify(filterObj);
|
||||
}
|
||||
}
|
||||
|
||||
// Add common filters
|
||||
const commonFilters = this.filterService.getFilterValues();
|
||||
const filterDefinitions = this.filterService.getFilters();
|
||||
Object.keys(commonFilters).forEach(filterId => {
|
||||
const filterValue = commonFilters[filterId];
|
||||
|
||||
// Find the filter definition to get the field name
|
||||
const filterDef = this.filterService.getFilters().find(f => f.id === filterId);
|
||||
|
||||
if (filterDef && filterDef.field) {
|
||||
const fieldName = filterDef.field;
|
||||
if (filterValue !== undefined && filterValue !== null && filterValue !== '') {
|
||||
filterObj[fieldName] = filterValue;
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
// Convert to JSON string for API call
|
||||
let drilldownFilterParams = '';
|
||||
if (Object.keys(filterObj).length > 0) {
|
||||
drilldownFilterParams = JSON.stringify(filterObj);
|
||||
}
|
||||
|
||||
console.log('Drilldown filter parameters:', drilldownFilterParams);
|
||||
|
||||
// For drilldown level, we pass the parameter value from the drilldown stack and drilldown filters
|
||||
@@ -342,17 +791,23 @@ export class BarChartComponent implements OnInit, OnChanges, OnDestroy {
|
||||
// Trigger change detection
|
||||
// this.barChartData = [...this.barChartData];
|
||||
console.log('Updated bar chart with drilldown data:', { labels: this.barChartLabels, data: this.barChartData });
|
||||
// Set loading state to false
|
||||
this.isLoading = false;
|
||||
} else if (data && data.labels && data.datasets) {
|
||||
// Backend has already filtered the data, just display it
|
||||
this.noDataAvailable = data.labels.length === 0;
|
||||
this.barChartLabels = data.labels;
|
||||
this.barChartData = data.datasets;
|
||||
console.log('Updated bar chart with drilldown legacy data format:', { labels: this.barChartLabels, data: this.barChartData });
|
||||
// Set loading state to false
|
||||
this.isLoading = false;
|
||||
} else {
|
||||
console.warn('Drilldown received data does not have expected structure', data);
|
||||
this.noDataAvailable = true;
|
||||
this.barChartLabels = [];
|
||||
this.barChartData = [];
|
||||
// Set loading state to false
|
||||
this.isLoading = false;
|
||||
}
|
||||
},
|
||||
(error) => {
|
||||
@@ -360,12 +815,17 @@ export class BarChartComponent implements OnInit, OnChanges, OnDestroy {
|
||||
this.noDataAvailable = true;
|
||||
this.barChartLabels = [];
|
||||
this.barChartData = [];
|
||||
// Set loading state to false
|
||||
this.isLoading = false;
|
||||
// Keep current data in case of error
|
||||
}
|
||||
);
|
||||
|
||||
// Add subscription to array for cleanup
|
||||
this.subscriptions.push(subscription);
|
||||
|
||||
// Set loading state
|
||||
this.isLoading = true;
|
||||
}
|
||||
|
||||
// Reset to original data (go back to base level)
|
||||
@@ -426,6 +886,18 @@ export class BarChartComponent implements OnInit, OnChanges, OnDestroy {
|
||||
}
|
||||
}
|
||||
|
||||
// Public method to refresh data when filters change
|
||||
refreshData(): void {
|
||||
this.fetchChartData();
|
||||
}
|
||||
|
||||
// Method to handle window resize events
|
||||
onResize(): void {
|
||||
if (this.chart) {
|
||||
this.chart.chart?.resize();
|
||||
}
|
||||
}
|
||||
|
||||
// Ensure labels and data arrays have the same length
|
||||
private syncLabelAndDataArrays(): void {
|
||||
// For bar charts, we need to ensure all datasets have the same number of data points
|
||||
@@ -557,6 +1029,12 @@ export class BarChartComponent implements OnInit, OnChanges, OnDestroy {
|
||||
this.originalBarChartLabels = [];
|
||||
this.originalBarChartData = [];
|
||||
|
||||
// Clear multiselect tracking
|
||||
this.openMultiselects.clear();
|
||||
|
||||
// Remove document click handler
|
||||
this.removeDocumentClickHandler();
|
||||
|
||||
console.log('BarChartComponent destroyed and cleaned up');
|
||||
}
|
||||
}
|
||||
@@ -1,28 +1,310 @@
|
||||
<div style="display:block">
|
||||
<!-- Drilldown mode indicator -->
|
||||
<div *ngIf="currentDrilldownLevel > 0" style="background-color: #e0e0e0; padding: 5px; margin-bottom: 10px; border-radius: 4px; text-align: center;">
|
||||
<span style="font-weight: bold; color: #333;">Drilldown Level: {{currentDrilldownLevel}}</span>
|
||||
<button (click)="navigateBack()" style="margin-left: 10px; padding: 2px 8px; background-color: #007cba; color: white; border: none; border-radius: 3px; cursor: pointer;">
|
||||
Back to Level {{currentDrilldownLevel - 1}}
|
||||
</button>
|
||||
<button (click)="resetToOriginalData()" style="margin-left: 10px; padding: 2px 8px; background-color: #dc3545; color: white; border: none; border-radius: 3px; cursor: pointer;">
|
||||
Back to Main View
|
||||
</button>
|
||||
<div style="display:block; height: 100%; width: 100%;">
|
||||
<!-- Filter Controls Section -->
|
||||
<div class="filter-section" *ngIf="hasActiveFilters()">
|
||||
<!-- Base Filters -->
|
||||
<div class="filter-group" *ngIf="baseFilters && baseFilters.length > 0">
|
||||
<h4>Base Filters</h4>
|
||||
<div class="filter-controls">
|
||||
<div *ngFor="let filter of baseFilters" class="filter-item">
|
||||
<div class="filter-label">{{ filter.field }} ({{ filter.type || 'text' }})</div>
|
||||
|
||||
<!-- Text Filter -->
|
||||
<div *ngIf="!filter.type || filter.type === 'text'" class="filter-input">
|
||||
<input type="text"
|
||||
[(ngModel)]="filter.value"
|
||||
(ngModelChange)="onBaseFilterChange(filter)"
|
||||
[placeholder]="filter.field"
|
||||
class="clr-input filter-text-input">
|
||||
</div>
|
||||
|
||||
<!-- Dropdown Filter -->
|
||||
<div *ngIf="filter.type === 'dropdown'" class="filter-input">
|
||||
<select [(ngModel)]="filter.value"
|
||||
(ngModelChange)="onBaseFilterChange(filter)"
|
||||
class="clr-select filter-select">
|
||||
<option value="">Select {{ filter.field }}</option>
|
||||
<option *ngFor="let option of getFilterOptions(filter)" [value]="option">{{ option }}</option>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<!-- Multi-Select Filter - Updated to show key first, then dropdown on click -->
|
||||
<div *ngIf="filter.type === 'multiselect'" class="filter-input multiselect-container">
|
||||
<div class="multiselect-display" (click)="toggleMultiselect(filter, 'base')">
|
||||
<span class="multiselect-label">{{ filter.field }}</span>
|
||||
<span class="multiselect-value" *ngIf="getSelectedOptionsCount(filter) > 0">
|
||||
({{ getSelectedOptionsCount(filter) }} selected)
|
||||
</span>
|
||||
<clr-icon shape="caret down" class="dropdown-icon"></clr-icon>
|
||||
</div>
|
||||
<div class="multiselect-dropdown" *ngIf="isMultiselectOpen(filter, 'base')">
|
||||
<div class="checkbox-group">
|
||||
<div *ngFor="let option of getFilterOptions(filter); let i = index" class="checkbox-item">
|
||||
<input type="checkbox"
|
||||
[checked]="isOptionSelected(filter, option)"
|
||||
(change)="onMultiSelectChange(filter, option, $event)"
|
||||
[id]="'base-' + filter.field + '-' + i"
|
||||
class="clr-checkbox">
|
||||
<label [for]="'base-' + filter.field + '-' + i" class="checkbox-label">{{ option }}</label>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Date Range Filter -->
|
||||
<div *ngIf="filter.type === 'date-range'" class="filter-input date-range">
|
||||
<div class="date-input-group">
|
||||
<input type="date"
|
||||
[(ngModel)]="filter.value.start"
|
||||
(ngModelChange)="onDateRangeChange(filter, { start: $event, end: filter.value.end })"
|
||||
placeholder="Start Date"
|
||||
class="clr-input filter-date">
|
||||
<span class="date-separator">to</span>
|
||||
<input type="date"
|
||||
[(ngModel)]="filter.value.end"
|
||||
(ngModelChange)="onDateRangeChange(filter, { start: filter.value.start, end: $event })"
|
||||
placeholder="End Date"
|
||||
class="clr-input filter-date">
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Toggle Filter -->
|
||||
<div *ngIf="filter.type === 'toggle'" class="filter-input toggle">
|
||||
<input type="checkbox"
|
||||
[(ngModel)]="filter.value"
|
||||
(ngModelChange)="onToggleChange(filter, $event)"
|
||||
clrToggle
|
||||
class="clr-toggle">
|
||||
<label class="toggle-label">{{ filter.field }}</label>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Drilldown Filters -->
|
||||
<div class="filter-group" *ngIf="drilldownFilters && drilldownFilters.length > 0 && currentDrilldownLevel > 0">
|
||||
<h4>Drilldown Filters</h4>
|
||||
<div class="filter-controls">
|
||||
<div *ngFor="let filter of drilldownFilters" class="filter-item">
|
||||
<div class="filter-label">{{ filter.field }} ({{ filter.type || 'text' }})</div>
|
||||
|
||||
<!-- Text Filter -->
|
||||
<div *ngIf="!filter.type || filter.type === 'text'" class="filter-input">
|
||||
<input type="text"
|
||||
[(ngModel)]="filter.value"
|
||||
(ngModelChange)="onDrilldownFilterChange(filter)"
|
||||
[placeholder]="filter.field"
|
||||
class="clr-input filter-text-input">
|
||||
</div>
|
||||
|
||||
<!-- Dropdown Filter -->
|
||||
<div *ngIf="filter.type === 'dropdown'" class="filter-input">
|
||||
<select [(ngModel)]="filter.value"
|
||||
(ngModelChange)="onDrilldownFilterChange(filter)"
|
||||
class="clr-select filter-select">
|
||||
<option value="">Select {{ filter.field }}</option>
|
||||
<option *ngFor="let option of getFilterOptions(filter)" [value]="option">{{ option }}</option>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<!-- Multi-Select Filter - Updated to show key first, then dropdown on click -->
|
||||
<div *ngIf="filter.type === 'multiselect'" class="filter-input multiselect-container">
|
||||
<div class="multiselect-display" (click)="toggleMultiselect(filter, 'drilldown')">
|
||||
<span class="multiselect-label">{{ filter.field }}</span>
|
||||
<span class="multiselect-value" *ngIf="getSelectedOptionsCount(filter) > 0">
|
||||
({{ getSelectedOptionsCount(filter) }} selected)
|
||||
</span>
|
||||
<clr-icon shape="caret down" class="dropdown-icon"></clr-icon>
|
||||
</div>
|
||||
<div class="multiselect-dropdown" *ngIf="isMultiselectOpen(filter, 'drilldown')">
|
||||
<div class="checkbox-group">
|
||||
<div *ngFor="let option of getFilterOptions(filter); let i = index" class="checkbox-item">
|
||||
<input type="checkbox"
|
||||
[checked]="isOptionSelected(filter, option)"
|
||||
(change)="onMultiSelectChange(filter, option, $event)"
|
||||
[id]="'drilldown-' + filter.field + '-' + i"
|
||||
class="clr-checkbox">
|
||||
<label [for]="'drilldown-' + filter.field + '-' + i" class="checkbox-label">{{ option }}</label>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Date Range Filter -->
|
||||
<div *ngIf="filter.type === 'date-range'" class="filter-input date-range">
|
||||
<div class="date-input-group">
|
||||
<input type="date"
|
||||
[(ngModel)]="filter.value.start"
|
||||
(ngModelChange)="onDateRangeChange(filter, { start: $event, end: filter.value.end })"
|
||||
placeholder="Start Date"
|
||||
class="clr-input filter-date">
|
||||
<span class="date-separator">to</span>
|
||||
<input type="date"
|
||||
[(ngModel)]="filter.value.end"
|
||||
(ngModelChange)="onDateRangeChange(filter, { start: filter.value.start, end: $event })"
|
||||
placeholder="End Date"
|
||||
class="clr-input filter-date">
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Toggle Filter -->
|
||||
<div *ngIf="filter.type === 'toggle'" class="filter-input toggle">
|
||||
<input type="checkbox"
|
||||
[(ngModel)]="filter.value"
|
||||
(ngModelChange)="onToggleChange(filter, $event)"
|
||||
clrToggle
|
||||
class="clr-toggle">
|
||||
<label class="toggle-label">{{ filter.field }}</label>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Layer Filters -->
|
||||
<div class="filter-group" *ngIf="hasActiveLayerFilters()">
|
||||
<h4>Layer Filters</h4>
|
||||
<div class="filter-controls">
|
||||
<div *ngFor="let filter of getActiveLayerFilters()" class="filter-item">
|
||||
<div class="filter-label">{{ filter.field }} ({{ filter.type || 'text' }})</div>
|
||||
|
||||
<!-- Text Filter -->
|
||||
<div *ngIf="!filter.type || filter.type === 'text'" class="filter-input">
|
||||
<input type="text"
|
||||
[(ngModel)]="filter.value"
|
||||
(ngModelChange)="onLayerFilterChange(filter)"
|
||||
[placeholder]="filter.field"
|
||||
class="clr-input filter-text-input">
|
||||
</div>
|
||||
|
||||
<!-- Dropdown Filter -->
|
||||
<div *ngIf="filter.type === 'dropdown'" class="filter-input">
|
||||
<select [(ngModel)]="filter.value"
|
||||
(ngModelChange)="onLayerFilterChange(filter)"
|
||||
class="clr-select filter-select">
|
||||
<option value="">Select {{ filter.field }}</option>
|
||||
<option *ngFor="let option of getFilterOptions(filter)" [value]="option">{{ option }}</option>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<!-- Multi-Select Filter - Updated to show key first, then dropdown on click -->
|
||||
<div *ngIf="filter.type === 'multiselect'" class="filter-input multiselect-container">
|
||||
<div class="multiselect-display" (click)="toggleMultiselect(filter, 'layer')">
|
||||
<span class="multiselect-label">{{ filter.field }}</span>
|
||||
<span class="multiselect-value" *ngIf="getSelectedOptionsCount(filter) > 0">
|
||||
({{ getSelectedOptionsCount(filter) }} selected)
|
||||
</span>
|
||||
<clr-icon shape="caret down" class="dropdown-icon"></clr-icon>
|
||||
</div>
|
||||
<div class="multiselect-dropdown" *ngIf="isMultiselectOpen(filter, 'layer')">
|
||||
<div class="checkbox-group">
|
||||
<div *ngFor="let option of getFilterOptions(filter); let i = index" class="checkbox-item">
|
||||
<input type="checkbox"
|
||||
[checked]="isOptionSelected(filter, option)"
|
||||
(change)="onMultiSelectChange(filter, option, $event)"
|
||||
[id]="'layer-' + filter.field + '-' + i"
|
||||
class="clr-checkbox">
|
||||
<label [for]="'layer-' + filter.field + '-' + i" class="checkbox-label">{{ option }}</label>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Date Range Filter -->
|
||||
<div *ngIf="filter.type === 'date-range'" class="filter-input date-range">
|
||||
<div class="date-input-group">
|
||||
<input type="date"
|
||||
[(ngModel)]="filter.value.start"
|
||||
(ngModelChange)="onDateRangeChange(filter, { start: $event, end: filter.value.end })"
|
||||
placeholder="Start Date"
|
||||
class="clr-input filter-date">
|
||||
<span class="date-separator">to</span>
|
||||
<input type="date"
|
||||
[(ngModel)]="filter.value.end"
|
||||
(ngModelChange)="onDateRangeChange(filter, { start: filter.value.start, end: $event })"
|
||||
placeholder="End Date"
|
||||
class="clr-input filter-date">
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Toggle Filter -->
|
||||
<div *ngIf="filter.type === 'toggle'" class="filter-input toggle">
|
||||
<input type="checkbox"
|
||||
[(ngModel)]="filter.value"
|
||||
(ngModelChange)="onToggleChange(filter, $event)"
|
||||
clrToggle
|
||||
class="clr-toggle">
|
||||
<label class="toggle-label">{{ filter.field }}</label>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Clear Filters Button -->
|
||||
<div class="filter-actions">
|
||||
<button class="btn btn-sm btn-outline" (click)="clearAllFilters()">Clear All Filters</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- No data message -->
|
||||
<div *ngIf="noDataAvailable" style="text-align: center; padding: 20px; color: #666; font-style: italic;">
|
||||
No data available
|
||||
<!-- Header row with chart title and drilldown navigation -->
|
||||
<div class="clr-row header-row">
|
||||
<div class="clr-col-6">
|
||||
<h3 class="chart-title">{{charttitle || 'Bubble Chart'}}</h3>
|
||||
</div>
|
||||
<div class="clr-col-6" style="text-align: right;">
|
||||
<!-- Add drilldown navigation controls -->
|
||||
<button class="btn btn-sm btn-link" *ngIf="drilldownEnabled && drilldownStack.length > 0" (click)="navigateBack()">
|
||||
<cds-icon shape="arrow" direction="left"></cds-icon>
|
||||
Back to {{drilldownStack.length > 1 ? 'Previous Level' : 'Main Data'}}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Chart display -->
|
||||
<div *ngIf="!noDataAvailable">
|
||||
<!-- Show current drilldown level -->
|
||||
<div class="clr-row" *ngIf="drilldownEnabled && drilldownStack.length > 0">
|
||||
<div class="clr-col-12">
|
||||
<div class="alert alert-info" style="padding: 8px 12px; margin-bottom: 12px;">
|
||||
<div class="alert-items">
|
||||
<div class="alert-item static">
|
||||
<div class="alert-icon-wrapper">
|
||||
<cds-icon class="alert-icon" shape="info-circle"></cds-icon>
|
||||
</div>
|
||||
<span class="alert-text">
|
||||
Drilldown Level: {{currentDrilldownLevel}}
|
||||
<span *ngIf="drilldownStack.length > 0">
|
||||
(Clicked on: {{drilldownStack[drilldownStack.length - 1].clickedLabel}})
|
||||
</span>
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Chart container -->
|
||||
<div style="position: relative; height: calc(100% - 80px); width: 100%; padding: 0 10px 30px 10px;">
|
||||
<!-- Loading indicator -->
|
||||
<div *ngIf="!dataLoaded" style="text-align: center; padding: 20px; color: #666; position: absolute; top: 50%; left: 50%; transform: translate(-50%, -50%); z-index: 10; width: 100%;">
|
||||
Loading data...
|
||||
</div>
|
||||
|
||||
<!-- No data message -->
|
||||
<div *ngIf="dataLoaded && (noDataAvailable || !isChartDataValid())" style="text-align: center; padding: 20px; color: #666; font-style: italic; position: absolute; top: 50%; left: 50%; transform: translate(-50%, -50%); z-index: 10; width: 100%;">
|
||||
No data available
|
||||
</div>
|
||||
|
||||
<!-- Chart display - Always render the canvas but conditionally show/hide with CSS -->
|
||||
<canvas baseChart
|
||||
[datasets]="bubbleChartData"
|
||||
[type]="bubbleChartType"
|
||||
[options]="bubbleChartOptions"
|
||||
(chartHover)="chartHovered($event)"
|
||||
(chartClick)="chartClicked($event)">
|
||||
(chartClick)="chartClicked($event)"
|
||||
[style.visibility]="dataLoaded && !noDataAvailable && isChartDataValid() ? 'visible' : 'hidden'"
|
||||
[style.position]="'absolute'"
|
||||
[style.top]="'0'"
|
||||
[style.left]="'0'"
|
||||
[style.height]="'100%'"
|
||||
[style.width]="'100%'"
|
||||
[style.padding]="'0 10px 20px 10px'">
|
||||
</canvas>
|
||||
</div>
|
||||
</div>
|
||||
@@ -0,0 +1,192 @@
|
||||
.filter-section {
|
||||
margin-bottom: 20px;
|
||||
padding: 15px;
|
||||
border: 1px solid #ddd;
|
||||
border-radius: 4px;
|
||||
background-color: #f9f9f9;
|
||||
}
|
||||
|
||||
.filter-group {
|
||||
margin-bottom: 15px;
|
||||
|
||||
h4 {
|
||||
margin-top: 0;
|
||||
margin-bottom: 10px;
|
||||
color: #333;
|
||||
font-weight: 600;
|
||||
}
|
||||
}
|
||||
|
||||
.filter-controls {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 15px;
|
||||
}
|
||||
|
||||
.filter-item {
|
||||
flex: 1 1 300px;
|
||||
min-width: 250px;
|
||||
padding: 10px;
|
||||
background: white;
|
||||
border: 1px solid #e0e0e0;
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
.filter-label {
|
||||
font-weight: 500;
|
||||
margin-bottom: 8px;
|
||||
color: #555;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.filter-input {
|
||||
width: 100%;
|
||||
|
||||
.filter-text-input,
|
||||
.filter-select,
|
||||
.filter-date {
|
||||
width: 100%;
|
||||
padding: 6px 12px;
|
||||
border: 1px solid #ccc;
|
||||
border-radius: 4px;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.filter-select {
|
||||
height: 34px;
|
||||
}
|
||||
}
|
||||
|
||||
.multiselect-container {
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.multiselect-display {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
padding: 6px 12px;
|
||||
border: 1px solid #ccc;
|
||||
border-radius: 4px;
|
||||
background: white;
|
||||
cursor: pointer;
|
||||
min-height: 34px;
|
||||
|
||||
.multiselect-label {
|
||||
flex: 1;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.multiselect-value {
|
||||
color: #666;
|
||||
font-size: 12px;
|
||||
margin-right: 8px;
|
||||
}
|
||||
|
||||
.dropdown-icon {
|
||||
flex-shrink: 0;
|
||||
transition: transform 0.2s ease;
|
||||
}
|
||||
|
||||
&:hover {
|
||||
border-color: #999;
|
||||
}
|
||||
}
|
||||
|
||||
.multiselect-dropdown {
|
||||
position: absolute;
|
||||
top: 100%;
|
||||
left: 0;
|
||||
right: 0;
|
||||
z-index: 1000;
|
||||
background: white;
|
||||
border: 1px solid #ccc;
|
||||
border-top: none;
|
||||
border-radius: 0 0 4px 4px;
|
||||
box-shadow: 0 2px 4px rgba(0,0,0,0.1);
|
||||
max-height: 200px;
|
||||
overflow-y: auto;
|
||||
|
||||
.checkbox-group {
|
||||
padding: 8px;
|
||||
|
||||
.checkbox-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
padding: 4px 0;
|
||||
|
||||
.checkbox-label {
|
||||
margin: 0;
|
||||
font-size: 14px;
|
||||
cursor: pointer;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.date-range {
|
||||
.date-input-group {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.date-separator {
|
||||
margin: 0 5px;
|
||||
color: #777;
|
||||
}
|
||||
}
|
||||
|
||||
.toggle {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
|
||||
.toggle-label {
|
||||
margin: 0;
|
||||
font-size: 14px;
|
||||
cursor: pointer;
|
||||
}
|
||||
}
|
||||
|
||||
.filter-actions {
|
||||
margin-top: 15px;
|
||||
padding-top: 15px;
|
||||
border-top: 1px solid #eee;
|
||||
|
||||
.btn {
|
||||
font-size: 13px;
|
||||
}
|
||||
}
|
||||
|
||||
// New header row styling
|
||||
.header-row {
|
||||
margin-bottom: 15px;
|
||||
padding-bottom: 10px;
|
||||
border-bottom: 1px solid #eee;
|
||||
|
||||
.chart-title {
|
||||
margin: 0;
|
||||
font-size: 18px;
|
||||
font-weight: 600;
|
||||
color: #333;
|
||||
}
|
||||
}
|
||||
|
||||
// Responsive design
|
||||
@media (max-width: 768px) {
|
||||
.filter-controls {
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.filter-item {
|
||||
min-width: 100%;
|
||||
}
|
||||
|
||||
.header-row {
|
||||
.chart-title {
|
||||
font-size: 16px;
|
||||
}
|
||||
}
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
@@ -1,44 +1,315 @@
|
||||
<div class="doughnut-chart-container">
|
||||
<!-- Drilldown mode indicator -->
|
||||
<div *ngIf="currentDrilldownLevel > 0" style="background-color: #e0e0e0; padding: 5px; margin-bottom: 10px; border-radius: 4px; text-align: center;">
|
||||
<span style="font-weight: bold; color: #333;">Drilldown Level: {{currentDrilldownLevel}}</span>
|
||||
<button (click)="navigateBack()" style="margin-left: 10px; padding: 2px 8px; background-color: #007cba; color: white; border: none; border-radius: 3px; cursor: pointer;">
|
||||
Back to Level {{currentDrilldownLevel - 1}}
|
||||
</button>
|
||||
<button (click)="resetToOriginalData()" style="margin-left: 10px; padding: 2px 8px; background-color: #dc3545; color: white; border: none; border-radius: 3px; cursor: pointer;">
|
||||
Back to Main View
|
||||
</button>
|
||||
<div class="chart-container">
|
||||
<!-- Filter Controls Section -->
|
||||
<div class="filter-section" *ngIf="hasActiveFilters()">
|
||||
<!-- Base Filters -->
|
||||
<div class="filter-group" *ngIf="baseFilters && baseFilters.length > 0">
|
||||
<h4>Base Filters</h4>
|
||||
<div class="filter-controls">
|
||||
<div *ngFor="let filter of baseFilters" class="filter-item">
|
||||
<div class="filter-label">{{ filter.field }} ({{ filter.type || 'text' }})</div>
|
||||
|
||||
<!-- Text Filter -->
|
||||
<div *ngIf="!filter.type || filter.type === 'text'" class="filter-input">
|
||||
<input type="text"
|
||||
[(ngModel)]="filter.value"
|
||||
(ngModelChange)="onBaseFilterChange(filter)"
|
||||
[placeholder]="filter.field"
|
||||
class="clr-input filter-text-input">
|
||||
</div>
|
||||
|
||||
<!-- Dropdown Filter -->
|
||||
<div *ngIf="filter.type === 'dropdown'" class="filter-input">
|
||||
<select [(ngModel)]="filter.value"
|
||||
(ngModelChange)="onBaseFilterChange(filter)"
|
||||
class="clr-select filter-select">
|
||||
<option value="">Select {{ filter.field }}</option>
|
||||
<option *ngFor="let option of getFilterOptions(filter)" [value]="option">{{ option }}</option>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<!-- Multi-Select Filter - Updated to show key first, then dropdown on click -->
|
||||
<div *ngIf="filter.type === 'multiselect'" class="filter-input multiselect-container">
|
||||
<div class="multiselect-display" (click)="toggleMultiselect(filter, 'base')">
|
||||
<span class="multiselect-label">{{ filter.field }}</span>
|
||||
<span class="multiselect-value" *ngIf="getSelectedOptionsCount(filter) > 0">
|
||||
({{ getSelectedOptionsCount(filter) }} selected)
|
||||
</span>
|
||||
<clr-icon shape="caret down" class="dropdown-icon"></clr-icon>
|
||||
</div>
|
||||
<div class="multiselect-dropdown" *ngIf="isMultiselectOpen(filter, 'base')">
|
||||
<div class="checkbox-group">
|
||||
<div *ngFor="let option of getFilterOptions(filter); let i = index" class="checkbox-item">
|
||||
<input type="checkbox"
|
||||
[checked]="isOptionSelected(filter, option)"
|
||||
(change)="onMultiSelectChange(filter, option, $event)"
|
||||
[id]="'base-' + filter.field + '-' + i"
|
||||
class="clr-checkbox">
|
||||
<label [for]="'base-' + filter.field + '-' + i" class="checkbox-label">{{ option }}</label>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Date Range Filter -->
|
||||
<div *ngIf="filter.type === 'date-range'" class="filter-input date-range">
|
||||
<div class="date-input-group">
|
||||
<input type="date"
|
||||
[(ngModel)]="filter.value.start"
|
||||
(ngModelChange)="onDateRangeChange(filter, { start: $event, end: filter.value.end })"
|
||||
placeholder="Start Date"
|
||||
class="clr-input filter-date">
|
||||
<span class="date-separator">to</span>
|
||||
<input type="date"
|
||||
[(ngModel)]="filter.value.end"
|
||||
(ngModelChange)="onDateRangeChange(filter, { start: filter.value.start, end: $event })"
|
||||
placeholder="End Date"
|
||||
class="clr-input filter-date">
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Toggle Filter -->
|
||||
<div *ngIf="filter.type === 'toggle'" class="filter-input toggle">
|
||||
<input type="checkbox"
|
||||
[(ngModel)]="filter.value"
|
||||
(ngModelChange)="onToggleChange(filter, $event)"
|
||||
clrToggle
|
||||
class="clr-toggle">
|
||||
<label class="toggle-label">{{ filter.field }}</label>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Drilldown Filters -->
|
||||
<div class="filter-group" *ngIf="drilldownFilters && drilldownFilters.length > 0 && currentDrilldownLevel > 0">
|
||||
<h4>Drilldown Filters</h4>
|
||||
<div class="filter-controls">
|
||||
<div *ngFor="let filter of drilldownFilters" class="filter-item">
|
||||
<div class="filter-label">{{ filter.field }} ({{ filter.type || 'text' }})</div>
|
||||
|
||||
<!-- Text Filter -->
|
||||
<div *ngIf="!filter.type || filter.type === 'text'" class="filter-input">
|
||||
<input type="text"
|
||||
[(ngModel)]="filter.value"
|
||||
(ngModelChange)="onDrilldownFilterChange(filter)"
|
||||
[placeholder]="filter.field"
|
||||
class="clr-input filter-text-input">
|
||||
</div>
|
||||
|
||||
<!-- Dropdown Filter -->
|
||||
<div *ngIf="filter.type === 'dropdown'" class="filter-input">
|
||||
<select [(ngModel)]="filter.value"
|
||||
(ngModelChange)="onDrilldownFilterChange(filter)"
|
||||
class="clr-select filter-select">
|
||||
<option value="">Select {{ filter.field }}</option>
|
||||
<option *ngFor="let option of getFilterOptions(filter)" [value]="option">{{ option }}</option>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<!-- Multi-Select Filter - Updated to show key first, then dropdown on click -->
|
||||
<div *ngIf="filter.type === 'multiselect'" class="filter-input multiselect-container">
|
||||
<div class="multiselect-display" (click)="toggleMultiselect(filter, 'drilldown')">
|
||||
<span class="multiselect-label">{{ filter.field }}</span>
|
||||
<span class="multiselect-value" *ngIf="getSelectedOptionsCount(filter) > 0">
|
||||
({{ getSelectedOptionsCount(filter) }} selected)
|
||||
</span>
|
||||
<clr-icon shape="caret down" class="dropdown-icon"></clr-icon>
|
||||
</div>
|
||||
<div class="multiselect-dropdown" *ngIf="isMultiselectOpen(filter, 'drilldown')">
|
||||
<div class="checkbox-group">
|
||||
<div *ngFor="let option of getFilterOptions(filter); let i = index" class="checkbox-item">
|
||||
<input type="checkbox"
|
||||
[checked]="isOptionSelected(filter, option)"
|
||||
(change)="onMultiSelectChange(filter, option, $event)"
|
||||
[id]="'drilldown-' + filter.field + '-' + i"
|
||||
class="clr-checkbox">
|
||||
<label [for]="'drilldown-' + filter.field + '-' + i" class="checkbox-label">{{ option }}</label>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Date Range Filter -->
|
||||
<div *ngIf="filter.type === 'date-range'" class="filter-input date-range">
|
||||
<div class="date-input-group">
|
||||
<input type="date"
|
||||
[(ngModel)]="filter.value.start"
|
||||
(ngModelChange)="onDateRangeChange(filter, { start: $event, end: filter.value.end })"
|
||||
placeholder="Start Date"
|
||||
class="clr-input filter-date">
|
||||
<span class="date-separator">to</span>
|
||||
<input type="date"
|
||||
[(ngModel)]="filter.value.end"
|
||||
(ngModelChange)="onDateRangeChange(filter, { start: filter.value.start, end: $event })"
|
||||
placeholder="End Date"
|
||||
class="clr-input filter-date">
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Toggle Filter -->
|
||||
<div *ngIf="filter.type === 'toggle'" class="filter-input toggle">
|
||||
<input type="checkbox"
|
||||
[(ngModel)]="filter.value"
|
||||
(ngModelChange)="onToggleChange(filter, $event)"
|
||||
clrToggle
|
||||
class="clr-toggle">
|
||||
<label class="toggle-label">{{ filter.field }}</label>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Layer Filters -->
|
||||
<div class="filter-group" *ngIf="hasActiveLayerFilters()">
|
||||
<h4>Layer Filters</h4>
|
||||
<div class="filter-controls">
|
||||
<div *ngFor="let filter of getActiveLayerFilters()" class="filter-item">
|
||||
<div class="filter-label">{{ filter.field }} ({{ filter.type || 'text' }})</div>
|
||||
|
||||
<!-- Text Filter -->
|
||||
<div *ngIf="!filter.type || filter.type === 'text'" class="filter-input">
|
||||
<input type="text"
|
||||
[(ngModel)]="filter.value"
|
||||
(ngModelChange)="onLayerFilterChange(filter)"
|
||||
[placeholder]="filter.field"
|
||||
class="clr-input filter-text-input">
|
||||
</div>
|
||||
|
||||
<!-- Dropdown Filter -->
|
||||
<div *ngIf="filter.type === 'dropdown'" class="filter-input">
|
||||
<select [(ngModel)]="filter.value"
|
||||
(ngModelChange)="onLayerFilterChange(filter)"
|
||||
class="clr-select filter-select">
|
||||
<option value="">Select {{ filter.field }}</option>
|
||||
<option *ngFor="let option of getFilterOptions(filter)" [value]="option">{{ option }}</option>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<!-- Multi-Select Filter - Updated to show key first, then dropdown on click -->
|
||||
<div *ngIf="filter.type === 'multiselect'" class="filter-input multiselect-container">
|
||||
<div class="multiselect-display" (click)="toggleMultiselect(filter, 'layer')">
|
||||
<span class="multiselect-label">{{ filter.field }}</span>
|
||||
<span class="multiselect-value" *ngIf="getSelectedOptionsCount(filter) > 0">
|
||||
({{ getSelectedOptionsCount(filter) }} selected)
|
||||
</span>
|
||||
<clr-icon shape="caret down" class="dropdown-icon"></clr-icon>
|
||||
</div>
|
||||
<div class="multiselect-dropdown" *ngIf="isMultiselectOpen(filter, 'layer')">
|
||||
<div class="checkbox-group">
|
||||
<div *ngFor="let option of getFilterOptions(filter); let i = index" class="checkbox-item">
|
||||
<input type="checkbox"
|
||||
[checked]="isOptionSelected(filter, option)"
|
||||
(change)="onMultiSelectChange(filter, option, $event)"
|
||||
[id]="'layer-' + filter.field + '-' + i"
|
||||
class="clr-checkbox">
|
||||
<label [for]="'layer-' + filter.field + '-' + i" class="checkbox-label">{{ option }}</label>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Date Range Filter -->
|
||||
<div *ngIf="filter.type === 'date-range'" class="filter-input date-range">
|
||||
<div class="date-input-group">
|
||||
<input type="date"
|
||||
[(ngModel)]="filter.value.start"
|
||||
(ngModelChange)="onDateRangeChange(filter, { start: $event, end: filter.value.end })"
|
||||
placeholder="Start Date"
|
||||
class="clr-input filter-date">
|
||||
<span class="date-separator">to</span>
|
||||
<input type="date"
|
||||
[(ngModel)]="filter.value.end"
|
||||
(ngModelChange)="onDateRangeChange(filter, { start: filter.value.start, end: $event })"
|
||||
placeholder="End Date"
|
||||
class="clr-input filter-date">
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Toggle Filter -->
|
||||
<div *ngIf="filter.type === 'toggle'" class="filter-input toggle">
|
||||
<input type="checkbox"
|
||||
[(ngModel)]="filter.value"
|
||||
(ngModelChange)="onToggleChange(filter, $event)"
|
||||
clrToggle
|
||||
class="clr-toggle">
|
||||
<label class="toggle-label">{{ filter.field }}</label>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Clear Filters Button -->
|
||||
<div class="filter-actions">
|
||||
<button class="btn btn-sm btn-outline" (click)="clearAllFilters()">Clear All Filters</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Header row with chart title and drilldown navigation -->
|
||||
<div class="chart-header">
|
||||
<div class="clr-row header-row">
|
||||
<div class="clr-col-6">
|
||||
<h3 class="chart-title" *ngIf="charttitle">{{ charttitle }}</h3>
|
||||
</div>
|
||||
<div class="clr-col-6" style="text-align: right;">
|
||||
<!-- Add drilldown navigation controls -->
|
||||
<button class="btn btn-sm btn-link" *ngIf="drilldownEnabled && drilldownStack.length > 0" (click)="navigateBack()">
|
||||
<cds-icon shape="arrow" direction="left"></cds-icon>
|
||||
Back to {{drilldownStack.length > 1 ? 'Previous Level' : 'Main Data'}}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Show current drilldown level -->
|
||||
<div class="clr-row" *ngIf="drilldownEnabled && drilldownStack.length > 0">
|
||||
<div class="clr-col-12">
|
||||
<div class="alert alert-info" style="padding: 8px 12px; margin-bottom: 12px;">
|
||||
<div class="alert-items">
|
||||
<div class="alert-item static">
|
||||
<div class="alert-icon-wrapper">
|
||||
<cds-icon class="alert-icon" shape="info-circle"></cds-icon>
|
||||
</div>
|
||||
<span class="alert-text">
|
||||
Drilldown Level: {{currentDrilldownLevel}}
|
||||
<span *ngIf="drilldownStack.length > 0">
|
||||
(Clicked on: {{drilldownStack[drilldownStack.length - 1].clickedLabel}})
|
||||
</span>
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<h3 class="chart-title" *ngIf="charttitle">{{ charttitle }}</h3>
|
||||
<div class="chart-wrapper">
|
||||
<!-- Show loading indicator -->
|
||||
<div class="loading-indicator" *ngIf="doughnutChartLabels.length === 0 && doughnutChartData.length === 0 && !noDataAvailable">
|
||||
<div class="spinner"></div>
|
||||
<p>Loading chart data...</p>
|
||||
</div>
|
||||
<div class="chart-content" [class.loading]="isLoading">
|
||||
<!-- Show no data message -->
|
||||
<div class="no-data-message" *ngIf="noDataAvailable && doughnutChartLabels.length === 0">
|
||||
<p>No chart data available</p>
|
||||
</div>
|
||||
|
||||
<!-- Show no data message -->
|
||||
<div class="no-data-message" *ngIf="noDataAvailable">
|
||||
<p>No chart data available</p>
|
||||
</div>
|
||||
<!-- Show chart when data is available -->
|
||||
<canvas baseChart
|
||||
[datasets]="doughnutChartData"
|
||||
[labels]="doughnutChartLabels"
|
||||
[type]="doughnutChartType"
|
||||
[options]="doughnutChartOptions"
|
||||
(chartHover)="chartHovered($event)"
|
||||
(chartClick)="chartClicked($event)">
|
||||
</canvas>
|
||||
|
||||
<!-- Show chart when data is available -->
|
||||
<canvas baseChart
|
||||
*ngIf="!noDataAvailable && doughnutChartLabels.length > 0 && doughnutChartData.length > 0"
|
||||
[data]="doughnutChartData"
|
||||
[labels]="doughnutChartLabels"
|
||||
[type]="doughnutChartType"
|
||||
[options]="doughnutChartOptions"
|
||||
(chartHover)="chartHovered($event)"
|
||||
(chartClick)="chartClicked($event)">
|
||||
</canvas>
|
||||
<!-- Loading overlay -->
|
||||
<div class="loading-overlay" *ngIf="isLoading">
|
||||
<div class="shimmer-donut"></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="chart-legend" *ngIf="!noDataAvailable && showlabel && doughnutChartLabels && doughnutChartLabels.length > 0">
|
||||
|
||||
<div class="chart-legend" *ngIf="showlabel && doughnutChartLabels && doughnutChartLabels.length > 0">
|
||||
<div class="legend-item" *ngFor="let label of doughnutChartLabels; let i = index">
|
||||
<span class="legend-color" [style.background-color]="getLegendColor(i)"></span>
|
||||
<span class="legend-label">{{ label }}</span>
|
||||
<span class="legend-value">{{ doughnutChartData && doughnutChartData[i] !== undefined ? doughnutChartData[i] : 0 }}</span>
|
||||
<span class="legend-value">{{ doughnutChartData && doughnutChartData[0] && doughnutChartData[0].data && doughnutChartData[0].data[i] !== undefined ? doughnutChartData[0].data[i] : 0 }}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -1,184 +1,343 @@
|
||||
.doughnut-chart-container {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
height: 400px;
|
||||
min-height: 400px;
|
||||
padding: 20px;
|
||||
background: linear-gradient(135deg, #ffffff 0%, #f8f9fa 100%);
|
||||
border-radius: 12px;
|
||||
box-shadow: 0 4px 20px rgba(0, 0, 0, 0.15);
|
||||
font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
|
||||
transition: all 0.3s ease;
|
||||
border: 1px solid #eaeaea;
|
||||
}
|
||||
|
||||
.doughnut-chart-container:hover {
|
||||
box-shadow: 0 6px 25px rgba(0, 0, 0, 0.2);
|
||||
transform: translateY(-2px);
|
||||
}
|
||||
|
||||
|
||||
|
||||
.chart-title {
|
||||
font-size: 26px;
|
||||
font-weight: 700;
|
||||
color: #2c3e50;
|
||||
margin-bottom: 20px;
|
||||
text-align: center;
|
||||
padding-bottom: 15px;
|
||||
border-bottom: 2px solid #3498db;
|
||||
text-shadow: 0 1px 2px rgba(0, 0, 0, 0.1);
|
||||
}
|
||||
|
||||
.chart-wrapper {
|
||||
position: relative;
|
||||
flex: 1;
|
||||
min-height: 250px;
|
||||
margin: 15px 0;
|
||||
background: #f8f9fa;
|
||||
border: 1px solid #e9ecef;
|
||||
border-radius: 8px;
|
||||
padding: 10px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.chart-wrapper canvas {
|
||||
max-width: 100%;
|
||||
max-height: 100%;
|
||||
filter: drop-shadow(0 2px 4px rgba(0,0,0,0.1));
|
||||
}
|
||||
|
||||
.chart-wrapper canvas:hover {
|
||||
filter: drop-shadow(0 4px 8px rgba(0,0,0,0.15));
|
||||
transform: scale(1.02);
|
||||
transition: all 0.3s ease;
|
||||
}
|
||||
|
||||
.chart-legend {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
justify-content: center;
|
||||
gap: 15px;
|
||||
margin-top: 20px;
|
||||
padding: 20px;
|
||||
background: linear-gradient(135deg, #f8f9fa 0%, #e9ecef 100%);
|
||||
border-radius: 8px;
|
||||
border: 1px solid #dee2e6;
|
||||
box-shadow: inset 0 1px 3px rgba(0, 0, 0, 0.05);
|
||||
}
|
||||
|
||||
.legend-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
padding: 12px 20px;
|
||||
background: linear-gradient(135deg, #ffffff 0%, #f8f9fa 100%);
|
||||
border-radius: 25px;
|
||||
box-shadow: 0 2px 6px rgba(0, 0, 0, 0.1);
|
||||
transition: all 0.3s ease;
|
||||
border: 1px solid #eaeaea;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.legend-item:hover {
|
||||
transform: translateY(-3px);
|
||||
box-shadow: 0 5px 15px rgba(0, 0, 0, 0.2);
|
||||
border-color: #3498db;
|
||||
background: linear-gradient(135deg, #f8f9fa 0%, #e9ecef 100%);
|
||||
}
|
||||
|
||||
.legend-color {
|
||||
width: 20px;
|
||||
height: 20px;
|
||||
border-radius: 50%;
|
||||
margin-right: 12px;
|
||||
display: inline-block;
|
||||
border: 2px solid white;
|
||||
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.2);
|
||||
}
|
||||
|
||||
.legend-label {
|
||||
font-size: 16px;
|
||||
font-weight: 600;
|
||||
color: #2c3e50;
|
||||
margin-right: 15px;
|
||||
white-space: nowrap;
|
||||
text-shadow: 0 1px 1px rgba(0, 0, 0, 0.1);
|
||||
}
|
||||
|
||||
.legend-value {
|
||||
font-size: 16px;
|
||||
font-weight: 700;
|
||||
color: #3498db;
|
||||
background: linear-gradient(135deg, #e9ecef 0%, #dde1e5 100%);
|
||||
padding: 6px 12px;
|
||||
border-radius: 12px;
|
||||
box-shadow: 0 1px 2px rgba(0, 0, 0, 0.1);
|
||||
min-width: 40px;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.loading-indicator, .no-data-message {
|
||||
text-align: center;
|
||||
padding: 30px;
|
||||
color: #666;
|
||||
font-size: 18px;
|
||||
font-style: italic;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
// Chart container structure - simplified to match shield dashboard
|
||||
.chart-container {
|
||||
height: 100%;
|
||||
}
|
||||
min-height: 400px; // Ensure minimum height
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
|
||||
.loading-indicator p, .no-data-message p {
|
||||
margin: 10px 0 0 0;
|
||||
}
|
||||
|
||||
.spinner {
|
||||
border: 4px solid #f3f3f3;
|
||||
border-top: 4px solid #3498db;
|
||||
border-radius: 50%;
|
||||
width: 40px;
|
||||
height: 40px;
|
||||
animation: spin 1s linear infinite;
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
|
||||
@keyframes spin {
|
||||
0% { transform: rotate(0deg); }
|
||||
100% { transform: rotate(360deg); }
|
||||
}
|
||||
|
||||
/* Responsive design */
|
||||
@media (max-width: 768px) {
|
||||
.doughnut-chart-container {
|
||||
// Filter section styling
|
||||
.filter-section {
|
||||
margin-bottom: 20px;
|
||||
padding: 15px;
|
||||
border: 1px solid #ddd;
|
||||
border-radius: 4px;
|
||||
background-color: #f9f9f9;
|
||||
}
|
||||
|
||||
.chart-title {
|
||||
font-size: 20px;
|
||||
.filter-group {
|
||||
margin-bottom: 15px;
|
||||
|
||||
h4 {
|
||||
margin-top: 0;
|
||||
margin-bottom: 10px;
|
||||
color: #333;
|
||||
font-weight: 600;
|
||||
}
|
||||
}
|
||||
|
||||
.chart-wrapper {
|
||||
min-height: 200px;
|
||||
.filter-controls {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 15px;
|
||||
}
|
||||
|
||||
.chart-legend {
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
.filter-item {
|
||||
flex: 1 1 300px;
|
||||
min-width: 250px;
|
||||
padding: 10px;
|
||||
background: white;
|
||||
border: 1px solid #e0e0e0;
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
.legend-item {
|
||||
.filter-label {
|
||||
font-weight: 500;
|
||||
margin-bottom: 8px;
|
||||
color: #555;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.filter-input {
|
||||
width: 100%;
|
||||
max-width: 300px;
|
||||
justify-content: space-between;
|
||||
|
||||
.filter-text-input,
|
||||
.filter-select,
|
||||
.filter-date {
|
||||
width: 100%;
|
||||
padding: 6px 12px;
|
||||
border: 1px solid #ccc;
|
||||
border-radius: 4px;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.filter-select {
|
||||
height: 34px;
|
||||
}
|
||||
}
|
||||
|
||||
.no-data-message {
|
||||
font-size: 16px;
|
||||
padding: 20px;
|
||||
.multiselect-container {
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.multiselect-display {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
padding: 6px 12px;
|
||||
border: 1px solid #ccc;
|
||||
border-radius: 4px;
|
||||
background: white;
|
||||
cursor: pointer;
|
||||
min-height: 34px;
|
||||
|
||||
.multiselect-label {
|
||||
flex: 1;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.multiselect-value {
|
||||
color: #666;
|
||||
font-size: 12px;
|
||||
margin-right: 8px;
|
||||
}
|
||||
|
||||
.dropdown-icon {
|
||||
flex-shrink: 0;
|
||||
transition: transform 0.2s ease;
|
||||
}
|
||||
|
||||
&:hover {
|
||||
border-color: #999;
|
||||
}
|
||||
}
|
||||
|
||||
.multiselect-dropdown {
|
||||
position: absolute;
|
||||
top: 100%;
|
||||
left: 0;
|
||||
right: 0;
|
||||
z-index: 1000;
|
||||
background: white;
|
||||
border: 1px solid #ccc;
|
||||
border-top: none;
|
||||
border-radius: 0 0 4px 4px;
|
||||
box-shadow: 0 2px 4px rgba(0,0,0,0.1);
|
||||
max-height: 200px;
|
||||
overflow-y: auto;
|
||||
|
||||
.checkbox-group {
|
||||
padding: 8px;
|
||||
|
||||
.checkbox-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
padding: 4px 0;
|
||||
|
||||
.checkbox-label {
|
||||
margin: 0;
|
||||
font-size: 14px;
|
||||
cursor: pointer;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.date-range {
|
||||
.date-input-group {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.date-separator {
|
||||
margin: 0 5px;
|
||||
color: #777;
|
||||
}
|
||||
}
|
||||
|
||||
.toggle {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
|
||||
.toggle-label {
|
||||
margin: 0;
|
||||
font-size: 14px;
|
||||
cursor: pointer;
|
||||
}
|
||||
}
|
||||
|
||||
.filter-actions {
|
||||
margin-top: 15px;
|
||||
padding-top: 15px;
|
||||
border-top: 1px solid #eee;
|
||||
|
||||
.btn {
|
||||
font-size: 13px;
|
||||
}
|
||||
}
|
||||
|
||||
// Chart header styling
|
||||
.chart-header {
|
||||
margin-bottom: 20px;
|
||||
|
||||
.header-row {
|
||||
margin-bottom: 15px;
|
||||
padding-bottom: 10px;
|
||||
border-bottom: 1px solid #eee;
|
||||
|
||||
.chart-title {
|
||||
margin: 0;
|
||||
font-size: 18px;
|
||||
font-weight: 600;
|
||||
color: #0a192f;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Chart wrapper and content - simplified to match shield dashboard
|
||||
.chart-wrapper {
|
||||
flex: 1;
|
||||
position: relative;
|
||||
|
||||
.chart-content {
|
||||
position: relative;
|
||||
height: 100%;
|
||||
min-height: 300px;
|
||||
|
||||
&.loading {
|
||||
opacity: 0.7;
|
||||
|
||||
canvas {
|
||||
filter: blur(2px);
|
||||
}
|
||||
}
|
||||
|
||||
.no-data-message {
|
||||
text-align: center;
|
||||
padding: 20px;
|
||||
color: #666;
|
||||
font-style: italic;
|
||||
}
|
||||
|
||||
canvas {
|
||||
max-width: 100%;
|
||||
max-height: calc(100% - 40px); // Leave space for legend
|
||||
transition: filter 0.3s ease;
|
||||
}
|
||||
|
||||
.loading-overlay {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
background: rgba(255, 255, 255, 0.8);
|
||||
|
||||
.shimmer-donut {
|
||||
width: 120px;
|
||||
height: 120px;
|
||||
border-radius: 50%;
|
||||
background: linear-gradient(90deg, #f0f0f0 25%, #e0e0e0 50%, #f0f0f0 75%);
|
||||
background-size: 200% 100%;
|
||||
animation: shimmer 1.5s infinite;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Chart legend - simplified
|
||||
.chart-legend {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
justify-content: center;
|
||||
gap: 15px;
|
||||
margin: 20px 0;
|
||||
padding: 15px;
|
||||
|
||||
.legend-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
padding: 8px 15px;
|
||||
background: #f8f9fa;
|
||||
border-radius: 20px;
|
||||
border: 1px solid #e9ecef;
|
||||
min-width: 120px;
|
||||
justify-content: space-between;
|
||||
}
|
||||
|
||||
.legend-color {
|
||||
width: 15px;
|
||||
height: 15px;
|
||||
border-radius: 50%;
|
||||
margin-right: 8px;
|
||||
display: inline-block;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.legend-label {
|
||||
font-size: 14px;
|
||||
font-weight: 500;
|
||||
color: #2c3e50;
|
||||
margin-right: 10px;
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
|
||||
.legend-value {
|
||||
font-size: 14px;
|
||||
font-weight: 600;
|
||||
color: #3498db;
|
||||
min-width: 30px;
|
||||
text-align: right;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes shimmer {
|
||||
0% {
|
||||
background-position: -200% 0;
|
||||
}
|
||||
100% {
|
||||
background-position: 200% 0;
|
||||
}
|
||||
}
|
||||
|
||||
// Responsive design
|
||||
@media (max-width: 768px) {
|
||||
.chart-container {
|
||||
.filter-controls {
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.filter-item {
|
||||
min-width: 100%;
|
||||
}
|
||||
|
||||
.chart-header {
|
||||
.header-row {
|
||||
.chart-title {
|
||||
font-size: 16px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.chart-content {
|
||||
min-height: 250px;
|
||||
}
|
||||
|
||||
.chart-legend {
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
|
||||
.legend-item {
|
||||
width: 100%;
|
||||
max-width: 300px;
|
||||
justify-content: space-between;
|
||||
}
|
||||
}
|
||||
|
||||
.chart-content {
|
||||
min-height: 250px;
|
||||
|
||||
canvas {
|
||||
max-height: calc(100% - 60px); // More space for legend on mobile
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,12 +1,14 @@
|
||||
import { Component, OnInit, Input, OnChanges, SimpleChanges, AfterViewChecked } from '@angular/core';
|
||||
import { Component, OnInit, Input, OnChanges, SimpleChanges, AfterViewChecked, OnDestroy } from '@angular/core';
|
||||
import { Dashboard3Service } from 'src/app/services/builder/dashboard3.service';
|
||||
import { FilterService } from '../../common-filter/filter.service';
|
||||
import { Subscription } from 'rxjs';
|
||||
|
||||
@Component({
|
||||
selector: 'app-doughnut-chart',
|
||||
templateUrl: './doughnut-chart.component.html',
|
||||
styleUrls: ['./doughnut-chart.component.scss']
|
||||
})
|
||||
export class DoughnutChartComponent implements OnInit, OnChanges, AfterViewChecked {
|
||||
export class DoughnutChartComponent implements OnInit, OnChanges, AfterViewChecked, OnDestroy {
|
||||
@Input() xAxis: string;
|
||||
@Input() yAxis: string | string[];
|
||||
@Input() table: string;
|
||||
@@ -34,7 +36,21 @@ export class DoughnutChartComponent implements OnInit, OnChanges, AfterViewCheck
|
||||
@Input() drilldownLayers: any[] = []; // Array of drilldown layer configurations
|
||||
|
||||
public doughnutChartLabels: string[] = ["Category A", "Category B", "Category C"];
|
||||
public doughnutChartData: number[] = [30, 50, 20];
|
||||
public doughnutChartData: any[] = [
|
||||
{
|
||||
data: [30, 50, 20],
|
||||
backgroundColor: [
|
||||
'#FF6384',
|
||||
'#36A2EB',
|
||||
'#FFCE56'
|
||||
],
|
||||
hoverBackgroundColor: [
|
||||
'#FF6384',
|
||||
'#36A2EB',
|
||||
'#FFCE56'
|
||||
]
|
||||
}
|
||||
];
|
||||
public doughnutChartType: string = "doughnut";
|
||||
public doughnutChartOptions: any = {
|
||||
responsive: true,
|
||||
@@ -70,6 +86,14 @@ export class DoughnutChartComponent implements OnInit, OnChanges, AfterViewCheck
|
||||
borderWidth: 2,
|
||||
borderColor: '#fff'
|
||||
}
|
||||
},
|
||||
layout: {
|
||||
padding: {
|
||||
top: 20,
|
||||
bottom: 20,
|
||||
left: 20,
|
||||
right: 20
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
@@ -94,15 +118,40 @@ export class DoughnutChartComponent implements OnInit, OnChanges, AfterViewCheck
|
||||
// No data state
|
||||
noDataAvailable: boolean = false;
|
||||
|
||||
// Loading state
|
||||
isLoading: boolean = false;
|
||||
|
||||
// Flag to prevent infinite loops
|
||||
private isFetchingData: boolean = false;
|
||||
|
||||
constructor(private dashboardService: Dashboard3Service) { }
|
||||
// Subscriptions to unsubscribe on destroy
|
||||
private subscriptions: Subscription[] = [];
|
||||
|
||||
// Add properties for filter functionality
|
||||
private openMultiselects: Map<string, string> = new Map(); // Map of filterId -> context
|
||||
private documentClickHandler: ((event: MouseEvent) => void) | null = null;
|
||||
private filtersInitialized: boolean = false;
|
||||
|
||||
constructor(
|
||||
private dashboardService: Dashboard3Service,
|
||||
private filterService: FilterService
|
||||
) { }
|
||||
|
||||
ngOnInit(): void {
|
||||
// Subscribe to filter changes
|
||||
this.subscriptions.push(
|
||||
this.filterService.filterState$.subscribe(filters => {
|
||||
// When filters change, refresh the chart data
|
||||
this.fetchChartData();
|
||||
})
|
||||
);
|
||||
|
||||
// Validate initial data
|
||||
this.validateChartData();
|
||||
this.fetchChartData();
|
||||
// Only fetch data if we have the required inputs, otherwise show default data
|
||||
if (this.table && this.xAxis && this.yAxis) {
|
||||
this.fetchChartData();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -122,17 +171,33 @@ export class DoughnutChartComponent implements OnInit, OnChanges, AfterViewCheck
|
||||
if (this.doughnutChartLabels.length === 0 && this.doughnutChartData.length === 0) {
|
||||
// Add default data to ensure chart visibility
|
||||
this.doughnutChartLabels = ["Category A", "Category B", "Category C"];
|
||||
this.doughnutChartData = [30, 50, 20];
|
||||
this.doughnutChartData = [
|
||||
{
|
||||
data: [30, 50, 20],
|
||||
backgroundColor: [
|
||||
'#FF6384',
|
||||
'#36A2EB',
|
||||
'#FFCE56'
|
||||
],
|
||||
hoverBackgroundColor: [
|
||||
'#FF6384',
|
||||
'#36A2EB',
|
||||
'#FFCE56'
|
||||
]
|
||||
}
|
||||
];
|
||||
}
|
||||
|
||||
// Ensure we have matching arrays
|
||||
if (this.doughnutChartLabels.length !== this.doughnutChartData.length) {
|
||||
const maxLength = Math.max(this.doughnutChartLabels.length, this.doughnutChartData.length);
|
||||
if (this.doughnutChartLabels.length !== (this.doughnutChartData[0]?.data?.length || 0)) {
|
||||
const maxLength = Math.max(this.doughnutChartLabels.length, this.doughnutChartData[0]?.data?.length || 0);
|
||||
while (this.doughnutChartLabels.length < maxLength) {
|
||||
this.doughnutChartLabels.push(`Label ${this.doughnutChartLabels.length + 1}`);
|
||||
}
|
||||
while (this.doughnutChartData.length < maxLength) {
|
||||
this.doughnutChartData.push(0);
|
||||
if (this.doughnutChartData[0]) {
|
||||
while (this.doughnutChartData[0].data.length < maxLength) {
|
||||
this.doughnutChartData[0].data.push(0);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -148,6 +213,12 @@ export class DoughnutChartComponent implements OnInit, OnChanges, AfterViewCheck
|
||||
ngOnChanges(changes: SimpleChanges): void {
|
||||
console.log('DoughnutChartComponent input changes:', changes);
|
||||
|
||||
// Initialize filter values if they haven't been initialized yet
|
||||
if (!this.filtersInitialized && (changes.baseFilters || changes.drilldownFilters || changes.drilldownLayers)) {
|
||||
this.initializeFilterValues();
|
||||
this.filtersInitialized = true;
|
||||
}
|
||||
|
||||
// Check if any of the key properties have changed
|
||||
const xAxisChanged = changes.xAxis && !changes.xAxis.firstChange;
|
||||
const yAxisChanged = changes.yAxis && !changes.yAxis.firstChange;
|
||||
@@ -169,6 +240,12 @@ export class DoughnutChartComponent implements OnInit, OnChanges, AfterViewCheck
|
||||
console.log('Chart configuration changed, fetching new data');
|
||||
this.fetchChartData();
|
||||
}
|
||||
|
||||
// If we have the required inputs and haven't fetched data yet, fetch it
|
||||
if ((xAxisChanged || yAxisChanged || tableChanged) && this.table && this.xAxis && this.yAxis && !this.isFetchingData) {
|
||||
console.log('Required inputs available, fetching data');
|
||||
this.fetchChartData();
|
||||
}
|
||||
}
|
||||
|
||||
ngAfterViewChecked() {
|
||||
@@ -180,15 +257,340 @@ export class DoughnutChartComponent implements OnInit, OnChanges, AfterViewCheck
|
||||
});
|
||||
}
|
||||
|
||||
ngOnDestroy(): void {
|
||||
this.subscriptions.forEach(sub => sub.unsubscribe());
|
||||
// Clean up document click handler
|
||||
this.removeDocumentClickHandler();
|
||||
}
|
||||
|
||||
// Initialize filter values with proper default values based on type
|
||||
private initializeFilterValues(): void {
|
||||
console.log('Initializing filter values');
|
||||
|
||||
// Initialize base filters
|
||||
if (this.baseFilters) {
|
||||
this.baseFilters.forEach(filter => {
|
||||
if (filter.value === undefined || filter.value === null) {
|
||||
switch (filter.type) {
|
||||
case 'multiselect':
|
||||
filter.value = [];
|
||||
break;
|
||||
case 'date-range':
|
||||
filter.value = { start: null, end: null };
|
||||
break;
|
||||
case 'toggle':
|
||||
filter.value = false;
|
||||
break;
|
||||
default:
|
||||
filter.value = '';
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// Initialize drilldown filters
|
||||
if (this.drilldownFilters) {
|
||||
this.drilldownFilters.forEach(filter => {
|
||||
if (filter.value === undefined || filter.value === null) {
|
||||
switch (filter.type) {
|
||||
case 'multiselect':
|
||||
filter.value = [];
|
||||
break;
|
||||
case 'date-range':
|
||||
filter.value = { start: null, end: null };
|
||||
break;
|
||||
case 'toggle':
|
||||
filter.value = false;
|
||||
break;
|
||||
default:
|
||||
filter.value = '';
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// Initialize layer filters
|
||||
if (this.drilldownLayers) {
|
||||
this.drilldownLayers.forEach(layer => {
|
||||
if (layer.filters) {
|
||||
layer.filters.forEach((filter: any) => {
|
||||
if (filter.value === undefined || filter.value === null) {
|
||||
switch (filter.type) {
|
||||
case 'multiselect':
|
||||
filter.value = [];
|
||||
break;
|
||||
case 'date-range':
|
||||
filter.value = { start: null, end: null };
|
||||
break;
|
||||
case 'toggle':
|
||||
filter.value = false;
|
||||
break;
|
||||
default:
|
||||
filter.value = '';
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
console.log('Filter values initialized:', {
|
||||
baseFilters: this.baseFilters,
|
||||
drilldownFilters: this.drilldownFilters,
|
||||
drilldownLayers: this.drilldownLayers
|
||||
});
|
||||
}
|
||||
|
||||
// Check if there are active filters
|
||||
hasActiveFilters(): boolean {
|
||||
return (this.baseFilters && this.baseFilters.length > 0) ||
|
||||
(this.drilldownFilters && this.drilldownFilters.length > 0) ||
|
||||
this.hasActiveLayerFilters();
|
||||
}
|
||||
|
||||
// Check if there are active layer filters for current drilldown level
|
||||
hasActiveLayerFilters(): boolean {
|
||||
if (this.currentDrilldownLevel > 1 && this.drilldownLayers && this.drilldownLayers.length > 0) {
|
||||
const layerIndex = this.currentDrilldownLevel - 2; // -2 because level 1 is base drilldown
|
||||
return layerIndex < this.drilldownLayers.length &&
|
||||
this.drilldownLayers[layerIndex].filters &&
|
||||
this.drilldownLayers[layerIndex].filters.length > 0;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
// Get active layer filters for current drilldown level
|
||||
getActiveLayerFilters(): any[] {
|
||||
if (this.currentDrilldownLevel > 1 && this.drilldownLayers && this.drilldownLayers.length > 0) {
|
||||
const layerIndex = this.currentDrilldownLevel - 2; // -2 because level 1 is base drilldown
|
||||
if (layerIndex < this.drilldownLayers.length &&
|
||||
this.drilldownLayers[layerIndex].filters) {
|
||||
return this.drilldownLayers[layerIndex].filters;
|
||||
}
|
||||
}
|
||||
return [];
|
||||
}
|
||||
|
||||
// Get filter options for dropdown/multiselect filters
|
||||
getFilterOptions(filter: any): string[] {
|
||||
if (filter.options) {
|
||||
if (Array.isArray(filter.options)) {
|
||||
return filter.options;
|
||||
} else if (typeof filter.options === 'string') {
|
||||
return filter.options.split(',').map(opt => opt.trim()).filter(opt => opt);
|
||||
}
|
||||
}
|
||||
return [];
|
||||
}
|
||||
|
||||
// Check if an option is selected for multiselect filters
|
||||
isOptionSelected(filter: any, option: string): boolean {
|
||||
if (!filter.value) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (Array.isArray(filter.value)) {
|
||||
return filter.value.includes(option);
|
||||
}
|
||||
|
||||
return filter.value === option;
|
||||
}
|
||||
|
||||
// Handle base filter changes
|
||||
onBaseFilterChange(filter: any): void {
|
||||
console.log('Base filter changed:', filter);
|
||||
// Refresh data when filter changes
|
||||
this.fetchChartData();
|
||||
}
|
||||
|
||||
// Handle drilldown filter changes
|
||||
onDrilldownFilterChange(filter: any): void {
|
||||
console.log('Drilldown filter changed:', filter);
|
||||
// Refresh data when filter changes
|
||||
this.fetchChartData();
|
||||
}
|
||||
|
||||
// Handle layer filter changes
|
||||
onLayerFilterChange(filter: any): void {
|
||||
console.log('Layer filter changed:', filter);
|
||||
// Refresh data when filter changes
|
||||
this.fetchChartData();
|
||||
}
|
||||
|
||||
// Handle multiselect changes
|
||||
onMultiSelectChange(filter: any, option: string, event: any): void {
|
||||
const checked = event.target.checked;
|
||||
|
||||
// Initialize filter.value as array if it's not already
|
||||
if (!Array.isArray(filter.value)) {
|
||||
filter.value = [];
|
||||
}
|
||||
|
||||
if (checked) {
|
||||
// Add option to array if not already present
|
||||
if (!filter.value.includes(option)) {
|
||||
filter.value.push(option);
|
||||
}
|
||||
} else {
|
||||
// Remove option from array
|
||||
filter.value = filter.value.filter((item: string) => item !== option);
|
||||
}
|
||||
|
||||
// Refresh data when filter changes
|
||||
this.fetchChartData();
|
||||
}
|
||||
|
||||
// Handle date range changes
|
||||
onDateRangeChange(filter: any, dateRange: { start: string | null, end: string | null }): void {
|
||||
filter.value = dateRange;
|
||||
// Refresh data when filter changes
|
||||
this.fetchChartData();
|
||||
}
|
||||
|
||||
// Handle toggle changes
|
||||
onToggleChange(filter: any, checked: boolean): void {
|
||||
filter.value = checked;
|
||||
// Refresh data when filter changes
|
||||
this.fetchChartData();
|
||||
}
|
||||
|
||||
// Toggle multiselect dropdown visibility
|
||||
toggleMultiselect(filter: any, context: string): void {
|
||||
const filterId = `${context}-${filter.field}`;
|
||||
if (this.isMultiselectOpen(filter, context)) {
|
||||
this.openMultiselects.delete(filterId);
|
||||
} else {
|
||||
// Close all other multiselects first
|
||||
this.openMultiselects.clear();
|
||||
this.openMultiselects.set(filterId, context);
|
||||
|
||||
// Add document click handler to close dropdown when clicking outside
|
||||
this.addDocumentClickHandler();
|
||||
}
|
||||
}
|
||||
|
||||
// Add document click handler to close dropdowns when clicking outside
|
||||
private addDocumentClickHandler(): void {
|
||||
if (!this.documentClickHandler) {
|
||||
this.documentClickHandler = (event: MouseEvent) => {
|
||||
const target = event.target as HTMLElement;
|
||||
// Check if click is outside any multiselect dropdown
|
||||
if (!target.closest('.multiselect-container')) {
|
||||
this.openMultiselects.clear();
|
||||
this.removeDocumentClickHandler();
|
||||
}
|
||||
};
|
||||
|
||||
// Use setTimeout to ensure the click event that opened the dropdown doesn't immediately close it
|
||||
setTimeout(() => {
|
||||
document.addEventListener('click', this.documentClickHandler!);
|
||||
}, 0);
|
||||
}
|
||||
}
|
||||
|
||||
// Remove document click handler
|
||||
private removeDocumentClickHandler(): void {
|
||||
if (this.documentClickHandler) {
|
||||
document.removeEventListener('click', this.documentClickHandler);
|
||||
this.documentClickHandler = null;
|
||||
}
|
||||
}
|
||||
|
||||
// Check if multiselect dropdown is open
|
||||
isMultiselectOpen(filter: any, context: string): boolean {
|
||||
const filterId = `${context}-${filter.field}`;
|
||||
return this.openMultiselects.has(filterId);
|
||||
}
|
||||
|
||||
// Get count of selected options for a multiselect filter
|
||||
getSelectedOptionsCount(filter: any): number {
|
||||
if (!filter.value) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
if (Array.isArray(filter.value)) {
|
||||
return filter.value.length;
|
||||
}
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
// Clear all filters
|
||||
clearAllFilters(): void {
|
||||
// Clear base filters
|
||||
if (this.baseFilters) {
|
||||
this.baseFilters.forEach(filter => {
|
||||
if (filter.type === 'multiselect') {
|
||||
filter.value = [];
|
||||
} else if (filter.type === 'date-range') {
|
||||
filter.value = { start: null, end: null };
|
||||
} else if (filter.type === 'toggle') {
|
||||
filter.value = false;
|
||||
} else {
|
||||
filter.value = '';
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// Clear drilldown filters
|
||||
if (this.drilldownFilters) {
|
||||
this.drilldownFilters.forEach(filter => {
|
||||
if (filter.type === 'multiselect') {
|
||||
filter.value = [];
|
||||
} else if (filter.type === 'date-range') {
|
||||
filter.value = { start: null, end: null };
|
||||
} else if (filter.type === 'toggle') {
|
||||
filter.value = false;
|
||||
} else {
|
||||
filter.value = '';
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// Clear layer filters
|
||||
if (this.drilldownLayers) {
|
||||
this.drilldownLayers.forEach(layer => {
|
||||
if (layer.filters) {
|
||||
layer.filters.forEach((filter: any) => {
|
||||
if (filter.type === 'multiselect') {
|
||||
filter.value = [];
|
||||
} else if (filter.type === 'date-range') {
|
||||
filter.value = { start: null, end: null };
|
||||
} else if (filter.type === 'toggle') {
|
||||
filter.value = false;
|
||||
} else {
|
||||
filter.value = '';
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// Close all multiselect dropdowns
|
||||
this.openMultiselects.clear();
|
||||
|
||||
// Refresh data
|
||||
this.fetchChartData();
|
||||
}
|
||||
|
||||
// Public method to refresh data when filters change
|
||||
refreshData(): void {
|
||||
this.fetchChartData();
|
||||
}
|
||||
|
||||
fetchChartData(): void {
|
||||
// Set loading state
|
||||
this.isLoading = true;
|
||||
|
||||
// Set flag to prevent recursive calls
|
||||
this.isFetchingData = true;
|
||||
|
||||
// If we're in drilldown mode, fetch the appropriate drilldown data
|
||||
if (this.currentDrilldownLevel > 0 && this.drilldownStack.length > 0) {
|
||||
this.fetchDrilldownData();
|
||||
// Reset flag after fetching
|
||||
// Reset flags after fetching
|
||||
this.isFetchingData = false;
|
||||
this.isLoading = false;
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -212,11 +614,53 @@ export class DoughnutChartComponent implements OnInit, OnChanges, AfterViewCheck
|
||||
filterParams = JSON.stringify(filterObj);
|
||||
}
|
||||
}
|
||||
console.log('Base filter parameters:', filterParams);
|
||||
|
||||
// Add common filters to filter parameters
|
||||
const commonFilters = this.filterService.getFilterValues();
|
||||
console.log('Common filters from service:', commonFilters);
|
||||
|
||||
if (Object.keys(commonFilters).length > 0) {
|
||||
// Merge common filters with base filters
|
||||
const mergedFilterObj = {};
|
||||
|
||||
// Add base filters first
|
||||
if (filterParams) {
|
||||
try {
|
||||
const baseFilterObj = JSON.parse(filterParams);
|
||||
Object.assign(mergedFilterObj, baseFilterObj);
|
||||
} catch (e) {
|
||||
console.warn('Failed to parse base filter parameters:', e);
|
||||
}
|
||||
}
|
||||
|
||||
// Add common filters using the field name as the key, not the filter id
|
||||
Object.keys(commonFilters).forEach(filterId => {
|
||||
const filterValue = commonFilters[filterId];
|
||||
// Find the filter definition to get the field name
|
||||
const filterDef = this.filterService.getFilters().find(f => f.id === filterId);
|
||||
if (filterDef && filterDef.field) {
|
||||
const fieldName = filterDef.field;
|
||||
if (filterValue !== undefined && filterValue !== null && filterValue !== '') {
|
||||
mergedFilterObj[fieldName] = filterValue;
|
||||
}
|
||||
} else {
|
||||
// Fallback to using filterId as field name if no field is defined
|
||||
if (filterValue !== undefined && filterValue !== null && filterValue !== '') {
|
||||
mergedFilterObj[filterId] = filterValue;
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
if (Object.keys(mergedFilterObj).length > 0) {
|
||||
filterParams = JSON.stringify(mergedFilterObj);
|
||||
}
|
||||
}
|
||||
|
||||
console.log('Final filter parameters:', filterParams);
|
||||
|
||||
// Log the URL that will be called
|
||||
const url = `chart/getdashjson/doughnut?tableName=${this.table}&xAxis=${this.xAxis}&yAxes=${yAxisString}${this.connection ? `&sureId=${this.connection}` : ''}`;
|
||||
console.log('Doughnut chart data URL:', url);
|
||||
console.log('Chart data URL:', url);
|
||||
|
||||
// Fetch data from the dashboard service with parameter field and value
|
||||
// For base level, we pass empty parameter and value, but now also pass filters
|
||||
@@ -224,89 +668,113 @@ export class DoughnutChartComponent implements OnInit, OnChanges, AfterViewCheck
|
||||
(data: any) => {
|
||||
console.log('Received doughnut chart data:', data);
|
||||
if (data === null) {
|
||||
console.warn('Doughnut chart API returned null data. Check if the API endpoint is working correctly.');
|
||||
console.warn('API returned null data. Check if the API endpoint is working correctly.');
|
||||
this.noDataAvailable = true;
|
||||
this.doughnutChartLabels = [];
|
||||
this.doughnutChartData = [];
|
||||
// Validate and sanitize data to show default data
|
||||
this.validateChartData();
|
||||
// Reset flag after fetching
|
||||
// Reset flags after fetching
|
||||
this.isFetchingData = false;
|
||||
this.isLoading = false;
|
||||
return;
|
||||
}
|
||||
|
||||
// Handle the actual data structure returned by the API
|
||||
if (data && data.chartLabels && data.chartData) {
|
||||
// For doughnut charts, we need to extract the data differently
|
||||
// The first dataset's data array contains the values for the doughnut chart
|
||||
// Backend has already filtered the data, just display it
|
||||
this.noDataAvailable = data.chartLabels.length === 0;
|
||||
this.doughnutChartLabels = data.chartLabels || [];
|
||||
if (data.chartData && data.chartData.length > 0) {
|
||||
this.doughnutChartData = data.chartData[0].data.map(value => {
|
||||
// Convert to number if it's not already
|
||||
const numValue = Number(value);
|
||||
return isNaN(numValue) ? 0 : numValue;
|
||||
});
|
||||
this.doughnutChartLabels = data.chartLabels;
|
||||
|
||||
// Handle different data structures
|
||||
let chartDataValues;
|
||||
if (Array.isArray(data.chartData)) {
|
||||
// If chartData is already an array of values
|
||||
if (data.chartData.length > 0 && typeof data.chartData[0] !== 'object') {
|
||||
chartDataValues = data.chartData;
|
||||
}
|
||||
// If chartData is an array with one object containing the data
|
||||
else if (data.chartData.length > 0 && data.chartData[0].data) {
|
||||
chartDataValues = data.chartData[0].data;
|
||||
}
|
||||
// Default case
|
||||
else {
|
||||
chartDataValues = data.chartData;
|
||||
}
|
||||
} else {
|
||||
this.doughnutChartData = [];
|
||||
chartDataValues = [data.chartData];
|
||||
}
|
||||
// Ensure labels and data arrays have the same length
|
||||
this.syncLabelAndDataArrays();
|
||||
// Validate and sanitize data
|
||||
this.validateChartData();
|
||||
// Trigger change detection
|
||||
this.doughnutChartData = [...this.doughnutChartData];
|
||||
|
||||
this.doughnutChartData = [
|
||||
{
|
||||
data: chartDataValues,
|
||||
backgroundColor: this.chartColors.slice(0, chartDataValues.length),
|
||||
hoverBackgroundColor: this.chartColors.slice(0, chartDataValues.length)
|
||||
}
|
||||
];
|
||||
console.log('Updated doughnut chart with data:', { labels: this.doughnutChartLabels, data: this.doughnutChartData });
|
||||
} else if (data && data.labels && data.data) {
|
||||
// Handle the original expected format as fallback
|
||||
} else if (data && data.labels && data.datasets) {
|
||||
// Backend has already filtered the data, just display it
|
||||
this.noDataAvailable = data.labels.length === 0;
|
||||
this.doughnutChartLabels = data.labels || [];
|
||||
this.doughnutChartData = data.data.map(value => {
|
||||
// Convert to number if it's not already
|
||||
const numValue = Number(value);
|
||||
return isNaN(numValue) ? 0 : numValue;
|
||||
});
|
||||
// Ensure labels and data arrays have the same length
|
||||
this.syncLabelAndDataArrays();
|
||||
// Validate and sanitize data
|
||||
this.validateChartData();
|
||||
// Trigger change detection
|
||||
this.doughnutChartData = [...this.doughnutChartData];
|
||||
this.doughnutChartLabels = data.labels;
|
||||
this.doughnutChartData = data.datasets;
|
||||
console.log('Updated doughnut chart with legacy data format:', { labels: this.doughnutChartLabels, data: this.doughnutChartData });
|
||||
} else {
|
||||
console.warn('Doughnut chart received data does not have expected structure', data);
|
||||
// Reset to default data
|
||||
console.warn('Received data does not have expected structure', data);
|
||||
this.noDataAvailable = true;
|
||||
this.doughnutChartLabels = [];
|
||||
this.doughnutChartData = [];
|
||||
// Validate and sanitize data to show default data
|
||||
this.validateChartData();
|
||||
// Keep default data instead of empty arrays
|
||||
this.doughnutChartLabels = ["Category A", "Category B", "Category C"];
|
||||
this.doughnutChartData = [
|
||||
{
|
||||
data: [30, 50, 20],
|
||||
backgroundColor: [
|
||||
'#FF6384',
|
||||
'#36A2EB',
|
||||
'#FFCE56'
|
||||
],
|
||||
hoverBackgroundColor: [
|
||||
'#FF6384',
|
||||
'#36A2EB',
|
||||
'#FFCE56'
|
||||
]
|
||||
}
|
||||
];
|
||||
}
|
||||
// Reset flag after fetching
|
||||
// Reset flags after fetching
|
||||
this.isFetchingData = false;
|
||||
this.isLoading = false;
|
||||
},
|
||||
(error) => {
|
||||
console.error('Error fetching doughnut chart data:', error);
|
||||
this.noDataAvailable = true;
|
||||
this.doughnutChartLabels = [];
|
||||
this.doughnutChartData = [];
|
||||
// Validate and sanitize data to show default data
|
||||
this.validateChartData();
|
||||
// Reset flag after fetching
|
||||
// Keep default data in case of error
|
||||
this.doughnutChartLabels = ["Category A", "Category B", "Category C"];
|
||||
this.doughnutChartData = [
|
||||
{
|
||||
data: [30, 50, 20],
|
||||
backgroundColor: [
|
||||
'#FF6384',
|
||||
'#36A2EB',
|
||||
'#FFCE56'
|
||||
],
|
||||
hoverBackgroundColor: [
|
||||
'#FF6384',
|
||||
'#36A2EB',
|
||||
'#FFCE56'
|
||||
]
|
||||
}
|
||||
];
|
||||
// Reset flags after fetching
|
||||
this.isFetchingData = false;
|
||||
this.isLoading = false;
|
||||
}
|
||||
);
|
||||
} else {
|
||||
console.log('Missing required data for doughnut chart, showing default data:', { table: this.table, xAxis: this.xAxis, yAxis: this.yAxis, connection: this.connection });
|
||||
// Don't set noDataAvailable to true when there's no required data
|
||||
// This allows static data to be displayed
|
||||
this.noDataAvailable = false;
|
||||
// Validate the chart data to ensure we have some data to display
|
||||
this.validateChartData();
|
||||
// Force a redraw to ensure the chart displays
|
||||
this.doughnutChartData = [...this.doughnutChartData];
|
||||
// Reset flag after fetching
|
||||
console.log('Missing required data for chart:', { table: this.table, xAxis: this.xAxis, yAxis: this.yAxis, connection: this.connection });
|
||||
this.noDataAvailable = true;
|
||||
this.doughnutChartLabels = [];
|
||||
this.doughnutChartData = [];
|
||||
// Reset flags after fetching
|
||||
this.isFetchingData = false;
|
||||
this.isLoading = false;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -402,6 +870,35 @@ export class DoughnutChartComponent implements OnInit, OnChanges, AfterViewCheck
|
||||
}
|
||||
}
|
||||
|
||||
// Add common filters to drilldown filter parameters
|
||||
const commonFilters = this.filterService.getFilterValues();
|
||||
if (Object.keys(commonFilters).length > 0) {
|
||||
// Merge common filters with drilldown filters
|
||||
const mergedFilterObj = {};
|
||||
|
||||
// Add drilldown filters first
|
||||
if (filterParams) {
|
||||
try {
|
||||
const drilldownFilterObj = JSON.parse(filterParams);
|
||||
Object.assign(mergedFilterObj, drilldownFilterObj);
|
||||
} catch (e) {
|
||||
console.warn('Failed to parse drilldown filter parameters:', e);
|
||||
}
|
||||
}
|
||||
|
||||
// Add common filters
|
||||
Object.keys(commonFilters).forEach(key => {
|
||||
const value = commonFilters[key];
|
||||
if (value !== undefined && value !== null && value !== '') {
|
||||
mergedFilterObj[key] = value;
|
||||
}
|
||||
});
|
||||
|
||||
if (Object.keys(mergedFilterObj).length > 0) {
|
||||
filterParams = JSON.stringify(mergedFilterObj);
|
||||
}
|
||||
}
|
||||
|
||||
// Log the URL that will be called
|
||||
const url = `chart/getdashjson/doughnut?tableName=${actualApiUrl}&xAxis=${drilldownConfig.xAxis}&yAxes=${drilldownConfig.yAxis}${this.connection ? `&sureId=${this.connection}` : ''}`;
|
||||
console.log('Drilldown data URL:', url);
|
||||
@@ -421,59 +918,66 @@ export class DoughnutChartComponent implements OnInit, OnChanges, AfterViewCheck
|
||||
|
||||
// Handle the actual data structure returned by the API
|
||||
if (data && data.chartLabels && data.chartData) {
|
||||
// For doughnut charts, we need to extract the data differently
|
||||
// The first dataset's data array contains the values for the doughnut chart
|
||||
// Backend has already filtered the data, just display it
|
||||
this.noDataAvailable = data.chartLabels.length === 0;
|
||||
this.doughnutChartLabels = data.chartLabels || [];
|
||||
if (data.chartData && data.chartData.length > 0) {
|
||||
this.doughnutChartData = data.chartData[0].data.map(value => {
|
||||
// Convert to number if it's not already
|
||||
const numValue = Number(value);
|
||||
return isNaN(numValue) ? 0 : numValue;
|
||||
});
|
||||
this.doughnutChartLabels = data.chartLabels;
|
||||
|
||||
// Handle different data structures
|
||||
let chartDataValues;
|
||||
if (Array.isArray(data.chartData)) {
|
||||
// If chartData is already an array of values
|
||||
if (data.chartData.length > 0 && typeof data.chartData[0] !== 'object') {
|
||||
chartDataValues = data.chartData;
|
||||
}
|
||||
// If chartData is an array with one object containing the data
|
||||
else if (data.chartData.length > 0 && data.chartData[0].data) {
|
||||
chartDataValues = data.chartData[0].data;
|
||||
}
|
||||
// Default case
|
||||
else {
|
||||
chartDataValues = data.chartData;
|
||||
}
|
||||
} else {
|
||||
this.doughnutChartData = [];
|
||||
chartDataValues = [data.chartData];
|
||||
}
|
||||
// Ensure labels and data arrays have the same length
|
||||
this.syncLabelAndDataArrays();
|
||||
// Validate and sanitize data
|
||||
this.validateChartData();
|
||||
// Trigger change detection
|
||||
this.doughnutChartData = [...this.doughnutChartData];
|
||||
|
||||
this.doughnutChartData = [
|
||||
{
|
||||
data: chartDataValues,
|
||||
backgroundColor: this.chartColors.slice(0, chartDataValues.length),
|
||||
hoverBackgroundColor: this.chartColors.slice(0, chartDataValues.length)
|
||||
}
|
||||
];
|
||||
console.log('Updated doughnut chart with drilldown data:', { labels: this.doughnutChartLabels, data: this.doughnutChartData });
|
||||
} else if (data && data.labels && data.data) {
|
||||
// Handle the original expected format as fallback
|
||||
// Set loading state to false
|
||||
this.isLoading = false;
|
||||
} else if (data && data.labels && data.datasets) {
|
||||
// Backend has already filtered the data, just display it
|
||||
this.noDataAvailable = data.labels.length === 0;
|
||||
this.doughnutChartLabels = data.labels || [];
|
||||
this.doughnutChartData = data.data.map(value => {
|
||||
// Convert to number if it's not already
|
||||
const numValue = Number(value);
|
||||
return isNaN(numValue) ? 0 : numValue;
|
||||
});
|
||||
// Ensure labels and data arrays have the same length
|
||||
this.syncLabelAndDataArrays();
|
||||
// Validate and sanitize data
|
||||
this.validateChartData();
|
||||
// Trigger change detection
|
||||
this.doughnutChartData = [...this.doughnutChartData];
|
||||
this.doughnutChartLabels = data.labels;
|
||||
this.doughnutChartData = data.datasets;
|
||||
console.log('Updated doughnut chart with drilldown legacy data format:', { labels: this.doughnutChartLabels, data: this.doughnutChartData });
|
||||
// Set loading state to false
|
||||
this.isLoading = false;
|
||||
} else {
|
||||
console.warn('Drilldown received data does not have expected structure', data);
|
||||
this.noDataAvailable = true;
|
||||
this.doughnutChartLabels = [];
|
||||
this.doughnutChartData = [];
|
||||
// Validate and sanitize data
|
||||
this.validateChartData();
|
||||
// Keep current data instead of empty arrays
|
||||
// Set loading state to false
|
||||
this.isLoading = false;
|
||||
}
|
||||
},
|
||||
(error) => {
|
||||
console.error('Error fetching drilldown data:', error);
|
||||
this.noDataAvailable = true;
|
||||
this.doughnutChartLabels = [];
|
||||
this.doughnutChartData = [];
|
||||
// Keep current data in case of error
|
||||
// Set loading state to false
|
||||
this.isLoading = false;
|
||||
}
|
||||
);
|
||||
|
||||
// Set loading state
|
||||
this.isLoading = true;
|
||||
}
|
||||
|
||||
// Reset to original data (go back to base level)
|
||||
@@ -490,7 +994,7 @@ export class DoughnutChartComponent implements OnInit, OnChanges, AfterViewCheck
|
||||
console.log('Restored original labels');
|
||||
}
|
||||
if (this.originalDoughnutChartData.length > 0) {
|
||||
this.doughnutChartData = [...this.originalDoughnutChartData];
|
||||
this.doughnutChartData = JSON.parse(JSON.stringify(this.originalDoughnutChartData));
|
||||
console.log('Restored original data');
|
||||
}
|
||||
|
||||
@@ -532,44 +1036,11 @@ export class DoughnutChartComponent implements OnInit, OnChanges, AfterViewCheck
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get color for legend item
|
||||
* @param index Index of the legend item
|
||||
*/
|
||||
public getLegendColor(index: number): string {
|
||||
// Get legend color for a specific index
|
||||
getLegendColor(index: number): string {
|
||||
return this.chartColors[index % this.chartColors.length];
|
||||
}
|
||||
|
||||
/**
|
||||
* Ensure labels and data arrays have the same length
|
||||
*/
|
||||
private syncLabelAndDataArrays(): void {
|
||||
// Handle empty arrays
|
||||
if (this.doughnutChartLabels.length === 0 && this.doughnutChartData.length === 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
const maxLength = Math.max(this.doughnutChartLabels.length, this.doughnutChartData.length);
|
||||
|
||||
// Pad the shorter array with default values
|
||||
while (this.doughnutChartLabels.length < maxLength) {
|
||||
this.doughnutChartLabels.push(`Label ${this.doughnutChartLabels.length + 1}`);
|
||||
}
|
||||
|
||||
while (this.doughnutChartData.length < maxLength) {
|
||||
this.doughnutChartData.push(0);
|
||||
}
|
||||
|
||||
// Truncate the longer array if needed
|
||||
if (this.doughnutChartLabels.length > maxLength) {
|
||||
this.doughnutChartLabels = this.doughnutChartLabels.slice(0, maxLength);
|
||||
}
|
||||
|
||||
if (this.doughnutChartData.length > maxLength) {
|
||||
this.doughnutChartData = this.doughnutChartData.slice(0, maxLength);
|
||||
}
|
||||
}
|
||||
|
||||
// events
|
||||
public chartClicked(e: any): void {
|
||||
console.log('Doughnut chart clicked:', e);
|
||||
@@ -656,6 +1127,6 @@ export class DoughnutChartComponent implements OnInit, OnChanges, AfterViewCheck
|
||||
}
|
||||
|
||||
public chartHovered(e: any): void {
|
||||
console.log(e);
|
||||
console.log('Doughnut chart hovered:', e);
|
||||
}
|
||||
}
|
||||
@@ -1,4 +1,285 @@
|
||||
<div class="dynamic-chart-container">
|
||||
<!-- Filter Controls Section -->
|
||||
<div class="filter-section" *ngIf="hasActiveFilters()">
|
||||
<!-- Base Filters -->
|
||||
<div class="filter-group" *ngIf="baseFilters && baseFilters.length > 0">
|
||||
<h4>Base Filters</h4>
|
||||
<div class="filter-controls">
|
||||
<div *ngFor="let filter of baseFilters" class="filter-item">
|
||||
<div class="filter-label">{{ filter.field }} ({{ filter.type || 'text' }})</div>
|
||||
|
||||
<!-- Text Filter -->
|
||||
<div *ngIf="!filter.type || filter.type === 'text'" class="filter-input">
|
||||
<input type="text"
|
||||
[(ngModel)]="filter.value"
|
||||
(ngModelChange)="onBaseFilterChange(filter)"
|
||||
[placeholder]="filter.field"
|
||||
class="clr-input filter-text-input">
|
||||
</div>
|
||||
|
||||
<!-- Dropdown Filter -->
|
||||
<div *ngIf="filter.type === 'dropdown'" class="filter-input">
|
||||
<select [(ngModel)]="filter.value"
|
||||
(ngModelChange)="onBaseFilterChange(filter)"
|
||||
class="clr-select filter-select">
|
||||
<option value="">Select {{ filter.field }}</option>
|
||||
<option *ngFor="let option of getFilterOptions(filter)" [value]="option">{{ option }}</option>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<!-- Multi-Select Filter - Updated to show key first, then dropdown on click -->
|
||||
<div *ngIf="filter.type === 'multiselect'" class="filter-input multiselect-container">
|
||||
<div class="multiselect-display" (click)="toggleMultiselect(filter, 'base')">
|
||||
<span class="multiselect-label">{{ filter.field }}</span>
|
||||
<span class="multiselect-value" *ngIf="getSelectedOptionsCount(filter) > 0">
|
||||
({{ getSelectedOptionsCount(filter) }} selected)
|
||||
</span>
|
||||
<clr-icon shape="caret down" class="dropdown-icon"></clr-icon>
|
||||
</div>
|
||||
<div class="multiselect-dropdown" *ngIf="isMultiselectOpen(filter, 'base')">
|
||||
<div class="checkbox-group">
|
||||
<div *ngFor="let option of getFilterOptions(filter); let i = index" class="checkbox-item">
|
||||
<input type="checkbox"
|
||||
[checked]="isOptionSelected(filter, option)"
|
||||
(change)="onMultiSelectChange(filter, option, $event)"
|
||||
[id]="'base-' + filter.field + '-' + i"
|
||||
class="clr-checkbox">
|
||||
<label [for]="'base-' + filter.field + '-' + i" class="checkbox-label">{{ option }}</label>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Date Range Filter -->
|
||||
<div *ngIf="filter.type === 'date-range'" class="filter-input date-range">
|
||||
<div class="date-input-group">
|
||||
<input type="date"
|
||||
[(ngModel)]="filter.value.start"
|
||||
(ngModelChange)="onDateRangeChange(filter, { start: $event, end: filter.value.end })"
|
||||
placeholder="Start Date"
|
||||
class="clr-input filter-date">
|
||||
<span class="date-separator">to</span>
|
||||
<input type="date"
|
||||
[(ngModel)]="filter.value.end"
|
||||
(ngModelChange)="onDateRangeChange(filter, { start: filter.value.start, end: $event })"
|
||||
placeholder="End Date"
|
||||
class="clr-input filter-date">
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Toggle Filter -->
|
||||
<div *ngIf="filter.type === 'toggle'" class="filter-input toggle">
|
||||
<input type="checkbox"
|
||||
[(ngModel)]="filter.value"
|
||||
(ngModelChange)="onToggleChange(filter, $event)"
|
||||
clrToggle
|
||||
class="clr-toggle">
|
||||
<label class="toggle-label">{{ filter.field }}</label>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Drilldown Filters -->
|
||||
<div class="filter-group" *ngIf="drilldownFilters && drilldownFilters.length > 0 && currentDrilldownLevel > 0">
|
||||
<h4>Drilldown Filters</h4>
|
||||
<div class="filter-controls">
|
||||
<div *ngFor="let filter of drilldownFilters" class="filter-item">
|
||||
<div class="filter-label">{{ filter.field }} ({{ filter.type || 'text' }})</div>
|
||||
|
||||
<!-- Text Filter -->
|
||||
<div *ngIf="!filter.type || filter.type === 'text'" class="filter-input">
|
||||
<input type="text"
|
||||
[(ngModel)]="filter.value"
|
||||
(ngModelChange)="onDrilldownFilterChange(filter)"
|
||||
[placeholder]="filter.field"
|
||||
class="clr-input filter-text-input">
|
||||
</div>
|
||||
|
||||
<!-- Dropdown Filter -->
|
||||
<div *ngIf="filter.type === 'dropdown'" class="filter-input">
|
||||
<select [(ngModel)]="filter.value"
|
||||
(ngModelChange)="onDrilldownFilterChange(filter)"
|
||||
class="clr-select filter-select">
|
||||
<option value="">Select {{ filter.field }}</option>
|
||||
<option *ngFor="let option of getFilterOptions(filter)" [value]="option">{{ option }}</option>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<!-- Multi-Select Filter - Updated to show key first, then dropdown on click -->
|
||||
<div *ngIf="filter.type === 'multiselect'" class="filter-input multiselect-container">
|
||||
<div class="multiselect-display" (click)="toggleMultiselect(filter, 'drilldown')">
|
||||
<span class="multiselect-label">{{ filter.field }}</span>
|
||||
<span class="multiselect-value" *ngIf="getSelectedOptionsCount(filter) > 0">
|
||||
({{ getSelectedOptionsCount(filter) }} selected)
|
||||
</span>
|
||||
<clr-icon shape="caret down" class="dropdown-icon"></clr-icon>
|
||||
</div>
|
||||
<div class="multiselect-dropdown" *ngIf="isMultiselectOpen(filter, 'drilldown')">
|
||||
<div class="checkbox-group">
|
||||
<div *ngFor="let option of getFilterOptions(filter); let i = index" class="checkbox-item">
|
||||
<input type="checkbox"
|
||||
[checked]="isOptionSelected(filter, option)"
|
||||
(change)="onMultiSelectChange(filter, option, $event)"
|
||||
[id]="'drilldown-' + filter.field + '-' + i"
|
||||
class="clr-checkbox">
|
||||
<label [for]="'drilldown-' + filter.field + '-' + i" class="checkbox-label">{{ option }}</label>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Date Range Filter -->
|
||||
<div *ngIf="filter.type === 'date-range'" class="filter-input date-range">
|
||||
<div class="date-input-group">
|
||||
<input type="date"
|
||||
[(ngModel)]="filter.value.start"
|
||||
(ngModelChange)="onDateRangeChange(filter, { start: $event, end: filter.value.end })"
|
||||
placeholder="Start Date"
|
||||
class="clr-input filter-date">
|
||||
<span class="date-separator">to</span>
|
||||
<input type="date"
|
||||
[(ngModel)]="filter.value.end"
|
||||
(ngModelChange)="onDateRangeChange(filter, { start: filter.value.start, end: $event })"
|
||||
placeholder="End Date"
|
||||
class="clr-input filter-date">
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Toggle Filter -->
|
||||
<div *ngIf="filter.type === 'toggle'" class="filter-input toggle">
|
||||
<input type="checkbox"
|
||||
[(ngModel)]="filter.value"
|
||||
(ngModelChange)="onToggleChange(filter, $event)"
|
||||
clrToggle
|
||||
class="clr-toggle">
|
||||
<label class="toggle-label">{{ filter.field }}</label>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Layer Filters -->
|
||||
<div class="filter-group" *ngIf="hasActiveLayerFilters()">
|
||||
<h4>Layer Filters</h4>
|
||||
<div class="filter-controls">
|
||||
<div *ngFor="let filter of getActiveLayerFilters()" class="filter-item">
|
||||
<div class="filter-label">{{ filter.field }} ({{ filter.type || 'text' }})</div>
|
||||
|
||||
<!-- Text Filter -->
|
||||
<div *ngIf="!filter.type || filter.type === 'text'" class="filter-input">
|
||||
<input type="text"
|
||||
[(ngModel)]="filter.value"
|
||||
(ngModelChange)="onLayerFilterChange(filter)"
|
||||
[placeholder]="filter.field"
|
||||
class="clr-input filter-text-input">
|
||||
</div>
|
||||
|
||||
<!-- Dropdown Filter -->
|
||||
<div *ngIf="filter.type === 'dropdown'" class="filter-input">
|
||||
<select [(ngModel)]="filter.value"
|
||||
(ngModelChange)="onLayerFilterChange(filter)"
|
||||
class="clr-select filter-select">
|
||||
<option value="">Select {{ filter.field }}</option>
|
||||
<option *ngFor="let option of getFilterOptions(filter)" [value]="option">{{ option }}</option>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<!-- Multi-Select Filter - Updated to show key first, then dropdown on click -->
|
||||
<div *ngIf="filter.type === 'multiselect'" class="filter-input multiselect-container">
|
||||
<div class="multiselect-display" (click)="toggleMultiselect(filter, 'layer')">
|
||||
<span class="multiselect-label">{{ filter.field }}</span>
|
||||
<span class="multiselect-value" *ngIf="getSelectedOptionsCount(filter) > 0">
|
||||
({{ getSelectedOptionsCount(filter) }} selected)
|
||||
</span>
|
||||
<clr-icon shape="caret down" class="dropdown-icon"></clr-icon>
|
||||
</div>
|
||||
<div class="multiselect-dropdown" *ngIf="isMultiselectOpen(filter, 'layer')">
|
||||
<div class="checkbox-group">
|
||||
<div *ngFor="let option of getFilterOptions(filter); let i = index" class="checkbox-item">
|
||||
<input type="checkbox"
|
||||
[checked]="isOptionSelected(filter, option)"
|
||||
(change)="onMultiSelectChange(filter, option, $event)"
|
||||
[id]="'layer-' + filter.field + '-' + i"
|
||||
class="clr-checkbox">
|
||||
<label [for]="'layer-' + filter.field + '-' + i" class="checkbox-label">{{ option }}</label>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Date Range Filter -->
|
||||
<div *ngIf="filter.type === 'date-range'" class="filter-input date-range">
|
||||
<div class="date-input-group">
|
||||
<input type="date"
|
||||
[(ngModel)]="filter.value.start"
|
||||
(ngModelChange)="onDateRangeChange(filter, { start: $event, end: filter.value.end })"
|
||||
placeholder="Start Date"
|
||||
class="clr-input filter-date">
|
||||
<span class="date-separator">to</span>
|
||||
<input type="date"
|
||||
[(ngModel)]="filter.value.end"
|
||||
(ngModelChange)="onDateRangeChange(filter, { start: filter.value.start, end: $event })"
|
||||
placeholder="End Date"
|
||||
class="clr-input filter-date">
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Toggle Filter -->
|
||||
<div *ngIf="filter.type === 'toggle'" class="filter-input toggle">
|
||||
<input type="checkbox"
|
||||
[(ngModel)]="filter.value"
|
||||
(ngModelChange)="onToggleChange(filter, $event)"
|
||||
clrToggle
|
||||
class="clr-toggle">
|
||||
<label class="toggle-label">{{ filter.field }}</label>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Clear Filters Button -->
|
||||
<div class="filter-actions">
|
||||
<button class="btn btn-sm btn-outline" (click)="clearAllFilters()">Clear All Filters</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Header row with chart title and drilldown navigation -->
|
||||
<div class="clr-row header-row">
|
||||
<div class="clr-col-6">
|
||||
<h3 class="chart-title">{{charttitle || 'Dynamic Chart'}}</h3>
|
||||
</div>
|
||||
<div class="clr-col-6" style="text-align: right;">
|
||||
<!-- Add drilldown navigation controls -->
|
||||
<button class="btn btn-sm btn-link" *ngIf="drilldownEnabled && drilldownStack.length > 0" (click)="navigateBack()">
|
||||
<cds-icon shape="arrow" direction="left"></cds-icon>
|
||||
Back to {{drilldownStack.length > 1 ? 'Previous Level' : 'Main Data'}}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Show current drilldown level -->
|
||||
<div class="clr-row" *ngIf="drilldownEnabled && drilldownStack.length > 0">
|
||||
<div class="clr-col-12">
|
||||
<div class="alert alert-info" style="padding: 8px 12px; margin-bottom: 12px;">
|
||||
<div class="alert-items">
|
||||
<div class="alert-item static">
|
||||
<div class="alert-icon-wrapper">
|
||||
<cds-icon class="alert-icon" shape="info-circle"></cds-icon>
|
||||
</div>
|
||||
<span class="alert-text">
|
||||
Drilldown Level: {{currentDrilldownLevel}}
|
||||
<span *ngIf="drilldownStack.length > 0">
|
||||
(Clicked on: {{drilldownStack[drilldownStack.length - 1].clickedLabel}})
|
||||
</span>
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Existing content -->
|
||||
<!-- Drilldown mode indicator -->
|
||||
<div *ngIf="currentDrilldownLevel > 0" style="background-color: #e0e0e0; padding: 5px; margin-bottom: 10px; border-radius: 4px; text-align: center;">
|
||||
<span style="font-weight: bold; color: #333;">Drilldown Level: {{currentDrilldownLevel}}</span>
|
||||
|
||||
@@ -0,0 +1,192 @@
|
||||
.filter-section {
|
||||
margin-bottom: 20px;
|
||||
padding: 15px;
|
||||
border: 1px solid #ddd;
|
||||
border-radius: 4px;
|
||||
background-color: #f9f9f9;
|
||||
}
|
||||
|
||||
.filter-group {
|
||||
margin-bottom: 15px;
|
||||
|
||||
h4 {
|
||||
margin-top: 0;
|
||||
margin-bottom: 10px;
|
||||
color: #333;
|
||||
font-weight: 600;
|
||||
}
|
||||
}
|
||||
|
||||
.filter-controls {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 15px;
|
||||
}
|
||||
|
||||
.filter-item {
|
||||
flex: 1 1 300px;
|
||||
min-width: 250px;
|
||||
padding: 10px;
|
||||
background: white;
|
||||
border: 1px solid #e0e0e0;
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
.filter-label {
|
||||
font-weight: 500;
|
||||
margin-bottom: 8px;
|
||||
color: #555;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.filter-input {
|
||||
width: 100%;
|
||||
|
||||
.filter-text-input,
|
||||
.filter-select,
|
||||
.filter-date {
|
||||
width: 100%;
|
||||
padding: 6px 12px;
|
||||
border: 1px solid #ccc;
|
||||
border-radius: 4px;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.filter-select {
|
||||
height: 34px;
|
||||
}
|
||||
}
|
||||
|
||||
.multiselect-container {
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.multiselect-display {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
padding: 6px 12px;
|
||||
border: 1px solid #ccc;
|
||||
border-radius: 4px;
|
||||
background: white;
|
||||
cursor: pointer;
|
||||
min-height: 34px;
|
||||
|
||||
.multiselect-label {
|
||||
flex: 1;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.multiselect-value {
|
||||
color: #666;
|
||||
font-size: 12px;
|
||||
margin-right: 8px;
|
||||
}
|
||||
|
||||
.dropdown-icon {
|
||||
flex-shrink: 0;
|
||||
transition: transform 0.2s ease;
|
||||
}
|
||||
|
||||
&:hover {
|
||||
border-color: #999;
|
||||
}
|
||||
}
|
||||
|
||||
.multiselect-dropdown {
|
||||
position: absolute;
|
||||
top: 100%;
|
||||
left: 0;
|
||||
right: 0;
|
||||
z-index: 1000;
|
||||
background: white;
|
||||
border: 1px solid #ccc;
|
||||
border-top: none;
|
||||
border-radius: 0 0 4px 4px;
|
||||
box-shadow: 0 2px 4px rgba(0,0,0,0.1);
|
||||
max-height: 200px;
|
||||
overflow-y: auto;
|
||||
|
||||
.checkbox-group {
|
||||
padding: 8px;
|
||||
|
||||
.checkbox-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
padding: 4px 0;
|
||||
|
||||
.checkbox-label {
|
||||
margin: 0;
|
||||
font-size: 14px;
|
||||
cursor: pointer;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.date-range {
|
||||
.date-input-group {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.date-separator {
|
||||
margin: 0 5px;
|
||||
color: #777;
|
||||
}
|
||||
}
|
||||
|
||||
.toggle {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
|
||||
.toggle-label {
|
||||
margin: 0;
|
||||
font-size: 14px;
|
||||
cursor: pointer;
|
||||
}
|
||||
}
|
||||
|
||||
.filter-actions {
|
||||
margin-top: 15px;
|
||||
padding-top: 15px;
|
||||
border-top: 1px solid #eee;
|
||||
|
||||
.btn {
|
||||
font-size: 13px;
|
||||
}
|
||||
}
|
||||
|
||||
// New header row styling
|
||||
.header-row {
|
||||
margin-bottom: 15px;
|
||||
padding-bottom: 10px;
|
||||
border-bottom: 1px solid #eee;
|
||||
|
||||
.chart-title {
|
||||
margin: 0;
|
||||
font-size: 18px;
|
||||
font-weight: 600;
|
||||
color: #333;
|
||||
}
|
||||
}
|
||||
|
||||
// Responsive design
|
||||
@media (max-width: 768px) {
|
||||
.filter-controls {
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.filter-item {
|
||||
min-width: 100%;
|
||||
}
|
||||
|
||||
.header-row {
|
||||
.chart-title {
|
||||
font-size: 16px;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -2,6 +2,8 @@ import { Component, OnInit, ViewChild, Input, OnChanges, SimpleChanges } from '@
|
||||
import { ChartConfiguration, ChartData, ChartDataset } from 'chart.js';
|
||||
import { BaseChartDirective } from 'ng2-charts';
|
||||
import { Dashboard3Service } from 'src/app/services/builder/dashboard3.service';
|
||||
import { FilterService } from '../../common-filter/filter.service';
|
||||
import { Subscription } from 'rxjs';
|
||||
|
||||
@Component({
|
||||
selector: 'app-dynamic-chart',
|
||||
@@ -37,9 +39,20 @@ export class DynamicChartComponent implements OnInit, OnChanges {
|
||||
|
||||
@ViewChild(BaseChartDirective) chart: BaseChartDirective | undefined;
|
||||
|
||||
constructor(private dashboardService: Dashboard3Service) { }
|
||||
constructor(
|
||||
private dashboardService: Dashboard3Service,
|
||||
private filterService: FilterService
|
||||
) { }
|
||||
|
||||
ngOnInit(): void {
|
||||
// Subscribe to filter changes
|
||||
this.subscriptions.push(
|
||||
this.filterService.filterState$.subscribe(filters => {
|
||||
// When filters change, refresh the chart data
|
||||
this.fetchChartData();
|
||||
})
|
||||
);
|
||||
|
||||
// Initialize with default data
|
||||
this.fetchChartData();
|
||||
}
|
||||
@@ -47,6 +60,12 @@ export class DynamicChartComponent implements OnInit, OnChanges {
|
||||
ngOnChanges(changes: SimpleChanges): void {
|
||||
console.log('DynamicChartComponent input changes:', changes);
|
||||
|
||||
// Initialize filter values if they haven't been initialized yet
|
||||
if (!this.filtersInitialized && (changes.baseFilters || changes.drilldownFilters || changes.drilldownLayers)) {
|
||||
this.initializeFilterValues();
|
||||
this.filtersInitialized = true;
|
||||
}
|
||||
|
||||
// Check if any of the key properties have changed
|
||||
const xAxisChanged = changes.xAxis && !changes.xAxis.firstChange;
|
||||
const yAxisChanged = changes.yAxis && !changes.yAxis.firstChange;
|
||||
@@ -107,6 +126,14 @@ export class DynamicChartComponent implements OnInit, OnChanges {
|
||||
// Flag to prevent infinite loops
|
||||
private isFetchingData: boolean = false;
|
||||
|
||||
// Add properties for filter functionality
|
||||
private openMultiselects: Map<string, string> = new Map(); // Map of filterId -> context
|
||||
private documentClickHandler: ((event: MouseEvent) => void) | null = null;
|
||||
private filtersInitialized: boolean = false;
|
||||
|
||||
// Subscriptions to unsubscribe on destroy
|
||||
private subscriptions: Subscription[] = [];
|
||||
|
||||
fetchChartData(): void {
|
||||
// Set flag to prevent recursive calls
|
||||
this.isFetchingData = true;
|
||||
@@ -139,7 +166,49 @@ export class DynamicChartComponent implements OnInit, OnChanges {
|
||||
filterParams = JSON.stringify(filterObj);
|
||||
}
|
||||
}
|
||||
console.log('Base filter parameters:', filterParams);
|
||||
|
||||
// Add common filters to filter parameters
|
||||
const commonFilters = this.filterService.getFilterValues();
|
||||
console.log('Common filters from service:', commonFilters);
|
||||
|
||||
if (Object.keys(commonFilters).length > 0) {
|
||||
// Merge common filters with base filters
|
||||
const mergedFilterObj = {};
|
||||
|
||||
// Add base filters first
|
||||
if (filterParams) {
|
||||
try {
|
||||
const baseFilterObj = JSON.parse(filterParams);
|
||||
Object.assign(mergedFilterObj, baseFilterObj);
|
||||
} catch (e) {
|
||||
console.warn('Failed to parse base filter parameters:', e);
|
||||
}
|
||||
}
|
||||
|
||||
// Add common filters using the field name as the key, not the filter id
|
||||
Object.keys(commonFilters).forEach(filterId => {
|
||||
const filterValue = commonFilters[filterId];
|
||||
// Find the filter definition to get the field name
|
||||
const filterDef = this.filterService.getFilters().find(f => f.id === filterId);
|
||||
if (filterDef && filterDef.field) {
|
||||
const fieldName = filterDef.field;
|
||||
if (filterValue !== undefined && filterValue !== null && filterValue !== '') {
|
||||
mergedFilterObj[fieldName] = filterValue;
|
||||
}
|
||||
} else {
|
||||
// Fallback to using filterId as field name if no field is defined
|
||||
if (filterValue !== undefined && filterValue !== null && filterValue !== '') {
|
||||
mergedFilterObj[filterId] = filterValue;
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
if (Object.keys(mergedFilterObj).length > 0) {
|
||||
filterParams = JSON.stringify(mergedFilterObj);
|
||||
}
|
||||
}
|
||||
|
||||
console.log('Final filter parameters:', filterParams);
|
||||
|
||||
// Log the URL that will be called
|
||||
const url = `chart/getdashjson/dynamic?tableName=${this.table}&xAxis=${this.xAxis}&yAxes=${yAxisString}${this.connection ? `&sureId=${this.connection}` : ''}`;
|
||||
@@ -504,4 +573,322 @@ export class DynamicChartComponent implements OnInit, OnChanges {
|
||||
}
|
||||
this.dynamicChartData = _dynamicChartData;
|
||||
}
|
||||
|
||||
ngOnDestroy(): void {
|
||||
// Unsubscribe from all subscriptions to prevent memory leaks
|
||||
this.subscriptions.forEach(subscription => subscription.unsubscribe());
|
||||
|
||||
// Remove document click handler if it exists
|
||||
this.removeDocumentClickHandler();
|
||||
}
|
||||
|
||||
// Initialize filter values with proper default values based on type
|
||||
private initializeFilterValues(): void {
|
||||
console.log('Initializing filter values');
|
||||
|
||||
// Initialize base filters
|
||||
if (this.baseFilters) {
|
||||
this.baseFilters.forEach(filter => {
|
||||
if (filter.value === undefined || filter.value === null) {
|
||||
switch (filter.type) {
|
||||
case 'multiselect':
|
||||
filter.value = [];
|
||||
break;
|
||||
case 'date-range':
|
||||
filter.value = { start: null, end: null };
|
||||
break;
|
||||
case 'toggle':
|
||||
filter.value = false;
|
||||
break;
|
||||
default:
|
||||
filter.value = '';
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// Initialize drilldown filters
|
||||
if (this.drilldownFilters) {
|
||||
this.drilldownFilters.forEach(filter => {
|
||||
if (filter.value === undefined || filter.value === null) {
|
||||
switch (filter.type) {
|
||||
case 'multiselect':
|
||||
filter.value = [];
|
||||
break;
|
||||
case 'date-range':
|
||||
filter.value = { start: null, end: null };
|
||||
break;
|
||||
case 'toggle':
|
||||
filter.value = false;
|
||||
break;
|
||||
default:
|
||||
filter.value = '';
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// Initialize layer filters
|
||||
if (this.drilldownLayers) {
|
||||
this.drilldownLayers.forEach(layer => {
|
||||
if (layer.filters) {
|
||||
layer.filters.forEach((filter: any) => {
|
||||
if (filter.value === undefined || filter.value === null) {
|
||||
switch (filter.type) {
|
||||
case 'multiselect':
|
||||
filter.value = [];
|
||||
break;
|
||||
case 'date-range':
|
||||
filter.value = { start: null, end: null };
|
||||
break;
|
||||
case 'toggle':
|
||||
filter.value = false;
|
||||
break;
|
||||
default:
|
||||
filter.value = '';
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
console.log('Filter values initialized:', {
|
||||
baseFilters: this.baseFilters,
|
||||
drilldownFilters: this.drilldownFilters,
|
||||
drilldownLayers: this.drilldownLayers
|
||||
});
|
||||
}
|
||||
|
||||
// Check if there are active filters
|
||||
hasActiveFilters(): boolean {
|
||||
return (this.baseFilters && this.baseFilters.length > 0) ||
|
||||
(this.drilldownFilters && this.drilldownFilters.length > 0) ||
|
||||
this.hasActiveLayerFilters();
|
||||
}
|
||||
|
||||
// Check if there are active layer filters for current drilldown level
|
||||
hasActiveLayerFilters(): boolean {
|
||||
if (this.currentDrilldownLevel > 1 && this.drilldownLayers && this.drilldownLayers.length > 0) {
|
||||
const layerIndex = this.currentDrilldownLevel - 2; // -2 because level 1 is base drilldown
|
||||
return layerIndex < this.drilldownLayers.length &&
|
||||
this.drilldownLayers[layerIndex].filters &&
|
||||
this.drilldownLayers[layerIndex].filters.length > 0;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
// Get active layer filters for current drilldown level
|
||||
getActiveLayerFilters(): any[] {
|
||||
if (this.currentDrilldownLevel > 1 && this.drilldownLayers && this.drilldownLayers.length > 0) {
|
||||
const layerIndex = this.currentDrilldownLevel - 2; // -2 because level 1 is base drilldown
|
||||
if (layerIndex < this.drilldownLayers.length &&
|
||||
this.drilldownLayers[layerIndex].filters) {
|
||||
return this.drilldownLayers[layerIndex].filters;
|
||||
}
|
||||
}
|
||||
return [];
|
||||
}
|
||||
|
||||
// Get filter options for dropdown/multiselect filters
|
||||
getFilterOptions(filter: any): string[] {
|
||||
if (filter.options) {
|
||||
if (Array.isArray(filter.options)) {
|
||||
return filter.options;
|
||||
} else if (typeof filter.options === 'string') {
|
||||
return filter.options.split(',').map(opt => opt.trim()).filter(opt => opt);
|
||||
}
|
||||
}
|
||||
return [];
|
||||
}
|
||||
|
||||
// Check if an option is selected for multiselect filters
|
||||
isOptionSelected(filter: any, option: string): boolean {
|
||||
if (!filter.value) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (Array.isArray(filter.value)) {
|
||||
return filter.value.includes(option);
|
||||
}
|
||||
|
||||
return filter.value === option;
|
||||
}
|
||||
|
||||
// Handle base filter changes
|
||||
onBaseFilterChange(filter: any): void {
|
||||
console.log('Base filter changed:', filter);
|
||||
// Refresh data when filter changes
|
||||
this.fetchChartData();
|
||||
}
|
||||
|
||||
// Handle drilldown filter changes
|
||||
onDrilldownFilterChange(filter: any): void {
|
||||
console.log('Drilldown filter changed:', filter);
|
||||
// Refresh data when filter changes
|
||||
this.fetchChartData();
|
||||
}
|
||||
|
||||
// Handle layer filter changes
|
||||
onLayerFilterChange(filter: any): void {
|
||||
console.log('Layer filter changed:', filter);
|
||||
// Refresh data when filter changes
|
||||
this.fetchChartData();
|
||||
}
|
||||
|
||||
// Handle multiselect changes
|
||||
onMultiSelectChange(filter: any, option: string, event: any): void {
|
||||
const checked = event.target.checked;
|
||||
|
||||
// Initialize filter.value as array if it's not already
|
||||
if (!Array.isArray(filter.value)) {
|
||||
filter.value = [];
|
||||
}
|
||||
|
||||
if (checked) {
|
||||
// Add option to array if not already present
|
||||
if (!filter.value.includes(option)) {
|
||||
filter.value.push(option);
|
||||
}
|
||||
} else {
|
||||
// Remove option from array
|
||||
filter.value = filter.value.filter((item: string) => item !== option);
|
||||
}
|
||||
|
||||
// Refresh data when filter changes
|
||||
this.fetchChartData();
|
||||
}
|
||||
|
||||
// Handle date range changes
|
||||
onDateRangeChange(filter: any, dateRange: { start: string | null, end: string | null }): void {
|
||||
filter.value = dateRange;
|
||||
// Refresh data when filter changes
|
||||
this.fetchChartData();
|
||||
}
|
||||
|
||||
// Handle toggle changes
|
||||
onToggleChange(filter: any, checked: boolean): void {
|
||||
filter.value = checked;
|
||||
// Refresh data when filter changes
|
||||
this.fetchChartData();
|
||||
}
|
||||
|
||||
// Toggle multiselect dropdown visibility
|
||||
toggleMultiselect(filter: any, context: string): void {
|
||||
const filterId = `${context}-${filter.field}`;
|
||||
if (this.isMultiselectOpen(filter, context)) {
|
||||
this.openMultiselects.delete(filterId);
|
||||
} else {
|
||||
// Close all other multiselects first
|
||||
this.openMultiselects.clear();
|
||||
this.openMultiselects.set(filterId, context);
|
||||
|
||||
// Add document click handler to close dropdown when clicking outside
|
||||
this.addDocumentClickHandler();
|
||||
}
|
||||
}
|
||||
|
||||
// Add document click handler to close dropdowns when clicking outside
|
||||
private addDocumentClickHandler(): void {
|
||||
if (!this.documentClickHandler) {
|
||||
this.documentClickHandler = (event: MouseEvent) => {
|
||||
const target = event.target as HTMLElement;
|
||||
// Check if click is outside any multiselect dropdown
|
||||
if (!target.closest('.multiselect-container')) {
|
||||
this.openMultiselects.clear();
|
||||
this.removeDocumentClickHandler();
|
||||
}
|
||||
};
|
||||
|
||||
// Use setTimeout to ensure the click event that opened the dropdown doesn't immediately close it
|
||||
setTimeout(() => {
|
||||
document.addEventListener('click', this.documentClickHandler!);
|
||||
}, 0);
|
||||
}
|
||||
}
|
||||
|
||||
// Remove document click handler
|
||||
private removeDocumentClickHandler(): void {
|
||||
if (this.documentClickHandler) {
|
||||
document.removeEventListener('click', this.documentClickHandler);
|
||||
this.documentClickHandler = null;
|
||||
}
|
||||
}
|
||||
|
||||
// Check if multiselect dropdown is open
|
||||
isMultiselectOpen(filter: any, context: string): boolean {
|
||||
const filterId = `${context}-${filter.field}`;
|
||||
return this.openMultiselects.has(filterId);
|
||||
}
|
||||
|
||||
// Get count of selected options for a multiselect filter
|
||||
getSelectedOptionsCount(filter: any): number {
|
||||
if (!filter.value) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
if (Array.isArray(filter.value)) {
|
||||
return filter.value.length;
|
||||
}
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
// Clear all filters
|
||||
clearAllFilters(): void {
|
||||
// Clear base filters
|
||||
if (this.baseFilters) {
|
||||
this.baseFilters.forEach(filter => {
|
||||
if (filter.type === 'multiselect') {
|
||||
filter.value = [];
|
||||
} else if (filter.type === 'date-range') {
|
||||
filter.value = { start: null, end: null };
|
||||
} else if (filter.type === 'toggle') {
|
||||
filter.value = false;
|
||||
} else {
|
||||
filter.value = '';
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// Clear drilldown filters
|
||||
if (this.drilldownFilters) {
|
||||
this.drilldownFilters.forEach(filter => {
|
||||
if (filter.type === 'multiselect') {
|
||||
filter.value = [];
|
||||
} else if (filter.type === 'date-range') {
|
||||
filter.value = { start: null, end: null };
|
||||
} else if (filter.type === 'toggle') {
|
||||
filter.value = false;
|
||||
} else {
|
||||
filter.value = '';
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// Clear layer filters
|
||||
if (this.drilldownLayers) {
|
||||
this.drilldownLayers.forEach(layer => {
|
||||
if (layer.filters) {
|
||||
layer.filters.forEach((filter: any) => {
|
||||
if (filter.type === 'multiselect') {
|
||||
filter.value = [];
|
||||
} else if (filter.type === 'date-range') {
|
||||
filter.value = { start: null, end: null };
|
||||
} else if (filter.type === 'toggle') {
|
||||
filter.value = false;
|
||||
} else {
|
||||
filter.value = '';
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// Close all multiselect dropdowns
|
||||
this.openMultiselects.clear();
|
||||
|
||||
// Refresh data
|
||||
this.fetchChartData();
|
||||
}
|
||||
}
|
||||
@@ -1,4 +1,285 @@
|
||||
<div class="financial-chart-container">
|
||||
<!-- Filter Controls Section -->
|
||||
<div class="filter-section" *ngIf="hasActiveFilters()">
|
||||
<!-- Base Filters -->
|
||||
<div class="filter-group" *ngIf="baseFilters && baseFilters.length > 0">
|
||||
<h4>Base Filters</h4>
|
||||
<div class="filter-controls">
|
||||
<div *ngFor="let filter of baseFilters" class="filter-item">
|
||||
<div class="filter-label">{{ filter.field }} ({{ filter.type || 'text' }})</div>
|
||||
|
||||
<!-- Text Filter -->
|
||||
<div *ngIf="!filter.type || filter.type === 'text'" class="filter-input">
|
||||
<input type="text"
|
||||
[(ngModel)]="filter.value"
|
||||
(ngModelChange)="onBaseFilterChange(filter)"
|
||||
[placeholder]="filter.field"
|
||||
class="clr-input filter-text-input">
|
||||
</div>
|
||||
|
||||
<!-- Dropdown Filter -->
|
||||
<div *ngIf="filter.type === 'dropdown'" class="filter-input">
|
||||
<select [(ngModel)]="filter.value"
|
||||
(ngModelChange)="onBaseFilterChange(filter)"
|
||||
class="clr-select filter-select">
|
||||
<option value="">Select {{ filter.field }}</option>
|
||||
<option *ngFor="let option of getFilterOptions(filter)" [value]="option">{{ option }}</option>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<!-- Multi-Select Filter - Updated to show key first, then dropdown on click -->
|
||||
<div *ngIf="filter.type === 'multiselect'" class="filter-input multiselect-container">
|
||||
<div class="multiselect-display" (click)="toggleMultiselect(filter, 'base')">
|
||||
<span class="multiselect-label">{{ filter.field }}</span>
|
||||
<span class="multiselect-value" *ngIf="getSelectedOptionsCount(filter) > 0">
|
||||
({{ getSelectedOptionsCount(filter) }} selected)
|
||||
</span>
|
||||
<clr-icon shape="caret down" class="dropdown-icon"></clr-icon>
|
||||
</div>
|
||||
<div class="multiselect-dropdown" *ngIf="isMultiselectOpen(filter, 'base')">
|
||||
<div class="checkbox-group">
|
||||
<div *ngFor="let option of getFilterOptions(filter); let i = index" class="checkbox-item">
|
||||
<input type="checkbox"
|
||||
[checked]="isOptionSelected(filter, option)"
|
||||
(change)="onMultiSelectChange(filter, option, $event)"
|
||||
[id]="'base-' + filter.field + '-' + i"
|
||||
class="clr-checkbox">
|
||||
<label [for]="'base-' + filter.field + '-' + i" class="checkbox-label">{{ option }}</label>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Date Range Filter -->
|
||||
<div *ngIf="filter.type === 'date-range'" class="filter-input date-range">
|
||||
<div class="date-input-group">
|
||||
<input type="date"
|
||||
[(ngModel)]="filter.value.start"
|
||||
(ngModelChange)="onDateRangeChange(filter, { start: $event, end: filter.value.end })"
|
||||
placeholder="Start Date"
|
||||
class="clr-input filter-date">
|
||||
<span class="date-separator">to</span>
|
||||
<input type="date"
|
||||
[(ngModel)]="filter.value.end"
|
||||
(ngModelChange)="onDateRangeChange(filter, { start: filter.value.start, end: $event })"
|
||||
placeholder="End Date"
|
||||
class="clr-input filter-date">
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Toggle Filter -->
|
||||
<div *ngIf="filter.type === 'toggle'" class="filter-input toggle">
|
||||
<input type="checkbox"
|
||||
[(ngModel)]="filter.value"
|
||||
(ngModelChange)="onToggleChange(filter, $event)"
|
||||
clrToggle
|
||||
class="clr-toggle">
|
||||
<label class="toggle-label">{{ filter.field }}</label>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Drilldown Filters -->
|
||||
<div class="filter-group" *ngIf="drilldownFilters && drilldownFilters.length > 0 && currentDrilldownLevel > 0">
|
||||
<h4>Drilldown Filters</h4>
|
||||
<div class="filter-controls">
|
||||
<div *ngFor="let filter of drilldownFilters" class="filter-item">
|
||||
<div class="filter-label">{{ filter.field }} ({{ filter.type || 'text' }})</div>
|
||||
|
||||
<!-- Text Filter -->
|
||||
<div *ngIf="!filter.type || filter.type === 'text'" class="filter-input">
|
||||
<input type="text"
|
||||
[(ngModel)]="filter.value"
|
||||
(ngModelChange)="onDrilldownFilterChange(filter)"
|
||||
[placeholder]="filter.field"
|
||||
class="clr-input filter-text-input">
|
||||
</div>
|
||||
|
||||
<!-- Dropdown Filter -->
|
||||
<div *ngIf="filter.type === 'dropdown'" class="filter-input">
|
||||
<select [(ngModel)]="filter.value"
|
||||
(ngModelChange)="onDrilldownFilterChange(filter)"
|
||||
class="clr-select filter-select">
|
||||
<option value="">Select {{ filter.field }}</option>
|
||||
<option *ngFor="let option of getFilterOptions(filter)" [value]="option">{{ option }}</option>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<!-- Multi-Select Filter - Updated to show key first, then dropdown on click -->
|
||||
<div *ngIf="filter.type === 'multiselect'" class="filter-input multiselect-container">
|
||||
<div class="multiselect-display" (click)="toggleMultiselect(filter, 'drilldown')">
|
||||
<span class="multiselect-label">{{ filter.field }}</span>
|
||||
<span class="multiselect-value" *ngIf="getSelectedOptionsCount(filter) > 0">
|
||||
({{ getSelectedOptionsCount(filter) }} selected)
|
||||
</span>
|
||||
<clr-icon shape="caret down" class="dropdown-icon"></clr-icon>
|
||||
</div>
|
||||
<div class="multiselect-dropdown" *ngIf="isMultiselectOpen(filter, 'drilldown')">
|
||||
<div class="checkbox-group">
|
||||
<div *ngFor="let option of getFilterOptions(filter); let i = index" class="checkbox-item">
|
||||
<input type="checkbox"
|
||||
[checked]="isOptionSelected(filter, option)"
|
||||
(change)="onMultiSelectChange(filter, option, $event)"
|
||||
[id]="'drilldown-' + filter.field + '-' + i"
|
||||
class="clr-checkbox">
|
||||
<label [for]="'drilldown-' + filter.field + '-' + i" class="checkbox-label">{{ option }}</label>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Date Range Filter -->
|
||||
<div *ngIf="filter.type === 'date-range'" class="filter-input date-range">
|
||||
<div class="date-input-group">
|
||||
<input type="date"
|
||||
[(ngModel)]="filter.value.start"
|
||||
(ngModelChange)="onDateRangeChange(filter, { start: $event, end: filter.value.end })"
|
||||
placeholder="Start Date"
|
||||
class="clr-input filter-date">
|
||||
<span class="date-separator">to</span>
|
||||
<input type="date"
|
||||
[(ngModel)]="filter.value.end"
|
||||
(ngModelChange)="onDateRangeChange(filter, { start: filter.value.start, end: $event })"
|
||||
placeholder="End Date"
|
||||
class="clr-input filter-date">
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Toggle Filter -->
|
||||
<div *ngIf="filter.type === 'toggle'" class="filter-input toggle">
|
||||
<input type="checkbox"
|
||||
[(ngModel)]="filter.value"
|
||||
(ngModelChange)="onToggleChange(filter, $event)"
|
||||
clrToggle
|
||||
class="clr-toggle">
|
||||
<label class="toggle-label">{{ filter.field }}</label>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Layer Filters -->
|
||||
<div class="filter-group" *ngIf="hasActiveLayerFilters()">
|
||||
<h4>Layer Filters</h4>
|
||||
<div class="filter-controls">
|
||||
<div *ngFor="let filter of getActiveLayerFilters()" class="filter-item">
|
||||
<div class="filter-label">{{ filter.field }} ({{ filter.type || 'text' }})</div>
|
||||
|
||||
<!-- Text Filter -->
|
||||
<div *ngIf="!filter.type || filter.type === 'text'" class="filter-input">
|
||||
<input type="text"
|
||||
[(ngModel)]="filter.value"
|
||||
(ngModelChange)="onLayerFilterChange(filter)"
|
||||
[placeholder]="filter.field"
|
||||
class="clr-input filter-text-input">
|
||||
</div>
|
||||
|
||||
<!-- Dropdown Filter -->
|
||||
<div *ngIf="filter.type === 'dropdown'" class="filter-input">
|
||||
<select [(ngModel)]="filter.value"
|
||||
(ngModelChange)="onLayerFilterChange(filter)"
|
||||
class="clr-select filter-select">
|
||||
<option value="">Select {{ filter.field }}</option>
|
||||
<option *ngFor="let option of getFilterOptions(filter)" [value]="option">{{ option }}</option>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<!-- Multi-Select Filter - Updated to show key first, then dropdown on click -->
|
||||
<div *ngIf="filter.type === 'multiselect'" class="filter-input multiselect-container">
|
||||
<div class="multiselect-display" (click)="toggleMultiselect(filter, 'layer')">
|
||||
<span class="multiselect-label">{{ filter.field }}</span>
|
||||
<span class="multiselect-value" *ngIf="getSelectedOptionsCount(filter) > 0">
|
||||
({{ getSelectedOptionsCount(filter) }} selected)
|
||||
</span>
|
||||
<clr-icon shape="caret down" class="dropdown-icon"></clr-icon>
|
||||
</div>
|
||||
<div class="multiselect-dropdown" *ngIf="isMultiselectOpen(filter, 'layer')">
|
||||
<div class="checkbox-group">
|
||||
<div *ngFor="let option of getFilterOptions(filter); let i = index" class="checkbox-item">
|
||||
<input type="checkbox"
|
||||
[checked]="isOptionSelected(filter, option)"
|
||||
(change)="onMultiSelectChange(filter, option, $event)"
|
||||
[id]="'layer-' + filter.field + '-' + i"
|
||||
class="clr-checkbox">
|
||||
<label [for]="'layer-' + filter.field + '-' + i" class="checkbox-label">{{ option }}</label>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Date Range Filter -->
|
||||
<div *ngIf="filter.type === 'date-range'" class="filter-input date-range">
|
||||
<div class="date-input-group">
|
||||
<input type="date"
|
||||
[(ngModel)]="filter.value.start"
|
||||
(ngModelChange)="onDateRangeChange(filter, { start: $event, end: filter.value.end })"
|
||||
placeholder="Start Date"
|
||||
class="clr-input filter-date">
|
||||
<span class="date-separator">to</span>
|
||||
<input type="date"
|
||||
[(ngModel)]="filter.value.end"
|
||||
(ngModelChange)="onDateRangeChange(filter, { start: filter.value.start, end: $event })"
|
||||
placeholder="End Date"
|
||||
class="clr-input filter-date">
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Toggle Filter -->
|
||||
<div *ngIf="filter.type === 'toggle'" class="filter-input toggle">
|
||||
<input type="checkbox"
|
||||
[(ngModel)]="filter.value"
|
||||
(ngModelChange)="onToggleChange(filter, $event)"
|
||||
clrToggle
|
||||
class="clr-toggle">
|
||||
<label class="toggle-label">{{ filter.field }}</label>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Clear Filters Button -->
|
||||
<div class="filter-actions">
|
||||
<button class="btn btn-sm btn-outline" (click)="clearAllFilters()">Clear All Filters</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Header row with chart title and drilldown navigation -->
|
||||
<div class="clr-row header-row">
|
||||
<div class="clr-col-6">
|
||||
<h3 class="chart-title">{{charttitle || 'Financial Chart'}}</h3>
|
||||
</div>
|
||||
<div class="clr-col-6" style="text-align: right;">
|
||||
<!-- Add drilldown navigation controls -->
|
||||
<button class="btn btn-sm btn-link" *ngIf="drilldownEnabled && drilldownStack.length > 0" (click)="navigateBack()">
|
||||
<cds-icon shape="arrow" direction="left"></cds-icon>
|
||||
Back to {{drilldownStack.length > 1 ? 'Previous Level' : 'Main Data'}}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Show current drilldown level -->
|
||||
<div class="clr-row" *ngIf="drilldownEnabled && drilldownStack.length > 0">
|
||||
<div class="clr-col-12">
|
||||
<div class="alert alert-info" style="padding: 8px 12px; margin-bottom: 12px;">
|
||||
<div class="alert-items">
|
||||
<div class="alert-item static">
|
||||
<div class="alert-icon-wrapper">
|
||||
<cds-icon class="alert-icon" shape="info-circle"></cds-icon>
|
||||
</div>
|
||||
<span class="alert-text">
|
||||
Drilldown Level: {{currentDrilldownLevel}}
|
||||
<span *ngIf="drilldownStack.length > 0">
|
||||
(Clicked on: {{drilldownStack[drilldownStack.length - 1].clickedLabel}})
|
||||
</span>
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Existing content -->
|
||||
<!-- Drilldown mode indicator -->
|
||||
<div *ngIf="currentDrilldownLevel > 0" style="background-color: #e0e0e0; padding: 5px; margin-bottom: 10px; border-radius: 4px; text-align: center;">
|
||||
<span style="font-weight: bold; color: #333;">Drilldown Level: {{currentDrilldownLevel}}</span>
|
||||
|
||||
@@ -1,108 +1,192 @@
|
||||
.financial-chart-container {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
height: 400px;
|
||||
min-height: 400px;
|
||||
padding: 20px;
|
||||
background: linear-gradient(135deg, #ffffff 0%, #f8f9fa 100%);
|
||||
border-radius: 12px;
|
||||
box-shadow: 0 4px 20px rgba(0, 0, 0, 0.15);
|
||||
font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
|
||||
transition: all 0.3s ease;
|
||||
border: 1px solid #eaeaea;
|
||||
}
|
||||
|
||||
.financial-chart-container:hover {
|
||||
box-shadow: 0 6px 25px rgba(0, 0, 0, 0.2);
|
||||
transform: translateY(-2px);
|
||||
}
|
||||
|
||||
.chart-title {
|
||||
font-size: 26px;
|
||||
font-weight: 700;
|
||||
color: #2c3e50;
|
||||
.filter-section {
|
||||
margin-bottom: 20px;
|
||||
text-align: center;
|
||||
padding-bottom: 15px;
|
||||
border-bottom: 2px solid #3498db;
|
||||
text-shadow: 0 1px 2px rgba(0, 0, 0, 0.1);
|
||||
padding: 15px;
|
||||
border: 1px solid #ddd;
|
||||
border-radius: 4px;
|
||||
background-color: #f9f9f9;
|
||||
}
|
||||
|
||||
.chart-wrapper {
|
||||
position: relative;
|
||||
flex: 1;
|
||||
min-height: 250px;
|
||||
margin: 15px 0;
|
||||
background: #f8f9fa;
|
||||
border: 1px solid #e9ecef;
|
||||
border-radius: 8px;
|
||||
padding: 10px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
.filter-group {
|
||||
margin-bottom: 15px;
|
||||
|
||||
.chart-wrapper canvas {
|
||||
max-width: 100%;
|
||||
max-height: 100%;
|
||||
filter: drop-shadow(0 2px 4px rgba(0,0,0,0.1));
|
||||
}
|
||||
|
||||
.chart-wrapper canvas:hover {
|
||||
filter: drop-shadow(0 4px 8px rgba(0,0,0,0.15));
|
||||
transform: scale(1.02);
|
||||
transition: all 0.3s ease;
|
||||
}
|
||||
|
||||
.loading-indicator, .no-data-message {
|
||||
text-align: center;
|
||||
padding: 30px;
|
||||
color: #666;
|
||||
font-size: 18px;
|
||||
font-style: italic;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.loading-indicator p, .no-data-message p {
|
||||
margin: 10px 0 0 0;
|
||||
}
|
||||
|
||||
.spinner {
|
||||
border: 4px solid #f3f3f3;
|
||||
border-top: 4px solid #3498db;
|
||||
border-radius: 50%;
|
||||
width: 40px;
|
||||
height: 40px;
|
||||
animation: spin 1s linear infinite;
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
|
||||
@keyframes spin {
|
||||
0% { transform: rotate(0deg); }
|
||||
100% { transform: rotate(360deg); }
|
||||
}
|
||||
|
||||
/* Responsive design */
|
||||
@media (max-width: 768px) {
|
||||
.financial-chart-container {
|
||||
padding: 15px;
|
||||
h4 {
|
||||
margin-top: 0;
|
||||
margin-bottom: 10px;
|
||||
color: #333;
|
||||
font-weight: 600;
|
||||
}
|
||||
}
|
||||
|
||||
.filter-controls {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 15px;
|
||||
}
|
||||
|
||||
.filter-item {
|
||||
flex: 1 1 300px;
|
||||
min-width: 250px;
|
||||
padding: 10px;
|
||||
background: white;
|
||||
border: 1px solid #e0e0e0;
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
.filter-label {
|
||||
font-weight: 500;
|
||||
margin-bottom: 8px;
|
||||
color: #555;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.filter-input {
|
||||
width: 100%;
|
||||
|
||||
.filter-text-input,
|
||||
.filter-select,
|
||||
.filter-date {
|
||||
width: 100%;
|
||||
padding: 6px 12px;
|
||||
border: 1px solid #ccc;
|
||||
border-radius: 4px;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.filter-select {
|
||||
height: 34px;
|
||||
}
|
||||
}
|
||||
|
||||
.multiselect-container {
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.multiselect-display {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
padding: 6px 12px;
|
||||
border: 1px solid #ccc;
|
||||
border-radius: 4px;
|
||||
background: white;
|
||||
cursor: pointer;
|
||||
min-height: 34px;
|
||||
|
||||
.multiselect-label {
|
||||
flex: 1;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.multiselect-value {
|
||||
color: #666;
|
||||
font-size: 12px;
|
||||
margin-right: 8px;
|
||||
}
|
||||
|
||||
.dropdown-icon {
|
||||
flex-shrink: 0;
|
||||
transition: transform 0.2s ease;
|
||||
}
|
||||
|
||||
&:hover {
|
||||
border-color: #999;
|
||||
}
|
||||
}
|
||||
|
||||
.multiselect-dropdown {
|
||||
position: absolute;
|
||||
top: 100%;
|
||||
left: 0;
|
||||
right: 0;
|
||||
z-index: 1000;
|
||||
background: white;
|
||||
border: 1px solid #ccc;
|
||||
border-top: none;
|
||||
border-radius: 0 0 4px 4px;
|
||||
box-shadow: 0 2px 4px rgba(0,0,0,0.1);
|
||||
max-height: 200px;
|
||||
overflow-y: auto;
|
||||
|
||||
.checkbox-group {
|
||||
padding: 8px;
|
||||
|
||||
.checkbox-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
padding: 4px 0;
|
||||
|
||||
.checkbox-label {
|
||||
margin: 0;
|
||||
font-size: 14px;
|
||||
cursor: pointer;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.date-range {
|
||||
.date-input-group {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.date-separator {
|
||||
margin: 0 5px;
|
||||
color: #777;
|
||||
}
|
||||
}
|
||||
|
||||
.toggle {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
|
||||
.toggle-label {
|
||||
margin: 0;
|
||||
font-size: 14px;
|
||||
cursor: pointer;
|
||||
}
|
||||
}
|
||||
|
||||
.filter-actions {
|
||||
margin-top: 15px;
|
||||
padding-top: 15px;
|
||||
border-top: 1px solid #eee;
|
||||
|
||||
.btn {
|
||||
font-size: 13px;
|
||||
}
|
||||
}
|
||||
|
||||
// New header row styling
|
||||
.header-row {
|
||||
margin-bottom: 15px;
|
||||
padding-bottom: 10px;
|
||||
border-bottom: 1px solid #eee;
|
||||
|
||||
.chart-title {
|
||||
font-size: 20px;
|
||||
margin-bottom: 15px;
|
||||
}
|
||||
|
||||
.chart-wrapper {
|
||||
min-height: 200px;
|
||||
}
|
||||
|
||||
.no-data-message {
|
||||
font-size: 16px;
|
||||
padding: 20px;
|
||||
margin: 0;
|
||||
font-size: 18px;
|
||||
font-weight: 600;
|
||||
color: #333;
|
||||
}
|
||||
}
|
||||
|
||||
// Responsive design
|
||||
@media (max-width: 768px) {
|
||||
.filter-controls {
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.filter-item {
|
||||
min-width: 100%;
|
||||
}
|
||||
|
||||
.header-row {
|
||||
.chart-title {
|
||||
font-size: 16px;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,5 +1,8 @@
|
||||
import { Component, OnInit, Input, OnChanges, SimpleChanges } from '@angular/core';
|
||||
import { Dashboard3Service } from 'src/app/services/builder/dashboard3.service';
|
||||
import { FilterService } from '../../common-filter/filter.service';
|
||||
import { Subscription } from 'rxjs';
|
||||
import { AlertsService } from 'src/app/services/fnd/alerts.service';
|
||||
|
||||
@Component({
|
||||
selector: 'app-financial-chart',
|
||||
@@ -33,9 +36,21 @@ export class FinancialChartComponent implements OnInit, OnChanges {
|
||||
// Multi-layer drilldown configuration inputs
|
||||
@Input() drilldownLayers: any[] = []; // Array of drilldown layer configurations
|
||||
|
||||
constructor(private dashboardService: Dashboard3Service) { }
|
||||
constructor(
|
||||
private dashboardService: Dashboard3Service,
|
||||
private filterService: FilterService,
|
||||
private alertService: AlertsService
|
||||
) { }
|
||||
|
||||
ngOnInit(): void {
|
||||
// Subscribe to filter changes
|
||||
this.subscriptions.push(
|
||||
this.filterService.filterState$.subscribe(filters => {
|
||||
// When filters change, refresh the chart data
|
||||
this.fetchChartData();
|
||||
})
|
||||
);
|
||||
|
||||
// Initialize with default data
|
||||
this.fetchChartData();
|
||||
}
|
||||
@@ -43,6 +58,14 @@ export class FinancialChartComponent implements OnInit, OnChanges {
|
||||
ngOnChanges(changes: SimpleChanges): void {
|
||||
console.log('FinancialChartComponent input changes:', changes);
|
||||
|
||||
// Initialize filter values if they haven't been initialized yet
|
||||
if (!this.filtersInitialized && (changes.baseFilters || changes.drilldownFilters || changes.drilldownLayers)) {
|
||||
this.initializeFilterValues();
|
||||
// Load filter options for dropdown/multiselect filters
|
||||
this.loadFilterOptions();
|
||||
this.filtersInitialized = true;
|
||||
}
|
||||
|
||||
// Check if any of the key properties have changed
|
||||
const xAxisChanged = changes.xAxis && !changes.xAxis.firstChange;
|
||||
const yAxisChanged = changes.yAxis && !changes.yAxis.firstChange;
|
||||
@@ -86,6 +109,14 @@ export class FinancialChartComponent implements OnInit, OnChanges {
|
||||
// Flag to prevent infinite loops
|
||||
private isFetchingData: boolean = false;
|
||||
|
||||
// Add properties for filter functionality
|
||||
private openMultiselects: Map<string, string> = new Map(); // Map of filterId -> context
|
||||
private documentClickHandler: ((event: MouseEvent) => void) | null = null;
|
||||
private filtersInitialized: boolean = false;
|
||||
|
||||
// Subscriptions to unsubscribe on destroy
|
||||
private subscriptions: Subscription[] = [];
|
||||
|
||||
fetchChartData(): void {
|
||||
// Set flag to prevent recursive calls
|
||||
this.isFetchingData = true;
|
||||
@@ -118,7 +149,49 @@ export class FinancialChartComponent implements OnInit, OnChanges {
|
||||
filterParams = JSON.stringify(filterObj);
|
||||
}
|
||||
}
|
||||
console.log('Base filter parameters:', filterParams);
|
||||
|
||||
// Add common filters to filter parameters
|
||||
const commonFilters = this.filterService.getFilterValues();
|
||||
console.log('Common filters from service:', commonFilters);
|
||||
|
||||
if (Object.keys(commonFilters).length > 0) {
|
||||
// Merge common filters with base filters
|
||||
const mergedFilterObj = {};
|
||||
|
||||
// Add base filters first
|
||||
if (filterParams) {
|
||||
try {
|
||||
const baseFilterObj = JSON.parse(filterParams);
|
||||
Object.assign(mergedFilterObj, baseFilterObj);
|
||||
} catch (e) {
|
||||
console.warn('Failed to parse base filter parameters:', e);
|
||||
}
|
||||
}
|
||||
|
||||
// Add common filters using the field name as the key, not the filter id
|
||||
Object.keys(commonFilters).forEach(filterId => {
|
||||
const filterValue = commonFilters[filterId];
|
||||
// Find the filter definition to get the field name
|
||||
const filterDef = this.filterService.getFilters().find(f => f.id === filterId);
|
||||
if (filterDef && filterDef.field) {
|
||||
const fieldName = filterDef.field;
|
||||
if (filterValue !== undefined && filterValue !== null && filterValue !== '') {
|
||||
mergedFilterObj[fieldName] = filterValue;
|
||||
}
|
||||
} else {
|
||||
// Fallback to using filterId as field name if no field is defined
|
||||
if (filterValue !== undefined && filterValue !== null && filterValue !== '') {
|
||||
mergedFilterObj[filterId] = filterValue;
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
if (Object.keys(mergedFilterObj).length > 0) {
|
||||
filterParams = JSON.stringify(mergedFilterObj);
|
||||
}
|
||||
}
|
||||
|
||||
console.log('Final filter parameters:', filterParams);
|
||||
|
||||
// Log the URL that will be called
|
||||
const url = `chart/getdashjson/financial?tableName=${this.table}&xAxis=${this.xAxis}&yAxes=${yAxisString}${this.connection ? `&sureId=${this.connection}` : ''}`;
|
||||
@@ -496,4 +569,520 @@ export class FinancialChartComponent implements OnInit, OnChanges {
|
||||
public chartHovered(e: any): void {
|
||||
console.log(e);
|
||||
}
|
||||
|
||||
// Initialize filter values with proper default values based on type
|
||||
private initializeFilterValues(): void {
|
||||
console.log('Initializing filter values');
|
||||
|
||||
// Initialize base filters
|
||||
if (this.baseFilters) {
|
||||
this.baseFilters.forEach(filter => {
|
||||
// Ensure filter has required properties
|
||||
if (!filter.type) filter.type = 'text';
|
||||
if (!filter.options) filter.options = '';
|
||||
if (!filter.availableValues) filter.availableValues = '';
|
||||
|
||||
if (filter.value === undefined || filter.value === null) {
|
||||
switch (filter.type) {
|
||||
case 'multiselect':
|
||||
filter.value = [];
|
||||
break;
|
||||
case 'date-range':
|
||||
filter.value = { start: null, end: null };
|
||||
break;
|
||||
case 'toggle':
|
||||
filter.value = false;
|
||||
break;
|
||||
default:
|
||||
filter.value = '';
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// Initialize drilldown filters
|
||||
if (this.drilldownFilters) {
|
||||
this.drilldownFilters.forEach(filter => {
|
||||
// Ensure filter has required properties
|
||||
if (!filter.type) filter.type = 'text';
|
||||
if (!filter.options) filter.options = '';
|
||||
if (!filter.availableValues) filter.availableValues = '';
|
||||
|
||||
if (filter.value === undefined || filter.value === null) {
|
||||
switch (filter.type) {
|
||||
case 'multiselect':
|
||||
filter.value = [];
|
||||
break;
|
||||
case 'date-range':
|
||||
filter.value = { start: null, end: null };
|
||||
break;
|
||||
case 'toggle':
|
||||
filter.value = false;
|
||||
break;
|
||||
default:
|
||||
filter.value = '';
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// Initialize layer filters
|
||||
if (this.drilldownLayers) {
|
||||
this.drilldownLayers.forEach(layer => {
|
||||
if (layer.filters) {
|
||||
layer.filters.forEach((filter: any) => {
|
||||
// Ensure filter has required properties
|
||||
if (!filter.type) filter.type = 'text';
|
||||
if (!filter.options) filter.options = '';
|
||||
if (!filter.availableValues) filter.availableValues = '';
|
||||
|
||||
if (filter.value === undefined || filter.value === null) {
|
||||
switch (filter.type) {
|
||||
case 'multiselect':
|
||||
filter.value = [];
|
||||
break;
|
||||
case 'date-range':
|
||||
filter.value = { start: null, end: null };
|
||||
break;
|
||||
case 'toggle':
|
||||
filter.value = false;
|
||||
break;
|
||||
default:
|
||||
filter.value = '';
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
console.log('Filter values initialized:', {
|
||||
baseFilters: this.baseFilters,
|
||||
drilldownFilters: this.drilldownFilters,
|
||||
drilldownLayers: this.drilldownLayers
|
||||
});
|
||||
}
|
||||
|
||||
// Check if there are active filters
|
||||
hasActiveFilters(): boolean {
|
||||
return (this.baseFilters && this.baseFilters.length > 0) ||
|
||||
(this.drilldownFilters && this.drilldownFilters.length > 0) ||
|
||||
this.hasActiveLayerFilters();
|
||||
}
|
||||
|
||||
// Check if there are active layer filters for current drilldown level
|
||||
hasActiveLayerFilters(): boolean {
|
||||
if (this.currentDrilldownLevel > 1 && this.drilldownLayers && this.drilldownLayers.length > 0) {
|
||||
const layerIndex = this.currentDrilldownLevel - 2; // -2 because level 1 is base drilldown
|
||||
return layerIndex < this.drilldownLayers.length &&
|
||||
this.drilldownLayers[layerIndex].filters &&
|
||||
this.drilldownLayers[layerIndex].filters.length > 0;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
// Get active layer filters for current drilldown level
|
||||
getActiveLayerFilters(): any[] {
|
||||
if (this.currentDrilldownLevel > 1 && this.drilldownLayers && this.drilldownLayers.length > 0) {
|
||||
const layerIndex = this.currentDrilldownLevel - 2; // -2 because level 1 is base drilldown
|
||||
if (layerIndex < this.drilldownLayers.length &&
|
||||
this.drilldownLayers[layerIndex].filters) {
|
||||
return this.drilldownLayers[layerIndex].filters;
|
||||
}
|
||||
}
|
||||
return [];
|
||||
}
|
||||
|
||||
// Get filter options for dropdown/multiselect filters
|
||||
getFilterOptions(filter: any): string[] {
|
||||
if (filter.options) {
|
||||
if (Array.isArray(filter.options)) {
|
||||
return filter.options;
|
||||
} else if (typeof filter.options === 'string') {
|
||||
return filter.options.split(',').map(opt => opt.trim()).filter(opt => opt);
|
||||
}
|
||||
}
|
||||
return [];
|
||||
}
|
||||
|
||||
// Check if an option is selected for multiselect filters
|
||||
isOptionSelected(filter: any, option: string): boolean {
|
||||
if (!filter.value) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (Array.isArray(filter.value)) {
|
||||
return filter.value.includes(option);
|
||||
}
|
||||
|
||||
return filter.value === option;
|
||||
}
|
||||
|
||||
// Handle base filter changes
|
||||
onBaseFilterChange(filter: any): void {
|
||||
console.log('Base filter changed:', filter);
|
||||
// Refresh data when filter changes
|
||||
this.fetchChartData();
|
||||
}
|
||||
|
||||
// Handle drilldown filter changes
|
||||
onDrilldownFilterChange(filter: any): void {
|
||||
console.log('Drilldown filter changed:', filter);
|
||||
// Refresh data when filter changes
|
||||
this.fetchChartData();
|
||||
}
|
||||
|
||||
// Handle layer filter changes
|
||||
onLayerFilterChange(filter: any): void {
|
||||
console.log('Layer filter changed:', filter);
|
||||
// Refresh data when filter changes
|
||||
this.fetchChartData();
|
||||
}
|
||||
|
||||
// Handle multiselect changes
|
||||
onMultiSelectChange(filter: any, option: string, event: any): void {
|
||||
const checked = event.target.checked;
|
||||
|
||||
// Initialize filter.value as array if it's not already
|
||||
if (!Array.isArray(filter.value)) {
|
||||
filter.value = [];
|
||||
}
|
||||
|
||||
if (checked) {
|
||||
// Add option to array if not already present
|
||||
if (!filter.value.includes(option)) {
|
||||
filter.value.push(option);
|
||||
}
|
||||
} else {
|
||||
// Remove option from array
|
||||
filter.value = filter.value.filter((item: string) => item !== option);
|
||||
}
|
||||
|
||||
// Refresh data when filter changes
|
||||
this.fetchChartData();
|
||||
}
|
||||
|
||||
// Handle date range changes
|
||||
onDateRangeChange(filter: any, dateRange: { start: string | null, end: string | null }): void {
|
||||
filter.value = dateRange;
|
||||
// Refresh data when filter changes
|
||||
this.fetchChartData();
|
||||
}
|
||||
|
||||
// Handle toggle changes
|
||||
onToggleChange(filter: any, checked: boolean): void {
|
||||
filter.value = checked;
|
||||
// Refresh data when filter changes
|
||||
this.fetchChartData();
|
||||
}
|
||||
|
||||
// Toggle multiselect dropdown visibility
|
||||
toggleMultiselect(filter: any, context: string): void {
|
||||
const filterId = `${context}-${filter.field}`;
|
||||
if (this.isMultiselectOpen(filter, context)) {
|
||||
this.openMultiselects.delete(filterId);
|
||||
} else {
|
||||
// Close all other multiselects first
|
||||
this.openMultiselects.clear();
|
||||
this.openMultiselects.set(filterId, context);
|
||||
|
||||
// Add document click handler to close dropdown when clicking outside
|
||||
this.addDocumentClickHandler();
|
||||
}
|
||||
}
|
||||
|
||||
// Add document click handler to close dropdowns when clicking outside
|
||||
private addDocumentClickHandler(): void {
|
||||
if (!this.documentClickHandler) {
|
||||
this.documentClickHandler = (event: MouseEvent) => {
|
||||
const target = event.target as HTMLElement;
|
||||
// Check if click is outside any multiselect dropdown
|
||||
if (!target.closest('.multiselect-container')) {
|
||||
this.openMultiselects.clear();
|
||||
this.removeDocumentClickHandler();
|
||||
}
|
||||
};
|
||||
|
||||
// Use setTimeout to ensure the click event that opened the dropdown doesn't immediately close it
|
||||
setTimeout(() => {
|
||||
document.addEventListener('click', this.documentClickHandler!);
|
||||
}, 0);
|
||||
}
|
||||
}
|
||||
|
||||
// Remove document click handler
|
||||
private removeDocumentClickHandler(): void {
|
||||
if (this.documentClickHandler) {
|
||||
document.removeEventListener('click', this.documentClickHandler);
|
||||
this.documentClickHandler = null;
|
||||
}
|
||||
}
|
||||
|
||||
// Check if multiselect dropdown is open
|
||||
isMultiselectOpen(filter: any, context: string): boolean {
|
||||
const filterId = `${context}-${filter.field}`;
|
||||
return this.openMultiselects.has(filterId);
|
||||
}
|
||||
|
||||
// Get count of selected options for a multiselect filter
|
||||
getSelectedOptionsCount(filter: any): number {
|
||||
if (!filter.value) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
if (Array.isArray(filter.value)) {
|
||||
return filter.value.length;
|
||||
}
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
// Clear all filters
|
||||
clearAllFilters(): void {
|
||||
// Clear base filters
|
||||
if (this.baseFilters) {
|
||||
this.baseFilters.forEach(filter => {
|
||||
if (filter.type === 'multiselect') {
|
||||
filter.value = [];
|
||||
} else if (filter.type === 'date-range') {
|
||||
filter.value = { start: null, end: null };
|
||||
} else if (filter.type === 'toggle') {
|
||||
filter.value = false;
|
||||
} else {
|
||||
filter.value = '';
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// Clear drilldown filters
|
||||
if (this.drilldownFilters) {
|
||||
this.drilldownFilters.forEach(filter => {
|
||||
if (filter.type === 'multiselect') {
|
||||
filter.value = [];
|
||||
} else if (filter.type === 'date-range') {
|
||||
filter.value = { start: null, end: null };
|
||||
} else if (filter.type === 'toggle') {
|
||||
filter.value = false;
|
||||
} else {
|
||||
filter.value = '';
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// Clear layer filters
|
||||
if (this.drilldownLayers) {
|
||||
this.drilldownLayers.forEach(layer => {
|
||||
if (layer.filters) {
|
||||
layer.filters.forEach((filter: any) => {
|
||||
if (filter.type === 'multiselect') {
|
||||
filter.value = [];
|
||||
} else if (filter.type === 'date-range') {
|
||||
filter.value = { start: null, end: null };
|
||||
} else if (filter.type === 'toggle') {
|
||||
filter.value = false;
|
||||
} else {
|
||||
filter.value = '';
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// Close all multiselect dropdowns
|
||||
this.openMultiselects.clear();
|
||||
|
||||
// Refresh data
|
||||
this.fetchChartData();
|
||||
}
|
||||
|
||||
ngOnDestroy(): void {
|
||||
// Unsubscribe from all subscriptions to prevent memory leaks
|
||||
this.subscriptions.forEach(subscription => subscription.unsubscribe());
|
||||
|
||||
// Remove document click handler if it exists
|
||||
this.removeDocumentClickHandler();
|
||||
}
|
||||
|
||||
// Load filter options for dropdown and multiselect filters
|
||||
private loadFilterOptions(): void {
|
||||
console.log('Loading filter options');
|
||||
|
||||
// Load options for base filters
|
||||
if (this.baseFilters && this.table) {
|
||||
this.baseFilters.forEach((filter, index) => {
|
||||
if ((filter.type === 'dropdown' || filter.type === 'multiselect') && filter.field) {
|
||||
this.loadFilterValuesForField(this.table, this.connection, filter.field, index, 'base');
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// Load options for drilldown filters
|
||||
if (this.drilldownFilters && this.drilldownApiUrl) {
|
||||
this.drilldownFilters.forEach((filter, index) => {
|
||||
if ((filter.type === 'dropdown' || filter.type === 'multiselect') && filter.field) {
|
||||
this.loadFilterValuesForField(this.drilldownApiUrl, this.connection, filter.field, index, 'drilldown');
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// Load options for layer filters
|
||||
if (this.drilldownLayers) {
|
||||
this.drilldownLayers.forEach((layer, layerIndex) => {
|
||||
if (layer.filters && layer.apiUrl) {
|
||||
layer.filters.forEach((filter, filterIndex) => {
|
||||
if ((filter.type === 'dropdown' || filter.type === 'multiselect') && filter.field) {
|
||||
this.loadFilterValuesForField(layer.apiUrl, this.connection, filter.field, filterIndex, 'layer', layerIndex);
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// Load filter values for a specific field
|
||||
private loadFilterValuesForField(
|
||||
apiUrl: string,
|
||||
connectionId: number | undefined,
|
||||
field: string,
|
||||
filterIndex: number,
|
||||
filterType: 'base' | 'drilldown' | 'layer',
|
||||
layerIndex?: number
|
||||
): void {
|
||||
if (apiUrl && field) {
|
||||
this.alertService.getValuesFromUrl(apiUrl, connectionId, field).subscribe(
|
||||
(values: string[]) => {
|
||||
console.log(`Loaded filter values for ${filterType} filter ${field}:`, values);
|
||||
|
||||
// Update the filter with available values
|
||||
if (filterType === 'base') {
|
||||
const filter = this.baseFilters[filterIndex];
|
||||
if (filter) {
|
||||
filter.availableValues = values.join(', ');
|
||||
// For dropdown/multiselect types, also update the options
|
||||
if (filter.type === 'dropdown' || filter.type === 'multiselect') {
|
||||
filter.options = filter.availableValues;
|
||||
}
|
||||
}
|
||||
} else if (filterType === 'drilldown') {
|
||||
const filter = this.drilldownFilters[filterIndex];
|
||||
if (filter) {
|
||||
filter.availableValues = values.join(', ');
|
||||
// For dropdown/multiselect types, also update the options
|
||||
if (filter.type === 'dropdown' || filter.type === 'multiselect') {
|
||||
filter.options = filter.availableValues;
|
||||
}
|
||||
}
|
||||
} else if (filterType === 'layer' && layerIndex !== undefined) {
|
||||
const layer = this.drilldownLayers[layerIndex];
|
||||
if (layer && layer.filters) {
|
||||
const filter = layer.filters[filterIndex];
|
||||
if (filter) {
|
||||
filter.availableValues = values.join(', ');
|
||||
// For dropdown/multiselect types, also update the options
|
||||
if (filter.type === 'dropdown' || filter.type === 'multiselect') {
|
||||
filter.options = filter.availableValues;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
(error) => {
|
||||
console.error('Error loading available values for field:', field, error);
|
||||
}
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// Handle base filter field change
|
||||
onBaseFilterFieldChange(index: number, field: string): void {
|
||||
const filter = this.baseFilters[index];
|
||||
if (filter) {
|
||||
filter.field = field;
|
||||
// If field changes, reset value and options
|
||||
filter.value = '';
|
||||
filter.options = '';
|
||||
filter.availableValues = '';
|
||||
|
||||
// If we have a field and table URL, load available values
|
||||
if (field && this.table && (filter.type === 'dropdown' || filter.type === 'multiselect')) {
|
||||
this.loadFilterValuesForField(this.table, this.connection, field, index, 'base');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Handle base filter type change
|
||||
onBaseFilterTypeChange(index: number, type: string): void {
|
||||
const filter = this.baseFilters[index];
|
||||
if (filter) {
|
||||
filter.type = type;
|
||||
// If type changes to dropdown/multiselect and we have a field, load available values
|
||||
if ((type === 'dropdown' || type === 'multiselect') && filter.field && this.table) {
|
||||
this.loadFilterValuesForField(this.table, this.connection, filter.field, index, 'base');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Handle drilldown filter field change
|
||||
onDrilldownFilterFieldChange(index: number, field: string): void {
|
||||
const filter = this.drilldownFilters[index];
|
||||
if (filter) {
|
||||
filter.field = field;
|
||||
// If field changes, reset value and options
|
||||
filter.value = '';
|
||||
filter.options = '';
|
||||
filter.availableValues = '';
|
||||
|
||||
// If we have a field and drilldown API URL, load available values
|
||||
if (field && this.drilldownApiUrl && (filter.type === 'dropdown' || filter.type === 'multiselect')) {
|
||||
this.loadFilterValuesForField(this.drilldownApiUrl, this.connection, field, index, 'drilldown');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Handle drilldown filter type change
|
||||
onDrilldownFilterTypeChange(index: number, type: string): void {
|
||||
const filter = this.drilldownFilters[index];
|
||||
if (filter) {
|
||||
filter.type = type;
|
||||
// If type changes to dropdown/multiselect and we have a field, load available values
|
||||
if ((type === 'dropdown' || type === 'multiselect') && filter.field && this.drilldownApiUrl) {
|
||||
this.loadFilterValuesForField(this.drilldownApiUrl, this.connection, filter.field, index, 'drilldown');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Handle layer filter field change
|
||||
onLayerFilterFieldChange(layerIndex: number, filterIndex: number, field: string): void {
|
||||
const layer = this.drilldownLayers[layerIndex];
|
||||
if (layer && layer.filters) {
|
||||
const filter = layer.filters[filterIndex];
|
||||
if (filter) {
|
||||
filter.field = field;
|
||||
// If field changes, reset value and options
|
||||
filter.value = '';
|
||||
filter.options = '';
|
||||
filter.availableValues = '';
|
||||
|
||||
// If we have a field and layer API URL, load available values
|
||||
if (field && layer.apiUrl && (filter.type === 'dropdown' || filter.type === 'multiselect')) {
|
||||
this.loadFilterValuesForField(layer.apiUrl, this.connection, field, filterIndex, 'layer', layerIndex);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Handle layer filter type change
|
||||
onLayerFilterTypeChange(layerIndex: number, filterIndex: number, type: string): void {
|
||||
const layer = this.drilldownLayers[layerIndex];
|
||||
if (layer && layer.filters) {
|
||||
const filter = layer.filters[filterIndex];
|
||||
if (filter) {
|
||||
filter.type = type;
|
||||
// If type changes to dropdown/multiselect and we have a field, load available values
|
||||
if ((type === 'dropdown' || type === 'multiselect') && filter.field && layer.apiUrl) {
|
||||
this.loadFilterValuesForField(layer.apiUrl, this.connection, filter.field, filterIndex, 'layer', layerIndex);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,12 +1,289 @@
|
||||
<div style="display: block;">
|
||||
<div class="dg-wrapper">
|
||||
<div class="clr-row">
|
||||
<div class="clr-col-8">
|
||||
<h3>{{charttitle || 'Data Grid'}}</h3>
|
||||
<!-- Filter Controls Section -->
|
||||
<div class="filter-section" *ngIf="hasActiveFilters()">
|
||||
<!-- Base Filters -->
|
||||
<div class="filter-group" *ngIf="baseFilters && baseFilters.length > 0">
|
||||
<h4>Base Filters</h4>
|
||||
<div class="filter-controls">
|
||||
<div *ngFor="let filter of baseFilters" class="filter-item">
|
||||
<div class="filter-label">{{ filter.field }} ({{ filter.type || 'text' }})</div>
|
||||
|
||||
<!-- Text Filter -->
|
||||
<div *ngIf="!filter.type || filter.type === 'text'" class="filter-input">
|
||||
<input type="text"
|
||||
[(ngModel)]="filter.value"
|
||||
(ngModelChange)="onBaseFilterChange(filter)"
|
||||
[placeholder]="filter.field"
|
||||
class="clr-input filter-text-input">
|
||||
</div>
|
||||
|
||||
<!-- Dropdown Filter -->
|
||||
<div *ngIf="filter.type === 'dropdown'" class="filter-input">
|
||||
<select [(ngModel)]="filter.value"
|
||||
(ngModelChange)="onBaseFilterChange(filter)"
|
||||
class="clr-select filter-select">
|
||||
<option value="">Select {{ filter.field }}</option>
|
||||
<option *ngFor="let option of getFilterOptions(filter)" [value]="option">{{ option }}</option>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<!-- Multi-Select Filter - Updated to show key first, then dropdown on click -->
|
||||
<div *ngIf="filter.type === 'multiselect'" class="filter-input multiselect-container">
|
||||
<div class="multiselect-display" (click)="toggleMultiselect(filter, 'base')">
|
||||
<span class="multiselect-label">{{ filter.field }}</span>
|
||||
<span class="multiselect-value" *ngIf="getSelectedOptionsCount(filter) > 0">
|
||||
({{ getSelectedOptionsCount(filter) }} selected)
|
||||
</span>
|
||||
<clr-icon shape="caret down" class="dropdown-icon"></clr-icon>
|
||||
</div>
|
||||
<div class="multiselect-dropdown" *ngIf="isMultiselectOpen(filter, 'base')">
|
||||
<div class="checkbox-group">
|
||||
<div *ngFor="let option of getFilterOptions(filter); let i = index" class="checkbox-item">
|
||||
<input type="checkbox"
|
||||
[checked]="isOptionSelected(filter, option)"
|
||||
(change)="onMultiSelectChange(filter, option, $event)"
|
||||
[id]="'base-' + filter.field + '-' + i"
|
||||
class="clr-checkbox">
|
||||
<label [for]="'base-' + filter.field + '-' + i" class="checkbox-label">{{ option }}</label>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Date Range Filter -->
|
||||
<div *ngIf="filter.type === 'date-range'" class="filter-input date-range">
|
||||
<div class="date-input-group">
|
||||
<input type="date"
|
||||
[(ngModel)]="filter.value.start"
|
||||
(ngModelChange)="onDateRangeChange(filter, { start: $event, end: filter.value.end })"
|
||||
placeholder="Start Date"
|
||||
class="clr-input filter-date">
|
||||
<span class="date-separator">to</span>
|
||||
<input type="date"
|
||||
[(ngModel)]="filter.value.end"
|
||||
(ngModelChange)="onDateRangeChange(filter, { start: filter.value.start, end: $event })"
|
||||
placeholder="End Date"
|
||||
class="clr-input filter-date">
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Toggle Filter -->
|
||||
<div *ngIf="filter.type === 'toggle'" class="filter-input toggle">
|
||||
<input type="checkbox"
|
||||
[(ngModel)]="filter.value"
|
||||
(ngModelChange)="onToggleChange(filter, $event)"
|
||||
clrToggle
|
||||
class="clr-toggle">
|
||||
<label class="toggle-label">{{ filter.field }}</label>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Drilldown Filters -->
|
||||
<div class="filter-group" *ngIf="drilldownFilters && drilldownFilters.length > 0 && currentDrilldownLevel > 0">
|
||||
<h4>Drilldown Filters</h4>
|
||||
<div class="filter-controls">
|
||||
<div *ngFor="let filter of drilldownFilters" class="filter-item">
|
||||
<div class="filter-label">{{ filter.field }} ({{ filter.type || 'text' }})</div>
|
||||
|
||||
<!-- Text Filter -->
|
||||
<div *ngIf="!filter.type || filter.type === 'text'" class="filter-input">
|
||||
<input type="text"
|
||||
[(ngModel)]="filter.value"
|
||||
(ngModelChange)="onDrilldownFilterChange(filter)"
|
||||
[placeholder]="filter.field"
|
||||
class="clr-input filter-text-input">
|
||||
</div>
|
||||
|
||||
<!-- Dropdown Filter -->
|
||||
<div *ngIf="filter.type === 'dropdown'" class="filter-input">
|
||||
<select [(ngModel)]="filter.value"
|
||||
(ngModelChange)="onDrilldownFilterChange(filter)"
|
||||
class="clr-select filter-select">
|
||||
<option value="">Select {{ filter.field }}</option>
|
||||
<option *ngFor="let option of getFilterOptions(filter)" [value]="option">{{ option }}</option>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<!-- Multi-Select Filter - Updated to show key first, then dropdown on click -->
|
||||
<div *ngIf="filter.type === 'multiselect'" class="filter-input multiselect-container">
|
||||
<div class="multiselect-display" (click)="toggleMultiselect(filter, 'drilldown')">
|
||||
<span class="multiselect-label">{{ filter.field }}</span>
|
||||
<span class="multiselect-value" *ngIf="getSelectedOptionsCount(filter) > 0">
|
||||
({{ getSelectedOptionsCount(filter) }} selected)
|
||||
</span>
|
||||
<clr-icon shape="caret down" class="dropdown-icon"></clr-icon>
|
||||
</div>
|
||||
<div class="multiselect-dropdown" *ngIf="isMultiselectOpen(filter, 'drilldown')">
|
||||
<div class="checkbox-group">
|
||||
<div *ngFor="let option of getFilterOptions(filter); let i = index" class="checkbox-item">
|
||||
<input type="checkbox"
|
||||
[checked]="isOptionSelected(filter, option)"
|
||||
(change)="onMultiSelectChange(filter, option, $event)"
|
||||
[id]="'drilldown-' + filter.field + '-' + i"
|
||||
class="clr-checkbox">
|
||||
<label [for]="'drilldown-' + filter.field + '-' + i" class="checkbox-label">{{ option }}</label>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Date Range Filter -->
|
||||
<div *ngIf="filter.type === 'date-range'" class="filter-input date-range">
|
||||
<div class="date-input-group">
|
||||
<input type="date"
|
||||
[(ngModel)]="filter.value.start"
|
||||
(ngModelChange)="onDateRangeChange(filter, { start: $event, end: filter.value.end })"
|
||||
placeholder="Start Date"
|
||||
class="clr-input filter-date">
|
||||
<span class="date-separator">to</span>
|
||||
<input type="date"
|
||||
[(ngModel)]="filter.value.end"
|
||||
(ngModelChange)="onDateRangeChange(filter, { start: filter.value.start, end: $event })"
|
||||
placeholder="End Date"
|
||||
class="clr-input filter-date">
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Toggle Filter -->
|
||||
<div *ngIf="filter.type === 'toggle'" class="filter-input toggle">
|
||||
<input type="checkbox"
|
||||
[(ngModel)]="filter.value"
|
||||
(ngModelChange)="onToggleChange(filter, $event)"
|
||||
clrToggle
|
||||
class="clr-toggle">
|
||||
<label class="toggle-label">{{ filter.field }}</label>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Layer Filters -->
|
||||
<div class="filter-group" *ngIf="hasActiveLayerFilters()">
|
||||
<h4>Layer Filters</h4>
|
||||
<div class="filter-controls">
|
||||
<div *ngFor="let filter of getActiveLayerFilters()" class="filter-item">
|
||||
<div class="filter-label">{{ filter.field }} ({{ filter.type || 'text' }})</div>
|
||||
|
||||
<!-- Text Filter -->
|
||||
<div *ngIf="!filter.type || filter.type === 'text'" class="filter-input">
|
||||
<input type="text"
|
||||
[(ngModel)]="filter.value"
|
||||
(ngModelChange)="onLayerFilterChange(filter)"
|
||||
[placeholder]="filter.field"
|
||||
class="clr-input filter-text-input">
|
||||
</div>
|
||||
|
||||
<!-- Dropdown Filter -->
|
||||
<div *ngIf="filter.type === 'dropdown'" class="filter-input">
|
||||
<select [(ngModel)]="filter.value"
|
||||
(ngModelChange)="onLayerFilterChange(filter)"
|
||||
class="clr-select filter-select">
|
||||
<option value="">Select {{ filter.field }}</option>
|
||||
<option *ngFor="let option of getFilterOptions(filter)" [value]="option">{{ option }}</option>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<!-- Multi-Select Filter - Updated to show key first, then dropdown on click -->
|
||||
<div *ngIf="filter.type === 'multiselect'" class="filter-input multiselect-container">
|
||||
<div class="multiselect-display" (click)="toggleMultiselect(filter, 'layer')">
|
||||
<span class="multiselect-label">{{ filter.field }}</span>
|
||||
<span class="multiselect-value" *ngIf="getSelectedOptionsCount(filter) > 0">
|
||||
({{ getSelectedOptionsCount(filter) }} selected)
|
||||
</span>
|
||||
<clr-icon shape="caret down" class="dropdown-icon"></clr-icon>
|
||||
</div>
|
||||
<div class="multiselect-dropdown" *ngIf="isMultiselectOpen(filter, 'layer')">
|
||||
<div class="checkbox-group">
|
||||
<div *ngFor="let option of getFilterOptions(filter); let i = index" class="checkbox-item">
|
||||
<input type="checkbox"
|
||||
[checked]="isOptionSelected(filter, option)"
|
||||
(change)="onMultiSelectChange(filter, option, $event)"
|
||||
[id]="'layer-' + filter.field + '-' + i"
|
||||
class="clr-checkbox">
|
||||
<label [for]="'layer-' + filter.field + '-' + i" class="checkbox-label">{{ option }}</label>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Date Range Filter -->
|
||||
<div *ngIf="filter.type === 'date-range'" class="filter-input date-range">
|
||||
<div class="date-input-group">
|
||||
<input type="date"
|
||||
[(ngModel)]="filter.value.start"
|
||||
(ngModelChange)="onDateRangeChange(filter, { start: $event, end: filter.value.end })"
|
||||
placeholder="Start Date"
|
||||
class="clr-input filter-date">
|
||||
<span class="date-separator">to</span>
|
||||
<input type="date"
|
||||
[(ngModel)]="filter.value.end"
|
||||
(ngModelChange)="onDateRangeChange(filter, { start: filter.value.start, end: $event })"
|
||||
placeholder="End Date"
|
||||
class="clr-input filter-date">
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Toggle Filter -->
|
||||
<div *ngIf="filter.type === 'toggle'" class="filter-input toggle">
|
||||
<input type="checkbox"
|
||||
[(ngModel)]="filter.value"
|
||||
(ngModelChange)="onToggleChange(filter, $event)"
|
||||
clrToggle
|
||||
class="clr-toggle">
|
||||
<label class="toggle-label">{{ filter.field }}</label>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Clear Filters Button -->
|
||||
<div class="filter-actions">
|
||||
<button class="btn btn-sm btn-outline" (click)="clearAllFilters()">Clear All Filters</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="clr-row">
|
||||
<!-- <div class="clr-col-8">
|
||||
<h3>{{charttitle || 'Data Grid'}}</h3>
|
||||
</div> -->
|
||||
<!-- Add drilldown navigation controls -->
|
||||
<div class="clr-col-4" *ngIf="drilldownEnabled && drilldownStack.length > 0" style="text-align: right;">
|
||||
<button class="btn btn-sm btn-link" (click)="navigateBack()">
|
||||
<cds-icon shape="arrow" direction="left"></cds-icon>
|
||||
Back to {{drilldownStack.length > 1 ? 'Previous Level' : 'Main Data'}}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Show current drilldown level -->
|
||||
<div class="clr-row" *ngIf="drilldownEnabled && drilldownStack.length > 0">
|
||||
<div class="clr-col-12">
|
||||
<div class="alert alert-info" style="padding: 8px 12px; margin-bottom: 12px;">
|
||||
<div class="alert-items">
|
||||
<div class="alert-item static">
|
||||
<div class="alert-icon-wrapper">
|
||||
<cds-icon class="alert-icon" shape="info-circle"></cds-icon>
|
||||
</div>
|
||||
<span class="alert-text">
|
||||
Drilldown Level: {{currentDrilldownLevel}}
|
||||
<span *ngIf="drilldownStack.length > 0">
|
||||
(Clicked on: {{drilldownStack[drilldownStack.length - 1].clickedKey}} = {{drilldownStack[drilldownStack.length - 1].clickedValue}})
|
||||
</span>
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<clr-datagrid [clrDgLoading]="loading">
|
||||
<clr-dg-placeholder> <ng-template #loadingSpinner><clr-spinner>Loading ... </clr-spinner></ng-template>
|
||||
<clr-dg-placeholder>
|
||||
<ng-template #loadingSpinner>
|
||||
<clr-spinner>Loading ... </clr-spinner>
|
||||
</ng-template>
|
||||
<div *ngIf="error;else loadingSpinner">{{error}}</div>
|
||||
</clr-dg-placeholder>
|
||||
|
||||
@@ -19,7 +296,7 @@
|
||||
|
||||
<clr-dg-row *clrDgItems="let item of givendata" [clrDgItem]="item">
|
||||
<!-- Dynamic cells based on response keys -->
|
||||
<clr-dg-cell *ngFor="let header of dynamicHeaders">
|
||||
<clr-dg-cell *ngFor="let header of dynamicHeaders" (click)="onRowClick(item, header.key)">
|
||||
{{item[header.key]}}
|
||||
</clr-dg-cell>
|
||||
</clr-dg-row>
|
||||
|
||||
@@ -1,12 +1,180 @@
|
||||
@import '../../../../../../../styles1.scss';
|
||||
input.ng-invalid.ng-touched {
|
||||
border-color: red;
|
||||
.filter-section {
|
||||
margin-bottom: 20px;
|
||||
padding: 15px;
|
||||
border: 1px solid #ddd;
|
||||
border-radius: 4px;
|
||||
background-color: #f9f9f9;
|
||||
}
|
||||
|
||||
.error_mess {
|
||||
color: red;
|
||||
.filter-group {
|
||||
margin-bottom: 15px;
|
||||
|
||||
h4 {
|
||||
margin-top: 0;
|
||||
margin-bottom: 10px;
|
||||
color: #333;
|
||||
font-weight: 600;
|
||||
}
|
||||
}
|
||||
clr-datagrid{
|
||||
height: 400px; /* Adjust the height as needed */
|
||||
|
||||
.filter-controls {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 15px;
|
||||
}
|
||||
|
||||
.filter-item {
|
||||
flex: 1 1 300px;
|
||||
min-width: 250px;
|
||||
padding: 10px;
|
||||
background: white;
|
||||
border: 1px solid #e0e0e0;
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
.filter-label {
|
||||
font-weight: 500;
|
||||
margin-bottom: 8px;
|
||||
color: #555;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.filter-input {
|
||||
width: 100%;
|
||||
|
||||
.filter-text-input,
|
||||
.filter-select,
|
||||
.filter-date {
|
||||
width: 100%;
|
||||
padding: 6px 12px;
|
||||
border: 1px solid #ccc;
|
||||
border-radius: 4px;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.filter-select {
|
||||
height: 34px;
|
||||
}
|
||||
}
|
||||
|
||||
.multiselect-container {
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.multiselect-display {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
padding: 6px 12px;
|
||||
border: 1px solid #ccc;
|
||||
border-radius: 4px;
|
||||
background: white;
|
||||
cursor: pointer;
|
||||
min-height: 34px;
|
||||
|
||||
.multiselect-label {
|
||||
flex: 1;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.multiselect-value {
|
||||
color: #666;
|
||||
font-size: 12px;
|
||||
margin-right: 8px;
|
||||
}
|
||||
|
||||
.dropdown-icon {
|
||||
flex-shrink: 0;
|
||||
transition: transform 0.2s ease;
|
||||
}
|
||||
|
||||
&:hover {
|
||||
border-color: #999;
|
||||
}
|
||||
}
|
||||
|
||||
.multiselect-dropdown {
|
||||
position: absolute;
|
||||
top: 100%;
|
||||
left: 0;
|
||||
right: 0;
|
||||
z-index: 1000;
|
||||
background: white;
|
||||
border: 1px solid #ccc;
|
||||
border-top: none;
|
||||
border-radius: 0 0 4px 4px;
|
||||
box-shadow: 0 2px 4px rgba(0,0,0,0.1);
|
||||
max-height: 200px;
|
||||
overflow-y: auto;
|
||||
|
||||
.checkbox-group {
|
||||
padding: 8px;
|
||||
|
||||
.checkbox-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
padding: 4px 0;
|
||||
|
||||
.checkbox-label {
|
||||
margin: 0;
|
||||
font-size: 14px;
|
||||
cursor: pointer;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.date-range {
|
||||
.date-input-group {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.date-separator {
|
||||
margin: 0 5px;
|
||||
color: #777;
|
||||
}
|
||||
}
|
||||
|
||||
.toggle {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
|
||||
.toggle-label {
|
||||
margin: 0;
|
||||
font-size: 14px;
|
||||
cursor: pointer;
|
||||
}
|
||||
}
|
||||
|
||||
.filter-actions {
|
||||
margin-top: 15px;
|
||||
padding-top: 15px;
|
||||
border-top: 1px solid #eee;
|
||||
|
||||
.btn {
|
||||
font-size: 13px;
|
||||
}
|
||||
}
|
||||
|
||||
.dg-wrapper {
|
||||
padding: 15px;
|
||||
}
|
||||
|
||||
clr-datagrid {
|
||||
margin-top: 10px;
|
||||
}
|
||||
|
||||
// Responsive design
|
||||
@media (max-width: 768px) {
|
||||
.filter-controls {
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.filter-item {
|
||||
min-width: 100%;
|
||||
}
|
||||
}
|
||||
@@ -1,13 +1,17 @@
|
||||
import { Component, OnInit, Input, OnChanges, SimpleChanges } from '@angular/core';
|
||||
import { Component, OnInit, Input, OnChanges, OnDestroy, SimpleChanges, ViewChild } from '@angular/core';
|
||||
import { UsergrpmaintainceService } from 'src/app/services/admin/usergrpmaintaince.service';
|
||||
import { Dashboard3Service } from 'src/app/services/builder/dashboard3.service';
|
||||
// Add FilterService import
|
||||
import { FilterService } from '../../common-filter/filter.service';
|
||||
// Add Subscription import
|
||||
import { Subscription } from 'rxjs';
|
||||
|
||||
@Component({
|
||||
selector: 'app-grid-view',
|
||||
templateUrl: './grid-view.component.html',
|
||||
styleUrls: ['./grid-view.component.scss']
|
||||
})
|
||||
export class GridViewComponent implements OnInit, OnChanges {
|
||||
export class GridViewComponent implements OnInit, OnChanges, OnDestroy {
|
||||
@Input() xAxis: string;
|
||||
@Input() yAxis: string | string[];
|
||||
@Input() table: string;
|
||||
@@ -23,6 +27,16 @@ export class GridViewComponent implements OnInit, OnChanges {
|
||||
@Input() datasource: string;
|
||||
@Input() fieldName: string;
|
||||
@Input() connection: number; // Add connection input
|
||||
// Drilldown configuration inputs
|
||||
@Input() drilldownEnabled: boolean = false;
|
||||
@Input() drilldownApiUrl: string;
|
||||
@Input() drilldownXAxis: string;
|
||||
@Input() drilldownYAxis: string;
|
||||
@Input() drilldownParameter: string; // Add drilldown parameter input
|
||||
@Input() baseFilters: any[] = []; // Add base filters input
|
||||
@Input() drilldownFilters: any[] = []; // Add drilldown filters input
|
||||
// Multi-layer drilldown configuration inputs
|
||||
@Input() drilldownLayers: any[] = []; // Array of drilldown layer configurations
|
||||
|
||||
loading = false;
|
||||
givendata: any[] = [];
|
||||
@@ -38,13 +52,46 @@ export class GridViewComponent implements OnInit, OnChanges {
|
||||
submitted = false;
|
||||
dynamicHeaders: any[] = [];
|
||||
|
||||
constructor(
|
||||
// Multi-layer drilldown state tracking
|
||||
drilldownStack: any[] = []; // Stack to track drilldown navigation history
|
||||
currentDrilldownLevel: number = 0; // Current drilldown level (0 = base level)
|
||||
originalGridData: any[] = [];
|
||||
|
||||
// No data state
|
||||
noDataAvailable: boolean = false;
|
||||
|
||||
// Flag to prevent infinite loops
|
||||
private isFetchingData: boolean = false;
|
||||
|
||||
// Add subscriptions to unsubscribe on destroy
|
||||
private subscriptions: Subscription[] = [];
|
||||
|
||||
// Add a flag to track if filters have been initialized
|
||||
private filtersInitialized: boolean = false;
|
||||
|
||||
// Add properties to track open multiselect dropdowns
|
||||
private openMultiselects: Map<string, string> = new Map(); // Map of filterId -> context
|
||||
|
||||
// Add property to track document click handler
|
||||
private documentClickHandler: ((event: MouseEvent) => void) | null = null;
|
||||
|
||||
constructor(
|
||||
private mainservice: UsergrpmaintainceService,
|
||||
private dashboardService: Dashboard3Service,
|
||||
// Add FilterService to constructor
|
||||
private filterService: FilterService
|
||||
) { }
|
||||
|
||||
ngOnInit(): void {
|
||||
// Subscribe to filter changes
|
||||
this.subscriptions.push(
|
||||
this.filterService.filterState$.subscribe(filters => {
|
||||
// When filters change, refresh the grid data
|
||||
console.log('GridView: Filter state changed:', filters);
|
||||
this.fetchGridData();
|
||||
})
|
||||
);
|
||||
|
||||
this.fetchGridData();
|
||||
}
|
||||
|
||||
@@ -55,33 +102,193 @@ export class GridViewComponent implements OnInit, OnChanges {
|
||||
const xAxisChanged = changes.xAxis && !changes.xAxis.firstChange;
|
||||
const yAxisChanged = changes.yAxis && !changes.yAxis.firstChange;
|
||||
const tableChanged = changes.table && !changes.table.firstChange;
|
||||
const connectionChanged = changes.connection && !changes.connection.firstChange; // Add connection change detection
|
||||
const connectionChanged = changes.connection && !changes.connection.firstChange;
|
||||
const baseFiltersChanged = changes.baseFilters && !changes.baseFilters.firstChange;
|
||||
// Drilldown configuration changes
|
||||
const drilldownEnabledChanged = changes.drilldownEnabled && !changes.drilldownEnabled.firstChange;
|
||||
const drilldownApiUrlChanged = changes.drilldownApiUrl && !changes.drilldownApiUrl.firstChange;
|
||||
const drilldownXAxisChanged = changes.drilldownXAxis && !changes.drilldownXAxis.firstChange;
|
||||
const drilldownYAxisChanged = changes.drilldownYAxis && !changes.drilldownYAxis.firstChange;
|
||||
const drilldownLayersChanged = changes.drilldownLayers && !changes.drilldownLayers.firstChange;
|
||||
|
||||
// Initialize filter values if they haven't been initialized yet
|
||||
if (!this.filtersInitialized && (changes.baseFilters || changes.drilldownFilters || changes.drilldownLayers)) {
|
||||
this.initializeFilterValues();
|
||||
this.filtersInitialized = true;
|
||||
}
|
||||
|
||||
// Respond to input changes
|
||||
if (xAxisChanged || yAxisChanged || tableChanged || connectionChanged) {
|
||||
console.log('X or Y axis or table or connection changed, fetching new data');
|
||||
// Only fetch data if xAxis, yAxis, table, or connection has changed (and it's not the first change)
|
||||
if (!this.isFetchingData && (xAxisChanged || yAxisChanged || tableChanged || connectionChanged || baseFiltersChanged ||
|
||||
drilldownEnabledChanged || drilldownApiUrlChanged || drilldownXAxisChanged || drilldownYAxisChanged ||
|
||||
drilldownLayersChanged)) {
|
||||
console.log('X or Y axis or table or connection or base filters or drilldown config changed, fetching new data');
|
||||
// Only fetch data if xAxis, yAxis, table, connection, baseFilters or drilldown config has changed (and it's not the first change)
|
||||
this.fetchGridData();
|
||||
}
|
||||
}
|
||||
|
||||
// Initialize filter values with proper default values based on type
|
||||
private initializeFilterValues(): void {
|
||||
console.log('Initializing filter values');
|
||||
|
||||
// Initialize base filters
|
||||
if (this.baseFilters) {
|
||||
this.baseFilters.forEach(filter => {
|
||||
if (filter.value === undefined || filter.value === null) {
|
||||
switch (filter.type) {
|
||||
case 'multiselect':
|
||||
filter.value = [];
|
||||
break;
|
||||
case 'date-range':
|
||||
filter.value = { start: null, end: null };
|
||||
break;
|
||||
case 'toggle':
|
||||
filter.value = false;
|
||||
break;
|
||||
default:
|
||||
filter.value = '';
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// Initialize drilldown filters
|
||||
if (this.drilldownFilters) {
|
||||
this.drilldownFilters.forEach(filter => {
|
||||
if (filter.value === undefined || filter.value === null) {
|
||||
switch (filter.type) {
|
||||
case 'multiselect':
|
||||
filter.value = [];
|
||||
break;
|
||||
case 'date-range':
|
||||
filter.value = { start: null, end: null };
|
||||
break;
|
||||
case 'toggle':
|
||||
filter.value = false;
|
||||
break;
|
||||
default:
|
||||
filter.value = '';
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// Initialize layer filters
|
||||
if (this.drilldownLayers) {
|
||||
this.drilldownLayers.forEach(layer => {
|
||||
if (layer.filters) {
|
||||
layer.filters.forEach((filter: any) => {
|
||||
if (filter.value === undefined || filter.value === null) {
|
||||
switch (filter.type) {
|
||||
case 'multiselect':
|
||||
filter.value = [];
|
||||
break;
|
||||
case 'date-range':
|
||||
filter.value = { start: null, end: null };
|
||||
break;
|
||||
case 'toggle':
|
||||
filter.value = false;
|
||||
break;
|
||||
default:
|
||||
filter.value = '';
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
console.log('Filter values initialized:', {
|
||||
baseFilters: this.baseFilters,
|
||||
drilldownFilters: this.drilldownFilters,
|
||||
drilldownLayers: this.drilldownLayers
|
||||
});
|
||||
}
|
||||
|
||||
// Dynamic headers for the grid
|
||||
|
||||
fetchGridData(): void {
|
||||
// Set flag to prevent recursive calls
|
||||
this.isFetchingData = true;
|
||||
|
||||
// If we're in drilldown mode, fetch the appropriate drilldown data
|
||||
if (this.currentDrilldownLevel > 0 && this.drilldownStack.length > 0) {
|
||||
this.fetchDrilldownData();
|
||||
// Reset flag after fetching
|
||||
this.isFetchingData = false;
|
||||
return;
|
||||
}
|
||||
|
||||
// If we have the necessary data, fetch grid data from the service
|
||||
if (this.table && this.xAxis) {
|
||||
console.log('Fetching grid data for:', { table: this.table, xAxis: this.xAxis, yAxis: this.yAxis, connection: this.connection });
|
||||
// if (this.table && this.xAxis) {
|
||||
if (this.table) {
|
||||
|
||||
console.log('=== GRID VIEW DEBUG INFO ===');
|
||||
console.log('Table:', this.table);
|
||||
console.log('X-Axis:', this.xAxis);
|
||||
console.log('Y-Axis:', this.yAxis);
|
||||
console.log('Connection:', this.connection);
|
||||
|
||||
// Convert yAxis to string if it's an array
|
||||
const yAxisString = Array.isArray(this.yAxis) ? this.yAxis.join(',') : this.yAxis;
|
||||
|
||||
// Get the parameter value from the drilldown stack for base level (should be empty)
|
||||
let parameterValue = '';
|
||||
|
||||
// Log the URL that will be called
|
||||
let url = `chart/getdashjson/grid?tableName=${this.table}&xAxis=${this.xAxis}&yAxes=${yAxisString}${this.connection ? `&sureId=${this.connection}` : ''}`;
|
||||
console.log('Grid data URL:', url);
|
||||
|
||||
// Get filter parameters from base filters
|
||||
const filterObj = {};
|
||||
|
||||
// Add base filters
|
||||
if (this.baseFilters && this.baseFilters.length > 0) {
|
||||
this.baseFilters.forEach(filter => {
|
||||
if (filter.field && filter.value) {
|
||||
filterObj[filter.field] = filter.value;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// Add common filters directly as key-value pairs
|
||||
const commonFilters = this.filterService.getFilterValues();
|
||||
const filterDefinitions = this.filterService.getFilters();
|
||||
|
||||
// Add common filters using the field name as the key
|
||||
Object.keys(commonFilters).forEach(filterId => {
|
||||
const filterValue = commonFilters[filterId];
|
||||
|
||||
// Find the filter definition to get the field name
|
||||
const filterDef = this.filterService.getFilters().find(f => f.id === filterId);
|
||||
|
||||
if (filterDef && filterDef.field) {
|
||||
const fieldName = filterDef.field;
|
||||
if (filterValue !== undefined && filterValue !== null && filterValue !== '') {
|
||||
filterObj[fieldName] = filterValue;
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
// Convert to JSON string for API call
|
||||
let filterParams = '';
|
||||
if (Object.keys(filterObj).length > 0) {
|
||||
filterParams = JSON.stringify(filterObj);
|
||||
}
|
||||
|
||||
console.log('GridView: Final filter object to send to API:', filterObj);
|
||||
|
||||
// Fetch data from the dashboard service, similar to other chart components
|
||||
this.dashboardService.getChartData(this.table, 'grid', this.xAxis, yAxisString, this.connection).subscribe(
|
||||
this.dashboardService.getChartData(this.table, 'grid', this.xAxis, yAxisString, this.connection, '', '', filterParams).subscribe(
|
||||
(data: any) => {
|
||||
console.log('=== GRID VIEW DATA RESPONSE ===');
|
||||
console.log('Received grid data:', data);
|
||||
if (data === null) {
|
||||
console.warn('Grid API returned null data. Check if the API endpoint is working correctly.');
|
||||
this.error = "No data Available";
|
||||
this.noDataAvailable = true;
|
||||
// Reset flag after fetching
|
||||
this.isFetchingData = false;
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -90,49 +297,387 @@ export class GridViewComponent implements OnInit, OnChanges {
|
||||
this.givendata = data.chartData;
|
||||
this.extractDynamicHeaders(data.chartData);
|
||||
this.error = this.givendata.length === 0 ? "No data Available" : undefined;
|
||||
this.noDataAvailable = this.givendata.length === 0;
|
||||
console.log('Updated grid with data:', this.givendata);
|
||||
} else if (data && data.data) {
|
||||
// Handle the original expected format as fallback
|
||||
this.givendata = data.data;
|
||||
this.extractDynamicHeaders(data.data);
|
||||
this.error = this.givendata.length === 0 ? "No data Available" : undefined;
|
||||
this.noDataAvailable = this.givendata.length === 0;
|
||||
console.log('Updated grid with legacy data format:', this.givendata);
|
||||
} else if (Array.isArray(data)) {
|
||||
// Handle case where data is directly an array
|
||||
this.givendata = data;
|
||||
this.extractDynamicHeaders(data);
|
||||
this.error = this.givendata.length === 0 ? "No data Available" : undefined;
|
||||
this.noDataAvailable = this.givendata.length === 0;
|
||||
console.log('Updated grid with array data:', this.givendata);
|
||||
} else {
|
||||
console.warn('Grid received data does not have expected structure', data);
|
||||
this.error = "No valid data received";
|
||||
this.givendata = [];
|
||||
this.noDataAvailable = true;
|
||||
}
|
||||
// Reset flag after fetching
|
||||
this.isFetchingData = false;
|
||||
}, (error) => {
|
||||
console.log('Error fetching grid data:', error);
|
||||
this.error = "Server Error";
|
||||
this.noDataAvailable = true;
|
||||
// Reset flag after fetching
|
||||
this.isFetchingData = false;
|
||||
});
|
||||
} else if (this.table) {
|
||||
console.log('Missing xAxis, falling back to default data fetching');
|
||||
// Fallback to default data fetching when only table is provided
|
||||
// Convert yAxis to string if it's an array
|
||||
// Convert yAxis to string if it's an array
|
||||
const yAxisString = Array.isArray(this.yAxis) ? this.yAxis.join(',') : this.yAxis;
|
||||
|
||||
// Fetch data from the dashboard service, similar to other chart components
|
||||
this.dashboardService.getChartData(this.table, 'grid', this.xAxis, yAxisString, this.connection).subscribe(
|
||||
(data: any) => {
|
||||
// this.mainservice.getAll().subscribe((data: any) => {
|
||||
console.log('recv data ', data);
|
||||
this.givendata = Array.isArray(data) ? data : [];
|
||||
this.extractDynamicHeaders(data);
|
||||
this.error = this.givendata && this.givendata.length === 0 ? "No data Available" : undefined;
|
||||
}, (error) => {
|
||||
console.log(error);
|
||||
this.error = "Server Error";
|
||||
});
|
||||
// this.mainservice.getAll().subscribe((data: any) => {
|
||||
console.log('recv data ', data);
|
||||
this.givendata = Array.isArray(data) ? data : [];
|
||||
this.extractDynamicHeaders(data);
|
||||
this.error = this.givendata && this.givendata.length === 0 ? "No data Available" : undefined;
|
||||
this.noDataAvailable = this.givendata && this.givendata.length === 0;
|
||||
// Reset flag after fetching
|
||||
this.isFetchingData = false;
|
||||
}, (error) => {
|
||||
console.log(error);
|
||||
this.error = "Server Error";
|
||||
this.noDataAvailable = true;
|
||||
// Reset flag after fetching
|
||||
this.isFetchingData = false;
|
||||
});
|
||||
} else {
|
||||
console.log('Missing required data for grid:', { table: this.table });
|
||||
this.error = "Table name is required";
|
||||
this.noDataAvailable = true;
|
||||
// Reset flag after fetching
|
||||
this.isFetchingData = false;
|
||||
}
|
||||
}
|
||||
|
||||
// Fetch drilldown data based on current drilldown level
|
||||
fetchDrilldownData(): void {
|
||||
console.log('Fetching drilldown data, current level:', this.currentDrilldownLevel);
|
||||
console.log('Drilldown stack:', this.drilldownStack);
|
||||
|
||||
// Get the current drilldown configuration based on the current level
|
||||
let drilldownConfig;
|
||||
if (this.currentDrilldownLevel === 1) {
|
||||
// Base drilldown level
|
||||
drilldownConfig = {
|
||||
apiUrl: this.drilldownApiUrl,
|
||||
xAxis: this.drilldownXAxis,
|
||||
yAxis: this.drilldownYAxis,
|
||||
parameter: this.drilldownParameter
|
||||
};
|
||||
} else {
|
||||
// Multi-layer drilldown level
|
||||
const layerIndex = this.currentDrilldownLevel - 2; // -2 because level 1 is base drilldown
|
||||
if (layerIndex >= 0 && layerIndex < this.drilldownLayers.length) {
|
||||
drilldownConfig = this.drilldownLayers[layerIndex];
|
||||
} else {
|
||||
console.warn('Invalid drilldown layer index:', layerIndex);
|
||||
this.error = "Invalid drilldown configuration";
|
||||
this.givendata = [];
|
||||
this.noDataAvailable = true;
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
console.log('Drilldown config for level', this.currentDrilldownLevel, ':', drilldownConfig);
|
||||
|
||||
// Check if we have valid drilldown configuration
|
||||
if (!drilldownConfig || !drilldownConfig.apiUrl || !drilldownConfig.xAxis || !drilldownConfig.yAxis) {
|
||||
console.warn('Missing drilldown configuration for level:', this.currentDrilldownLevel);
|
||||
this.error = "Missing drilldown configuration";
|
||||
this.givendata = [];
|
||||
this.noDataAvailable = true;
|
||||
return;
|
||||
}
|
||||
|
||||
// Get the parameter value from the drilldown stack
|
||||
let parameterValue = '';
|
||||
if (this.drilldownStack.length > 0) {
|
||||
const lastEntry = this.drilldownStack[this.drilldownStack.length - 1];
|
||||
parameterValue = lastEntry.clickedValue || '';
|
||||
console.log('Parameter value from last click:', parameterValue);
|
||||
}
|
||||
|
||||
// Get the parameter field from drilldown config
|
||||
const parameterField = drilldownConfig.parameter || '';
|
||||
console.log('Parameter field:', parameterField);
|
||||
|
||||
console.log('Fetching drilldown data for level:', this.currentDrilldownLevel, {
|
||||
apiUrl: drilldownConfig.apiUrl,
|
||||
xAxis: drilldownConfig.xAxis,
|
||||
yAxis: drilldownConfig.yAxis,
|
||||
parameterField: parameterField,
|
||||
parameterValue: parameterValue,
|
||||
connection: this.connection
|
||||
});
|
||||
|
||||
// Build the actual API URL with parameter replacement
|
||||
let actualApiUrl = drilldownConfig.apiUrl;
|
||||
console.log('Original API URL:', actualApiUrl);
|
||||
console.log('Parameter value to use:', parameterValue);
|
||||
console.log('Parameter field:', parameterField);
|
||||
|
||||
// Check if the URL contains angle brackets for parameter replacement
|
||||
const hasAngleBrackets = /<[^>]+>/.test(actualApiUrl);
|
||||
|
||||
if (hasAngleBrackets && parameterValue) {
|
||||
// Replace angle brackets placeholder with actual value
|
||||
console.log('Replacing angle brackets with parameter value');
|
||||
const encodedValue = encodeURIComponent(parameterValue);
|
||||
actualApiUrl = actualApiUrl.replace(/<[^>]+>/g, encodedValue);
|
||||
console.log('URL after angle bracket replacement:', actualApiUrl);
|
||||
}
|
||||
|
||||
// Log the URL that will be called
|
||||
let url = `chart/getdashjson/grid?tableName=${actualApiUrl}&xAxis=${drilldownConfig.xAxis}&yAxes=${drilldownConfig.yAxis}${this.connection ? `&sureId=${this.connection}` : ''}`;
|
||||
if (parameterField && parameterValue) {
|
||||
url += `¶meter=${encodeURIComponent(parameterField)}¶meterValue=${encodeURIComponent(parameterValue)}`;
|
||||
}
|
||||
console.log('Drilldown data URL:', url);
|
||||
|
||||
// Convert drilldown layer filters to filter parameters (if applicable)
|
||||
const filterObj = {};
|
||||
|
||||
// Add drilldown layer filters
|
||||
if (drilldownConfig.filters && drilldownConfig.filters.length > 0) {
|
||||
drilldownConfig.filters.forEach((filter: any) => {
|
||||
if (filter.field && filter.value) {
|
||||
filterObj[filter.field] = filter.value;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// Add drilldownFilters
|
||||
if (this.drilldownFilters && this.drilldownFilters.length > 0) {
|
||||
this.drilldownFilters.forEach(filter => {
|
||||
if (filter.field && filter.value) {
|
||||
filterObj[filter.field] = filter.value;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// Add common filters
|
||||
const commonFilters = this.filterService.getFilterValues();
|
||||
const filterDefinitions = this.filterService.getFilters();
|
||||
Object.keys(commonFilters).forEach(filterId => {
|
||||
const filterValue = commonFilters[filterId];
|
||||
|
||||
// Find the filter definition to get the field name
|
||||
const filterDef = this.filterService.getFilters().find(f => f.id === filterId);
|
||||
|
||||
if (filterDef && filterDef.field) {
|
||||
const fieldName = filterDef.field;
|
||||
if (filterValue !== undefined && filterValue !== null && filterValue !== '') {
|
||||
filterObj[fieldName] = filterValue;
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
// Convert to JSON string for API call
|
||||
let drilldownFilterParams = '';
|
||||
if (Object.keys(filterObj).length > 0) {
|
||||
drilldownFilterParams = JSON.stringify(filterObj);
|
||||
}
|
||||
|
||||
console.log('Drilldown filter parameters:', drilldownFilterParams);
|
||||
|
||||
// For drilldown level, we pass the parameter value from the drilldown stack and drilldown filters
|
||||
this.dashboardService.getChartData(
|
||||
drilldownConfig.apiUrl, 'grid',
|
||||
drilldownConfig.xAxis, drilldownConfig.yAxis,
|
||||
this.connection,
|
||||
parameterField, parameterValue,
|
||||
drilldownFilterParams
|
||||
).subscribe(
|
||||
(data: any) => {
|
||||
console.log('Received drilldown data:', data);
|
||||
if (data === null) {
|
||||
console.warn('Drilldown API returned null data. Check if the API endpoint is working correctly.');
|
||||
this.error = "No data Available";
|
||||
this.givendata = [];
|
||||
this.noDataAvailable = true;
|
||||
return;
|
||||
}
|
||||
|
||||
// Handle the actual data structure returned by the API
|
||||
if (data && data.chartData) {
|
||||
this.givendata = data.chartData;
|
||||
this.extractDynamicHeaders(data.chartData);
|
||||
this.error = this.givendata.length === 0 ? "No data Available" : undefined;
|
||||
this.noDataAvailable = this.givendata.length === 0;
|
||||
console.log('Updated grid with drilldown data:', this.givendata);
|
||||
} else if (data && data.data) {
|
||||
// Handle the original expected format as fallback
|
||||
this.givendata = data.data;
|
||||
this.extractDynamicHeaders(data.data);
|
||||
this.error = this.givendata.length === 0 ? "No data Available" : undefined;
|
||||
this.noDataAvailable = this.givendata.length === 0;
|
||||
console.log('Updated grid with drilldown legacy data format:', this.givendata);
|
||||
} else if (Array.isArray(data)) {
|
||||
// Handle case where data is directly an array
|
||||
this.givendata = data;
|
||||
this.extractDynamicHeaders(data);
|
||||
this.error = this.givendata.length === 0 ? "No data Available" : undefined;
|
||||
this.noDataAvailable = this.givendata.length === 0;
|
||||
console.log('Updated grid with drilldown array data:', this.givendata);
|
||||
} else {
|
||||
console.warn('Drilldown received data does not have expected structure', data);
|
||||
this.error = "No valid data received";
|
||||
this.givendata = [];
|
||||
this.noDataAvailable = true;
|
||||
}
|
||||
},
|
||||
(error) => {
|
||||
console.error('Error fetching drilldown data:', error);
|
||||
this.error = "Server Error";
|
||||
this.givendata = [];
|
||||
this.noDataAvailable = true;
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
// Reset to original data (go back to base level)
|
||||
resetToOriginalData(): void {
|
||||
console.log('Resetting to original data');
|
||||
console.log('Current stack before reset:', this.drilldownStack);
|
||||
console.log('Current level before reset:', this.currentDrilldownLevel);
|
||||
|
||||
this.currentDrilldownLevel = 0;
|
||||
this.drilldownStack = [];
|
||||
|
||||
if (this.originalGridData.length > 0) {
|
||||
// Create a deep copy to avoid reference issues
|
||||
this.givendata = JSON.parse(JSON.stringify(this.originalGridData));
|
||||
this.extractDynamicHeaders(this.givendata);
|
||||
console.log('Restored original data');
|
||||
}
|
||||
|
||||
console.log('After reset - data:', this.givendata);
|
||||
|
||||
// Re-fetch original data
|
||||
this.fetchGridData();
|
||||
}
|
||||
|
||||
// Navigate back to previous drilldown level
|
||||
navigateBack(): void {
|
||||
console.log('Navigating back, current stack:', this.drilldownStack);
|
||||
console.log('Current level:', this.currentDrilldownLevel);
|
||||
|
||||
if (this.drilldownStack.length > 0) {
|
||||
// Remove the last entry from the stack
|
||||
const removedEntry = this.drilldownStack.pop();
|
||||
console.log('Removed entry from stack:', removedEntry);
|
||||
|
||||
// Update the current drilldown level
|
||||
this.currentDrilldownLevel = this.drilldownStack.length;
|
||||
console.log('New level after pop:', this.currentDrilldownLevel);
|
||||
console.log('Stack after pop:', this.drilldownStack);
|
||||
|
||||
if (this.drilldownStack.length > 0) {
|
||||
// Fetch data for the previous level
|
||||
console.log('Fetching data for previous level');
|
||||
this.fetchDrilldownData();
|
||||
} else {
|
||||
// Back to base level
|
||||
console.log('Back to base level, resetting to original data');
|
||||
this.resetToOriginalData();
|
||||
}
|
||||
} else {
|
||||
// Already at base level, reset to original data
|
||||
console.log('Already at base level, resetting to original data');
|
||||
this.resetToOriginalData();
|
||||
}
|
||||
}
|
||||
|
||||
// Method to handle grid row clicks for drilldown
|
||||
onRowClick(item: any, key: string): void {
|
||||
console.log('Grid row clicked:', { item, key });
|
||||
|
||||
// If drilldown is enabled
|
||||
if (this.drilldownEnabled) {
|
||||
// Get the value for the clicked key
|
||||
const clickedValue = item[key];
|
||||
|
||||
console.log('Clicked on row value:', { key, value: clickedValue });
|
||||
|
||||
// If we're not at the base level, store original data
|
||||
if (this.currentDrilldownLevel === 0) {
|
||||
// Store original data before entering drilldown mode
|
||||
// Create a deep copy to avoid reference issues
|
||||
this.originalGridData = JSON.parse(JSON.stringify(this.givendata));
|
||||
console.log('Stored original data for drilldown');
|
||||
}
|
||||
|
||||
// Determine the next drilldown level
|
||||
const nextDrilldownLevel = this.currentDrilldownLevel + 1;
|
||||
|
||||
console.log('Next drilldown level will be:', nextDrilldownLevel);
|
||||
|
||||
// Check if there's a drilldown configuration for this level
|
||||
let hasDrilldownConfig = false;
|
||||
let drilldownConfig;
|
||||
|
||||
if (nextDrilldownLevel === 1) {
|
||||
// Base drilldown level
|
||||
drilldownConfig = {
|
||||
apiUrl: this.drilldownApiUrl,
|
||||
xAxis: this.drilldownXAxis,
|
||||
yAxis: this.drilldownYAxis,
|
||||
parameter: this.drilldownParameter
|
||||
};
|
||||
hasDrilldownConfig = !!this.drilldownApiUrl && !!this.drilldownXAxis && !!this.drilldownYAxis;
|
||||
} else {
|
||||
// Multi-layer drilldown level
|
||||
const layerIndex = nextDrilldownLevel - 2; // -2 because level 1 is base drilldown
|
||||
if (layerIndex < this.drilldownLayers.length) {
|
||||
drilldownConfig = this.drilldownLayers[layerIndex];
|
||||
hasDrilldownConfig = drilldownConfig.enabled &&
|
||||
!!drilldownConfig.apiUrl &&
|
||||
!!drilldownConfig.xAxis &&
|
||||
!!drilldownConfig.yAxis;
|
||||
}
|
||||
}
|
||||
|
||||
console.log('Drilldown config for next level:', drilldownConfig);
|
||||
console.log('Has drilldown config:', hasDrilldownConfig);
|
||||
|
||||
// If there's a drilldown configuration for the next level, proceed
|
||||
if (hasDrilldownConfig) {
|
||||
// Add this click to the drilldown stack
|
||||
const stackEntry = {
|
||||
level: nextDrilldownLevel,
|
||||
clickedKey: key,
|
||||
clickedValue: clickedValue
|
||||
};
|
||||
|
||||
this.drilldownStack.push(stackEntry);
|
||||
|
||||
console.log('Added to drilldown stack:', stackEntry);
|
||||
console.log('Current drilldown stack:', this.drilldownStack);
|
||||
|
||||
// Update the current drilldown level
|
||||
this.currentDrilldownLevel = nextDrilldownLevel;
|
||||
|
||||
console.log('Entering drilldown level:', this.currentDrilldownLevel);
|
||||
|
||||
// Fetch drilldown data for the new level
|
||||
this.fetchDrilldownData();
|
||||
} else {
|
||||
console.log('No drilldown configuration for level:', nextDrilldownLevel);
|
||||
}
|
||||
} else {
|
||||
console.log('Drilldown not enabled');
|
||||
}
|
||||
}
|
||||
|
||||
@@ -175,4 +720,261 @@ export class GridViewComponent implements OnInit, OnChanges {
|
||||
.replace(/([A-Z])/g, ' $1')
|
||||
.replace(/^./, str => str.toUpperCase());
|
||||
}
|
||||
|
||||
// Check if there are active filters
|
||||
hasActiveFilters(): boolean {
|
||||
return (this.baseFilters && this.baseFilters.length > 0) ||
|
||||
(this.drilldownFilters && this.drilldownFilters.length > 0) ||
|
||||
this.hasActiveLayerFilters();
|
||||
}
|
||||
|
||||
// Check if there are active layer filters for current drilldown level
|
||||
hasActiveLayerFilters(): boolean {
|
||||
if (this.currentDrilldownLevel > 1 && this.drilldownLayers && this.drilldownLayers.length > 0) {
|
||||
const layerIndex = this.currentDrilldownLevel - 2; // -2 because level 1 is base drilldown
|
||||
return layerIndex < this.drilldownLayers.length &&
|
||||
this.drilldownLayers[layerIndex].filters &&
|
||||
this.drilldownLayers[layerIndex].filters.length > 0;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
// Get active layer filters for current drilldown level
|
||||
getActiveLayerFilters(): any[] {
|
||||
if (this.currentDrilldownLevel > 1 && this.drilldownLayers && this.drilldownLayers.length > 0) {
|
||||
const layerIndex = this.currentDrilldownLevel - 2; // -2 because level 1 is base drilldown
|
||||
if (layerIndex < this.drilldownLayers.length &&
|
||||
this.drilldownLayers[layerIndex].filters) {
|
||||
return this.drilldownLayers[layerIndex].filters;
|
||||
}
|
||||
}
|
||||
return [];
|
||||
}
|
||||
|
||||
// Get filter options for dropdown/multiselect filters
|
||||
getFilterOptions(filter: any): string[] {
|
||||
if (filter.options) {
|
||||
if (Array.isArray(filter.options)) {
|
||||
return filter.options;
|
||||
} else if (typeof filter.options === 'string') {
|
||||
return filter.options.split(',').map(opt => opt.trim()).filter(opt => opt);
|
||||
}
|
||||
}
|
||||
return [];
|
||||
}
|
||||
|
||||
// Check if an option is selected for multiselect filters
|
||||
isOptionSelected(filter: any, option: string): boolean {
|
||||
if (!filter.value) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (Array.isArray(filter.value)) {
|
||||
return filter.value.includes(option);
|
||||
}
|
||||
|
||||
return filter.value === option;
|
||||
}
|
||||
|
||||
// Handle base filter changes
|
||||
onBaseFilterChange(filter: any): void {
|
||||
console.log('Base filter changed:', filter);
|
||||
// Refresh data when filter changes
|
||||
this.fetchGridData();
|
||||
}
|
||||
|
||||
// Handle drilldown filter changes
|
||||
onDrilldownFilterChange(filter: any): void {
|
||||
console.log('Drilldown filter changed:', filter);
|
||||
// Refresh data when filter changes
|
||||
this.fetchGridData();
|
||||
}
|
||||
|
||||
// Handle layer filter changes
|
||||
onLayerFilterChange(filter: any): void {
|
||||
console.log('Layer filter changed:', filter);
|
||||
// Refresh data when filter changes
|
||||
this.fetchGridData();
|
||||
}
|
||||
|
||||
// Handle multiselect changes
|
||||
onMultiSelectChange(filter: any, option: string, event: any): void {
|
||||
const checked = event.target.checked;
|
||||
|
||||
// Initialize filter.value as array if it's not already
|
||||
if (!Array.isArray(filter.value)) {
|
||||
filter.value = [];
|
||||
}
|
||||
|
||||
if (checked) {
|
||||
// Add option to array if not already present
|
||||
if (!filter.value.includes(option)) {
|
||||
filter.value.push(option);
|
||||
}
|
||||
} else {
|
||||
// Remove option from array
|
||||
filter.value = filter.value.filter((item: string) => item !== option);
|
||||
}
|
||||
|
||||
// Refresh data when filter changes
|
||||
this.fetchGridData();
|
||||
}
|
||||
|
||||
// Handle date range changes
|
||||
onDateRangeChange(filter: any, dateRange: { start: string | null, end: string | null }): void {
|
||||
filter.value = dateRange;
|
||||
// Refresh data when filter changes
|
||||
this.fetchGridData();
|
||||
}
|
||||
|
||||
// Handle toggle changes
|
||||
onToggleChange(filter: any, checked: boolean): void {
|
||||
filter.value = checked;
|
||||
// Refresh data when filter changes
|
||||
this.fetchGridData();
|
||||
}
|
||||
|
||||
// Toggle multiselect dropdown visibility
|
||||
toggleMultiselect(filter: any, context: string): void {
|
||||
const filterId = `${context}-${filter.field}`;
|
||||
if (this.isMultiselectOpen(filter, context)) {
|
||||
this.openMultiselects.delete(filterId);
|
||||
} else {
|
||||
// Close all other multiselects first
|
||||
this.openMultiselects.clear();
|
||||
this.openMultiselects.set(filterId, context);
|
||||
|
||||
// Add document click handler to close dropdown when clicking outside
|
||||
this.addDocumentClickHandler();
|
||||
}
|
||||
}
|
||||
|
||||
// Add document click handler to close dropdowns when clicking outside
|
||||
private addDocumentClickHandler(): void {
|
||||
if (!this.documentClickHandler) {
|
||||
this.documentClickHandler = (event: MouseEvent) => {
|
||||
const target = event.target as HTMLElement;
|
||||
// Check if click is outside any multiselect dropdown
|
||||
if (!target.closest('.multiselect-container')) {
|
||||
this.openMultiselects.clear();
|
||||
this.removeDocumentClickHandler();
|
||||
}
|
||||
};
|
||||
|
||||
// Use setTimeout to ensure the click event that opened the dropdown doesn't immediately close it
|
||||
setTimeout(() => {
|
||||
document.addEventListener('click', this.documentClickHandler!);
|
||||
}, 0);
|
||||
}
|
||||
}
|
||||
|
||||
// Remove document click handler
|
||||
private removeDocumentClickHandler(): void {
|
||||
if (this.documentClickHandler) {
|
||||
document.removeEventListener('click', this.documentClickHandler);
|
||||
this.documentClickHandler = null;
|
||||
}
|
||||
}
|
||||
|
||||
// Check if multiselect dropdown is open
|
||||
isMultiselectOpen(filter: any, context: string): boolean {
|
||||
const filterId = `${context}-${filter.field}`;
|
||||
return this.openMultiselects.has(filterId);
|
||||
}
|
||||
|
||||
// Get count of selected options for a multiselect filter
|
||||
getSelectedOptionsCount(filter: any): number {
|
||||
if (!filter.value) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
if (Array.isArray(filter.value)) {
|
||||
return filter.value.length;
|
||||
}
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
// Clear all filters
|
||||
clearAllFilters(): void {
|
||||
// Clear base filters
|
||||
if (this.baseFilters) {
|
||||
this.baseFilters.forEach(filter => {
|
||||
if (filter.type === 'multiselect') {
|
||||
filter.value = [];
|
||||
} else if (filter.type === 'date-range') {
|
||||
filter.value = { start: null, end: null };
|
||||
} else if (filter.type === 'toggle') {
|
||||
filter.value = false;
|
||||
} else {
|
||||
filter.value = '';
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// Clear drilldown filters
|
||||
if (this.drilldownFilters) {
|
||||
this.drilldownFilters.forEach(filter => {
|
||||
if (filter.type === 'multiselect') {
|
||||
filter.value = [];
|
||||
} else if (filter.type === 'date-range') {
|
||||
filter.value = { start: null, end: null };
|
||||
} else if (filter.type === 'toggle') {
|
||||
filter.value = false;
|
||||
} else {
|
||||
filter.value = '';
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// Clear layer filters
|
||||
if (this.drilldownLayers) {
|
||||
this.drilldownLayers.forEach(layer => {
|
||||
if (layer.filters) {
|
||||
layer.filters.forEach((filter: any) => {
|
||||
if (filter.type === 'multiselect') {
|
||||
filter.value = [];
|
||||
} else if (filter.type === 'date-range') {
|
||||
filter.value = { start: null, end: null };
|
||||
} else if (filter.type === 'toggle') {
|
||||
filter.value = false;
|
||||
} else {
|
||||
filter.value = '';
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// Close all multiselect dropdowns
|
||||
this.openMultiselects.clear();
|
||||
|
||||
// Refresh data
|
||||
this.fetchGridData();
|
||||
}
|
||||
|
||||
ngOnDestroy() {
|
||||
// Unsubscribe from all subscriptions to prevent memory leaks
|
||||
console.log('GridViewComponent ngOnDestroy called, unsubscribing from', this.subscriptions.length, 'subscriptions');
|
||||
this.subscriptions.forEach(subscription => {
|
||||
if (subscription && !subscription.closed) {
|
||||
subscription.unsubscribe();
|
||||
}
|
||||
});
|
||||
this.subscriptions = [];
|
||||
|
||||
// Clear data to help with garbage collection
|
||||
this.givendata = [];
|
||||
this.dynamicHeaders = [];
|
||||
this.drilldownStack = [];
|
||||
this.originalGridData = [];
|
||||
|
||||
// Clear multiselect tracking
|
||||
this.openMultiselects.clear();
|
||||
|
||||
// Remove document click handler
|
||||
this.removeDocumentClickHandler();
|
||||
|
||||
console.log('GridViewComponent destroyed and cleaned up');
|
||||
}
|
||||
}
|
||||
@@ -1,13 +1,282 @@
|
||||
<div style="display: block">
|
||||
<!-- Drilldown mode indicator -->
|
||||
<div *ngIf="currentDrilldownLevel > 0" style="background-color: #e0e0e0; padding: 5px; margin-bottom: 10px; border-radius: 4px; text-align: center;">
|
||||
<span style="font-weight: bold; color: #333;">Drilldown Level: {{currentDrilldownLevel}}</span>
|
||||
<button (click)="navigateBack()" style="margin-left: 10px; padding: 2px 8px; background-color: #007cba; color: white; border: none; border-radius: 3px; cursor: pointer;">
|
||||
Back to Level {{currentDrilldownLevel - 1}}
|
||||
</button>
|
||||
<button (click)="resetToOriginalData()" style="margin-left: 10px; padding: 2px 8px; background-color: #dc3545; color: white; border: none; border-radius: 3px; cursor: pointer;">
|
||||
Back to Main View
|
||||
</button>
|
||||
<div style="display: block; height: 100%; width: 100%;">
|
||||
<!-- Filter Controls Section -->
|
||||
<div class="filter-section" *ngIf="hasActiveFilters()">
|
||||
<!-- Base Filters -->
|
||||
<div class="filter-group" *ngIf="baseFilters && baseFilters.length > 0">
|
||||
<h4>Base Filters</h4>
|
||||
<div class="filter-controls">
|
||||
<div *ngFor="let filter of baseFilters" class="filter-item">
|
||||
<div class="filter-label">{{ filter.field }} ({{ filter.type || 'text' }})</div>
|
||||
|
||||
<!-- Text Filter -->
|
||||
<div *ngIf="!filter.type || filter.type === 'text'" class="filter-input">
|
||||
<input type="text"
|
||||
[(ngModel)]="filter.value"
|
||||
(ngModelChange)="onBaseFilterChange(filter)"
|
||||
[placeholder]="filter.field"
|
||||
class="clr-input filter-text-input">
|
||||
</div>
|
||||
|
||||
<!-- Dropdown Filter -->
|
||||
<div *ngIf="filter.type === 'dropdown'" class="filter-input">
|
||||
<select [(ngModel)]="filter.value"
|
||||
(ngModelChange)="onBaseFilterChange(filter)"
|
||||
class="clr-select filter-select">
|
||||
<option value="">Select {{ filter.field }}</option>
|
||||
<option *ngFor="let option of getFilterOptions(filter)" [value]="option">{{ option }}</option>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<!-- Multi-Select Filter - Updated to show key first, then dropdown on click -->
|
||||
<div *ngIf="filter.type === 'multiselect'" class="filter-input multiselect-container">
|
||||
<div class="multiselect-display" (click)="toggleMultiselect(filter, 'base')">
|
||||
<span class="multiselect-label">{{ filter.field }}</span>
|
||||
<span class="multiselect-value" *ngIf="getSelectedOptionsCount(filter) > 0">
|
||||
({{ getSelectedOptionsCount(filter) }} selected)
|
||||
</span>
|
||||
<clr-icon shape="caret down" class="dropdown-icon"></clr-icon>
|
||||
</div>
|
||||
<div class="multiselect-dropdown" *ngIf="isMultiselectOpen(filter, 'base')">
|
||||
<div class="checkbox-group">
|
||||
<div *ngFor="let option of getFilterOptions(filter); let i = index" class="checkbox-item">
|
||||
<input type="checkbox"
|
||||
[checked]="isOptionSelected(filter, option)"
|
||||
(change)="onMultiSelectChange(filter, option, $event)"
|
||||
[id]="'base-' + filter.field + '-' + i"
|
||||
class="clr-checkbox">
|
||||
<label [for]="'base-' + filter.field + '-' + i" class="checkbox-label">{{ option }}</label>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Date Range Filter -->
|
||||
<div *ngIf="filter.type === 'date-range'" class="filter-input date-range">
|
||||
<div class="date-input-group">
|
||||
<input type="date"
|
||||
[(ngModel)]="filter.value.start"
|
||||
(ngModelChange)="onDateRangeChange(filter, { start: $event, end: filter.value.end })"
|
||||
placeholder="Start Date"
|
||||
class="clr-input filter-date">
|
||||
<span class="date-separator">to</span>
|
||||
<input type="date"
|
||||
[(ngModel)]="filter.value.end"
|
||||
(ngModelChange)="onDateRangeChange(filter, { start: filter.value.start, end: $event })"
|
||||
placeholder="End Date"
|
||||
class="clr-input filter-date">
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Toggle Filter -->
|
||||
<div *ngIf="filter.type === 'toggle'" class="filter-input toggle">
|
||||
<input type="checkbox"
|
||||
[(ngModel)]="filter.value"
|
||||
(ngModelChange)="onToggleChange(filter, $event)"
|
||||
clrToggle
|
||||
class="clr-toggle">
|
||||
<label class="toggle-label">{{ filter.field }}</label>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Drilldown Filters -->
|
||||
<div class="filter-group" *ngIf="drilldownFilters && drilldownFilters.length > 0 && currentDrilldownLevel > 0">
|
||||
<h4>Drilldown Filters</h4>
|
||||
<div class="filter-controls">
|
||||
<div *ngFor="let filter of drilldownFilters" class="filter-item">
|
||||
<div class="filter-label">{{ filter.field }} ({{ filter.type || 'text' }})</div>
|
||||
|
||||
<!-- Text Filter -->
|
||||
<div *ngIf="!filter.type || filter.type === 'text'" class="filter-input">
|
||||
<input type="text"
|
||||
[(ngModel)]="filter.value"
|
||||
(ngModelChange)="onDrilldownFilterChange(filter)"
|
||||
[placeholder]="filter.field"
|
||||
class="clr-input filter-text-input">
|
||||
</div>
|
||||
|
||||
<!-- Dropdown Filter -->
|
||||
<div *ngIf="filter.type === 'dropdown'" class="filter-input">
|
||||
<select [(ngModel)]="filter.value"
|
||||
(ngModelChange)="onDrilldownFilterChange(filter)"
|
||||
class="clr-select filter-select">
|
||||
<option value="">Select {{ filter.field }}</option>
|
||||
<option *ngFor="let option of getFilterOptions(filter)" [value]="option">{{ option }}</option>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<!-- Multi-Select Filter - Updated to show key first, then dropdown on click -->
|
||||
<div *ngIf="filter.type === 'multiselect'" class="filter-input multiselect-container">
|
||||
<div class="multiselect-display" (click)="toggleMultiselect(filter, 'drilldown')">
|
||||
<span class="multiselect-label">{{ filter.field }}</span>
|
||||
<span class="multiselect-value" *ngIf="getSelectedOptionsCount(filter) > 0">
|
||||
({{ getSelectedOptionsCount(filter) }} selected)
|
||||
</span>
|
||||
<clr-icon shape="caret down" class="dropdown-icon"></clr-icon>
|
||||
</div>
|
||||
<div class="multiselect-dropdown" *ngIf="isMultiselectOpen(filter, 'drilldown')">
|
||||
<div class="checkbox-group">
|
||||
<div *ngFor="let option of getFilterOptions(filter); let i = index" class="checkbox-item">
|
||||
<input type="checkbox"
|
||||
[checked]="isOptionSelected(filter, option)"
|
||||
(change)="onMultiSelectChange(filter, option, $event)"
|
||||
[id]="'drilldown-' + filter.field + '-' + i"
|
||||
class="clr-checkbox">
|
||||
<label [for]="'drilldown-' + filter.field + '-' + i" class="checkbox-label">{{ option }}</label>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Date Range Filter -->
|
||||
<div *ngIf="filter.type === 'date-range'" class="filter-input date-range">
|
||||
<div class="date-input-group">
|
||||
<input type="date"
|
||||
[(ngModel)]="filter.value.start"
|
||||
(ngModelChange)="onDateRangeChange(filter, { start: $event, end: filter.value.end })"
|
||||
placeholder="Start Date"
|
||||
class="clr-input filter-date">
|
||||
<span class="date-separator">to</span>
|
||||
<input type="date"
|
||||
[(ngModel)]="filter.value.end"
|
||||
(ngModelChange)="onDateRangeChange(filter, { start: filter.value.start, end: $event })"
|
||||
placeholder="End Date"
|
||||
class="clr-input filter-date">
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Toggle Filter -->
|
||||
<div *ngIf="filter.type === 'toggle'" class="filter-input toggle">
|
||||
<input type="checkbox"
|
||||
[(ngModel)]="filter.value"
|
||||
(ngModelChange)="onToggleChange(filter, $event)"
|
||||
clrToggle
|
||||
class="clr-toggle">
|
||||
<label class="toggle-label">{{ filter.field }}</label>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Layer Filters -->
|
||||
<div class="filter-group" *ngIf="hasActiveLayerFilters()">
|
||||
<h4>Layer Filters</h4>
|
||||
<div class="filter-controls">
|
||||
<div *ngFor="let filter of getActiveLayerFilters()" class="filter-item">
|
||||
<div class="filter-label">{{ filter.field }} ({{ filter.type || 'text' }})</div>
|
||||
|
||||
<!-- Text Filter -->
|
||||
<div *ngIf="!filter.type || filter.type === 'text'" class="filter-input">
|
||||
<input type="text"
|
||||
[(ngModel)]="filter.value"
|
||||
(ngModelChange)="onLayerFilterChange(filter)"
|
||||
[placeholder]="filter.field"
|
||||
class="clr-input filter-text-input">
|
||||
</div>
|
||||
|
||||
<!-- Dropdown Filter -->
|
||||
<div *ngIf="filter.type === 'dropdown'" class="filter-input">
|
||||
<select [(ngModel)]="filter.value"
|
||||
(ngModelChange)="onLayerFilterChange(filter)"
|
||||
class="clr-select filter-select">
|
||||
<option value="">Select {{ filter.field }}</option>
|
||||
<option *ngFor="let option of getFilterOptions(filter)" [value]="option">{{ option }}</option>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<!-- Multi-Select Filter - Updated to show key first, then dropdown on click -->
|
||||
<div *ngIf="filter.type === 'multiselect'" class="filter-input multiselect-container">
|
||||
<div class="multiselect-display" (click)="toggleMultiselect(filter, 'layer')">
|
||||
<span class="multiselect-label">{{ filter.field }}</span>
|
||||
<span class="multiselect-value" *ngIf="getSelectedOptionsCount(filter) > 0">
|
||||
({{ getSelectedOptionsCount(filter) }} selected)
|
||||
</span>
|
||||
<clr-icon shape="caret down" class="dropdown-icon"></clr-icon>
|
||||
</div>
|
||||
<div class="multiselect-dropdown" *ngIf="isMultiselectOpen(filter, 'layer')">
|
||||
<div class="checkbox-group">
|
||||
<div *ngFor="let option of getFilterOptions(filter); let i = index" class="checkbox-item">
|
||||
<input type="checkbox"
|
||||
[checked]="isOptionSelected(filter, option)"
|
||||
(change)="onMultiSelectChange(filter, option, $event)"
|
||||
[id]="'layer-' + filter.field + '-' + i"
|
||||
class="clr-checkbox">
|
||||
<label [for]="'layer-' + filter.field + '-' + i" class="checkbox-label">{{ option }}</label>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Date Range Filter -->
|
||||
<div *ngIf="filter.type === 'date-range'" class="filter-input date-range">
|
||||
<div class="date-input-group">
|
||||
<input type="date"
|
||||
[(ngModel)]="filter.value.start"
|
||||
(ngModelChange)="onDateRangeChange(filter, { start: $event, end: filter.value.end })"
|
||||
placeholder="Start Date"
|
||||
class="clr-input filter-date">
|
||||
<span class="date-separator">to</span>
|
||||
<input type="date"
|
||||
[(ngModel)]="filter.value.end"
|
||||
(ngModelChange)="onDateRangeChange(filter, { start: filter.value.start, end: $event })"
|
||||
placeholder="End Date"
|
||||
class="clr-input filter-date">
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Toggle Filter -->
|
||||
<div *ngIf="filter.type === 'toggle'" class="filter-input toggle">
|
||||
<input type="checkbox"
|
||||
[(ngModel)]="filter.value"
|
||||
(ngModelChange)="onToggleChange(filter, $event)"
|
||||
clrToggle
|
||||
class="clr-toggle">
|
||||
<label class="toggle-label">{{ filter.field }}</label>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Clear Filters Button -->
|
||||
<div class="filter-actions">
|
||||
<button class="btn btn-sm btn-outline" (click)="clearAllFilters()">Clear All Filters</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Header row with chart title and drilldown navigation -->
|
||||
<div class="clr-row header-row">
|
||||
<div class="clr-col-6">
|
||||
<h3 class="chart-title">{{charttitle || 'Line Chart'}}</h3>
|
||||
</div>
|
||||
<div class="clr-col-6" style="text-align: right;">
|
||||
<!-- Add drilldown navigation controls -->
|
||||
<button class="btn btn-sm btn-link" *ngIf="drilldownEnabled && drilldownStack.length > 0" (click)="navigateBack()">
|
||||
<cds-icon shape="arrow" direction="left"></cds-icon>
|
||||
Back to {{drilldownStack.length > 1 ? 'Previous Level' : 'Main Data'}}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Show current drilldown level -->
|
||||
<div class="clr-row" *ngIf="drilldownEnabled && drilldownStack.length > 0">
|
||||
<div class="clr-col-12">
|
||||
<div class="alert alert-info" style="padding: 8px 12px; margin-bottom: 12px;">
|
||||
<div class="alert-items">
|
||||
<div class="alert-item static">
|
||||
<div class="alert-icon-wrapper">
|
||||
<cds-icon class="alert-icon" shape="info-circle"></cds-icon>
|
||||
</div>
|
||||
<span class="alert-text">
|
||||
Drilldown Level: {{currentDrilldownLevel}}
|
||||
<span *ngIf="drilldownStack.length > 0">
|
||||
(Clicked on: {{drilldownStack[drilldownStack.length - 1].clickedLabel}})
|
||||
</span>
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- No data message -->
|
||||
@@ -16,12 +285,11 @@
|
||||
</div>
|
||||
|
||||
<!-- Chart display -->
|
||||
<div *ngIf="!noDataAvailable">
|
||||
<div *ngIf="!noDataAvailable" style="position: relative; height: calc(100% - 50px);">
|
||||
<canvas baseChart
|
||||
[datasets]="lineChartData"
|
||||
[labels]="lineChartLabels"
|
||||
[options]="lineChartOptions"
|
||||
|
||||
[legend]="lineChartLegend"
|
||||
[type]="lineChartType"
|
||||
(chartHover)="chartHovered($event)"
|
||||
|
||||
@@ -0,0 +1,192 @@
|
||||
.filter-section {
|
||||
margin-bottom: 20px;
|
||||
padding: 15px;
|
||||
border: 1px solid #ddd;
|
||||
border-radius: 4px;
|
||||
background-color: #f9f9f9;
|
||||
}
|
||||
|
||||
.filter-group {
|
||||
margin-bottom: 15px;
|
||||
|
||||
h4 {
|
||||
margin-top: 0;
|
||||
margin-bottom: 10px;
|
||||
color: #333;
|
||||
font-weight: 600;
|
||||
}
|
||||
}
|
||||
|
||||
.filter-controls {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 15px;
|
||||
}
|
||||
|
||||
.filter-item {
|
||||
flex: 1 1 300px;
|
||||
min-width: 250px;
|
||||
padding: 10px;
|
||||
background: white;
|
||||
border: 1px solid #e0e0e0;
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
.filter-label {
|
||||
font-weight: 500;
|
||||
margin-bottom: 8px;
|
||||
color: #555;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.filter-input {
|
||||
width: 100%;
|
||||
|
||||
.filter-text-input,
|
||||
.filter-select,
|
||||
.filter-date {
|
||||
width: 100%;
|
||||
padding: 6px 12px;
|
||||
border: 1px solid #ccc;
|
||||
border-radius: 4px;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.filter-select {
|
||||
height: 34px;
|
||||
}
|
||||
}
|
||||
|
||||
.multiselect-container {
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.multiselect-display {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
padding: 6px 12px;
|
||||
border: 1px solid #ccc;
|
||||
border-radius: 4px;
|
||||
background: white;
|
||||
cursor: pointer;
|
||||
min-height: 34px;
|
||||
|
||||
.multiselect-label {
|
||||
flex: 1;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.multiselect-value {
|
||||
color: #666;
|
||||
font-size: 12px;
|
||||
margin-right: 8px;
|
||||
}
|
||||
|
||||
.dropdown-icon {
|
||||
flex-shrink: 0;
|
||||
transition: transform 0.2s ease;
|
||||
}
|
||||
|
||||
&:hover {
|
||||
border-color: #999;
|
||||
}
|
||||
}
|
||||
|
||||
.multiselect-dropdown {
|
||||
position: absolute;
|
||||
top: 100%;
|
||||
left: 0;
|
||||
right: 0;
|
||||
z-index: 1000;
|
||||
background: white;
|
||||
border: 1px solid #ccc;
|
||||
border-top: none;
|
||||
border-radius: 0 0 4px 4px;
|
||||
box-shadow: 0 2px 4px rgba(0,0,0,0.1);
|
||||
max-height: 200px;
|
||||
overflow-y: auto;
|
||||
|
||||
.checkbox-group {
|
||||
padding: 8px;
|
||||
|
||||
.checkbox-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
padding: 4px 0;
|
||||
|
||||
.checkbox-label {
|
||||
margin: 0;
|
||||
font-size: 14px;
|
||||
cursor: pointer;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.date-range {
|
||||
.date-input-group {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.date-separator {
|
||||
margin: 0 5px;
|
||||
color: #777;
|
||||
}
|
||||
}
|
||||
|
||||
.toggle {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
|
||||
.toggle-label {
|
||||
margin: 0;
|
||||
font-size: 14px;
|
||||
cursor: pointer;
|
||||
}
|
||||
}
|
||||
|
||||
.filter-actions {
|
||||
margin-top: 15px;
|
||||
padding-top: 15px;
|
||||
border-top: 1px solid #eee;
|
||||
|
||||
.btn {
|
||||
font-size: 13px;
|
||||
}
|
||||
}
|
||||
|
||||
// New header row styling
|
||||
.header-row {
|
||||
margin-bottom: 15px;
|
||||
padding-bottom: 10px;
|
||||
border-bottom: 1px solid #eee;
|
||||
|
||||
.chart-title {
|
||||
margin: 0;
|
||||
font-size: 18px;
|
||||
font-weight: 600;
|
||||
color: #333;
|
||||
}
|
||||
}
|
||||
|
||||
// Responsive design
|
||||
@media (max-width: 768px) {
|
||||
.filter-controls {
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.filter-item {
|
||||
min-width: 100%;
|
||||
}
|
||||
|
||||
.header-row {
|
||||
.chart-title {
|
||||
font-size: 16px;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,12 +1,14 @@
|
||||
import { Component, OnInit, Input, OnChanges, SimpleChanges } from '@angular/core';
|
||||
import { Component, OnInit, Input, OnChanges, SimpleChanges, OnDestroy } from '@angular/core';
|
||||
import { Dashboard3Service } from 'src/app/services/builder/dashboard3.service';
|
||||
import { FilterService } from '../../common-filter/filter.service';
|
||||
import { Subscription } from 'rxjs';
|
||||
|
||||
@Component({
|
||||
selector: 'app-line-chart',
|
||||
templateUrl: './line-chart.component.html',
|
||||
styleUrls: ['./line-chart.component.scss']
|
||||
})
|
||||
export class LineChartComponent implements OnInit, OnChanges {
|
||||
export class LineChartComponent implements OnInit, OnChanges, OnDestroy {
|
||||
@Input() xAxis: string;
|
||||
@Input() yAxis: string | string[];
|
||||
@Input() table: string;
|
||||
@@ -83,9 +85,28 @@ export class LineChartComponent implements OnInit, OnChanges {
|
||||
// Flag to prevent infinite loops
|
||||
private isFetchingData: boolean = false;
|
||||
|
||||
constructor(private dashboardService: Dashboard3Service) { }
|
||||
// Subscriptions to unsubscribe on destroy
|
||||
private subscriptions: Subscription[] = [];
|
||||
|
||||
// Add properties for filter functionality
|
||||
private openMultiselects: Map<string, string> = new Map(); // Map of filterId -> context
|
||||
private documentClickHandler: ((event: MouseEvent) => void) | null = null;
|
||||
private filtersInitialized: boolean = false;
|
||||
|
||||
constructor(
|
||||
private dashboardService: Dashboard3Service,
|
||||
private filterService: FilterService
|
||||
) { }
|
||||
|
||||
ngOnInit(): void {
|
||||
// Subscribe to filter changes
|
||||
this.subscriptions.push(
|
||||
this.filterService.filterState$.subscribe(filters => {
|
||||
// When filters change, refresh the chart data
|
||||
this.fetchChartData();
|
||||
})
|
||||
);
|
||||
|
||||
// Initialize with default data
|
||||
this.fetchChartData();
|
||||
}
|
||||
@@ -93,6 +114,12 @@ export class LineChartComponent implements OnInit, OnChanges {
|
||||
ngOnChanges(changes: SimpleChanges): void {
|
||||
console.log('LineChartComponent input changes:', changes);
|
||||
|
||||
// Initialize filter values if they haven't been initialized yet
|
||||
if (!this.filtersInitialized && (changes.baseFilters || changes.drilldownFilters || changes.drilldownLayers)) {
|
||||
this.initializeFilterValues();
|
||||
this.filtersInitialized = true;
|
||||
}
|
||||
|
||||
// Check if any of the key properties have changed
|
||||
const xAxisChanged = changes.xAxis && !changes.xAxis.firstChange;
|
||||
const yAxisChanged = changes.yAxis && !changes.yAxis.firstChange;
|
||||
@@ -122,6 +149,327 @@ export class LineChartComponent implements OnInit, OnChanges {
|
||||
}
|
||||
}
|
||||
|
||||
ngOnDestroy(): void {
|
||||
this.subscriptions.forEach(sub => sub.unsubscribe());
|
||||
// Clean up document click handler
|
||||
this.removeDocumentClickHandler();
|
||||
}
|
||||
|
||||
// Initialize filter values with proper default values based on type
|
||||
private initializeFilterValues(): void {
|
||||
console.log('Initializing filter values');
|
||||
|
||||
// Initialize base filters
|
||||
if (this.baseFilters) {
|
||||
this.baseFilters.forEach(filter => {
|
||||
if (filter.value === undefined || filter.value === null) {
|
||||
switch (filter.type) {
|
||||
case 'multiselect':
|
||||
filter.value = [];
|
||||
break;
|
||||
case 'date-range':
|
||||
filter.value = { start: null, end: null };
|
||||
break;
|
||||
case 'toggle':
|
||||
filter.value = false;
|
||||
break;
|
||||
default:
|
||||
filter.value = '';
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// Initialize drilldown filters
|
||||
if (this.drilldownFilters) {
|
||||
this.drilldownFilters.forEach(filter => {
|
||||
if (filter.value === undefined || filter.value === null) {
|
||||
switch (filter.type) {
|
||||
case 'multiselect':
|
||||
filter.value = [];
|
||||
break;
|
||||
case 'date-range':
|
||||
filter.value = { start: null, end: null };
|
||||
break;
|
||||
case 'toggle':
|
||||
filter.value = false;
|
||||
break;
|
||||
default:
|
||||
filter.value = '';
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// Initialize layer filters
|
||||
if (this.drilldownLayers) {
|
||||
this.drilldownLayers.forEach(layer => {
|
||||
if (layer.filters) {
|
||||
layer.filters.forEach((filter: any) => {
|
||||
if (filter.value === undefined || filter.value === null) {
|
||||
switch (filter.type) {
|
||||
case 'multiselect':
|
||||
filter.value = [];
|
||||
break;
|
||||
case 'date-range':
|
||||
filter.value = { start: null, end: null };
|
||||
break;
|
||||
case 'toggle':
|
||||
filter.value = false;
|
||||
break;
|
||||
default:
|
||||
filter.value = '';
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
console.log('Filter values initialized:', {
|
||||
baseFilters: this.baseFilters,
|
||||
drilldownFilters: this.drilldownFilters,
|
||||
drilldownLayers: this.drilldownLayers
|
||||
});
|
||||
}
|
||||
|
||||
// Check if there are active filters
|
||||
hasActiveFilters(): boolean {
|
||||
return (this.baseFilters && this.baseFilters.length > 0) ||
|
||||
(this.drilldownFilters && this.drilldownFilters.length > 0) ||
|
||||
this.hasActiveLayerFilters();
|
||||
}
|
||||
|
||||
// Check if there are active layer filters for current drilldown level
|
||||
hasActiveLayerFilters(): boolean {
|
||||
if (this.currentDrilldownLevel > 1 && this.drilldownLayers && this.drilldownLayers.length > 0) {
|
||||
const layerIndex = this.currentDrilldownLevel - 2; // -2 because level 1 is base drilldown
|
||||
return layerIndex < this.drilldownLayers.length &&
|
||||
this.drilldownLayers[layerIndex].filters &&
|
||||
this.drilldownLayers[layerIndex].filters.length > 0;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
// Get active layer filters for current drilldown level
|
||||
getActiveLayerFilters(): any[] {
|
||||
if (this.currentDrilldownLevel > 1 && this.drilldownLayers && this.drilldownLayers.length > 0) {
|
||||
const layerIndex = this.currentDrilldownLevel - 2; // -2 because level 1 is base drilldown
|
||||
if (layerIndex < this.drilldownLayers.length &&
|
||||
this.drilldownLayers[layerIndex].filters) {
|
||||
return this.drilldownLayers[layerIndex].filters;
|
||||
}
|
||||
}
|
||||
return [];
|
||||
}
|
||||
|
||||
// Get filter options for dropdown/multiselect filters
|
||||
getFilterOptions(filter: any): string[] {
|
||||
if (filter.options) {
|
||||
if (Array.isArray(filter.options)) {
|
||||
return filter.options;
|
||||
} else if (typeof filter.options === 'string') {
|
||||
return filter.options.split(',').map(opt => opt.trim()).filter(opt => opt);
|
||||
}
|
||||
}
|
||||
return [];
|
||||
}
|
||||
|
||||
// Check if an option is selected for multiselect filters
|
||||
isOptionSelected(filter: any, option: string): boolean {
|
||||
if (!filter.value) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (Array.isArray(filter.value)) {
|
||||
return filter.value.includes(option);
|
||||
}
|
||||
|
||||
return filter.value === option;
|
||||
}
|
||||
|
||||
// Handle base filter changes
|
||||
onBaseFilterChange(filter: any): void {
|
||||
console.log('Base filter changed:', filter);
|
||||
// Refresh data when filter changes
|
||||
this.fetchChartData();
|
||||
}
|
||||
|
||||
// Handle drilldown filter changes
|
||||
onDrilldownFilterChange(filter: any): void {
|
||||
console.log('Drilldown filter changed:', filter);
|
||||
// Refresh data when filter changes
|
||||
this.fetchChartData();
|
||||
}
|
||||
|
||||
// Handle layer filter changes
|
||||
onLayerFilterChange(filter: any): void {
|
||||
console.log('Layer filter changed:', filter);
|
||||
// Refresh data when filter changes
|
||||
this.fetchChartData();
|
||||
}
|
||||
|
||||
// Handle multiselect changes
|
||||
onMultiSelectChange(filter: any, option: string, event: any): void {
|
||||
const checked = event.target.checked;
|
||||
|
||||
// Initialize filter.value as array if it's not already
|
||||
if (!Array.isArray(filter.value)) {
|
||||
filter.value = [];
|
||||
}
|
||||
|
||||
if (checked) {
|
||||
// Add option to array if not already present
|
||||
if (!filter.value.includes(option)) {
|
||||
filter.value.push(option);
|
||||
}
|
||||
} else {
|
||||
// Remove option from array
|
||||
filter.value = filter.value.filter((item: string) => item !== option);
|
||||
}
|
||||
|
||||
// Refresh data when filter changes
|
||||
this.fetchChartData();
|
||||
}
|
||||
|
||||
// Handle date range changes
|
||||
onDateRangeChange(filter: any, dateRange: { start: string | null, end: string | null }): void {
|
||||
filter.value = dateRange;
|
||||
// Refresh data when filter changes
|
||||
this.fetchChartData();
|
||||
}
|
||||
|
||||
// Handle toggle changes
|
||||
onToggleChange(filter: any, checked: boolean): void {
|
||||
filter.value = checked;
|
||||
// Refresh data when filter changes
|
||||
this.fetchChartData();
|
||||
}
|
||||
|
||||
// Toggle multiselect dropdown visibility
|
||||
toggleMultiselect(filter: any, context: string): void {
|
||||
const filterId = `${context}-${filter.field}`;
|
||||
if (this.isMultiselectOpen(filter, context)) {
|
||||
this.openMultiselects.delete(filterId);
|
||||
} else {
|
||||
// Close all other multiselects first
|
||||
this.openMultiselects.clear();
|
||||
this.openMultiselects.set(filterId, context);
|
||||
|
||||
// Add document click handler to close dropdown when clicking outside
|
||||
this.addDocumentClickHandler();
|
||||
}
|
||||
}
|
||||
|
||||
// Add document click handler to close dropdowns when clicking outside
|
||||
private addDocumentClickHandler(): void {
|
||||
if (!this.documentClickHandler) {
|
||||
this.documentClickHandler = (event: MouseEvent) => {
|
||||
const target = event.target as HTMLElement;
|
||||
// Check if click is outside any multiselect dropdown
|
||||
if (!target.closest('.multiselect-container')) {
|
||||
this.openMultiselects.clear();
|
||||
this.removeDocumentClickHandler();
|
||||
}
|
||||
};
|
||||
|
||||
// Use setTimeout to ensure the click event that opened the dropdown doesn't immediately close it
|
||||
setTimeout(() => {
|
||||
document.addEventListener('click', this.documentClickHandler!);
|
||||
}, 0);
|
||||
}
|
||||
}
|
||||
|
||||
// Remove document click handler
|
||||
private removeDocumentClickHandler(): void {
|
||||
if (this.documentClickHandler) {
|
||||
document.removeEventListener('click', this.documentClickHandler);
|
||||
this.documentClickHandler = null;
|
||||
}
|
||||
}
|
||||
|
||||
// Check if multiselect dropdown is open
|
||||
isMultiselectOpen(filter: any, context: string): boolean {
|
||||
const filterId = `${context}-${filter.field}`;
|
||||
return this.openMultiselects.has(filterId);
|
||||
}
|
||||
|
||||
// Get count of selected options for a multiselect filter
|
||||
getSelectedOptionsCount(filter: any): number {
|
||||
if (!filter.value) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
if (Array.isArray(filter.value)) {
|
||||
return filter.value.length;
|
||||
}
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
// Clear all filters
|
||||
clearAllFilters(): void {
|
||||
// Clear base filters
|
||||
if (this.baseFilters) {
|
||||
this.baseFilters.forEach(filter => {
|
||||
if (filter.type === 'multiselect') {
|
||||
filter.value = [];
|
||||
} else if (filter.type === 'date-range') {
|
||||
filter.value = { start: null, end: null };
|
||||
} else if (filter.type === 'toggle') {
|
||||
filter.value = false;
|
||||
} else {
|
||||
filter.value = '';
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// Clear drilldown filters
|
||||
if (this.drilldownFilters) {
|
||||
this.drilldownFilters.forEach(filter => {
|
||||
if (filter.type === 'multiselect') {
|
||||
filter.value = [];
|
||||
} else if (filter.type === 'date-range') {
|
||||
filter.value = { start: null, end: null };
|
||||
} else if (filter.type === 'toggle') {
|
||||
filter.value = false;
|
||||
} else {
|
||||
filter.value = '';
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// Clear layer filters
|
||||
if (this.drilldownLayers) {
|
||||
this.drilldownLayers.forEach(layer => {
|
||||
if (layer.filters) {
|
||||
layer.filters.forEach((filter: any) => {
|
||||
if (filter.type === 'multiselect') {
|
||||
filter.value = [];
|
||||
} else if (filter.type === 'date-range') {
|
||||
filter.value = { start: null, end: null };
|
||||
} else if (filter.type === 'toggle') {
|
||||
filter.value = false;
|
||||
} else {
|
||||
filter.value = '';
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// Close all multiselect dropdowns
|
||||
this.openMultiselects.clear();
|
||||
|
||||
// Refresh data
|
||||
this.fetchChartData();
|
||||
}
|
||||
|
||||
// Public method to refresh data when filters change
|
||||
refreshData(): void {
|
||||
this.fetchChartData();
|
||||
}
|
||||
|
||||
fetchChartData(): void {
|
||||
// Set flag to prevent recursive calls
|
||||
this.isFetchingData = true;
|
||||
@@ -154,7 +502,49 @@ export class LineChartComponent implements OnInit, OnChanges {
|
||||
filterParams = JSON.stringify(filterObj);
|
||||
}
|
||||
}
|
||||
console.log('Base filter parameters:', filterParams);
|
||||
|
||||
// Add common filters to filter parameters
|
||||
const commonFilters = this.filterService.getFilterValues();
|
||||
console.log('Common filters from service:', commonFilters);
|
||||
|
||||
if (Object.keys(commonFilters).length > 0) {
|
||||
// Merge common filters with base filters
|
||||
const mergedFilterObj = {};
|
||||
|
||||
// Add base filters first
|
||||
if (filterParams) {
|
||||
try {
|
||||
const baseFilterObj = JSON.parse(filterParams);
|
||||
Object.assign(mergedFilterObj, baseFilterObj);
|
||||
} catch (e) {
|
||||
console.warn('Failed to parse base filter parameters:', e);
|
||||
}
|
||||
}
|
||||
|
||||
// Add common filters using the field name as the key, not the filter id
|
||||
Object.keys(commonFilters).forEach(filterId => {
|
||||
const filterValue = commonFilters[filterId];
|
||||
// Find the filter definition to get the field name
|
||||
const filterDef = this.filterService.getFilters().find(f => f.id === filterId);
|
||||
if (filterDef && filterDef.field) {
|
||||
const fieldName = filterDef.field;
|
||||
if (filterValue !== undefined && filterValue !== null && filterValue !== '') {
|
||||
mergedFilterObj[fieldName] = filterValue;
|
||||
}
|
||||
} else {
|
||||
// Fallback to using filterId as field name if no field is defined
|
||||
if (filterValue !== undefined && filterValue !== null && filterValue !== '') {
|
||||
mergedFilterObj[filterId] = filterValue;
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
if (Object.keys(mergedFilterObj).length > 0) {
|
||||
filterParams = JSON.stringify(mergedFilterObj);
|
||||
}
|
||||
}
|
||||
|
||||
console.log('Final filter parameters:', filterParams);
|
||||
|
||||
// Log the URL that will be called
|
||||
const url = `chart/getdashjson/line?tableName=${this.table}&xAxis=${this.xAxis}&yAxes=${yAxisString}${this.connection ? `&sureId=${this.connection}` : ''}`;
|
||||
@@ -313,6 +703,35 @@ export class LineChartComponent implements OnInit, OnChanges {
|
||||
}
|
||||
}
|
||||
|
||||
// Add common filters to drilldown filter parameters
|
||||
const commonFilters = this.filterService.getFilterValues();
|
||||
if (Object.keys(commonFilters).length > 0) {
|
||||
// Merge common filters with drilldown filters
|
||||
const mergedFilterObj = {};
|
||||
|
||||
// Add drilldown filters first
|
||||
if (filterParams) {
|
||||
try {
|
||||
const drilldownFilterObj = JSON.parse(filterParams);
|
||||
Object.assign(mergedFilterObj, drilldownFilterObj);
|
||||
} catch (e) {
|
||||
console.warn('Failed to parse drilldown filter parameters:', e);
|
||||
}
|
||||
}
|
||||
|
||||
// Add common filters
|
||||
Object.keys(commonFilters).forEach(key => {
|
||||
const value = commonFilters[key];
|
||||
if (value !== undefined && value !== null && value !== '') {
|
||||
mergedFilterObj[key] = value;
|
||||
}
|
||||
});
|
||||
|
||||
if (Object.keys(mergedFilterObj).length > 0) {
|
||||
filterParams = JSON.stringify(mergedFilterObj);
|
||||
}
|
||||
}
|
||||
|
||||
// Log the URL that will be called
|
||||
const url = `chart/getdashjson/line?tableName=${actualApiUrl}&xAxis=${drilldownConfig.xAxis}&yAxes=${drilldownConfig.yAxis}${this.connection ? `&sureId=${this.connection}` : ''}`;
|
||||
console.log('Drilldown data URL:', url);
|
||||
|
||||
@@ -1,19 +1,287 @@
|
||||
<div class="pie-chart-container">
|
||||
<!-- Drilldown mode indicator -->
|
||||
<div *ngIf="currentDrilldownLevel > 0" style="background-color: #e0e0e0; padding: 5px; margin-bottom: 10px; border-radius: 4px; text-align: center;">
|
||||
<span style="font-weight: bold; color: #333;">Drilldown Level: {{currentDrilldownLevel}}</span>
|
||||
<button (click)="navigateBack()" style="margin-left: 10px; padding: 2px 8px; background-color: #007cba; color: white; border: none; border-radius: 3px; cursor: pointer;">
|
||||
Back to Level {{currentDrilldownLevel - 1}}
|
||||
</button>
|
||||
<button (click)="resetToOriginalData()" style="margin-left: 10px; padding: 2px 8px; background-color: #dc3545; color: white; border: none; border-radius: 3px; cursor: pointer;">
|
||||
Back to Main View
|
||||
</button>
|
||||
<!-- Filter Controls Section -->
|
||||
<div class="filter-section" *ngIf="hasActiveFilters()">
|
||||
<!-- Base Filters -->
|
||||
<div class="filter-group" *ngIf="baseFilters && baseFilters.length > 0">
|
||||
<h4>Base Filters</h4>
|
||||
<div class="filter-controls">
|
||||
<div *ngFor="let filter of baseFilters" class="filter-item">
|
||||
<div class="filter-label">{{ filter.field }} ({{ filter.type || 'text' }})</div>
|
||||
|
||||
<!-- Text Filter -->
|
||||
<div *ngIf="!filter.type || filter.type === 'text'" class="filter-input">
|
||||
<input type="text"
|
||||
[(ngModel)]="filter.value"
|
||||
(ngModelChange)="onBaseFilterChange(filter)"
|
||||
[placeholder]="filter.field"
|
||||
class="clr-input filter-text-input">
|
||||
</div>
|
||||
|
||||
<!-- Dropdown Filter -->
|
||||
<div *ngIf="filter.type === 'dropdown'" class="filter-input">
|
||||
<select [(ngModel)]="filter.value"
|
||||
(ngModelChange)="onBaseFilterChange(filter)"
|
||||
class="clr-select filter-select">
|
||||
<option value="">Select {{ filter.field }}</option>
|
||||
<option *ngFor="let option of getFilterOptions(filter)" [value]="option">{{ option }}</option>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<!-- Multi-Select Filter - Updated to show key first, then dropdown on click -->
|
||||
<div *ngIf="filter.type === 'multiselect'" class="filter-input multiselect-container">
|
||||
<div class="multiselect-display" (click)="toggleMultiselect(filter, 'base')">
|
||||
<span class="multiselect-label">{{ filter.field }}</span>
|
||||
<span class="multiselect-value" *ngIf="getSelectedOptionsCount(filter) > 0">
|
||||
({{ getSelectedOptionsCount(filter) }} selected)
|
||||
</span>
|
||||
<clr-icon shape="caret down" class="dropdown-icon"></clr-icon>
|
||||
</div>
|
||||
<div class="multiselect-dropdown" *ngIf="isMultiselectOpen(filter, 'base')">
|
||||
<div class="checkbox-group">
|
||||
<div *ngFor="let option of getFilterOptions(filter); let i = index" class="checkbox-item">
|
||||
<input type="checkbox"
|
||||
[checked]="isOptionSelected(filter, option)"
|
||||
(change)="onMultiSelectChange(filter, option, $event)"
|
||||
[id]="'base-' + filter.field + '-' + i"
|
||||
class="clr-checkbox">
|
||||
<label [for]="'base-' + filter.field + '-' + i" class="checkbox-label">{{ option }}</label>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Date Range Filter -->
|
||||
<div *ngIf="filter.type === 'date-range'" class="filter-input date-range">
|
||||
<div class="date-input-group">
|
||||
<input type="date"
|
||||
[(ngModel)]="filter.value.start"
|
||||
(ngModelChange)="onDateRangeChange(filter, { start: $event, end: filter.value.end })"
|
||||
placeholder="Start Date"
|
||||
class="clr-input filter-date">
|
||||
<span class="date-separator">to</span>
|
||||
<input type="date"
|
||||
[(ngModel)]="filter.value.end"
|
||||
(ngModelChange)="onDateRangeChange(filter, { start: filter.value.start, end: $event })"
|
||||
placeholder="End Date"
|
||||
class="clr-input filter-date">
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Toggle Filter -->
|
||||
<div *ngIf="filter.type === 'toggle'" class="filter-input toggle">
|
||||
<input type="checkbox"
|
||||
[(ngModel)]="filter.value"
|
||||
(ngModelChange)="onToggleChange(filter, $event)"
|
||||
clrToggle
|
||||
class="clr-toggle">
|
||||
<label class="toggle-label">{{ filter.field }}</label>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Drilldown Filters -->
|
||||
<div class="filter-group" *ngIf="drilldownFilters && drilldownFilters.length > 0 && currentDrilldownLevel > 0">
|
||||
<h4>Drilldown Filters</h4>
|
||||
<div class="filter-controls">
|
||||
<div *ngFor="let filter of drilldownFilters" class="filter-item">
|
||||
<div class="filter-label">{{ filter.field }} ({{ filter.type || 'text' }})</div>
|
||||
|
||||
<!-- Text Filter -->
|
||||
<div *ngIf="!filter.type || filter.type === 'text'" class="filter-input">
|
||||
<input type="text"
|
||||
[(ngModel)]="filter.value"
|
||||
(ngModelChange)="onDrilldownFilterChange(filter)"
|
||||
[placeholder]="filter.field"
|
||||
class="clr-input filter-text-input">
|
||||
</div>
|
||||
|
||||
<!-- Dropdown Filter -->
|
||||
<div *ngIf="filter.type === 'dropdown'" class="filter-input">
|
||||
<select [(ngModel)]="filter.value"
|
||||
(ngModelChange)="onDrilldownFilterChange(filter)"
|
||||
class="clr-select filter-select">
|
||||
<option value="">Select {{ filter.field }}</option>
|
||||
<option *ngFor="let option of getFilterOptions(filter)" [value]="option">{{ option }}</option>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<!-- Multi-Select Filter - Updated to show key first, then dropdown on click -->
|
||||
<div *ngIf="filter.type === 'multiselect'" class="filter-input multiselect-container">
|
||||
<div class="multiselect-display" (click)="toggleMultiselect(filter, 'drilldown')">
|
||||
<span class="multiselect-label">{{ filter.field }}</span>
|
||||
<span class="multiselect-value" *ngIf="getSelectedOptionsCount(filter) > 0">
|
||||
({{ getSelectedOptionsCount(filter) }} selected)
|
||||
</span>
|
||||
<clr-icon shape="caret down" class="dropdown-icon"></clr-icon>
|
||||
</div>
|
||||
<div class="multiselect-dropdown" *ngIf="isMultiselectOpen(filter, 'drilldown')">
|
||||
<div class="checkbox-group">
|
||||
<div *ngFor="let option of getFilterOptions(filter); let i = index" class="checkbox-item">
|
||||
<input type="checkbox"
|
||||
[checked]="isOptionSelected(filter, option)"
|
||||
(change)="onMultiSelectChange(filter, option, $event)"
|
||||
[id]="'drilldown-' + filter.field + '-' + i"
|
||||
class="clr-checkbox">
|
||||
<label [for]="'drilldown-' + filter.field + '-' + i" class="checkbox-label">{{ option }}</label>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Date Range Filter -->
|
||||
<div *ngIf="filter.type === 'date-range'" class="filter-input date-range">
|
||||
<div class="date-input-group">
|
||||
<input type="date"
|
||||
[(ngModel)]="filter.value.start"
|
||||
(ngModelChange)="onDateRangeChange(filter, { start: $event, end: filter.value.end })"
|
||||
placeholder="Start Date"
|
||||
class="clr-input filter-date">
|
||||
<span class="date-separator">to</span>
|
||||
<input type="date"
|
||||
[(ngModel)]="filter.value.end"
|
||||
(ngModelChange)="onDateRangeChange(filter, { start: filter.value.start, end: $event })"
|
||||
placeholder="End Date"
|
||||
class="clr-input filter-date">
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Toggle Filter -->
|
||||
<div *ngIf="filter.type === 'toggle'" class="filter-input toggle">
|
||||
<input type="checkbox"
|
||||
[(ngModel)]="filter.value"
|
||||
(ngModelChange)="onToggleChange(filter, $event)"
|
||||
clrToggle
|
||||
class="clr-toggle">
|
||||
<label class="toggle-label">{{ filter.field }}</label>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Layer Filters -->
|
||||
<div class="filter-group" *ngIf="hasActiveLayerFilters()">
|
||||
<h4>Layer Filters</h4>
|
||||
<div class="filter-controls">
|
||||
<div *ngFor="let filter of getActiveLayerFilters()" class="filter-item">
|
||||
<div class="filter-label">{{ filter.field }} ({{ filter.type || 'text' }})</div>
|
||||
|
||||
<!-- Text Filter -->
|
||||
<div *ngIf="!filter.type || filter.type === 'text'" class="filter-input">
|
||||
<input type="text"
|
||||
[(ngModel)]="filter.value"
|
||||
(ngModelChange)="onLayerFilterChange(filter)"
|
||||
[placeholder]="filter.field"
|
||||
class="clr-input filter-text-input">
|
||||
</div>
|
||||
|
||||
<!-- Dropdown Filter -->
|
||||
<div *ngIf="filter.type === 'dropdown'" class="filter-input">
|
||||
<select [(ngModel)]="filter.value"
|
||||
(ngModelChange)="onLayerFilterChange(filter)"
|
||||
class="clr-select filter-select">
|
||||
<option value="">Select {{ filter.field }}</option>
|
||||
<option *ngFor="let option of getFilterOptions(filter)" [value]="option">{{ option }}</option>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<!-- Multi-Select Filter - Updated to show key first, then dropdown on click -->
|
||||
<div *ngIf="filter.type === 'multiselect'" class="filter-input multiselect-container">
|
||||
<div class="multiselect-display" (click)="toggleMultiselect(filter, 'layer')">
|
||||
<span class="multiselect-label">{{ filter.field }}</span>
|
||||
<span class="multiselect-value" *ngIf="getSelectedOptionsCount(filter) > 0">
|
||||
({{ getSelectedOptionsCount(filter) }} selected)
|
||||
</span>
|
||||
<clr-icon shape="caret down" class="dropdown-icon"></clr-icon>
|
||||
</div>
|
||||
<div class="multiselect-dropdown" *ngIf="isMultiselectOpen(filter, 'layer')">
|
||||
<div class="checkbox-group">
|
||||
<div *ngFor="let option of getFilterOptions(filter); let i = index" class="checkbox-item">
|
||||
<input type="checkbox"
|
||||
[checked]="isOptionSelected(filter, option)"
|
||||
(change)="onMultiSelectChange(filter, option, $event)"
|
||||
[id]="'layer-' + filter.field + '-' + i"
|
||||
class="clr-checkbox">
|
||||
<label [for]="'layer-' + filter.field + '-' + i" class="checkbox-label">{{ option }}</label>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Date Range Filter -->
|
||||
<div *ngIf="filter.type === 'date-range'" class="filter-input date-range">
|
||||
<div class="date-input-group">
|
||||
<input type="date"
|
||||
[(ngModel)]="filter.value.start"
|
||||
(ngModelChange)="onDateRangeChange(filter, { start: $event, end: filter.value.end })"
|
||||
placeholder="Start Date"
|
||||
class="clr-input filter-date">
|
||||
<span class="date-separator">to</span>
|
||||
<input type="date"
|
||||
[(ngModel)]="filter.value.end"
|
||||
(ngModelChange)="onDateRangeChange(filter, { start: filter.value.start, end: $event })"
|
||||
placeholder="End Date"
|
||||
class="clr-input filter-date">
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Toggle Filter -->
|
||||
<div *ngIf="filter.type === 'toggle'" class="filter-input toggle">
|
||||
<input type="checkbox"
|
||||
[(ngModel)]="filter.value"
|
||||
(ngModelChange)="onToggleChange(filter, $event)"
|
||||
clrToggle
|
||||
class="clr-toggle">
|
||||
<label class="toggle-label">{{ filter.field }}</label>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Clear Filters Button -->
|
||||
<div class="filter-actions">
|
||||
<button class="btn btn-sm btn-outline" (click)="clearAllFilters()">Clear All Filters</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Header row with chart title and drilldown navigation -->
|
||||
<div class="clr-row header-row">
|
||||
<div class="clr-col-6">
|
||||
<h3 class="chart-title" *ngIf="charttitle">{{ charttitle }}</h3>
|
||||
</div>
|
||||
<div class="clr-col-6" style="text-align: right;">
|
||||
<!-- Add drilldown navigation controls -->
|
||||
<button class="btn btn-sm btn-link" *ngIf="drilldownEnabled && drilldownStack.length > 0" (click)="navigateBack()">
|
||||
<cds-icon shape="arrow" direction="left"></cds-icon>
|
||||
Back to {{drilldownStack.length > 1 ? 'Previous Level' : 'Main Data'}}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Show current drilldown level -->
|
||||
<div class="clr-row" *ngIf="drilldownEnabled && drilldownStack.length > 0">
|
||||
<div class="clr-col-12">
|
||||
<div class="alert alert-info" style="padding: 8px 12px; margin-bottom: 12px;">
|
||||
<div class="alert-items">
|
||||
<div class="alert-item static">
|
||||
<div class="alert-icon-wrapper">
|
||||
<cds-icon class="alert-icon" shape="info-circle"></cds-icon>
|
||||
</div>
|
||||
<span class="alert-text">
|
||||
Drilldown Level: {{currentDrilldownLevel}}
|
||||
<span *ngIf="drilldownStack.length > 0">
|
||||
(Clicked on: {{drilldownStack[drilldownStack.length - 1].clickedLabel}})
|
||||
</span>
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<h3 class="chart-title" *ngIf="charttitle">{{ charttitle }}</h3>
|
||||
<div class="chart-wrapper">
|
||||
<!-- Show loading indicator -->
|
||||
<div class="loading-indicator" *ngIf="pieChartLabels.length === 0 && pieChartData.length === 0 && !noDataAvailable">
|
||||
<div class="loading-indicator" *ngIf="pieChartLabels.length === 0 && pieChartData.length === 0 && !noDataAvailable && !isFetchingData">
|
||||
<div class="spinner"></div>
|
||||
<p>Loading chart data...</p>
|
||||
</div>
|
||||
@@ -23,15 +291,15 @@
|
||||
<p>No chart data available</p>
|
||||
</div>
|
||||
|
||||
<!-- Show chart when data is available -->
|
||||
<!-- Show chart when data is available or show default data -->
|
||||
<canvas baseChart
|
||||
*ngIf="pieChartLabels.length > 0 && pieChartData.length > 0"
|
||||
[data]="pieChartData"
|
||||
[datasets]="pieChartDatasets"
|
||||
[labels]="pieChartLabels"
|
||||
[type]="pieChartType"
|
||||
[options]="pieChartOptions"
|
||||
(chartHover)="chartHovered($event)"
|
||||
(chartClick)="chartClicked($event)">
|
||||
(chartClick)="chartClicked($event)"
|
||||
[style.display]="shouldShowChart() ? 'block' : 'none'">
|
||||
</canvas>
|
||||
</div>
|
||||
<div class="chart-legend" *ngIf="showlabel && pieChartLabels && pieChartLabels.length > 0">
|
||||
|
||||
@@ -149,10 +149,192 @@
|
||||
100% { transform: rotate(360deg); }
|
||||
}
|
||||
|
||||
/* Responsive design */
|
||||
// Filter section styles
|
||||
.filter-section {
|
||||
margin-bottom: 20px;
|
||||
padding: 15px;
|
||||
border: 1px solid #ddd;
|
||||
border-radius: 4px;
|
||||
background-color: #f9f9f9;
|
||||
}
|
||||
|
||||
.filter-group {
|
||||
margin-bottom: 15px;
|
||||
|
||||
h4 {
|
||||
margin-top: 0;
|
||||
margin-bottom: 10px;
|
||||
color: #333;
|
||||
font-weight: 600;
|
||||
}
|
||||
}
|
||||
|
||||
.filter-controls {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 15px;
|
||||
}
|
||||
|
||||
.filter-item {
|
||||
flex: 1 1 300px;
|
||||
min-width: 250px;
|
||||
padding: 10px;
|
||||
background: white;
|
||||
border: 1px solid #e0e0e0;
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
.filter-label {
|
||||
font-weight: 500;
|
||||
margin-bottom: 8px;
|
||||
color: #555;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.filter-input {
|
||||
width: 100%;
|
||||
|
||||
.filter-text-input,
|
||||
.filter-select,
|
||||
.filter-date {
|
||||
width: 100%;
|
||||
padding: 6px 12px;
|
||||
border: 1px solid #ccc;
|
||||
border-radius: 4px;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.filter-select {
|
||||
height: 34px;
|
||||
}
|
||||
}
|
||||
|
||||
.multiselect-container {
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.multiselect-display {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
padding: 6px 12px;
|
||||
border: 1px solid #ccc;
|
||||
border-radius: 4px;
|
||||
background: white;
|
||||
cursor: pointer;
|
||||
min-height: 34px;
|
||||
|
||||
.multiselect-label {
|
||||
flex: 1;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.multiselect-value {
|
||||
color: #666;
|
||||
font-size: 12px;
|
||||
margin-right: 8px;
|
||||
}
|
||||
|
||||
.dropdown-icon {
|
||||
flex-shrink: 0;
|
||||
transition: transform 0.2s ease;
|
||||
}
|
||||
|
||||
&:hover {
|
||||
border-color: #999;
|
||||
}
|
||||
}
|
||||
|
||||
.multiselect-dropdown {
|
||||
position: absolute;
|
||||
top: 100%;
|
||||
left: 0;
|
||||
right: 0;
|
||||
z-index: 1000;
|
||||
background: white;
|
||||
border: 1px solid #ccc;
|
||||
border-top: none;
|
||||
border-radius: 0 0 4px 4px;
|
||||
box-shadow: 0 2px 4px rgba(0,0,0,0.1);
|
||||
max-height: 200px;
|
||||
overflow-y: auto;
|
||||
|
||||
.checkbox-group {
|
||||
padding: 8px;
|
||||
|
||||
.checkbox-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
padding: 4px 0;
|
||||
|
||||
.checkbox-label {
|
||||
margin: 0;
|
||||
font-size: 14px;
|
||||
cursor: pointer;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.date-range {
|
||||
.date-input-group {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.date-separator {
|
||||
margin: 0 5px;
|
||||
color: #777;
|
||||
}
|
||||
}
|
||||
|
||||
.toggle {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
|
||||
.toggle-label {
|
||||
margin: 0;
|
||||
font-size: 14px;
|
||||
cursor: pointer;
|
||||
}
|
||||
}
|
||||
|
||||
.filter-actions {
|
||||
margin-top: 15px;
|
||||
padding-top: 15px;
|
||||
border-top: 1px solid #eee;
|
||||
|
||||
.btn {
|
||||
font-size: 13px;
|
||||
}
|
||||
}
|
||||
|
||||
// New header row styling
|
||||
.header-row {
|
||||
margin-bottom: 15px;
|
||||
padding-bottom: 10px;
|
||||
border-bottom: 1px solid #eee;
|
||||
|
||||
.chart-title {
|
||||
margin: 0;
|
||||
font-size: 18px;
|
||||
font-weight: 600;
|
||||
color: #333;
|
||||
text-align: left;
|
||||
padding-bottom: 0;
|
||||
border-bottom: none;
|
||||
}
|
||||
}
|
||||
|
||||
// Responsive design
|
||||
@media (max-width: 768px) {
|
||||
.pie-chart-container {
|
||||
padding: 15px;
|
||||
height: auto;
|
||||
min-height: 300px;
|
||||
}
|
||||
|
||||
.chart-title {
|
||||
@@ -179,4 +361,18 @@
|
||||
font-size: 16px;
|
||||
padding: 20px;
|
||||
}
|
||||
|
||||
.filter-controls {
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.filter-item {
|
||||
min-width: 100%;
|
||||
}
|
||||
|
||||
.header-row {
|
||||
.chart-title {
|
||||
font-size: 16px;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,12 +1,14 @@
|
||||
import { Component, OnInit, Input, OnChanges, SimpleChanges, AfterViewChecked } from '@angular/core';
|
||||
import { Component, OnInit, Input, OnChanges, SimpleChanges, AfterViewChecked, OnDestroy } from '@angular/core';
|
||||
import { Dashboard3Service } from 'src/app/services/builder/dashboard3.service';
|
||||
import { FilterService } from '../../common-filter/filter.service';
|
||||
import { Subscription } from 'rxjs';
|
||||
|
||||
@Component({
|
||||
selector: 'app-pie-chart',
|
||||
templateUrl: './pie-chart.component.html',
|
||||
styleUrls: ['./pie-chart.component.scss']
|
||||
})
|
||||
export class PieChartComponent implements OnInit, OnChanges, AfterViewChecked {
|
||||
export class PieChartComponent implements OnInit, OnChanges, AfterViewChecked, OnDestroy {
|
||||
@Input() xAxis: string;
|
||||
@Input() yAxis: string | string[];
|
||||
@Input() table: string;
|
||||
@@ -35,6 +37,12 @@ export class PieChartComponent implements OnInit, OnChanges, AfterViewChecked {
|
||||
|
||||
public pieChartLabels: string[] = ['Category A', 'Category B', 'Category C'];
|
||||
public pieChartData: number[] = [30, 50, 20];
|
||||
public pieChartDatasets: any[] = [
|
||||
{
|
||||
data: [30, 50, 20],
|
||||
label: 'Dataset 1'
|
||||
}
|
||||
];
|
||||
public pieChartType: string = 'pie';
|
||||
public pieChartOptions: any = {
|
||||
responsive: true,
|
||||
@@ -94,9 +102,20 @@ export class PieChartComponent implements OnInit, OnChanges, AfterViewChecked {
|
||||
noDataAvailable: boolean = false;
|
||||
|
||||
// Flag to prevent infinite loops
|
||||
private isFetchingData: boolean = false;
|
||||
isFetchingData: boolean = false;
|
||||
|
||||
constructor(private dashboardService: Dashboard3Service) { }
|
||||
// Subscriptions to unsubscribe on destroy
|
||||
private subscriptions: Subscription[] = [];
|
||||
|
||||
// Add properties for filter functionality
|
||||
private openMultiselects: Map<string, string> = new Map(); // Map of filterId -> context
|
||||
private documentClickHandler: ((event: MouseEvent) => void) | null = null;
|
||||
private filtersInitialized: boolean = false;
|
||||
|
||||
constructor(
|
||||
private dashboardService: Dashboard3Service,
|
||||
private filterService: FilterService
|
||||
) { }
|
||||
|
||||
/**
|
||||
* Force chart redraw
|
||||
@@ -108,15 +127,39 @@ export class PieChartComponent implements OnInit, OnChanges, AfterViewChecked {
|
||||
}
|
||||
|
||||
ngOnInit(): void {
|
||||
// Subscribe to filter changes
|
||||
this.subscriptions.push(
|
||||
this.filterService.filterState$.subscribe(filters => {
|
||||
// When filters change, refresh the chart data
|
||||
this.fetchChartData();
|
||||
})
|
||||
);
|
||||
|
||||
console.log('PieChartComponent initialized with default data:', { labels: this.pieChartLabels, data: this.pieChartData });
|
||||
// Validate initial data
|
||||
this.validateChartData();
|
||||
this.fetchChartData();
|
||||
// Initialize datasets with default data
|
||||
this.pieChartDatasets = [
|
||||
{
|
||||
data: this.pieChartData,
|
||||
label: 'Dataset 1'
|
||||
}
|
||||
];
|
||||
// Only fetch data if we have the required inputs, otherwise show default data
|
||||
if (this.table && this.xAxis && this.yAxis) {
|
||||
this.fetchChartData();
|
||||
}
|
||||
}
|
||||
|
||||
ngOnChanges(changes: SimpleChanges): void {
|
||||
console.log('PieChartComponent input changes:', changes);
|
||||
|
||||
// Initialize filter values if they haven't been initialized yet
|
||||
if (!this.filtersInitialized && (changes.baseFilters || changes.drilldownFilters || changes.drilldownLayers)) {
|
||||
this.initializeFilterValues();
|
||||
this.filtersInitialized = true;
|
||||
}
|
||||
|
||||
// Check if any of the key properties have changed
|
||||
const xAxisChanged = changes.xAxis && !changes.xAxis.firstChange;
|
||||
const yAxisChanged = changes.yAxis && !changes.yAxis.firstChange;
|
||||
@@ -138,6 +181,333 @@ export class PieChartComponent implements OnInit, OnChanges, AfterViewChecked {
|
||||
console.log('Chart configuration changed, fetching new data');
|
||||
this.fetchChartData();
|
||||
}
|
||||
|
||||
// If we have the required inputs and haven't fetched data yet, fetch it
|
||||
if ((xAxisChanged || yAxisChanged || tableChanged) && this.table && this.xAxis && this.yAxis && !this.isFetchingData) {
|
||||
console.log('Required inputs available, fetching data');
|
||||
this.fetchChartData();
|
||||
}
|
||||
}
|
||||
|
||||
ngOnDestroy(): void {
|
||||
this.subscriptions.forEach(sub => sub.unsubscribe());
|
||||
// Clean up document click handler
|
||||
this.removeDocumentClickHandler();
|
||||
}
|
||||
|
||||
// Initialize filter values with proper default values based on type
|
||||
private initializeFilterValues(): void {
|
||||
console.log('Initializing filter values');
|
||||
|
||||
// Initialize base filters
|
||||
if (this.baseFilters) {
|
||||
this.baseFilters.forEach(filter => {
|
||||
if (filter.value === undefined || filter.value === null) {
|
||||
switch (filter.type) {
|
||||
case 'multiselect':
|
||||
filter.value = [];
|
||||
break;
|
||||
case 'date-range':
|
||||
filter.value = { start: null, end: null };
|
||||
break;
|
||||
case 'toggle':
|
||||
filter.value = false;
|
||||
break;
|
||||
default:
|
||||
filter.value = '';
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// Initialize drilldown filters
|
||||
if (this.drilldownFilters) {
|
||||
this.drilldownFilters.forEach(filter => {
|
||||
if (filter.value === undefined || filter.value === null) {
|
||||
switch (filter.type) {
|
||||
case 'multiselect':
|
||||
filter.value = [];
|
||||
break;
|
||||
case 'date-range':
|
||||
filter.value = { start: null, end: null };
|
||||
break;
|
||||
case 'toggle':
|
||||
filter.value = false;
|
||||
break;
|
||||
default:
|
||||
filter.value = '';
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// Initialize layer filters
|
||||
if (this.drilldownLayers) {
|
||||
this.drilldownLayers.forEach(layer => {
|
||||
if (layer.filters) {
|
||||
layer.filters.forEach((filter: any) => {
|
||||
if (filter.value === undefined || filter.value === null) {
|
||||
switch (filter.type) {
|
||||
case 'multiselect':
|
||||
filter.value = [];
|
||||
break;
|
||||
case 'date-range':
|
||||
filter.value = { start: null, end: null };
|
||||
break;
|
||||
case 'toggle':
|
||||
filter.value = false;
|
||||
break;
|
||||
default:
|
||||
filter.value = '';
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
console.log('Filter values initialized:', {
|
||||
baseFilters: this.baseFilters,
|
||||
drilldownFilters: this.drilldownFilters,
|
||||
drilldownLayers: this.drilldownLayers
|
||||
});
|
||||
}
|
||||
|
||||
// Check if there are active filters
|
||||
hasActiveFilters(): boolean {
|
||||
return (this.baseFilters && this.baseFilters.length > 0) ||
|
||||
(this.drilldownFilters && this.drilldownFilters.length > 0) ||
|
||||
this.hasActiveLayerFilters();
|
||||
}
|
||||
|
||||
// Check if there are active layer filters for current drilldown level
|
||||
hasActiveLayerFilters(): boolean {
|
||||
if (this.currentDrilldownLevel > 1 && this.drilldownLayers && this.drilldownLayers.length > 0) {
|
||||
const layerIndex = this.currentDrilldownLevel - 2; // -2 because level 1 is base drilldown
|
||||
return layerIndex < this.drilldownLayers.length &&
|
||||
this.drilldownLayers[layerIndex].filters &&
|
||||
this.drilldownLayers[layerIndex].filters.length > 0;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
// Get active layer filters for current drilldown level
|
||||
getActiveLayerFilters(): any[] {
|
||||
if (this.currentDrilldownLevel > 1 && this.drilldownLayers && this.drilldownLayers.length > 0) {
|
||||
const layerIndex = this.currentDrilldownLevel - 2; // -2 because level 1 is base drilldown
|
||||
if (layerIndex < this.drilldownLayers.length &&
|
||||
this.drilldownLayers[layerIndex].filters) {
|
||||
return this.drilldownLayers[layerIndex].filters;
|
||||
}
|
||||
}
|
||||
return [];
|
||||
}
|
||||
|
||||
// Get filter options for dropdown/multiselect filters
|
||||
getFilterOptions(filter: any): string[] {
|
||||
if (filter.options) {
|
||||
if (Array.isArray(filter.options)) {
|
||||
return filter.options;
|
||||
} else if (typeof filter.options === 'string') {
|
||||
return filter.options.split(',').map(opt => opt.trim()).filter(opt => opt);
|
||||
}
|
||||
}
|
||||
return [];
|
||||
}
|
||||
|
||||
// Check if an option is selected for multiselect filters
|
||||
isOptionSelected(filter: any, option: string): boolean {
|
||||
if (!filter.value) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (Array.isArray(filter.value)) {
|
||||
return filter.value.includes(option);
|
||||
}
|
||||
|
||||
return filter.value === option;
|
||||
}
|
||||
|
||||
// Handle base filter changes
|
||||
onBaseFilterChange(filter: any): void {
|
||||
console.log('Base filter changed:', filter);
|
||||
// Refresh data when filter changes
|
||||
this.fetchChartData();
|
||||
}
|
||||
|
||||
// Handle drilldown filter changes
|
||||
onDrilldownFilterChange(filter: any): void {
|
||||
console.log('Drilldown filter changed:', filter);
|
||||
// Refresh data when filter changes
|
||||
this.fetchChartData();
|
||||
}
|
||||
|
||||
// Handle layer filter changes
|
||||
onLayerFilterChange(filter: any): void {
|
||||
console.log('Layer filter changed:', filter);
|
||||
// Refresh data when filter changes
|
||||
this.fetchChartData();
|
||||
}
|
||||
|
||||
// Handle multiselect changes
|
||||
onMultiSelectChange(filter: any, option: string, event: any): void {
|
||||
const checked = event.target.checked;
|
||||
|
||||
// Initialize filter.value as array if it's not already
|
||||
if (!Array.isArray(filter.value)) {
|
||||
filter.value = [];
|
||||
}
|
||||
|
||||
if (checked) {
|
||||
// Add option to array if not already present
|
||||
if (!filter.value.includes(option)) {
|
||||
filter.value.push(option);
|
||||
}
|
||||
} else {
|
||||
// Remove option from array
|
||||
filter.value = filter.value.filter((item: string) => item !== option);
|
||||
}
|
||||
|
||||
// Refresh data when filter changes
|
||||
this.fetchChartData();
|
||||
}
|
||||
|
||||
// Handle date range changes
|
||||
onDateRangeChange(filter: any, dateRange: { start: string | null, end: string | null }): void {
|
||||
filter.value = dateRange;
|
||||
// Refresh data when filter changes
|
||||
this.fetchChartData();
|
||||
}
|
||||
|
||||
// Handle toggle changes
|
||||
onToggleChange(filter: any, checked: boolean): void {
|
||||
filter.value = checked;
|
||||
// Refresh data when filter changes
|
||||
this.fetchChartData();
|
||||
}
|
||||
|
||||
// Toggle multiselect dropdown visibility
|
||||
toggleMultiselect(filter: any, context: string): void {
|
||||
const filterId = `${context}-${filter.field}`;
|
||||
if (this.isMultiselectOpen(filter, context)) {
|
||||
this.openMultiselects.delete(filterId);
|
||||
} else {
|
||||
// Close all other multiselects first
|
||||
this.openMultiselects.clear();
|
||||
this.openMultiselects.set(filterId, context);
|
||||
|
||||
// Add document click handler to close dropdown when clicking outside
|
||||
this.addDocumentClickHandler();
|
||||
}
|
||||
}
|
||||
|
||||
// Add document click handler to close dropdowns when clicking outside
|
||||
private addDocumentClickHandler(): void {
|
||||
if (!this.documentClickHandler) {
|
||||
this.documentClickHandler = (event: MouseEvent) => {
|
||||
const target = event.target as HTMLElement;
|
||||
// Check if click is outside any multiselect dropdown
|
||||
if (!target.closest('.multiselect-container')) {
|
||||
this.openMultiselects.clear();
|
||||
this.removeDocumentClickHandler();
|
||||
}
|
||||
};
|
||||
|
||||
// Use setTimeout to ensure the click event that opened the dropdown doesn't immediately close it
|
||||
setTimeout(() => {
|
||||
document.addEventListener('click', this.documentClickHandler!);
|
||||
}, 0);
|
||||
}
|
||||
}
|
||||
|
||||
// Remove document click handler
|
||||
private removeDocumentClickHandler(): void {
|
||||
if (this.documentClickHandler) {
|
||||
document.removeEventListener('click', this.documentClickHandler);
|
||||
this.documentClickHandler = null;
|
||||
}
|
||||
}
|
||||
|
||||
// Check if multiselect dropdown is open
|
||||
isMultiselectOpen(filter: any, context: string): boolean {
|
||||
const filterId = `${context}-${filter.field}`;
|
||||
return this.openMultiselects.has(filterId);
|
||||
}
|
||||
|
||||
// Get count of selected options for a multiselect filter
|
||||
getSelectedOptionsCount(filter: any): number {
|
||||
if (!filter.value) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
if (Array.isArray(filter.value)) {
|
||||
return filter.value.length;
|
||||
}
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
// Clear all filters
|
||||
clearAllFilters(): void {
|
||||
// Clear base filters
|
||||
if (this.baseFilters) {
|
||||
this.baseFilters.forEach(filter => {
|
||||
if (filter.type === 'multiselect') {
|
||||
filter.value = [];
|
||||
} else if (filter.type === 'date-range') {
|
||||
filter.value = { start: null, end: null };
|
||||
} else if (filter.type === 'toggle') {
|
||||
filter.value = false;
|
||||
} else {
|
||||
filter.value = '';
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// Clear drilldown filters
|
||||
if (this.drilldownFilters) {
|
||||
this.drilldownFilters.forEach(filter => {
|
||||
if (filter.type === 'multiselect') {
|
||||
filter.value = [];
|
||||
} else if (filter.type === 'date-range') {
|
||||
filter.value = { start: null, end: null };
|
||||
} else if (filter.type === 'toggle') {
|
||||
filter.value = false;
|
||||
} else {
|
||||
filter.value = '';
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// Clear layer filters
|
||||
if (this.drilldownLayers) {
|
||||
this.drilldownLayers.forEach(layer => {
|
||||
if (layer.filters) {
|
||||
layer.filters.forEach((filter: any) => {
|
||||
if (filter.type === 'multiselect') {
|
||||
filter.value = [];
|
||||
} else if (filter.type === 'date-range') {
|
||||
filter.value = { start: null, end: null };
|
||||
} else if (filter.type === 'toggle') {
|
||||
filter.value = false;
|
||||
} else {
|
||||
filter.value = '';
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// Close all multiselect dropdowns
|
||||
this.openMultiselects.clear();
|
||||
|
||||
// Refresh data
|
||||
this.fetchChartData();
|
||||
}
|
||||
|
||||
// Public method to refresh data when filters change
|
||||
refreshData(): void {
|
||||
this.fetchChartData();
|
||||
}
|
||||
|
||||
fetchChartData(): void {
|
||||
@@ -172,11 +542,53 @@ export class PieChartComponent implements OnInit, OnChanges, AfterViewChecked {
|
||||
filterParams = JSON.stringify(filterObj);
|
||||
}
|
||||
}
|
||||
console.log('Base filter parameters:', filterParams);
|
||||
|
||||
// Add common filters to filter parameters
|
||||
const commonFilters = this.filterService.getFilterValues();
|
||||
console.log('Common filters from service:', commonFilters);
|
||||
|
||||
if (Object.keys(commonFilters).length > 0) {
|
||||
// Merge common filters with base filters
|
||||
const mergedFilterObj = {};
|
||||
|
||||
// Add base filters first
|
||||
if (filterParams) {
|
||||
try {
|
||||
const baseFilterObj = JSON.parse(filterParams);
|
||||
Object.assign(mergedFilterObj, baseFilterObj);
|
||||
} catch (e) {
|
||||
console.warn('Failed to parse base filter parameters:', e);
|
||||
}
|
||||
}
|
||||
|
||||
// Add common filters using the field name as the key, not the filter id
|
||||
Object.keys(commonFilters).forEach(filterId => {
|
||||
const filterValue = commonFilters[filterId];
|
||||
// Find the filter definition to get the field name
|
||||
const filterDef = this.filterService.getFilters().find(f => f.id === filterId);
|
||||
if (filterDef && filterDef.field) {
|
||||
const fieldName = filterDef.field;
|
||||
if (filterValue !== undefined && filterValue !== null && filterValue !== '') {
|
||||
mergedFilterObj[fieldName] = filterValue;
|
||||
}
|
||||
} else {
|
||||
// Fallback to using filterId as field name if no field is defined
|
||||
if (filterValue !== undefined && filterValue !== null && filterValue !== '') {
|
||||
mergedFilterObj[filterId] = filterValue;
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
if (Object.keys(mergedFilterObj).length > 0) {
|
||||
filterParams = JSON.stringify(mergedFilterObj);
|
||||
}
|
||||
}
|
||||
|
||||
console.log('Final filter parameters:', filterParams);
|
||||
|
||||
// Log the URL that will be called
|
||||
const url = `chart/getdashjson/pie?tableName=${this.table}&xAxis=${this.xAxis}&yAxes=${yAxisString}${this.connection ? `&sureId=${this.connection}` : ''}`;
|
||||
console.log('Pie chart data URL:', url);
|
||||
console.log('Chart data URL:', url);
|
||||
|
||||
// Fetch data from the dashboard service with parameter field and value
|
||||
// For base level, we pass empty parameter and value, but now also pass filters
|
||||
@@ -184,12 +596,8 @@ export class PieChartComponent implements OnInit, OnChanges, AfterViewChecked {
|
||||
(data: any) => {
|
||||
console.log('Received pie chart data:', data);
|
||||
if (data === null) {
|
||||
console.warn('Pie chart API returned null data. Check if the API endpoint is working correctly.');
|
||||
console.warn('API returned null data. Check if the API endpoint is working correctly.');
|
||||
this.noDataAvailable = true;
|
||||
this.pieChartLabels = [];
|
||||
this.pieChartData = [];
|
||||
// Validate and sanitize data to show default data
|
||||
this.validateChartData();
|
||||
// Reset flag after fetching
|
||||
this.isFetchingData = false;
|
||||
return;
|
||||
@@ -197,50 +605,57 @@ export class PieChartComponent implements OnInit, OnChanges, AfterViewChecked {
|
||||
|
||||
// Handle the actual data structure returned by the API
|
||||
if (data && data.chartLabels && data.chartData) {
|
||||
// For pie charts, we need to extract the data differently
|
||||
// The first dataset's data array contains the values for the pie chart
|
||||
// Backend has already filtered the data, just display it
|
||||
this.noDataAvailable = data.chartLabels.length === 0;
|
||||
this.pieChartLabels = data.chartLabels || [];
|
||||
if (data.chartData && data.chartData.length > 0) {
|
||||
this.pieChartData = data.chartData[0].data.map(value => {
|
||||
// Convert to number if it's not already
|
||||
const numValue = Number(value);
|
||||
return isNaN(numValue) ? 0 : numValue;
|
||||
});
|
||||
this.pieChartLabels = data.chartLabels;
|
||||
|
||||
// Extract the actual data values from the chartData array
|
||||
// chartData is an array with one object containing the data
|
||||
if (data.chartData.length > 0 && data.chartData[0].data) {
|
||||
this.pieChartData = data.chartData[0].data;
|
||||
} else {
|
||||
this.pieChartData = [];
|
||||
}
|
||||
// Ensure labels and data arrays have the same length
|
||||
this.syncLabelAndDataArrays();
|
||||
// Validate and sanitize data
|
||||
this.validateChartData();
|
||||
|
||||
// Trigger change detection
|
||||
this.pieChartLabels = [...this.pieChartLabels];
|
||||
this.pieChartData = [...this.pieChartData];
|
||||
this.pieChartDatasets = [
|
||||
{
|
||||
data: this.pieChartData,
|
||||
label: 'Dataset 1'
|
||||
}
|
||||
];
|
||||
console.log('Updated pie chart with data:', { labels: this.pieChartLabels, data: this.pieChartData });
|
||||
} else if (data && data.labels && data.data) {
|
||||
// Handle the original expected format as fallback
|
||||
} else if (data && data.labels && data.datasets) {
|
||||
// Backend has already filtered the data, just display it
|
||||
this.noDataAvailable = data.labels.length === 0;
|
||||
this.pieChartLabels = data.labels || [];
|
||||
this.pieChartData = data.data.map(value => {
|
||||
// Convert to number if it's not already
|
||||
const numValue = Number(value);
|
||||
return isNaN(numValue) ? 0 : numValue;
|
||||
});
|
||||
// Ensure labels and data arrays have the same length
|
||||
this.syncLabelAndDataArrays();
|
||||
// Validate and sanitize data
|
||||
this.validateChartData();
|
||||
this.pieChartLabels = data.labels;
|
||||
this.pieChartData = data.datasets[0]?.data || [];
|
||||
// Trigger change detection
|
||||
this.pieChartLabels = [...this.pieChartLabels];
|
||||
this.pieChartData = [...this.pieChartData];
|
||||
this.pieChartDatasets = [
|
||||
{
|
||||
data: this.pieChartData,
|
||||
label: 'Dataset 1'
|
||||
}
|
||||
];
|
||||
console.log('Updated pie chart with legacy data format:', { labels: this.pieChartLabels, data: this.pieChartData });
|
||||
} else {
|
||||
console.warn('Pie chart received data does not have expected structure', data);
|
||||
// Reset to default data
|
||||
console.warn('Received data does not have expected structure', data);
|
||||
this.noDataAvailable = true;
|
||||
this.pieChartLabels = [];
|
||||
this.pieChartData = [];
|
||||
// Validate and sanitize data to show default data
|
||||
this.validateChartData();
|
||||
// Keep default data if no data is available
|
||||
if (this.pieChartLabels.length === 0 && this.pieChartData.length === 0) {
|
||||
this.pieChartLabels = ['Category A', 'Category B', 'Category C'];
|
||||
this.pieChartData = [30, 50, 20];
|
||||
this.pieChartDatasets = [
|
||||
{
|
||||
data: this.pieChartData,
|
||||
label: 'Dataset 1'
|
||||
}
|
||||
];
|
||||
}
|
||||
}
|
||||
// Reset flag after fetching
|
||||
this.isFetchingData = false;
|
||||
@@ -248,23 +663,13 @@ export class PieChartComponent implements OnInit, OnChanges, AfterViewChecked {
|
||||
(error) => {
|
||||
console.error('Error fetching pie chart data:', error);
|
||||
this.noDataAvailable = true;
|
||||
this.pieChartLabels = [];
|
||||
this.pieChartData = [];
|
||||
// Validate and sanitize data to show default data
|
||||
this.validateChartData();
|
||||
// Reset flag after fetching
|
||||
this.isFetchingData = false;
|
||||
}
|
||||
);
|
||||
} else {
|
||||
console.log('Missing required data for pie chart, showing default data:', { table: this.table, xAxis: this.xAxis, yAxis: this.yAxis, connection: this.connection });
|
||||
// Don't set noDataAvailable to true when there's no required data
|
||||
// This allows static data to be displayed
|
||||
this.noDataAvailable = false;
|
||||
// Validate the chart data to ensure we have some data to display
|
||||
this.validateChartData();
|
||||
// Force a redraw to ensure the chart displays
|
||||
this.pieChartData = [...this.pieChartData];
|
||||
console.log('Missing required data for chart:', { table: this.table, xAxis: this.xAxis, yAxis: this.yAxis, connection: this.connection });
|
||||
this.noDataAvailable = true;
|
||||
// Reset flag after fetching
|
||||
this.isFetchingData = false;
|
||||
}
|
||||
@@ -293,8 +698,6 @@ export class PieChartComponent implements OnInit, OnChanges, AfterViewChecked {
|
||||
} else {
|
||||
console.warn('Invalid drilldown layer index:', layerIndex);
|
||||
this.noDataAvailable = true;
|
||||
this.pieChartLabels = [];
|
||||
this.pieChartData = [];
|
||||
return;
|
||||
}
|
||||
}
|
||||
@@ -305,8 +708,6 @@ export class PieChartComponent implements OnInit, OnChanges, AfterViewChecked {
|
||||
if (!drilldownConfig || !drilldownConfig.apiUrl || !drilldownConfig.xAxis || !drilldownConfig.yAxis) {
|
||||
console.warn('Missing drilldown configuration for level:', this.currentDrilldownLevel);
|
||||
this.noDataAvailable = true;
|
||||
this.pieChartLabels = [];
|
||||
this.pieChartData = [];
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -362,6 +763,35 @@ export class PieChartComponent implements OnInit, OnChanges, AfterViewChecked {
|
||||
}
|
||||
}
|
||||
|
||||
// Add common filters to drilldown filter parameters
|
||||
const commonFilters = this.filterService.getFilterValues();
|
||||
if (Object.keys(commonFilters).length > 0) {
|
||||
// Merge common filters with drilldown filters
|
||||
const mergedFilterObj = {};
|
||||
|
||||
// Add drilldown filters first
|
||||
if (filterParams) {
|
||||
try {
|
||||
const drilldownFilterObj = JSON.parse(filterParams);
|
||||
Object.assign(mergedFilterObj, drilldownFilterObj);
|
||||
} catch (e) {
|
||||
console.warn('Failed to parse drilldown filter parameters:', e);
|
||||
}
|
||||
}
|
||||
|
||||
// Add common filters
|
||||
Object.keys(commonFilters).forEach(key => {
|
||||
const value = commonFilters[key];
|
||||
if (value !== undefined && value !== null && value !== '') {
|
||||
mergedFilterObj[key] = value;
|
||||
}
|
||||
});
|
||||
|
||||
if (Object.keys(mergedFilterObj).length > 0) {
|
||||
filterParams = JSON.stringify(mergedFilterObj);
|
||||
}
|
||||
}
|
||||
|
||||
// Log the URL that will be called
|
||||
const url = `chart/getdashjson/pie?tableName=${actualApiUrl}&xAxis=${drilldownConfig.xAxis}&yAxes=${drilldownConfig.yAxis}${this.connection ? `&sureId=${this.connection}` : ''}`;
|
||||
console.log('Drilldown data URL:', url);
|
||||
@@ -374,64 +804,57 @@ export class PieChartComponent implements OnInit, OnChanges, AfterViewChecked {
|
||||
if (data === null) {
|
||||
console.warn('Drilldown API returned null data. Check if the API endpoint is working correctly.');
|
||||
this.noDataAvailable = true;
|
||||
this.pieChartLabels = [];
|
||||
this.pieChartData = [];
|
||||
return;
|
||||
}
|
||||
|
||||
// Handle the actual data structure returned by the API
|
||||
if (data && data.chartLabels && data.chartData) {
|
||||
// For pie charts, we need to extract the data differently
|
||||
// The first dataset's data array contains the values for the pie chart
|
||||
// Backend has already filtered the data, just display it
|
||||
this.noDataAvailable = data.chartLabels.length === 0;
|
||||
this.pieChartLabels = data.chartLabels || [];
|
||||
if (data.chartData && data.chartData.length > 0) {
|
||||
this.pieChartData = data.chartData[0].data.map(value => {
|
||||
// Convert to number if it's not already
|
||||
const numValue = Number(value);
|
||||
return isNaN(numValue) ? 0 : numValue;
|
||||
});
|
||||
this.pieChartLabels = data.chartLabels;
|
||||
|
||||
// Extract the actual data values from the chartData array
|
||||
// chartData is an array with one object containing the data
|
||||
if (data.chartData.length > 0 && data.chartData[0].data) {
|
||||
this.pieChartData = data.chartData[0].data;
|
||||
} else {
|
||||
this.pieChartData = [];
|
||||
}
|
||||
// Ensure labels and data arrays have the same length
|
||||
this.syncLabelAndDataArrays();
|
||||
// Validate and sanitize data
|
||||
this.validateChartData();
|
||||
|
||||
// Trigger change detection
|
||||
this.pieChartLabels = [...this.pieChartLabels];
|
||||
this.pieChartData = [...this.pieChartData];
|
||||
this.pieChartDatasets = [
|
||||
{
|
||||
data: this.pieChartData,
|
||||
label: 'Dataset 1'
|
||||
}
|
||||
];
|
||||
console.log('Updated pie chart with drilldown data:', { labels: this.pieChartLabels, data: this.pieChartData });
|
||||
} else if (data && data.labels && data.data) {
|
||||
// Handle the original expected format as fallback
|
||||
} else if (data && data.labels && data.datasets) {
|
||||
// Backend has already filtered the data, just display it
|
||||
this.noDataAvailable = data.labels.length === 0;
|
||||
this.pieChartLabels = data.labels || [];
|
||||
this.pieChartData = data.data.map(value => {
|
||||
// Convert to number if it's not already
|
||||
const numValue = Number(value);
|
||||
return isNaN(numValue) ? 0 : numValue;
|
||||
});
|
||||
// Ensure labels and data arrays have the same length
|
||||
this.syncLabelAndDataArrays();
|
||||
// Validate and sanitize data
|
||||
this.validateChartData();
|
||||
this.pieChartLabels = data.labels;
|
||||
this.pieChartData = data.datasets[0]?.data || [];
|
||||
// Trigger change detection
|
||||
this.pieChartLabels = [...this.pieChartLabels];
|
||||
this.pieChartData = [...this.pieChartData];
|
||||
this.pieChartDatasets = [
|
||||
{
|
||||
data: this.pieChartData,
|
||||
label: 'Dataset 1'
|
||||
}
|
||||
];
|
||||
console.log('Updated pie chart with drilldown legacy data format:', { labels: this.pieChartLabels, data: this.pieChartData });
|
||||
} else {
|
||||
console.warn('Drilldown received data does not have expected structure', data);
|
||||
this.noDataAvailable = true;
|
||||
this.pieChartLabels = [];
|
||||
this.pieChartData = [];
|
||||
// Validate and sanitize data
|
||||
this.validateChartData();
|
||||
// Keep current data if no data is available
|
||||
}
|
||||
},
|
||||
(error) => {
|
||||
console.error('Error fetching drilldown data:', error);
|
||||
this.noDataAvailable = true;
|
||||
this.pieChartLabels = [];
|
||||
this.pieChartData = [];
|
||||
// Keep current data in case of error
|
||||
}
|
||||
);
|
||||
}
|
||||
@@ -492,82 +915,52 @@ export class PieChartComponent implements OnInit, OnChanges, AfterViewChecked {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get color for legend item
|
||||
* @param index Index of the legend item
|
||||
*/
|
||||
public getLegendColor(index: number): string {
|
||||
// Validate chart data to ensure labels and data arrays have the same length
|
||||
private validateChartData(): void {
|
||||
if (this.pieChartLabels && this.pieChartData) {
|
||||
// For pie charts, we need to ensure labels and data arrays have the same length
|
||||
const labelCount = this.pieChartLabels.length;
|
||||
const dataCount = this.pieChartData.length;
|
||||
|
||||
if (labelCount !== dataCount) {
|
||||
console.warn('Pie chart labels and data arrays have different lengths:', { labels: labelCount, data: dataCount });
|
||||
// Pad or truncate data array to match label count
|
||||
if (dataCount < labelCount) {
|
||||
// Pad with zeros
|
||||
while (this.pieChartData.length < labelCount) {
|
||||
this.pieChartData.push(0);
|
||||
}
|
||||
} else if (dataCount > labelCount) {
|
||||
// Truncate data array
|
||||
this.pieChartData = this.pieChartData.slice(0, labelCount);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Get legend color for a specific index
|
||||
getLegendColor(index: number): string {
|
||||
return this.chartColors[index % this.chartColors.length];
|
||||
}
|
||||
|
||||
/**
|
||||
* Ensure labels and data arrays have the same length
|
||||
*/
|
||||
private syncLabelAndDataArrays(): void {
|
||||
// Ensure we have matching arrays
|
||||
if (this.pieChartLabels.length !== this.pieChartData.length) {
|
||||
const maxLength = Math.max(this.pieChartLabels.length, this.pieChartData.length);
|
||||
while (this.pieChartLabels.length < maxLength) {
|
||||
this.pieChartLabels.push(`Label ${this.pieChartLabels.length + 1}`);
|
||||
}
|
||||
while (this.pieChartData.length < maxLength) {
|
||||
this.pieChartData.push(0);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Validate and sanitize chart data
|
||||
*/
|
||||
private validateChartData(): void {
|
||||
console.log('Validating chart data:', { labels: this.pieChartLabels, data: this.pieChartData });
|
||||
|
||||
// Ensure we have valid arrays
|
||||
if (!Array.isArray(this.pieChartLabels)) {
|
||||
this.pieChartLabels = [];
|
||||
// Method to determine if chart should be displayed
|
||||
shouldShowChart(): boolean {
|
||||
// Show chart if we have data
|
||||
if (this.pieChartLabels.length > 0 && this.pieChartData.length > 0) {
|
||||
return true;
|
||||
}
|
||||
|
||||
if (!Array.isArray(this.pieChartData)) {
|
||||
this.pieChartData = [];
|
||||
// Show chart if we're still fetching data
|
||||
if (this.isFetchingData) {
|
||||
return true;
|
||||
}
|
||||
|
||||
// Ensure we have some data to display
|
||||
if (this.pieChartLabels.length === 0 && this.pieChartData.length === 0) {
|
||||
// Add default data to ensure chart visibility
|
||||
this.pieChartLabels = ['Category A', 'Category B', 'Category C'];
|
||||
this.pieChartData = [30, 50, 20];
|
||||
console.log('Added default data for chart display');
|
||||
// Show chart if we have default data
|
||||
if (this.pieChartLabels.length > 0 && this.originalPieChartLabels.length > 0) {
|
||||
return true;
|
||||
}
|
||||
|
||||
// Ensure labels and data arrays have the same length
|
||||
this.syncLabelAndDataArrays();
|
||||
|
||||
// Ensure all data values are numbers
|
||||
this.pieChartData = this.pieChartData.map(value => {
|
||||
const numValue = Number(value);
|
||||
return isNaN(numValue) ? 0 : numValue;
|
||||
});
|
||||
|
||||
console.log('After validation:', { labels: this.pieChartLabels, data: this.pieChartData });
|
||||
}
|
||||
|
||||
ngAfterViewChecked() {
|
||||
// Debugging: Log component state after view checks
|
||||
console.log('PieChartComponent state:', {
|
||||
labels: this.pieChartLabels,
|
||||
data: this.pieChartData,
|
||||
hasData: this.pieChartLabels.length > 0 && this.pieChartData.length > 0
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if chart data is valid and ready to display
|
||||
*/
|
||||
public isChartDataValid(): boolean {
|
||||
return this.pieChartLabels && this.pieChartData &&
|
||||
Array.isArray(this.pieChartLabels) && Array.isArray(this.pieChartData) &&
|
||||
this.pieChartLabels.length > 0 && this.pieChartData.length > 0 &&
|
||||
this.pieChartLabels.length === this.pieChartData.length;
|
||||
return false;
|
||||
}
|
||||
|
||||
// events
|
||||
@@ -656,6 +1049,10 @@ export class PieChartComponent implements OnInit, OnChanges, AfterViewChecked {
|
||||
}
|
||||
|
||||
public chartHovered(e: any): void {
|
||||
console.log(e);
|
||||
console.log('Pie chart hovered:', e);
|
||||
}
|
||||
|
||||
ngAfterViewChecked(): void {
|
||||
// This lifecycle hook can be used if needed for post-render operations
|
||||
}
|
||||
}
|
||||
@@ -1,10 +1,292 @@
|
||||
<div style="display: block; height: 100%; width: 100%;">
|
||||
<!-- Filter Controls Section -->
|
||||
<div class="filter-section" *ngIf="hasActiveFilters()">
|
||||
<!-- Base Filters -->
|
||||
<div class="filter-group" *ngIf="baseFilters && baseFilters.length > 0">
|
||||
<h4>Base Filters</h4>
|
||||
<div class="filter-controls">
|
||||
<div *ngFor="let filter of baseFilters" class="filter-item">
|
||||
<div class="filter-label">{{ filter.field }} ({{ filter.type || 'text' }})</div>
|
||||
|
||||
<div style="display: block">
|
||||
<canvas baseChart
|
||||
[datasets]="polarAreaChartData"
|
||||
[labels]="polarAreaChartLabels"
|
||||
[type]="polarAreaChartType"
|
||||
(chartHover)="chartHovered($event)"
|
||||
(chartClick)="chartClicked($event)">
|
||||
</canvas>
|
||||
<!-- Text Filter -->
|
||||
<div *ngIf="!filter.type || filter.type === 'text'" class="filter-input">
|
||||
<input type="text"
|
||||
[(ngModel)]="filter.value"
|
||||
(ngModelChange)="onBaseFilterChange(filter)"
|
||||
[placeholder]="filter.field"
|
||||
class="clr-input filter-text-input">
|
||||
</div>
|
||||
|
||||
<!-- Dropdown Filter -->
|
||||
<div *ngIf="filter.type === 'dropdown'" class="filter-input">
|
||||
<select [(ngModel)]="filter.value"
|
||||
(ngModelChange)="onBaseFilterChange(filter)"
|
||||
class="clr-select filter-select">
|
||||
<option value="">Select {{ filter.field }}</option>
|
||||
<option *ngFor="let option of getFilterOptions(filter)" [value]="option">{{ option }}</option>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<!-- Multi-Select Filter - Updated to show key first, then dropdown on click -->
|
||||
<div *ngIf="filter.type === 'multiselect'" class="filter-input multiselect-container">
|
||||
<div class="multiselect-display" (click)="toggleMultiselect(filter, 'base')">
|
||||
<span class="multiselect-label">{{ filter.field }}</span>
|
||||
<span class="multiselect-value" *ngIf="getSelectedOptionsCount(filter) > 0">
|
||||
({{ getSelectedOptionsCount(filter) }} selected)
|
||||
</span>
|
||||
<clr-icon shape="caret down" class="dropdown-icon"></clr-icon>
|
||||
</div>
|
||||
<div class="multiselect-dropdown" *ngIf="isMultiselectOpen(filter, 'base')">
|
||||
<div class="checkbox-group">
|
||||
<div *ngFor="let option of getFilterOptions(filter); let i = index" class="checkbox-item">
|
||||
<input type="checkbox"
|
||||
[checked]="isOptionSelected(filter, option)"
|
||||
(change)="onMultiSelectChange(filter, option, $event)"
|
||||
[id]="'base-' + filter.field + '-' + i"
|
||||
class="clr-checkbox">
|
||||
<label [for]="'base-' + filter.field + '-' + i" class="checkbox-label">{{ option }}</label>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Date Range Filter -->
|
||||
<div *ngIf="filter.type === 'date-range'" class="filter-input date-range">
|
||||
<div class="date-input-group">
|
||||
<input type="date"
|
||||
[(ngModel)]="filter.value.start"
|
||||
(ngModelChange)="onDateRangeChange(filter, { start: $event, end: filter.value.end })"
|
||||
placeholder="Start Date"
|
||||
class="clr-input filter-date">
|
||||
<span class="date-separator">to</span>
|
||||
<input type="date"
|
||||
[(ngModel)]="filter.value.end"
|
||||
(ngModelChange)="onDateRangeChange(filter, { start: filter.value.start, end: $event })"
|
||||
placeholder="End Date"
|
||||
class="clr-input filter-date">
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Toggle Filter -->
|
||||
<div *ngIf="filter.type === 'toggle'" class="filter-input toggle">
|
||||
<input type="checkbox"
|
||||
[(ngModel)]="filter.value"
|
||||
(ngModelChange)="onToggleChange(filter, $event)"
|
||||
clrToggle
|
||||
class="clr-toggle">
|
||||
<label class="toggle-label">{{ filter.field }}</label>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Drilldown Filters -->
|
||||
<div class="filter-group" *ngIf="drilldownFilters && drilldownFilters.length > 0 && currentDrilldownLevel > 0">
|
||||
<h4>Drilldown Filters</h4>
|
||||
<div class="filter-controls">
|
||||
<div *ngFor="let filter of drilldownFilters" class="filter-item">
|
||||
<div class="filter-label">{{ filter.field }} ({{ filter.type || 'text' }})</div>
|
||||
|
||||
<!-- Text Filter -->
|
||||
<div *ngIf="!filter.type || filter.type === 'text'" class="filter-input">
|
||||
<input type="text"
|
||||
[(ngModel)]="filter.value"
|
||||
(ngModelChange)="onDrilldownFilterChange(filter)"
|
||||
[placeholder]="filter.field"
|
||||
class="clr-input filter-text-input">
|
||||
</div>
|
||||
|
||||
<!-- Dropdown Filter -->
|
||||
<div *ngIf="filter.type === 'dropdown'" class="filter-input">
|
||||
<select [(ngModel)]="filter.value"
|
||||
(ngModelChange)="onDrilldownFilterChange(filter)"
|
||||
class="clr-select filter-select">
|
||||
<option value="">Select {{ filter.field }}</option>
|
||||
<option *ngFor="let option of getFilterOptions(filter)" [value]="option">{{ option }}</option>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<!-- Multi-Select Filter - Updated to show key first, then dropdown on click -->
|
||||
<div *ngIf="filter.type === 'multiselect'" class="filter-input multiselect-container">
|
||||
<div class="multiselect-display" (click)="toggleMultiselect(filter, 'drilldown')">
|
||||
<span class="multiselect-label">{{ filter.field }}</span>
|
||||
<span class="multiselect-value" *ngIf="getSelectedOptionsCount(filter) > 0">
|
||||
({{ getSelectedOptionsCount(filter) }} selected)
|
||||
</span>
|
||||
<clr-icon shape="caret down" class="dropdown-icon"></clr-icon>
|
||||
</div>
|
||||
<div class="multiselect-dropdown" *ngIf="isMultiselectOpen(filter, 'drilldown')">
|
||||
<div class="checkbox-group">
|
||||
<div *ngFor="let option of getFilterOptions(filter); let i = index" class="checkbox-item">
|
||||
<input type="checkbox"
|
||||
[checked]="isOptionSelected(filter, option)"
|
||||
(change)="onMultiSelectChange(filter, option, $event)"
|
||||
[id]="'drilldown-' + filter.field + '-' + i"
|
||||
class="clr-checkbox">
|
||||
<label [for]="'drilldown-' + filter.field + '-' + i" class="checkbox-label">{{ option }}</label>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Date Range Filter -->
|
||||
<div *ngIf="filter.type === 'date-range'" class="filter-input date-range">
|
||||
<div class="date-input-group">
|
||||
<input type="date"
|
||||
[(ngModel)]="filter.value.start"
|
||||
(ngModelChange)="onDateRangeChange(filter, { start: $event, end: filter.value.end })"
|
||||
placeholder="Start Date"
|
||||
class="clr-input filter-date">
|
||||
<span class="date-separator">to</span>
|
||||
<input type="date"
|
||||
[(ngModel)]="filter.value.end"
|
||||
(ngModelChange)="onDateRangeChange(filter, { start: filter.value.start, end: $event })"
|
||||
placeholder="End Date"
|
||||
class="clr-input filter-date">
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Toggle Filter -->
|
||||
<div *ngIf="filter.type === 'toggle'" class="filter-input toggle">
|
||||
<input type="checkbox"
|
||||
[(ngModel)]="filter.value"
|
||||
(ngModelChange)="onToggleChange(filter, $event)"
|
||||
clrToggle
|
||||
class="clr-toggle">
|
||||
<label class="toggle-label">{{ filter.field }}</label>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Layer Filters -->
|
||||
<div class="filter-group" *ngIf="hasActiveLayerFilters()">
|
||||
<h4>Layer Filters</h4>
|
||||
<div class="filter-controls">
|
||||
<div *ngFor="let filter of getActiveLayerFilters()" class="filter-item">
|
||||
<div class="filter-label">{{ filter.field }} ({{ filter.type || 'text' }})</div>
|
||||
|
||||
<!-- Text Filter -->
|
||||
<div *ngIf="!filter.type || filter.type === 'text'" class="filter-input">
|
||||
<input type="text"
|
||||
[(ngModel)]="filter.value"
|
||||
(ngModelChange)="onLayerFilterChange(filter)"
|
||||
[placeholder]="filter.field"
|
||||
class="clr-input filter-text-input">
|
||||
</div>
|
||||
|
||||
<!-- Dropdown Filter -->
|
||||
<div *ngIf="filter.type === 'dropdown'" class="filter-input">
|
||||
<select [(ngModel)]="filter.value"
|
||||
(ngModelChange)="onLayerFilterChange(filter)"
|
||||
class="clr-select filter-select">
|
||||
<option value="">Select {{ filter.field }}</option>
|
||||
<option *ngFor="let option of getFilterOptions(filter)" [value]="option">{{ option }}</option>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<!-- Multi-Select Filter - Updated to show key first, then dropdown on click -->
|
||||
<div *ngIf="filter.type === 'multiselect'" class="filter-input multiselect-container">
|
||||
<div class="multiselect-display" (click)="toggleMultiselect(filter, 'layer')">
|
||||
<span class="multiselect-label">{{ filter.field }}</span>
|
||||
<span class="multiselect-value" *ngIf="getSelectedOptionsCount(filter) > 0">
|
||||
({{ getSelectedOptionsCount(filter) }} selected)
|
||||
</span>
|
||||
<clr-icon shape="caret down" class="dropdown-icon"></clr-icon>
|
||||
</div>
|
||||
<div class="multiselect-dropdown" *ngIf="isMultiselectOpen(filter, 'layer')">
|
||||
<div class="checkbox-group">
|
||||
<div *ngFor="let option of getFilterOptions(filter); let i = index" class="checkbox-item">
|
||||
<input type="checkbox"
|
||||
[checked]="isOptionSelected(filter, option)"
|
||||
(change)="onMultiSelectChange(filter, option, $event)"
|
||||
[id]="'layer-' + filter.field + '-' + i"
|
||||
class="clr-checkbox">
|
||||
<label [for]="'layer-' + filter.field + '-' + i" class="checkbox-label">{{ option }}</label>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Date Range Filter -->
|
||||
<div *ngIf="filter.type === 'date-range'" class="filter-input date-range">
|
||||
<div class="date-input-group">
|
||||
<input type="date"
|
||||
[(ngModel)]="filter.value.start"
|
||||
(ngModelChange)="onDateRangeChange(filter, { start: $event, end: filter.value.end })"
|
||||
placeholder="Start Date"
|
||||
class="clr-input filter-date">
|
||||
<span class="date-separator">to</span>
|
||||
<input type="date"
|
||||
[(ngModel)]="filter.value.end"
|
||||
(ngModelChange)="onDateRangeChange(filter, { start: filter.value.start, end: $event })"
|
||||
placeholder="End Date"
|
||||
class="clr-input filter-date">
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Toggle Filter -->
|
||||
<div *ngIf="filter.type === 'toggle'" class="filter-input toggle">
|
||||
<input type="checkbox"
|
||||
[(ngModel)]="filter.value"
|
||||
(ngModelChange)="onToggleChange(filter, $event)"
|
||||
clrToggle
|
||||
class="clr-toggle">
|
||||
<label class="toggle-label">{{ filter.field }}</label>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Clear Filters Button -->
|
||||
<div class="filter-actions">
|
||||
<button class="btn btn-sm btn-outline" (click)="clearAllFilters()">Clear All Filters</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Header row with chart title and drilldown navigation -->
|
||||
<div class="clr-row header-row">
|
||||
<div class="clr-col-6">
|
||||
<h3 class="chart-title">{{charttitle || 'Polar Chart'}}</h3>
|
||||
</div>
|
||||
<div class="clr-col-6" style="text-align: right;">
|
||||
<!-- Add drilldown navigation controls -->
|
||||
<button class="btn btn-sm btn-link" *ngIf="drilldownEnabled && drilldownStack.length > 0" (click)="navigateBack()">
|
||||
<cds-icon shape="arrow" direction="left"></cds-icon>
|
||||
Back to {{drilldownStack.length > 1 ? 'Previous Level' : 'Main Data'}}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Show current drilldown level -->
|
||||
<div class="clr-row" *ngIf="drilldownEnabled && drilldownStack.length > 0">
|
||||
<div class="clr-col-12">
|
||||
<div class="alert alert-info" style="padding: 8px 12px; margin-bottom: 12px;">
|
||||
<div class="alert-items">
|
||||
<div class="alert-item static">
|
||||
<div class="alert-icon-wrapper">
|
||||
<cds-icon class="alert-icon" shape="info-circle"></cds-icon>
|
||||
</div>
|
||||
<span class="alert-text">
|
||||
Drilldown Level: {{currentDrilldownLevel}}
|
||||
<span *ngIf="drilldownStack.length > 0">
|
||||
(Clicked on: {{drilldownStack[drilldownStack.length - 1].clickedLabel}})
|
||||
</span>
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div style="position: relative; height: calc(100% - 80px); padding: 0 10px 30px 10px;">
|
||||
<canvas baseChart
|
||||
[datasets]="polarAreaChartData"
|
||||
[labels]="polarAreaChartLabels"
|
||||
[options]="polarAreaChartOptions"
|
||||
[type]="polarAreaChartType"
|
||||
(chartHover)="chartHovered($event)"
|
||||
(chartClick)="chartClicked($event)">
|
||||
</canvas>
|
||||
</div>
|
||||
</div>
|
||||
@@ -1,18 +1,192 @@
|
||||
// Polar Chart Component Styles
|
||||
div[style*="display: block"] {
|
||||
.filter-section {
|
||||
margin-bottom: 20px;
|
||||
padding: 15px;
|
||||
border: 1px solid #ddd;
|
||||
border-radius: 4px;
|
||||
background-color: #f9f9f9;
|
||||
}
|
||||
|
||||
.filter-group {
|
||||
margin-bottom: 15px;
|
||||
|
||||
h4 {
|
||||
margin-top: 0;
|
||||
margin-bottom: 10px;
|
||||
color: #333;
|
||||
font-weight: 600;
|
||||
}
|
||||
}
|
||||
|
||||
.filter-controls {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 15px;
|
||||
}
|
||||
|
||||
.filter-item {
|
||||
flex: 1 1 300px;
|
||||
min-width: 250px;
|
||||
padding: 10px;
|
||||
background: white;
|
||||
border: 1px solid #e0e0e0;
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
.filter-label {
|
||||
font-weight: 500;
|
||||
margin-bottom: 8px;
|
||||
color: #555;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.filter-input {
|
||||
width: 100%;
|
||||
|
||||
.filter-text-input,
|
||||
.filter-select,
|
||||
.filter-date {
|
||||
width: 100%;
|
||||
padding: 6px 12px;
|
||||
border: 1px solid #ccc;
|
||||
border-radius: 4px;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.filter-select {
|
||||
height: 34px;
|
||||
}
|
||||
}
|
||||
|
||||
.multiselect-container {
|
||||
position: relative;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
canvas {
|
||||
max-width: 100%;
|
||||
max-height: 100%;
|
||||
.multiselect-display {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
padding: 6px 12px;
|
||||
border: 1px solid #ccc;
|
||||
border-radius: 4px;
|
||||
background: white;
|
||||
cursor: pointer;
|
||||
min-height: 34px;
|
||||
|
||||
.multiselect-label {
|
||||
flex: 1;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.multiselect-value {
|
||||
color: #666;
|
||||
font-size: 12px;
|
||||
margin-right: 8px;
|
||||
}
|
||||
|
||||
.dropdown-icon {
|
||||
flex-shrink: 0;
|
||||
transition: transform 0.2s ease;
|
||||
}
|
||||
|
||||
&:hover {
|
||||
border-color: #999;
|
||||
}
|
||||
}
|
||||
|
||||
// Ensure the chart container has proper sizing
|
||||
:host {
|
||||
display: block;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
.multiselect-dropdown {
|
||||
position: absolute;
|
||||
top: 100%;
|
||||
left: 0;
|
||||
right: 0;
|
||||
z-index: 1000;
|
||||
background: white;
|
||||
border: 1px solid #ccc;
|
||||
border-top: none;
|
||||
border-radius: 0 0 4px 4px;
|
||||
box-shadow: 0 2px 4px rgba(0,0,0,0.1);
|
||||
max-height: 200px;
|
||||
overflow-y: auto;
|
||||
|
||||
.checkbox-group {
|
||||
padding: 8px;
|
||||
|
||||
.checkbox-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
padding: 4px 0;
|
||||
|
||||
.checkbox-label {
|
||||
margin: 0;
|
||||
font-size: 14px;
|
||||
cursor: pointer;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.date-range {
|
||||
.date-input-group {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.date-separator {
|
||||
margin: 0 5px;
|
||||
color: #777;
|
||||
}
|
||||
}
|
||||
|
||||
.toggle {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
|
||||
.toggle-label {
|
||||
margin: 0;
|
||||
font-size: 14px;
|
||||
cursor: pointer;
|
||||
}
|
||||
}
|
||||
|
||||
.filter-actions {
|
||||
margin-top: 15px;
|
||||
padding-top: 15px;
|
||||
border-top: 1px solid #eee;
|
||||
|
||||
.btn {
|
||||
font-size: 13px;
|
||||
}
|
||||
}
|
||||
|
||||
// New header row styling
|
||||
.header-row {
|
||||
margin-bottom: 15px;
|
||||
padding-bottom: 10px;
|
||||
border-bottom: 1px solid #eee;
|
||||
|
||||
.chart-title {
|
||||
margin: 0;
|
||||
font-size: 18px;
|
||||
font-weight: 600;
|
||||
color: #333;
|
||||
}
|
||||
}
|
||||
|
||||
// Responsive design
|
||||
@media (max-width: 768px) {
|
||||
.filter-controls {
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.filter-item {
|
||||
min-width: 100%;
|
||||
}
|
||||
|
||||
.header-row {
|
||||
.chart-title {
|
||||
font-size: 16px;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,5 +1,7 @@
|
||||
import { Component, OnInit, Input, OnChanges, SimpleChanges } from '@angular/core';
|
||||
import { Dashboard3Service } from 'src/app/services/builder/dashboard3.service';
|
||||
import { FilterService } from '../../common-filter/filter.service';
|
||||
import { Subscription } from 'rxjs';
|
||||
|
||||
@Component({
|
||||
selector: 'app-polar-chart',
|
||||
@@ -33,9 +35,20 @@ export class PolarChartComponent implements OnInit, OnChanges {
|
||||
// Multi-layer drilldown configuration inputs
|
||||
@Input() drilldownLayers: any[] = []; // Array of drilldown layer configurations
|
||||
|
||||
constructor(private dashboardService: Dashboard3Service) { }
|
||||
constructor(
|
||||
private dashboardService: Dashboard3Service,
|
||||
private filterService: FilterService
|
||||
) { }
|
||||
|
||||
ngOnInit(): void {
|
||||
// Subscribe to filter changes
|
||||
this.subscriptions.push(
|
||||
this.filterService.filterState$.subscribe(filters => {
|
||||
// When filters change, refresh the chart data
|
||||
this.fetchChartData();
|
||||
})
|
||||
);
|
||||
|
||||
// Initialize with default data
|
||||
this.fetchChartData();
|
||||
}
|
||||
@@ -43,6 +56,12 @@ export class PolarChartComponent implements OnInit, OnChanges {
|
||||
ngOnChanges(changes: SimpleChanges): void {
|
||||
console.log('PolarChartComponent input changes:', changes);
|
||||
|
||||
// Initialize filter values if they haven't been initialized yet
|
||||
if (!this.filtersInitialized && (changes.baseFilters || changes.drilldownFilters || changes.drilldownLayers)) {
|
||||
this.initializeFilterValues();
|
||||
this.filtersInitialized = true;
|
||||
}
|
||||
|
||||
// Check if any of the key properties have changed
|
||||
const xAxisChanged = changes.xAxis && !changes.xAxis.firstChange;
|
||||
const yAxisChanged = changes.yAxis && !changes.yAxis.firstChange;
|
||||
@@ -71,6 +90,32 @@ export class PolarChartComponent implements OnInit, OnChanges {
|
||||
{ data: [ 300, 500, 100, 40, 120 ], label: 'Series 1'}
|
||||
];
|
||||
|
||||
public polarAreaChartOptions: any = {
|
||||
responsive: true,
|
||||
maintainAspectRatio: false,
|
||||
layout: {
|
||||
padding: {
|
||||
left: 10,
|
||||
right: 10,
|
||||
top: 10,
|
||||
bottom: 30
|
||||
}
|
||||
},
|
||||
plugins: {
|
||||
legend: {
|
||||
display: true,
|
||||
position: 'top'
|
||||
}
|
||||
},
|
||||
scales: {
|
||||
r: {
|
||||
ticks: {
|
||||
backdropColor: 'rgba(0, 0, 0, 0)'
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
public polarAreaChartType: string = 'polarArea';
|
||||
|
||||
// Multi-layer drilldown state tracking
|
||||
@@ -85,6 +130,324 @@ export class PolarChartComponent implements OnInit, OnChanges {
|
||||
// Flag to prevent infinite loops
|
||||
private isFetchingData: boolean = false;
|
||||
|
||||
// Subscriptions to unsubscribe on destroy
|
||||
private subscriptions: Subscription[] = [];
|
||||
|
||||
// Add properties for filter functionality
|
||||
private openMultiselects: Map<string, string> = new Map(); // Map of filterId -> context
|
||||
private documentClickHandler: ((event: MouseEvent) => void) | null = null;
|
||||
private filtersInitialized: boolean = false;
|
||||
|
||||
// Initialize filter values with proper default values based on type
|
||||
private initializeFilterValues(): void {
|
||||
console.log('Initializing filter values');
|
||||
|
||||
// Initialize base filters
|
||||
if (this.baseFilters) {
|
||||
this.baseFilters.forEach(filter => {
|
||||
if (filter.value === undefined || filter.value === null) {
|
||||
switch (filter.type) {
|
||||
case 'multiselect':
|
||||
filter.value = [];
|
||||
break;
|
||||
case 'date-range':
|
||||
filter.value = { start: null, end: null };
|
||||
break;
|
||||
case 'toggle':
|
||||
filter.value = false;
|
||||
break;
|
||||
default:
|
||||
filter.value = '';
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// Initialize drilldown filters
|
||||
if (this.drilldownFilters) {
|
||||
this.drilldownFilters.forEach(filter => {
|
||||
if (filter.value === undefined || filter.value === null) {
|
||||
switch (filter.type) {
|
||||
case 'multiselect':
|
||||
filter.value = [];
|
||||
break;
|
||||
case 'date-range':
|
||||
filter.value = { start: null, end: null };
|
||||
break;
|
||||
case 'toggle':
|
||||
filter.value = false;
|
||||
break;
|
||||
default:
|
||||
filter.value = '';
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// Initialize layer filters
|
||||
if (this.drilldownLayers) {
|
||||
this.drilldownLayers.forEach(layer => {
|
||||
if (layer.filters) {
|
||||
layer.filters.forEach((filter: any) => {
|
||||
if (filter.value === undefined || filter.value === null) {
|
||||
switch (filter.type) {
|
||||
case 'multiselect':
|
||||
filter.value = [];
|
||||
break;
|
||||
case 'date-range':
|
||||
filter.value = { start: null, end: null };
|
||||
break;
|
||||
case 'toggle':
|
||||
filter.value = false;
|
||||
break;
|
||||
default:
|
||||
filter.value = '';
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
console.log('Filter values initialized:', {
|
||||
baseFilters: this.baseFilters,
|
||||
drilldownFilters: this.drilldownFilters,
|
||||
drilldownLayers: this.drilldownLayers
|
||||
});
|
||||
}
|
||||
|
||||
// Check if there are active filters
|
||||
hasActiveFilters(): boolean {
|
||||
return (this.baseFilters && this.baseFilters.length > 0) ||
|
||||
(this.drilldownFilters && this.drilldownFilters.length > 0) ||
|
||||
this.hasActiveLayerFilters();
|
||||
}
|
||||
|
||||
// Check if there are active layer filters for current drilldown level
|
||||
hasActiveLayerFilters(): boolean {
|
||||
if (this.currentDrilldownLevel > 1 && this.drilldownLayers && this.drilldownLayers.length > 0) {
|
||||
const layerIndex = this.currentDrilldownLevel - 2; // -2 because level 1 is base drilldown
|
||||
return layerIndex < this.drilldownLayers.length &&
|
||||
this.drilldownLayers[layerIndex].filters &&
|
||||
this.drilldownLayers[layerIndex].filters.length > 0;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
// Get active layer filters for current drilldown level
|
||||
getActiveLayerFilters(): any[] {
|
||||
if (this.currentDrilldownLevel > 1 && this.drilldownLayers && this.drilldownLayers.length > 0) {
|
||||
const layerIndex = this.currentDrilldownLevel - 2; // -2 because level 1 is base drilldown
|
||||
if (layerIndex < this.drilldownLayers.length &&
|
||||
this.drilldownLayers[layerIndex].filters) {
|
||||
return this.drilldownLayers[layerIndex].filters;
|
||||
}
|
||||
}
|
||||
return [];
|
||||
}
|
||||
|
||||
// Get filter options for dropdown/multiselect filters
|
||||
getFilterOptions(filter: any): string[] {
|
||||
if (filter.options) {
|
||||
if (Array.isArray(filter.options)) {
|
||||
return filter.options;
|
||||
} else if (typeof filter.options === 'string') {
|
||||
return filter.options.split(',').map(opt => opt.trim()).filter(opt => opt);
|
||||
}
|
||||
}
|
||||
return [];
|
||||
}
|
||||
|
||||
// Check if an option is selected for multiselect filters
|
||||
isOptionSelected(filter: any, option: string): boolean {
|
||||
if (!filter.value) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (Array.isArray(filter.value)) {
|
||||
return filter.value.includes(option);
|
||||
}
|
||||
|
||||
return filter.value === option;
|
||||
}
|
||||
|
||||
// Handle base filter changes
|
||||
onBaseFilterChange(filter: any): void {
|
||||
console.log('Base filter changed:', filter);
|
||||
// Refresh data when filter changes
|
||||
this.fetchChartData();
|
||||
}
|
||||
|
||||
// Handle drilldown filter changes
|
||||
onDrilldownFilterChange(filter: any): void {
|
||||
console.log('Drilldown filter changed:', filter);
|
||||
// Refresh data when filter changes
|
||||
this.fetchChartData();
|
||||
}
|
||||
|
||||
// Handle layer filter changes
|
||||
onLayerFilterChange(filter: any): void {
|
||||
console.log('Layer filter changed:', filter);
|
||||
// Refresh data when filter changes
|
||||
this.fetchChartData();
|
||||
}
|
||||
|
||||
// Handle multiselect changes
|
||||
onMultiSelectChange(filter: any, option: string, event: any): void {
|
||||
const checked = event.target.checked;
|
||||
|
||||
// Initialize filter.value as array if it's not already
|
||||
if (!Array.isArray(filter.value)) {
|
||||
filter.value = [];
|
||||
}
|
||||
|
||||
if (checked) {
|
||||
// Add option to array if not already present
|
||||
if (!filter.value.includes(option)) {
|
||||
filter.value.push(option);
|
||||
}
|
||||
} else {
|
||||
// Remove option from array
|
||||
filter.value = filter.value.filter((item: string) => item !== option);
|
||||
}
|
||||
|
||||
// Refresh data when filter changes
|
||||
this.fetchChartData();
|
||||
}
|
||||
|
||||
// Handle date range changes
|
||||
onDateRangeChange(filter: any, dateRange: { start: string | null, end: string | null }): void {
|
||||
filter.value = dateRange;
|
||||
// Refresh data when filter changes
|
||||
this.fetchChartData();
|
||||
}
|
||||
|
||||
// Handle toggle changes
|
||||
onToggleChange(filter: any, checked: boolean): void {
|
||||
filter.value = checked;
|
||||
// Refresh data when filter changes
|
||||
this.fetchChartData();
|
||||
}
|
||||
|
||||
// Toggle multiselect dropdown visibility
|
||||
toggleMultiselect(filter: any, context: string): void {
|
||||
const filterId = `${context}-${filter.field}`;
|
||||
if (this.isMultiselectOpen(filter, context)) {
|
||||
this.openMultiselects.delete(filterId);
|
||||
} else {
|
||||
// Close all other multiselects first
|
||||
this.openMultiselects.clear();
|
||||
this.openMultiselects.set(filterId, context);
|
||||
|
||||
// Add document click handler to close dropdown when clicking outside
|
||||
this.addDocumentClickHandler();
|
||||
}
|
||||
}
|
||||
|
||||
// Add document click handler to close dropdowns when clicking outside
|
||||
private addDocumentClickHandler(): void {
|
||||
if (!this.documentClickHandler) {
|
||||
this.documentClickHandler = (event: MouseEvent) => {
|
||||
const target = event.target as HTMLElement;
|
||||
// Check if click is outside any multiselect dropdown
|
||||
if (!target.closest('.multiselect-container')) {
|
||||
this.openMultiselects.clear();
|
||||
this.removeDocumentClickHandler();
|
||||
}
|
||||
};
|
||||
|
||||
// Use setTimeout to ensure the click event that opened the dropdown doesn't immediately close it
|
||||
setTimeout(() => {
|
||||
document.addEventListener('click', this.documentClickHandler!);
|
||||
}, 0);
|
||||
}
|
||||
}
|
||||
|
||||
// Remove document click handler
|
||||
private removeDocumentClickHandler(): void {
|
||||
if (this.documentClickHandler) {
|
||||
document.removeEventListener('click', this.documentClickHandler);
|
||||
this.documentClickHandler = null;
|
||||
}
|
||||
}
|
||||
|
||||
// Check if multiselect dropdown is open
|
||||
isMultiselectOpen(filter: any, context: string): boolean {
|
||||
const filterId = `${context}-${filter.field}`;
|
||||
return this.openMultiselects.has(filterId);
|
||||
}
|
||||
|
||||
// Get count of selected options for a multiselect filter
|
||||
getSelectedOptionsCount(filter: any): number {
|
||||
if (!filter.value) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
if (Array.isArray(filter.value)) {
|
||||
return filter.value.length;
|
||||
}
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
// Clear all filters
|
||||
clearAllFilters(): void {
|
||||
// Clear base filters
|
||||
if (this.baseFilters) {
|
||||
this.baseFilters.forEach(filter => {
|
||||
if (filter.type === 'multiselect') {
|
||||
filter.value = [];
|
||||
} else if (filter.type === 'date-range') {
|
||||
filter.value = { start: null, end: null };
|
||||
} else if (filter.type === 'toggle') {
|
||||
filter.value = false;
|
||||
} else {
|
||||
filter.value = '';
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// Clear drilldown filters
|
||||
if (this.drilldownFilters) {
|
||||
this.drilldownFilters.forEach(filter => {
|
||||
if (filter.type === 'multiselect') {
|
||||
filter.value = [];
|
||||
} else if (filter.type === 'date-range') {
|
||||
filter.value = { start: null, end: null };
|
||||
} else if (filter.type === 'toggle') {
|
||||
filter.value = false;
|
||||
} else {
|
||||
filter.value = '';
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// Clear layer filters
|
||||
if (this.drilldownLayers) {
|
||||
this.drilldownLayers.forEach(layer => {
|
||||
if (layer.filters) {
|
||||
layer.filters.forEach((filter: any) => {
|
||||
if (filter.type === 'multiselect') {
|
||||
filter.value = [];
|
||||
} else if (filter.type === 'date-range') {
|
||||
filter.value = { start: null, end: null };
|
||||
} else if (filter.type === 'toggle') {
|
||||
filter.value = false;
|
||||
} else {
|
||||
filter.value = '';
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// Close all multiselect dropdowns
|
||||
this.openMultiselects.clear();
|
||||
|
||||
// Refresh data
|
||||
this.fetchChartData();
|
||||
}
|
||||
|
||||
fetchChartData(): void {
|
||||
// Set flag to prevent recursive calls
|
||||
this.isFetchingData = true;
|
||||
@@ -117,7 +480,49 @@ export class PolarChartComponent implements OnInit, OnChanges {
|
||||
filterParams = JSON.stringify(filterObj);
|
||||
}
|
||||
}
|
||||
console.log('Base filter parameters:', filterParams);
|
||||
|
||||
// Add common filters to filter parameters
|
||||
const commonFilters = this.filterService.getFilterValues();
|
||||
console.log('Common filters from service:', commonFilters);
|
||||
|
||||
if (Object.keys(commonFilters).length > 0) {
|
||||
// Merge common filters with base filters
|
||||
const mergedFilterObj = {};
|
||||
|
||||
// Add base filters first
|
||||
if (filterParams) {
|
||||
try {
|
||||
const baseFilterObj = JSON.parse(filterParams);
|
||||
Object.assign(mergedFilterObj, baseFilterObj);
|
||||
} catch (e) {
|
||||
console.warn('Failed to parse base filter parameters:', e);
|
||||
}
|
||||
}
|
||||
|
||||
// Add common filters using the field name as the key, not the filter id
|
||||
Object.keys(commonFilters).forEach(filterId => {
|
||||
const filterValue = commonFilters[filterId];
|
||||
// Find the filter definition to get the field name
|
||||
const filterDef = this.filterService.getFilters().find(f => f.id === filterId);
|
||||
if (filterDef && filterDef.field) {
|
||||
const fieldName = filterDef.field;
|
||||
if (filterValue !== undefined && filterValue !== null && filterValue !== '') {
|
||||
mergedFilterObj[fieldName] = filterValue;
|
||||
}
|
||||
} else {
|
||||
// Fallback to using filterId as field name if no field is defined
|
||||
if (filterValue !== undefined && filterValue !== null && filterValue !== '') {
|
||||
mergedFilterObj[filterId] = filterValue;
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
if (Object.keys(mergedFilterObj).length > 0) {
|
||||
filterParams = JSON.stringify(mergedFilterObj);
|
||||
}
|
||||
}
|
||||
|
||||
console.log('Final filter parameters:', filterParams);
|
||||
|
||||
// Log the URL that will be called
|
||||
const url = `chart/getdashjson/polar?tableName=${this.table}&xAxis=${this.xAxis}&yAxes=${yAxisString}${this.connection ? `&sureId=${this.connection}` : ''}`;
|
||||
@@ -132,7 +537,12 @@ export class PolarChartComponent implements OnInit, OnChanges {
|
||||
console.warn('Polar chart API returned null data. Check if the API endpoint is working correctly.');
|
||||
this.noDataAvailable = true;
|
||||
this.polarAreaChartLabels = [];
|
||||
this.polarAreaChartData = [];
|
||||
this.polarAreaChartData = [
|
||||
{
|
||||
data: [],
|
||||
label: 'Dataset 1'
|
||||
}
|
||||
];
|
||||
// Reset flag after fetching
|
||||
this.isFetchingData = false;
|
||||
return;
|
||||
@@ -145,32 +555,54 @@ export class PolarChartComponent implements OnInit, OnChanges {
|
||||
this.noDataAvailable = data.chartLabels.length === 0;
|
||||
this.polarAreaChartLabels = data.chartLabels;
|
||||
if (data.chartData && data.chartData.length > 0) {
|
||||
this.polarAreaChartData = data.chartData[0].data.map(value => {
|
||||
// Convert the data to the expected format for polar area charts
|
||||
const chartValues = data.chartData[0].data.map(value => {
|
||||
// Convert to number if it's not already
|
||||
return isNaN(Number(value)) ? 0 : Number(value);
|
||||
});
|
||||
// Assign data in the correct format (array of objects with data property)
|
||||
this.polarAreaChartData = [
|
||||
{
|
||||
data: chartValues,
|
||||
label: data.chartData[0].label || 'Dataset 1'
|
||||
}
|
||||
];
|
||||
} else {
|
||||
this.polarAreaChartData = [];
|
||||
this.polarAreaChartData = [
|
||||
{
|
||||
data: [],
|
||||
label: 'Dataset 1'
|
||||
}
|
||||
];
|
||||
}
|
||||
// Trigger change detection
|
||||
this.polarAreaChartData = [...this.polarAreaChartData];
|
||||
console.log('Updated polar chart with data:', { labels: this.polarAreaChartLabels, data: this.polarAreaChartData });
|
||||
} else if (data && data.labels && data.data) {
|
||||
// Handle the original expected format as fallback
|
||||
this.noDataAvailable = data.labels.length === 0;
|
||||
this.polarAreaChartLabels = data.labels;
|
||||
this.polarAreaChartData = data.data.map(value => {
|
||||
// Convert the data to the expected format for polar area charts
|
||||
const chartValues = data.data.map(value => {
|
||||
// Convert to number if it's not already
|
||||
return isNaN(Number(value)) ? 0 : Number(value);
|
||||
});
|
||||
// Trigger change detection
|
||||
this.polarAreaChartData = [...this.polarAreaChartData];
|
||||
// Assign data in the correct format (array of objects with data property)
|
||||
this.polarAreaChartData = [
|
||||
{
|
||||
data: chartValues,
|
||||
label: 'Dataset 1'
|
||||
}
|
||||
];
|
||||
console.log('Updated polar chart with legacy data format:', { labels: this.polarAreaChartLabels, data: this.polarAreaChartData });
|
||||
} else {
|
||||
console.warn('Polar chart received data does not have expected structure', data);
|
||||
this.noDataAvailable = true;
|
||||
this.polarAreaChartLabels = [];
|
||||
this.polarAreaChartData = [];
|
||||
this.polarAreaChartData = [
|
||||
{
|
||||
data: [],
|
||||
label: 'Dataset 1'
|
||||
}
|
||||
];
|
||||
}
|
||||
// Reset flag after fetching
|
||||
this.isFetchingData = false;
|
||||
@@ -179,7 +611,12 @@ export class PolarChartComponent implements OnInit, OnChanges {
|
||||
console.error('Error fetching polar chart data:', error);
|
||||
this.noDataAvailable = true;
|
||||
this.polarAreaChartLabels = [];
|
||||
this.polarAreaChartData = [];
|
||||
this.polarAreaChartData = [
|
||||
{
|
||||
data: [],
|
||||
label: 'Dataset 1'
|
||||
}
|
||||
];
|
||||
// Reset flag after fetching
|
||||
this.isFetchingData = false;
|
||||
// Keep default data in case of error
|
||||
@@ -189,7 +626,12 @@ export class PolarChartComponent implements OnInit, OnChanges {
|
||||
console.log('Missing required data for polar chart:', { table: this.table, xAxis: this.xAxis, yAxis: this.yAxis, connection: this.connection });
|
||||
this.noDataAvailable = true;
|
||||
this.polarAreaChartLabels = [];
|
||||
this.polarAreaChartData = [];
|
||||
this.polarAreaChartData = [
|
||||
{
|
||||
data: [],
|
||||
label: 'Dataset 1'
|
||||
}
|
||||
];
|
||||
// Reset flag after fetching
|
||||
this.isFetchingData = false;
|
||||
}
|
||||
@@ -219,7 +661,12 @@ export class PolarChartComponent implements OnInit, OnChanges {
|
||||
console.warn('Invalid drilldown layer index:', layerIndex);
|
||||
this.noDataAvailable = true;
|
||||
this.polarAreaChartLabels = [];
|
||||
this.polarAreaChartData = [];
|
||||
this.polarAreaChartData = [
|
||||
{
|
||||
data: [],
|
||||
label: 'Dataset 1'
|
||||
}
|
||||
];
|
||||
return;
|
||||
}
|
||||
}
|
||||
@@ -231,7 +678,12 @@ export class PolarChartComponent implements OnInit, OnChanges {
|
||||
console.warn('Missing drilldown configuration for level:', this.currentDrilldownLevel);
|
||||
this.noDataAvailable = true;
|
||||
this.polarAreaChartLabels = [];
|
||||
this.polarAreaChartData = [];
|
||||
this.polarAreaChartData = [
|
||||
{
|
||||
data: [],
|
||||
label: 'Dataset 1'
|
||||
}
|
||||
];
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -287,6 +739,35 @@ export class PolarChartComponent implements OnInit, OnChanges {
|
||||
}
|
||||
}
|
||||
|
||||
// Add common filters to drilldown filter parameters
|
||||
const commonFilters = this.filterService.getFilterValues();
|
||||
if (Object.keys(commonFilters).length > 0) {
|
||||
// Merge common filters with drilldown filters
|
||||
const mergedFilterObj = {};
|
||||
|
||||
// Add drilldown filters first
|
||||
if (filterParams) {
|
||||
try {
|
||||
const drilldownFilterObj = JSON.parse(filterParams);
|
||||
Object.assign(mergedFilterObj, drilldownFilterObj);
|
||||
} catch (e) {
|
||||
console.warn('Failed to parse drilldown filter parameters:', e);
|
||||
}
|
||||
}
|
||||
|
||||
// Add common filters
|
||||
Object.keys(commonFilters).forEach(key => {
|
||||
const value = commonFilters[key];
|
||||
if (value !== undefined && value !== null && value !== '') {
|
||||
mergedFilterObj[key] = value;
|
||||
}
|
||||
});
|
||||
|
||||
if (Object.keys(mergedFilterObj).length > 0) {
|
||||
filterParams = JSON.stringify(mergedFilterObj);
|
||||
}
|
||||
}
|
||||
|
||||
// Log the URL that will be called
|
||||
const url = `chart/getdashjson/polar?tableName=${actualApiUrl}&xAxis=${drilldownConfig.xAxis}&yAxes=${drilldownConfig.yAxis}${this.connection ? `&sureId=${this.connection}` : ''}`;
|
||||
console.log('Drilldown data URL:', url);
|
||||
@@ -300,23 +781,40 @@ export class PolarChartComponent implements OnInit, OnChanges {
|
||||
console.warn('Drilldown API returned null data. Check if the API endpoint is working correctly.');
|
||||
this.noDataAvailable = true;
|
||||
this.polarAreaChartLabels = [];
|
||||
this.polarAreaChartData = [];
|
||||
this.polarAreaChartData = [
|
||||
{
|
||||
data: [],
|
||||
label: 'Dataset 1'
|
||||
}
|
||||
];
|
||||
return;
|
||||
}
|
||||
|
||||
// Handle the actual data structure returned by the API
|
||||
if (data && data.chartLabels && data.chartData) {
|
||||
// For polar charts, we need to extract the data differently
|
||||
// The first dataset's data array contains the values for the polar chart
|
||||
this.noDataAvailable = data.chartLabels.length === 0;
|
||||
this.polarAreaChartLabels = data.chartLabels;
|
||||
if (data.chartData && data.chartData.length > 0) {
|
||||
this.polarAreaChartData = data.chartData[0].data.map(value => {
|
||||
// Convert the data to the expected format for polar area charts
|
||||
const chartValues = data.chartData[0].data.map(value => {
|
||||
// Convert to number if it's not already
|
||||
return isNaN(Number(value)) ? 0 : Number(value);
|
||||
});
|
||||
// Assign data in the correct format (array of objects with data property)
|
||||
this.polarAreaChartData = [
|
||||
{
|
||||
data: chartValues,
|
||||
label: data.chartData[0].label || 'Dataset 1'
|
||||
}
|
||||
];
|
||||
} else {
|
||||
this.polarAreaChartData = [];
|
||||
this.polarAreaChartData = [
|
||||
{
|
||||
data: [],
|
||||
label: 'Dataset 1'
|
||||
}
|
||||
];
|
||||
}
|
||||
// Trigger change detection
|
||||
this.polarAreaChartData = [...this.polarAreaChartData];
|
||||
@@ -325,10 +823,18 @@ export class PolarChartComponent implements OnInit, OnChanges {
|
||||
// Handle the original expected format as fallback
|
||||
this.noDataAvailable = data.labels.length === 0;
|
||||
this.polarAreaChartLabels = data.labels;
|
||||
this.polarAreaChartData = data.data.map(value => {
|
||||
// Convert the data to the expected format for polar area charts
|
||||
const chartValues = data.data.map(value => {
|
||||
// Convert to number if it's not already
|
||||
return isNaN(Number(value)) ? 0 : Number(value);
|
||||
});
|
||||
// Assign data in the correct format (array of objects with data property)
|
||||
this.polarAreaChartData = [
|
||||
{
|
||||
data: chartValues,
|
||||
label: 'Dataset 1'
|
||||
}
|
||||
];
|
||||
// Trigger change detection
|
||||
this.polarAreaChartData = [...this.polarAreaChartData];
|
||||
console.log('Updated polar chart with drilldown legacy data format:', { labels: this.polarAreaChartLabels, data: this.polarAreaChartData });
|
||||
@@ -336,14 +842,24 @@ export class PolarChartComponent implements OnInit, OnChanges {
|
||||
console.warn('Drilldown received data does not have expected structure', data);
|
||||
this.noDataAvailable = true;
|
||||
this.polarAreaChartLabels = [];
|
||||
this.polarAreaChartData = [];
|
||||
this.polarAreaChartData = [
|
||||
{
|
||||
data: [],
|
||||
label: 'Dataset 1'
|
||||
}
|
||||
];
|
||||
}
|
||||
},
|
||||
(error) => {
|
||||
console.error('Error fetching drilldown data:', error);
|
||||
this.noDataAvailable = true;
|
||||
this.polarAreaChartLabels = [];
|
||||
this.polarAreaChartData = [];
|
||||
this.polarAreaChartData = [
|
||||
{
|
||||
data: [],
|
||||
label: 'Dataset 1'
|
||||
}
|
||||
];
|
||||
// Keep current data in case of error
|
||||
}
|
||||
);
|
||||
@@ -417,13 +933,13 @@ export class PolarChartComponent implements OnInit, OnChanges {
|
||||
// Get the label of the clicked element
|
||||
const clickedLabel = this.polarAreaChartLabels[clickedIndex];
|
||||
|
||||
console.log('Clicked on polar area:', { index: clickedIndex, label: clickedLabel });
|
||||
console.log('Clicked on polar point:', { index: clickedIndex, label: clickedLabel });
|
||||
|
||||
// If we're not at the base level, store original data
|
||||
if (this.currentDrilldownLevel === 0) {
|
||||
// Store original data before entering drilldown mode
|
||||
this.originalPolarAreaChartLabels = [...this.polarAreaChartLabels];
|
||||
this.originalPolarAreaChartData = [...this.polarAreaChartData];
|
||||
this.originalPolarAreaChartData = JSON.parse(JSON.stringify(this.polarAreaChartData));
|
||||
console.log('Stored original data for drilldown');
|
||||
}
|
||||
|
||||
@@ -491,6 +1007,12 @@ export class PolarChartComponent implements OnInit, OnChanges {
|
||||
}
|
||||
|
||||
public chartHovered(e: any): void {
|
||||
console.log(e);
|
||||
console.log('Polar chart hovered:', e);
|
||||
}
|
||||
|
||||
ngOnDestroy(): void {
|
||||
this.subscriptions.forEach(sub => sub.unsubscribe());
|
||||
// Clean up document click handler
|
||||
this.removeDocumentClickHandler();
|
||||
}
|
||||
}
|
||||
@@ -1,13 +1,282 @@
|
||||
<div style="display: block">
|
||||
<!-- Drilldown mode indicator -->
|
||||
<div *ngIf="currentDrilldownLevel > 0" style="background-color: #e0e0e0; padding: 5px; margin-bottom: 10px; border-radius: 4px; text-align: center;">
|
||||
<span style="font-weight: bold; color: #333;">Drilldown Level: {{currentDrilldownLevel}}</span>
|
||||
<button (click)="navigateBack()" style="margin-left: 10px; padding: 2px 8px; background-color: #007cba; color: white; border: none; border-radius: 3px; cursor: pointer;">
|
||||
Back to Level {{currentDrilldownLevel - 1}}
|
||||
</button>
|
||||
<button (click)="resetToOriginalData()" style="margin-left: 10px; padding: 2px 8px; background-color: #dc3545; color: white; border: none; border-radius: 3px; cursor: pointer;">
|
||||
Back to Main View
|
||||
</button>
|
||||
<div style="display: block; height: 100%; width: 100%;">
|
||||
<!-- Filter Controls Section -->
|
||||
<div class="filter-section" *ngIf="hasActiveFilters()">
|
||||
<!-- Base Filters -->
|
||||
<div class="filter-group" *ngIf="baseFilters && baseFilters.length > 0">
|
||||
<h4>Base Filters</h4>
|
||||
<div class="filter-controls">
|
||||
<div *ngFor="let filter of baseFilters" class="filter-item">
|
||||
<div class="filter-label">{{ filter.field }} ({{ filter.type || 'text' }})</div>
|
||||
|
||||
<!-- Text Filter -->
|
||||
<div *ngIf="!filter.type || filter.type === 'text'" class="filter-input">
|
||||
<input type="text"
|
||||
[(ngModel)]="filter.value"
|
||||
(ngModelChange)="onBaseFilterChange(filter)"
|
||||
[placeholder]="filter.field"
|
||||
class="clr-input filter-text-input">
|
||||
</div>
|
||||
|
||||
<!-- Dropdown Filter -->
|
||||
<div *ngIf="filter.type === 'dropdown'" class="filter-input">
|
||||
<select [(ngModel)]="filter.value"
|
||||
(ngModelChange)="onBaseFilterChange(filter)"
|
||||
class="clr-select filter-select">
|
||||
<option value="">Select {{ filter.field }}</option>
|
||||
<option *ngFor="let option of getFilterOptions(filter)" [value]="option">{{ option }}</option>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<!-- Multi-Select Filter - Updated to show key first, then dropdown on click -->
|
||||
<div *ngIf="filter.type === 'multiselect'" class="filter-input multiselect-container">
|
||||
<div class="multiselect-display" (click)="toggleMultiselect(filter, 'base')">
|
||||
<span class="multiselect-label">{{ filter.field }}</span>
|
||||
<span class="multiselect-value" *ngIf="getSelectedOptionsCount(filter) > 0">
|
||||
({{ getSelectedOptionsCount(filter) }} selected)
|
||||
</span>
|
||||
<clr-icon shape="caret down" class="dropdown-icon"></clr-icon>
|
||||
</div>
|
||||
<div class="multiselect-dropdown" *ngIf="isMultiselectOpen(filter, 'base')">
|
||||
<div class="checkbox-group">
|
||||
<div *ngFor="let option of getFilterOptions(filter); let i = index" class="checkbox-item">
|
||||
<input type="checkbox"
|
||||
[checked]="isOptionSelected(filter, option)"
|
||||
(change)="onMultiSelectChange(filter, option, $event)"
|
||||
[id]="'base-' + filter.field + '-' + i"
|
||||
class="clr-checkbox">
|
||||
<label [for]="'base-' + filter.field + '-' + i" class="checkbox-label">{{ option }}</label>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Date Range Filter -->
|
||||
<div *ngIf="filter.type === 'date-range'" class="filter-input date-range">
|
||||
<div class="date-input-group">
|
||||
<input type="date"
|
||||
[(ngModel)]="filter.value.start"
|
||||
(ngModelChange)="onDateRangeChange(filter, { start: $event, end: filter.value.end })"
|
||||
placeholder="Start Date"
|
||||
class="clr-input filter-date">
|
||||
<span class="date-separator">to</span>
|
||||
<input type="date"
|
||||
[(ngModel)]="filter.value.end"
|
||||
(ngModelChange)="onDateRangeChange(filter, { start: filter.value.start, end: $event })"
|
||||
placeholder="End Date"
|
||||
class="clr-input filter-date">
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Toggle Filter -->
|
||||
<div *ngIf="filter.type === 'toggle'" class="filter-input toggle">
|
||||
<input type="checkbox"
|
||||
[(ngModel)]="filter.value"
|
||||
(ngModelChange)="onToggleChange(filter, $event)"
|
||||
clrToggle
|
||||
class="clr-toggle">
|
||||
<label class="toggle-label">{{ filter.field }}</label>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Drilldown Filters -->
|
||||
<div class="filter-group" *ngIf="drilldownFilters && drilldownFilters.length > 0 && currentDrilldownLevel > 0">
|
||||
<h4>Drilldown Filters</h4>
|
||||
<div class="filter-controls">
|
||||
<div *ngFor="let filter of drilldownFilters" class="filter-item">
|
||||
<div class="filter-label">{{ filter.field }} ({{ filter.type || 'text' }})</div>
|
||||
|
||||
<!-- Text Filter -->
|
||||
<div *ngIf="!filter.type || filter.type === 'text'" class="filter-input">
|
||||
<input type="text"
|
||||
[(ngModel)]="filter.value"
|
||||
(ngModelChange)="onDrilldownFilterChange(filter)"
|
||||
[placeholder]="filter.field"
|
||||
class="clr-input filter-text-input">
|
||||
</div>
|
||||
|
||||
<!-- Dropdown Filter -->
|
||||
<div *ngIf="filter.type === 'dropdown'" class="filter-input">
|
||||
<select [(ngModel)]="filter.value"
|
||||
(ngModelChange)="onDrilldownFilterChange(filter)"
|
||||
class="clr-select filter-select">
|
||||
<option value="">Select {{ filter.field }}</option>
|
||||
<option *ngFor="let option of getFilterOptions(filter)" [value]="option">{{ option }}</option>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<!-- Multi-Select Filter - Updated to show key first, then dropdown on click -->
|
||||
<div *ngIf="filter.type === 'multiselect'" class="filter-input multiselect-container">
|
||||
<div class="multiselect-display" (click)="toggleMultiselect(filter, 'drilldown')">
|
||||
<span class="multiselect-label">{{ filter.field }}</span>
|
||||
<span class="multiselect-value" *ngIf="getSelectedOptionsCount(filter) > 0">
|
||||
({{ getSelectedOptionsCount(filter) }} selected)
|
||||
</span>
|
||||
<clr-icon shape="caret down" class="dropdown-icon"></clr-icon>
|
||||
</div>
|
||||
<div class="multiselect-dropdown" *ngIf="isMultiselectOpen(filter, 'drilldown')">
|
||||
<div class="checkbox-group">
|
||||
<div *ngFor="let option of getFilterOptions(filter); let i = index" class="checkbox-item">
|
||||
<input type="checkbox"
|
||||
[checked]="isOptionSelected(filter, option)"
|
||||
(change)="onMultiSelectChange(filter, option, $event)"
|
||||
[id]="'drilldown-' + filter.field + '-' + i"
|
||||
class="clr-checkbox">
|
||||
<label [for]="'drilldown-' + filter.field + '-' + i" class="checkbox-label">{{ option }}</label>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Date Range Filter -->
|
||||
<div *ngIf="filter.type === 'date-range'" class="filter-input date-range">
|
||||
<div class="date-input-group">
|
||||
<input type="date"
|
||||
[(ngModel)]="filter.value.start"
|
||||
(ngModelChange)="onDateRangeChange(filter, { start: $event, end: filter.value.end })"
|
||||
placeholder="Start Date"
|
||||
class="clr-input filter-date">
|
||||
<span class="date-separator">to</span>
|
||||
<input type="date"
|
||||
[(ngModel)]="filter.value.end"
|
||||
(ngModelChange)="onDateRangeChange(filter, { start: filter.value.start, end: $event })"
|
||||
placeholder="End Date"
|
||||
class="clr-input filter-date">
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Toggle Filter -->
|
||||
<div *ngIf="filter.type === 'toggle'" class="filter-input toggle">
|
||||
<input type="checkbox"
|
||||
[(ngModel)]="filter.value"
|
||||
(ngModelChange)="onToggleChange(filter, $event)"
|
||||
clrToggle
|
||||
class="clr-toggle">
|
||||
<label class="toggle-label">{{ filter.field }}</label>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Layer Filters -->
|
||||
<div class="filter-group" *ngIf="hasActiveLayerFilters()">
|
||||
<h4>Layer Filters</h4>
|
||||
<div class="filter-controls">
|
||||
<div *ngFor="let filter of getActiveLayerFilters()" class="filter-item">
|
||||
<div class="filter-label">{{ filter.field }} ({{ filter.type || 'text' }})</div>
|
||||
|
||||
<!-- Text Filter -->
|
||||
<div *ngIf="!filter.type || filter.type === 'text'" class="filter-input">
|
||||
<input type="text"
|
||||
[(ngModel)]="filter.value"
|
||||
(ngModelChange)="onLayerFilterChange(filter)"
|
||||
[placeholder]="filter.field"
|
||||
class="clr-input filter-text-input">
|
||||
</div>
|
||||
|
||||
<!-- Dropdown Filter -->
|
||||
<div *ngIf="filter.type === 'dropdown'" class="filter-input">
|
||||
<select [(ngModel)]="filter.value"
|
||||
(ngModelChange)="onLayerFilterChange(filter)"
|
||||
class="clr-select filter-select">
|
||||
<option value="">Select {{ filter.field }}</option>
|
||||
<option *ngFor="let option of getFilterOptions(filter)" [value]="option">{{ option }}</option>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<!-- Multi-Select Filter - Updated to show key first, then dropdown on click -->
|
||||
<div *ngIf="filter.type === 'multiselect'" class="filter-input multiselect-container">
|
||||
<div class="multiselect-display" (click)="toggleMultiselect(filter, 'layer')">
|
||||
<span class="multiselect-label">{{ filter.field }}</span>
|
||||
<span class="multiselect-value" *ngIf="getSelectedOptionsCount(filter) > 0">
|
||||
({{ getSelectedOptionsCount(filter) }} selected)
|
||||
</span>
|
||||
<clr-icon shape="caret down" class="dropdown-icon"></clr-icon>
|
||||
</div>
|
||||
<div class="multiselect-dropdown" *ngIf="isMultiselectOpen(filter, 'layer')">
|
||||
<div class="checkbox-group">
|
||||
<div *ngFor="let option of getFilterOptions(filter); let i = index" class="checkbox-item">
|
||||
<input type="checkbox"
|
||||
[checked]="isOptionSelected(filter, option)"
|
||||
(change)="onMultiSelectChange(filter, option, $event)"
|
||||
[id]="'layer-' + filter.field + '-' + i"
|
||||
class="clr-checkbox">
|
||||
<label [for]="'layer-' + filter.field + '-' + i" class="checkbox-label">{{ option }}</label>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Date Range Filter -->
|
||||
<div *ngIf="filter.type === 'date-range'" class="filter-input date-range">
|
||||
<div class="date-input-group">
|
||||
<input type="date"
|
||||
[(ngModel)]="filter.value.start"
|
||||
(ngModelChange)="onDateRangeChange(filter, { start: $event, end: filter.value.end })"
|
||||
placeholder="Start Date"
|
||||
class="clr-input filter-date">
|
||||
<span class="date-separator">to</span>
|
||||
<input type="date"
|
||||
[(ngModel)]="filter.value.end"
|
||||
(ngModelChange)="onDateRangeChange(filter, { start: filter.value.start, end: $event })"
|
||||
placeholder="End Date"
|
||||
class="clr-input filter-date">
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Toggle Filter -->
|
||||
<div *ngIf="filter.type === 'toggle'" class="filter-input toggle">
|
||||
<input type="checkbox"
|
||||
[(ngModel)]="filter.value"
|
||||
(ngModelChange)="onToggleChange(filter, $event)"
|
||||
clrToggle
|
||||
class="clr-toggle">
|
||||
<label class="toggle-label">{{ filter.field }}</label>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Clear Filters Button -->
|
||||
<div class="filter-actions">
|
||||
<button class="btn btn-sm btn-outline" (click)="clearAllFilters()">Clear All Filters</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Header row with chart title and drilldown navigation -->
|
||||
<div class="clr-row header-row">
|
||||
<div class="clr-col-6">
|
||||
<h3 class="chart-title">{{charttitle || 'Radar Chart'}}</h3>
|
||||
</div>
|
||||
<div class="clr-col-6" style="text-align: right;">
|
||||
<!-- Add drilldown navigation controls -->
|
||||
<button class="btn btn-sm btn-link" *ngIf="drilldownEnabled && drilldownStack.length > 0" (click)="navigateBack()">
|
||||
<cds-icon shape="arrow" direction="left"></cds-icon>
|
||||
Back to {{drilldownStack.length > 1 ? 'Previous Level' : 'Main Data'}}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Show current drilldown level -->
|
||||
<div class="clr-row" *ngIf="drilldownEnabled && drilldownStack.length > 0">
|
||||
<div class="clr-col-12">
|
||||
<div class="alert alert-info" style="padding: 8px 12px; margin-bottom: 12px;">
|
||||
<div class="alert-items">
|
||||
<div class="alert-item static">
|
||||
<div class="alert-icon-wrapper">
|
||||
<cds-icon class="alert-icon" shape="info-circle"></cds-icon>
|
||||
</div>
|
||||
<span class="alert-text">
|
||||
Drilldown Level: {{currentDrilldownLevel}}
|
||||
<span *ngIf="drilldownStack.length > 0">
|
||||
(Clicked on: {{drilldownStack[drilldownStack.length - 1].clickedLabel}})
|
||||
</span>
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- No data message -->
|
||||
@@ -16,7 +285,7 @@
|
||||
</div>
|
||||
|
||||
<!-- Chart display -->
|
||||
<div *ngIf="!noDataAvailable">
|
||||
<div *ngIf="!noDataAvailable" style="position: relative; height: calc(100% - 50px);">
|
||||
<canvas baseChart
|
||||
[datasets]="radarChartData"
|
||||
[labels]="radarChartLabels"
|
||||
|
||||
@@ -0,0 +1,192 @@
|
||||
.filter-section {
|
||||
margin-bottom: 20px;
|
||||
padding: 15px;
|
||||
border: 1px solid #ddd;
|
||||
border-radius: 4px;
|
||||
background-color: #f9f9f9;
|
||||
}
|
||||
|
||||
.filter-group {
|
||||
margin-bottom: 15px;
|
||||
|
||||
h4 {
|
||||
margin-top: 0;
|
||||
margin-bottom: 10px;
|
||||
color: #333;
|
||||
font-weight: 600;
|
||||
}
|
||||
}
|
||||
|
||||
.filter-controls {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 15px;
|
||||
}
|
||||
|
||||
.filter-item {
|
||||
flex: 1 1 300px;
|
||||
min-width: 250px;
|
||||
padding: 10px;
|
||||
background: white;
|
||||
border: 1px solid #e0e0e0;
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
.filter-label {
|
||||
font-weight: 500;
|
||||
margin-bottom: 8px;
|
||||
color: #555;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.filter-input {
|
||||
width: 100%;
|
||||
|
||||
.filter-text-input,
|
||||
.filter-select,
|
||||
.filter-date {
|
||||
width: 100%;
|
||||
padding: 6px 12px;
|
||||
border: 1px solid #ccc;
|
||||
border-radius: 4px;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.filter-select {
|
||||
height: 34px;
|
||||
}
|
||||
}
|
||||
|
||||
.multiselect-container {
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.multiselect-display {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
padding: 6px 12px;
|
||||
border: 1px solid #ccc;
|
||||
border-radius: 4px;
|
||||
background: white;
|
||||
cursor: pointer;
|
||||
min-height: 34px;
|
||||
|
||||
.multiselect-label {
|
||||
flex: 1;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.multiselect-value {
|
||||
color: #666;
|
||||
font-size: 12px;
|
||||
margin-right: 8px;
|
||||
}
|
||||
|
||||
.dropdown-icon {
|
||||
flex-shrink: 0;
|
||||
transition: transform 0.2s ease;
|
||||
}
|
||||
|
||||
&:hover {
|
||||
border-color: #999;
|
||||
}
|
||||
}
|
||||
|
||||
.multiselect-dropdown {
|
||||
position: absolute;
|
||||
top: 100%;
|
||||
left: 0;
|
||||
right: 0;
|
||||
z-index: 1000;
|
||||
background: white;
|
||||
border: 1px solid #ccc;
|
||||
border-top: none;
|
||||
border-radius: 0 0 4px 4px;
|
||||
box-shadow: 0 2px 4px rgba(0,0,0,0.1);
|
||||
max-height: 200px;
|
||||
overflow-y: auto;
|
||||
|
||||
.checkbox-group {
|
||||
padding: 8px;
|
||||
|
||||
.checkbox-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
padding: 4px 0;
|
||||
|
||||
.checkbox-label {
|
||||
margin: 0;
|
||||
font-size: 14px;
|
||||
cursor: pointer;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.date-range {
|
||||
.date-input-group {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.date-separator {
|
||||
margin: 0 5px;
|
||||
color: #777;
|
||||
}
|
||||
}
|
||||
|
||||
.toggle {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
|
||||
.toggle-label {
|
||||
margin: 0;
|
||||
font-size: 14px;
|
||||
cursor: pointer;
|
||||
}
|
||||
}
|
||||
|
||||
.filter-actions {
|
||||
margin-top: 15px;
|
||||
padding-top: 15px;
|
||||
border-top: 1px solid #eee;
|
||||
|
||||
.btn {
|
||||
font-size: 13px;
|
||||
}
|
||||
}
|
||||
|
||||
// New header row styling
|
||||
.header-row {
|
||||
margin-bottom: 15px;
|
||||
padding-bottom: 10px;
|
||||
border-bottom: 1px solid #eee;
|
||||
|
||||
.chart-title {
|
||||
margin: 0;
|
||||
font-size: 18px;
|
||||
font-weight: 600;
|
||||
color: #333;
|
||||
}
|
||||
}
|
||||
|
||||
// Responsive design
|
||||
@media (max-width: 768px) {
|
||||
.filter-controls {
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.filter-item {
|
||||
min-width: 100%;
|
||||
}
|
||||
|
||||
.header-row {
|
||||
.chart-title {
|
||||
font-size: 16px;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,5 +1,7 @@
|
||||
import { Component, OnInit, Input, OnChanges, SimpleChanges } from '@angular/core';
|
||||
import { Dashboard3Service } from 'src/app/services/builder/dashboard3.service';
|
||||
import { FilterService } from '../../common-filter/filter.service';
|
||||
import { Subscription } from 'rxjs';
|
||||
|
||||
@Component({
|
||||
selector: 'app-radar-chart',
|
||||
@@ -62,15 +64,40 @@ export class RadarChartComponent implements OnInit, OnChanges {
|
||||
// Flag to prevent infinite loops
|
||||
private isFetchingData: boolean = false;
|
||||
|
||||
constructor(private dashboardService: Dashboard3Service) { }
|
||||
// Subscriptions to unsubscribe on destroy
|
||||
private subscriptions: Subscription[] = [];
|
||||
|
||||
// Add properties for filter functionality
|
||||
private openMultiselects: Map<string, string> = new Map(); // Map of filterId -> context
|
||||
private documentClickHandler: ((event: MouseEvent) => void) | null = null;
|
||||
private filtersInitialized: boolean = false;
|
||||
|
||||
constructor(
|
||||
private dashboardService: Dashboard3Service,
|
||||
private filterService: FilterService
|
||||
) { }
|
||||
|
||||
ngOnInit(): void {
|
||||
// Subscribe to filter changes
|
||||
this.subscriptions.push(
|
||||
this.filterService.filterState$.subscribe(filters => {
|
||||
// When filters change, refresh the chart data
|
||||
this.fetchChartData();
|
||||
})
|
||||
);
|
||||
|
||||
this.fetchChartData();
|
||||
}
|
||||
|
||||
ngOnChanges(changes: SimpleChanges): void {
|
||||
console.log('RadarChartComponent input changes:', changes);
|
||||
|
||||
// Initialize filter values if they haven't been initialized yet
|
||||
if (!this.filtersInitialized && (changes.baseFilters || changes.drilldownFilters || changes.drilldownLayers)) {
|
||||
this.initializeFilterValues();
|
||||
this.filtersInitialized = true;
|
||||
}
|
||||
|
||||
// Check if any of the key properties have changed
|
||||
const xAxisChanged = changes.xAxis && !changes.xAxis.firstChange;
|
||||
const yAxisChanged = changes.yAxis && !changes.yAxis.firstChange;
|
||||
@@ -94,6 +121,316 @@ export class RadarChartComponent implements OnInit, OnChanges {
|
||||
}
|
||||
}
|
||||
|
||||
// Initialize filter values with proper default values based on type
|
||||
private initializeFilterValues(): void {
|
||||
console.log('Initializing filter values');
|
||||
|
||||
// Initialize base filters
|
||||
if (this.baseFilters) {
|
||||
this.baseFilters.forEach(filter => {
|
||||
if (filter.value === undefined || filter.value === null) {
|
||||
switch (filter.type) {
|
||||
case 'multiselect':
|
||||
filter.value = [];
|
||||
break;
|
||||
case 'date-range':
|
||||
filter.value = { start: null, end: null };
|
||||
break;
|
||||
case 'toggle':
|
||||
filter.value = false;
|
||||
break;
|
||||
default:
|
||||
filter.value = '';
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// Initialize drilldown filters
|
||||
if (this.drilldownFilters) {
|
||||
this.drilldownFilters.forEach(filter => {
|
||||
if (filter.value === undefined || filter.value === null) {
|
||||
switch (filter.type) {
|
||||
case 'multiselect':
|
||||
filter.value = [];
|
||||
break;
|
||||
case 'date-range':
|
||||
filter.value = { start: null, end: null };
|
||||
break;
|
||||
case 'toggle':
|
||||
filter.value = false;
|
||||
break;
|
||||
default:
|
||||
filter.value = '';
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// Initialize layer filters
|
||||
if (this.drilldownLayers) {
|
||||
this.drilldownLayers.forEach(layer => {
|
||||
if (layer.filters) {
|
||||
layer.filters.forEach((filter: any) => {
|
||||
if (filter.value === undefined || filter.value === null) {
|
||||
switch (filter.type) {
|
||||
case 'multiselect':
|
||||
filter.value = [];
|
||||
break;
|
||||
case 'date-range':
|
||||
filter.value = { start: null, end: null };
|
||||
break;
|
||||
case 'toggle':
|
||||
filter.value = false;
|
||||
break;
|
||||
default:
|
||||
filter.value = '';
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
console.log('Filter values initialized:', {
|
||||
baseFilters: this.baseFilters,
|
||||
drilldownFilters: this.drilldownFilters,
|
||||
drilldownLayers: this.drilldownLayers
|
||||
});
|
||||
}
|
||||
|
||||
// Check if there are active filters
|
||||
hasActiveFilters(): boolean {
|
||||
return (this.baseFilters && this.baseFilters.length > 0) ||
|
||||
(this.drilldownFilters && this.drilldownFilters.length > 0) ||
|
||||
this.hasActiveLayerFilters();
|
||||
}
|
||||
|
||||
// Check if there are active layer filters for current drilldown level
|
||||
hasActiveLayerFilters(): boolean {
|
||||
if (this.currentDrilldownLevel > 1 && this.drilldownLayers && this.drilldownLayers.length > 0) {
|
||||
const layerIndex = this.currentDrilldownLevel - 2; // -2 because level 1 is base drilldown
|
||||
return layerIndex < this.drilldownLayers.length &&
|
||||
this.drilldownLayers[layerIndex].filters &&
|
||||
this.drilldownLayers[layerIndex].filters.length > 0;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
// Get active layer filters for current drilldown level
|
||||
getActiveLayerFilters(): any[] {
|
||||
if (this.currentDrilldownLevel > 1 && this.drilldownLayers && this.drilldownLayers.length > 0) {
|
||||
const layerIndex = this.currentDrilldownLevel - 2; // -2 because level 1 is base drilldown
|
||||
if (layerIndex < this.drilldownLayers.length &&
|
||||
this.drilldownLayers[layerIndex].filters) {
|
||||
return this.drilldownLayers[layerIndex].filters;
|
||||
}
|
||||
}
|
||||
return [];
|
||||
}
|
||||
|
||||
// Get filter options for dropdown/multiselect filters
|
||||
getFilterOptions(filter: any): string[] {
|
||||
if (filter.options) {
|
||||
if (Array.isArray(filter.options)) {
|
||||
return filter.options;
|
||||
} else if (typeof filter.options === 'string') {
|
||||
return filter.options.split(',').map(opt => opt.trim()).filter(opt => opt);
|
||||
}
|
||||
}
|
||||
return [];
|
||||
}
|
||||
|
||||
// Check if an option is selected for multiselect filters
|
||||
isOptionSelected(filter: any, option: string): boolean {
|
||||
if (!filter.value) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (Array.isArray(filter.value)) {
|
||||
return filter.value.includes(option);
|
||||
}
|
||||
|
||||
return filter.value === option;
|
||||
}
|
||||
|
||||
// Handle base filter changes
|
||||
onBaseFilterChange(filter: any): void {
|
||||
console.log('Base filter changed:', filter);
|
||||
// Refresh data when filter changes
|
||||
this.fetchChartData();
|
||||
}
|
||||
|
||||
// Handle drilldown filter changes
|
||||
onDrilldownFilterChange(filter: any): void {
|
||||
console.log('Drilldown filter changed:', filter);
|
||||
// Refresh data when filter changes
|
||||
this.fetchChartData();
|
||||
}
|
||||
|
||||
// Handle layer filter changes
|
||||
onLayerFilterChange(filter: any): void {
|
||||
console.log('Layer filter changed:', filter);
|
||||
// Refresh data when filter changes
|
||||
this.fetchChartData();
|
||||
}
|
||||
|
||||
// Handle multiselect changes
|
||||
onMultiSelectChange(filter: any, option: string, event: any): void {
|
||||
const checked = event.target.checked;
|
||||
|
||||
// Initialize filter.value as array if it's not already
|
||||
if (!Array.isArray(filter.value)) {
|
||||
filter.value = [];
|
||||
}
|
||||
|
||||
if (checked) {
|
||||
// Add option to array if not already present
|
||||
if (!filter.value.includes(option)) {
|
||||
filter.value.push(option);
|
||||
}
|
||||
} else {
|
||||
// Remove option from array
|
||||
filter.value = filter.value.filter((item: string) => item !== option);
|
||||
}
|
||||
|
||||
// Refresh data when filter changes
|
||||
this.fetchChartData();
|
||||
}
|
||||
|
||||
// Handle date range changes
|
||||
onDateRangeChange(filter: any, dateRange: { start: string | null, end: string | null }): void {
|
||||
filter.value = dateRange;
|
||||
// Refresh data when filter changes
|
||||
this.fetchChartData();
|
||||
}
|
||||
|
||||
// Handle toggle changes
|
||||
onToggleChange(filter: any, checked: boolean): void {
|
||||
filter.value = checked;
|
||||
// Refresh data when filter changes
|
||||
this.fetchChartData();
|
||||
}
|
||||
|
||||
// Toggle multiselect dropdown visibility
|
||||
toggleMultiselect(filter: any, context: string): void {
|
||||
const filterId = `${context}-${filter.field}`;
|
||||
if (this.isMultiselectOpen(filter, context)) {
|
||||
this.openMultiselects.delete(filterId);
|
||||
} else {
|
||||
// Close all other multiselects first
|
||||
this.openMultiselects.clear();
|
||||
this.openMultiselects.set(filterId, context);
|
||||
|
||||
// Add document click handler to close dropdown when clicking outside
|
||||
this.addDocumentClickHandler();
|
||||
}
|
||||
}
|
||||
|
||||
// Add document click handler to close dropdowns when clicking outside
|
||||
private addDocumentClickHandler(): void {
|
||||
if (!this.documentClickHandler) {
|
||||
this.documentClickHandler = (event: MouseEvent) => {
|
||||
const target = event.target as HTMLElement;
|
||||
// Check if click is outside any multiselect dropdown
|
||||
if (!target.closest('.multiselect-container')) {
|
||||
this.openMultiselects.clear();
|
||||
this.removeDocumentClickHandler();
|
||||
}
|
||||
};
|
||||
|
||||
// Use setTimeout to ensure the click event that opened the dropdown doesn't immediately close it
|
||||
setTimeout(() => {
|
||||
document.addEventListener('click', this.documentClickHandler!);
|
||||
}, 0);
|
||||
}
|
||||
}
|
||||
|
||||
// Remove document click handler
|
||||
private removeDocumentClickHandler(): void {
|
||||
if (this.documentClickHandler) {
|
||||
document.removeEventListener('click', this.documentClickHandler);
|
||||
this.documentClickHandler = null;
|
||||
}
|
||||
}
|
||||
|
||||
// Check if multiselect dropdown is open
|
||||
isMultiselectOpen(filter: any, context: string): boolean {
|
||||
const filterId = `${context}-${filter.field}`;
|
||||
return this.openMultiselects.has(filterId);
|
||||
}
|
||||
|
||||
// Get count of selected options for a multiselect filter
|
||||
getSelectedOptionsCount(filter: any): number {
|
||||
if (!filter.value) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
if (Array.isArray(filter.value)) {
|
||||
return filter.value.length;
|
||||
}
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
// Clear all filters
|
||||
clearAllFilters(): void {
|
||||
// Clear base filters
|
||||
if (this.baseFilters) {
|
||||
this.baseFilters.forEach(filter => {
|
||||
if (filter.type === 'multiselect') {
|
||||
filter.value = [];
|
||||
} else if (filter.type === 'date-range') {
|
||||
filter.value = { start: null, end: null };
|
||||
} else if (filter.type === 'toggle') {
|
||||
filter.value = false;
|
||||
} else {
|
||||
filter.value = '';
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// Clear drilldown filters
|
||||
if (this.drilldownFilters) {
|
||||
this.drilldownFilters.forEach(filter => {
|
||||
if (filter.type === 'multiselect') {
|
||||
filter.value = [];
|
||||
} else if (filter.type === 'date-range') {
|
||||
filter.value = { start: null, end: null };
|
||||
} else if (filter.type === 'toggle') {
|
||||
filter.value = false;
|
||||
} else {
|
||||
filter.value = '';
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// Clear layer filters
|
||||
if (this.drilldownLayers) {
|
||||
this.drilldownLayers.forEach(layer => {
|
||||
if (layer.filters) {
|
||||
layer.filters.forEach((filter: any) => {
|
||||
if (filter.type === 'multiselect') {
|
||||
filter.value = [];
|
||||
} else if (filter.type === 'date-range') {
|
||||
filter.value = { start: null, end: null };
|
||||
} else if (filter.type === 'toggle') {
|
||||
filter.value = false;
|
||||
} else {
|
||||
filter.value = '';
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// Close all multiselect dropdowns
|
||||
this.openMultiselects.clear();
|
||||
|
||||
// Refresh data
|
||||
this.fetchChartData();
|
||||
}
|
||||
|
||||
fetchChartData(): void {
|
||||
// Set flag to prevent recursive calls
|
||||
this.isFetchingData = true;
|
||||
@@ -126,7 +463,49 @@ export class RadarChartComponent implements OnInit, OnChanges {
|
||||
filterParams = JSON.stringify(filterObj);
|
||||
}
|
||||
}
|
||||
console.log('Base filter parameters:', filterParams);
|
||||
|
||||
// Add common filters to filter parameters
|
||||
const commonFilters = this.filterService.getFilterValues();
|
||||
console.log('Common filters from service:', commonFilters);
|
||||
|
||||
if (Object.keys(commonFilters).length > 0) {
|
||||
// Merge common filters with base filters
|
||||
const mergedFilterObj = {};
|
||||
|
||||
// Add base filters first
|
||||
if (filterParams) {
|
||||
try {
|
||||
const baseFilterObj = JSON.parse(filterParams);
|
||||
Object.assign(mergedFilterObj, baseFilterObj);
|
||||
} catch (e) {
|
||||
console.warn('Failed to parse base filter parameters:', e);
|
||||
}
|
||||
}
|
||||
|
||||
// Add common filters using the field name as the key, not the filter id
|
||||
Object.keys(commonFilters).forEach(filterId => {
|
||||
const filterValue = commonFilters[filterId];
|
||||
// Find the filter definition to get the field name
|
||||
const filterDef = this.filterService.getFilters().find(f => f.id === filterId);
|
||||
if (filterDef && filterDef.field) {
|
||||
const fieldName = filterDef.field;
|
||||
if (filterValue !== undefined && filterValue !== null && filterValue !== '') {
|
||||
mergedFilterObj[fieldName] = filterValue;
|
||||
}
|
||||
} else {
|
||||
// Fallback to using filterId as field name if no field is defined
|
||||
if (filterValue !== undefined && filterValue !== null && filterValue !== '') {
|
||||
mergedFilterObj[filterId] = filterValue;
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
if (Object.keys(mergedFilterObj).length > 0) {
|
||||
filterParams = JSON.stringify(mergedFilterObj);
|
||||
}
|
||||
}
|
||||
|
||||
console.log('Final filter parameters:', filterParams);
|
||||
|
||||
// Log the URL that will be called
|
||||
const url = `chart/getdashjson/radar?tableName=${this.table}&xAxis=${this.xAxis}&yAxes=${yAxisString}${this.connection ? `&sureId=${this.connection}` : ''}`;
|
||||
@@ -298,6 +677,35 @@ export class RadarChartComponent implements OnInit, OnChanges {
|
||||
}
|
||||
}
|
||||
|
||||
// Add common filters to drilldown filter parameters
|
||||
const commonFilters = this.filterService.getFilterValues();
|
||||
if (Object.keys(commonFilters).length > 0) {
|
||||
// Merge common filters with drilldown filters
|
||||
const mergedFilterObj = {};
|
||||
|
||||
// Add drilldown filters first
|
||||
if (filterParams) {
|
||||
try {
|
||||
const drilldownFilterObj = JSON.parse(filterParams);
|
||||
Object.assign(mergedFilterObj, drilldownFilterObj);
|
||||
} catch (e) {
|
||||
console.warn('Failed to parse drilldown filter parameters:', e);
|
||||
}
|
||||
}
|
||||
|
||||
// Add common filters
|
||||
Object.keys(commonFilters).forEach(key => {
|
||||
const value = commonFilters[key];
|
||||
if (value !== undefined && value !== null && value !== '') {
|
||||
mergedFilterObj[key] = value;
|
||||
}
|
||||
});
|
||||
|
||||
if (Object.keys(mergedFilterObj).length > 0) {
|
||||
filterParams = JSON.stringify(mergedFilterObj);
|
||||
}
|
||||
}
|
||||
|
||||
// Log the URL that will be called
|
||||
const url = `chart/getdashjson/radar?tableName=${actualApiUrl}&xAxis=${drilldownConfig.xAxis}&yAxes=${drilldownConfig.yAxis}${this.connection ? `&sureId=${this.connection}` : ''}`;
|
||||
console.log('Drilldown data URL:', url);
|
||||
@@ -321,7 +729,6 @@ export class RadarChartComponent implements OnInit, OnChanges {
|
||||
this.noDataAvailable = data.chartLabels.length === 0;
|
||||
this.radarChartLabels = data.chartLabels;
|
||||
// For radar charts, we need to ensure the data is properly formatted
|
||||
// Each dataset should have a data array with numeric values
|
||||
this.radarChartData = data.chartData.map(dataset => ({
|
||||
...dataset,
|
||||
data: dataset.data ? dataset.data.map(value => {
|
||||
@@ -358,6 +765,7 @@ export class RadarChartComponent implements OnInit, OnChanges {
|
||||
this.noDataAvailable = true;
|
||||
this.radarChartLabels = [];
|
||||
this.radarChartData = [];
|
||||
// Keep current data in case of error
|
||||
}
|
||||
);
|
||||
}
|
||||
@@ -436,7 +844,7 @@ export class RadarChartComponent implements OnInit, OnChanges {
|
||||
if (this.currentDrilldownLevel === 0) {
|
||||
// Store original data before entering drilldown mode
|
||||
this.originalRadarChartLabels = [...this.radarChartLabels];
|
||||
this.originalRadarChartData = [...this.radarChartData];
|
||||
this.originalRadarChartData = JSON.parse(JSON.stringify(this.radarChartData));
|
||||
console.log('Stored original data for drilldown');
|
||||
}
|
||||
|
||||
@@ -504,6 +912,12 @@ export class RadarChartComponent implements OnInit, OnChanges {
|
||||
}
|
||||
|
||||
public chartHovered(e: any): void {
|
||||
console.log(e);
|
||||
console.log('Radar chart hovered:', e);
|
||||
}
|
||||
|
||||
ngOnDestroy(): void {
|
||||
this.subscriptions.forEach(sub => sub.unsubscribe());
|
||||
// Clean up document click handler
|
||||
this.removeDocumentClickHandler();
|
||||
}
|
||||
}
|
||||
@@ -1,13 +1,282 @@
|
||||
<div style="display: block">
|
||||
<!-- Drilldown mode indicator -->
|
||||
<div *ngIf="currentDrilldownLevel > 0" style="background-color: #e0e0e0; padding: 5px; margin-bottom: 10px; border-radius: 4px; text-align: center;">
|
||||
<span style="font-weight: bold; color: #333;">Drilldown Level: {{currentDrilldownLevel}}</span>
|
||||
<button (click)="navigateBack()" style="margin-left: 10px; padding: 2px 8px; background-color: #007cba; color: white; border: none; border-radius: 3px; cursor: pointer;">
|
||||
Back to Level {{currentDrilldownLevel - 1}}
|
||||
</button>
|
||||
<button (click)="resetToOriginalData()" style="margin-left: 10px; padding: 2px 8px; background-color: #dc3545; color: white; border: none; border-radius: 3px; cursor: pointer;">
|
||||
Back to Main View
|
||||
</button>
|
||||
<div style="display: block; height: 100%; width: 100%;">
|
||||
<!-- Filter Controls Section -->
|
||||
<div class="filter-section" *ngIf="hasActiveFilters()">
|
||||
<!-- Base Filters -->
|
||||
<div class="filter-group" *ngIf="baseFilters && baseFilters.length > 0">
|
||||
<h4>Base Filters</h4>
|
||||
<div class="filter-controls">
|
||||
<div *ngFor="let filter of baseFilters" class="filter-item">
|
||||
<div class="filter-label">{{ filter.field }} ({{ filter.type || 'text' }})</div>
|
||||
|
||||
<!-- Text Filter -->
|
||||
<div *ngIf="!filter.type || filter.type === 'text'" class="filter-input">
|
||||
<input type="text"
|
||||
[(ngModel)]="filter.value"
|
||||
(ngModelChange)="onBaseFilterChange(filter)"
|
||||
[placeholder]="filter.field"
|
||||
class="clr-input filter-text-input">
|
||||
</div>
|
||||
|
||||
<!-- Dropdown Filter -->
|
||||
<div *ngIf="filter.type === 'dropdown'" class="filter-input">
|
||||
<select [(ngModel)]="filter.value"
|
||||
(ngModelChange)="onBaseFilterChange(filter)"
|
||||
class="clr-select filter-select">
|
||||
<option value="">Select {{ filter.field }}</option>
|
||||
<option *ngFor="let option of getFilterOptions(filter)" [value]="option">{{ option }}</option>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<!-- Multi-Select Filter - Updated to show key first, then dropdown on click -->
|
||||
<div *ngIf="filter.type === 'multiselect'" class="filter-input multiselect-container">
|
||||
<div class="multiselect-display" (click)="toggleMultiselect(filter, 'base')">
|
||||
<span class="multiselect-label">{{ filter.field }}</span>
|
||||
<span class="multiselect-value" *ngIf="getSelectedOptionsCount(filter) > 0">
|
||||
({{ getSelectedOptionsCount(filter) }} selected)
|
||||
</span>
|
||||
<clr-icon shape="caret down" class="dropdown-icon"></clr-icon>
|
||||
</div>
|
||||
<div class="multiselect-dropdown" *ngIf="isMultiselectOpen(filter, 'base')">
|
||||
<div class="checkbox-group">
|
||||
<div *ngFor="let option of getFilterOptions(filter); let i = index" class="checkbox-item">
|
||||
<input type="checkbox"
|
||||
[checked]="isOptionSelected(filter, option)"
|
||||
(change)="onMultiSelectChange(filter, option, $event)"
|
||||
[id]="'base-' + filter.field + '-' + i"
|
||||
class="clr-checkbox">
|
||||
<label [for]="'base-' + filter.field + '-' + i" class="checkbox-label">{{ option }}</label>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Date Range Filter -->
|
||||
<div *ngIf="filter.type === 'date-range'" class="filter-input date-range">
|
||||
<div class="date-input-group">
|
||||
<input type="date"
|
||||
[(ngModel)]="filter.value.start"
|
||||
(ngModelChange)="onDateRangeChange(filter, { start: $event, end: filter.value.end })"
|
||||
placeholder="Start Date"
|
||||
class="clr-input filter-date">
|
||||
<span class="date-separator">to</span>
|
||||
<input type="date"
|
||||
[(ngModel)]="filter.value.end"
|
||||
(ngModelChange)="onDateRangeChange(filter, { start: filter.value.start, end: $event })"
|
||||
placeholder="End Date"
|
||||
class="clr-input filter-date">
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Toggle Filter -->
|
||||
<div *ngIf="filter.type === 'toggle'" class="filter-input toggle">
|
||||
<input type="checkbox"
|
||||
[(ngModel)]="filter.value"
|
||||
(ngModelChange)="onToggleChange(filter, $event)"
|
||||
clrToggle
|
||||
class="clr-toggle">
|
||||
<label class="toggle-label">{{ filter.field }}</label>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Drilldown Filters -->
|
||||
<div class="filter-group" *ngIf="drilldownFilters && drilldownFilters.length > 0 && currentDrilldownLevel > 0">
|
||||
<h4>Drilldown Filters</h4>
|
||||
<div class="filter-controls">
|
||||
<div *ngFor="let filter of drilldownFilters" class="filter-item">
|
||||
<div class="filter-label">{{ filter.field }} ({{ filter.type || 'text' }})</div>
|
||||
|
||||
<!-- Text Filter -->
|
||||
<div *ngIf="!filter.type || filter.type === 'text'" class="filter-input">
|
||||
<input type="text"
|
||||
[(ngModel)]="filter.value"
|
||||
(ngModelChange)="onDrilldownFilterChange(filter)"
|
||||
[placeholder]="filter.field"
|
||||
class="clr-input filter-text-input">
|
||||
</div>
|
||||
|
||||
<!-- Dropdown Filter -->
|
||||
<div *ngIf="filter.type === 'dropdown'" class="filter-input">
|
||||
<select [(ngModel)]="filter.value"
|
||||
(ngModelChange)="onDrilldownFilterChange(filter)"
|
||||
class="clr-select filter-select">
|
||||
<option value="">Select {{ filter.field }}</option>
|
||||
<option *ngFor="let option of getFilterOptions(filter)" [value]="option">{{ option }}</option>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<!-- Multi-Select Filter - Updated to show key first, then dropdown on click -->
|
||||
<div *ngIf="filter.type === 'multiselect'" class="filter-input multiselect-container">
|
||||
<div class="multiselect-display" (click)="toggleMultiselect(filter, 'drilldown')">
|
||||
<span class="multiselect-label">{{ filter.field }}</span>
|
||||
<span class="multiselect-value" *ngIf="getSelectedOptionsCount(filter) > 0">
|
||||
({{ getSelectedOptionsCount(filter) }} selected)
|
||||
</span>
|
||||
<clr-icon shape="caret down" class="dropdown-icon"></clr-icon>
|
||||
</div>
|
||||
<div class="multiselect-dropdown" *ngIf="isMultiselectOpen(filter, 'drilldown')">
|
||||
<div class="checkbox-group">
|
||||
<div *ngFor="let option of getFilterOptions(filter); let i = index" class="checkbox-item">
|
||||
<input type="checkbox"
|
||||
[checked]="isOptionSelected(filter, option)"
|
||||
(change)="onMultiSelectChange(filter, option, $event)"
|
||||
[id]="'drilldown-' + filter.field + '-' + i"
|
||||
class="clr-checkbox">
|
||||
<label [for]="'drilldown-' + filter.field + '-' + i" class="checkbox-label">{{ option }}</label>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Date Range Filter -->
|
||||
<div *ngIf="filter.type === 'date-range'" class="filter-input date-range">
|
||||
<div class="date-input-group">
|
||||
<input type="date"
|
||||
[(ngModel)]="filter.value.start"
|
||||
(ngModelChange)="onDateRangeChange(filter, { start: $event, end: filter.value.end })"
|
||||
placeholder="Start Date"
|
||||
class="clr-input filter-date">
|
||||
<span class="date-separator">to</span>
|
||||
<input type="date"
|
||||
[(ngModel)]="filter.value.end"
|
||||
(ngModelChange)="onDateRangeChange(filter, { start: filter.value.start, end: $event })"
|
||||
placeholder="End Date"
|
||||
class="clr-input filter-date">
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Toggle Filter -->
|
||||
<div *ngIf="filter.type === 'toggle'" class="filter-input toggle">
|
||||
<input type="checkbox"
|
||||
[(ngModel)]="filter.value"
|
||||
(ngModelChange)="onToggleChange(filter, $event)"
|
||||
clrToggle
|
||||
class="clr-toggle">
|
||||
<label class="toggle-label">{{ filter.field }}</label>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Layer Filters -->
|
||||
<div class="filter-group" *ngIf="hasActiveLayerFilters()">
|
||||
<h4>Layer Filters</h4>
|
||||
<div class="filter-controls">
|
||||
<div *ngFor="let filter of getActiveLayerFilters()" class="filter-item">
|
||||
<div class="filter-label">{{ filter.field }} ({{ filter.type || 'text' }})</div>
|
||||
|
||||
<!-- Text Filter -->
|
||||
<div *ngIf="!filter.type || filter.type === 'text'" class="filter-input">
|
||||
<input type="text"
|
||||
[(ngModel)]="filter.value"
|
||||
(ngModelChange)="onLayerFilterChange(filter)"
|
||||
[placeholder]="filter.field"
|
||||
class="clr-input filter-text-input">
|
||||
</div>
|
||||
|
||||
<!-- Dropdown Filter -->
|
||||
<div *ngIf="filter.type === 'dropdown'" class="filter-input">
|
||||
<select [(ngModel)]="filter.value"
|
||||
(ngModelChange)="onLayerFilterChange(filter)"
|
||||
class="clr-select filter-select">
|
||||
<option value="">Select {{ filter.field }}</option>
|
||||
<option *ngFor="let option of getFilterOptions(filter)" [value]="option">{{ option }}</option>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<!-- Multi-Select Filter - Updated to show key first, then dropdown on click -->
|
||||
<div *ngIf="filter.type === 'multiselect'" class="filter-input multiselect-container">
|
||||
<div class="multiselect-display" (click)="toggleMultiselect(filter, 'layer')">
|
||||
<span class="multiselect-label">{{ filter.field }}</span>
|
||||
<span class="multiselect-value" *ngIf="getSelectedOptionsCount(filter) > 0">
|
||||
({{ getSelectedOptionsCount(filter) }} selected)
|
||||
</span>
|
||||
<clr-icon shape="caret down" class="dropdown-icon"></clr-icon>
|
||||
</div>
|
||||
<div class="multiselect-dropdown" *ngIf="isMultiselectOpen(filter, 'layer')">
|
||||
<div class="checkbox-group">
|
||||
<div *ngFor="let option of getFilterOptions(filter); let i = index" class="checkbox-item">
|
||||
<input type="checkbox"
|
||||
[checked]="isOptionSelected(filter, option)"
|
||||
(change)="onMultiSelectChange(filter, option, $event)"
|
||||
[id]="'layer-' + filter.field + '-' + i"
|
||||
class="clr-checkbox">
|
||||
<label [for]="'layer-' + filter.field + '-' + i" class="checkbox-label">{{ option }}</label>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Date Range Filter -->
|
||||
<div *ngIf="filter.type === 'date-range'" class="filter-input date-range">
|
||||
<div class="date-input-group">
|
||||
<input type="date"
|
||||
[(ngModel)]="filter.value.start"
|
||||
(ngModelChange)="onDateRangeChange(filter, { start: $event, end: filter.value.end })"
|
||||
placeholder="Start Date"
|
||||
class="clr-input filter-date">
|
||||
<span class="date-separator">to</span>
|
||||
<input type="date"
|
||||
[(ngModel)]="filter.value.end"
|
||||
(ngModelChange)="onDateRangeChange(filter, { start: filter.value.start, end: $event })"
|
||||
placeholder="End Date"
|
||||
class="clr-input filter-date">
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Toggle Filter -->
|
||||
<div *ngIf="filter.type === 'toggle'" class="filter-input toggle">
|
||||
<input type="checkbox"
|
||||
[(ngModel)]="filter.value"
|
||||
(ngModelChange)="onToggleChange(filter, $event)"
|
||||
clrToggle
|
||||
class="clr-toggle">
|
||||
<label class="toggle-label">{{ filter.field }}</label>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Clear Filters Button -->
|
||||
<div class="filter-actions">
|
||||
<button class="btn btn-sm btn-outline" (click)="clearAllFilters()">Clear All Filters</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Header row with chart title and drilldown navigation -->
|
||||
<div class="clr-row header-row">
|
||||
<div class="clr-col-6">
|
||||
<h3 class="chart-title">{{charttitle || 'Scatter Chart'}}</h3>
|
||||
</div>
|
||||
<div class="clr-col-6" style="text-align: right;">
|
||||
<!-- Add drilldown navigation controls -->
|
||||
<button class="btn btn-sm btn-link" *ngIf="drilldownEnabled && drilldownStack.length > 0" (click)="navigateBack()">
|
||||
<cds-icon shape="arrow" direction="left"></cds-icon>
|
||||
Back to {{drilldownStack.length > 1 ? 'Previous Level' : 'Main Data'}}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Show current drilldown level -->
|
||||
<div class="clr-row" *ngIf="drilldownEnabled && drilldownStack.length > 0">
|
||||
<div class="clr-col-12">
|
||||
<div class="alert alert-info" style="padding: 8px 12px; margin-bottom: 12px;">
|
||||
<div class="alert-items">
|
||||
<div class="alert-item static">
|
||||
<div class="alert-icon-wrapper">
|
||||
<cds-icon class="alert-icon" shape="info-circle"></cds-icon>
|
||||
</div>
|
||||
<span class="alert-text">
|
||||
Drilldown Level: {{currentDrilldownLevel}}
|
||||
<span *ngIf="drilldownStack.length > 0">
|
||||
(Clicked on: {{drilldownStack[drilldownStack.length - 1].clickedLabel}})
|
||||
</span>
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- No data message -->
|
||||
@@ -16,9 +285,10 @@
|
||||
</div>
|
||||
|
||||
<!-- Chart display -->
|
||||
<div *ngIf="!noDataAvailable">
|
||||
<div *ngIf="!noDataAvailable" style="position: relative; height: calc(100% - 70px); min-height: 300px;">
|
||||
<canvas baseChart
|
||||
[datasets]="scatterChartData"
|
||||
[options]="scatterChartOptions"
|
||||
[type]="scatterChartType"
|
||||
(chartHover)="chartHovered($event)"
|
||||
(chartClick)="chartClicked($event)">
|
||||
|
||||
@@ -0,0 +1,192 @@
|
||||
.filter-section {
|
||||
margin-bottom: 20px;
|
||||
padding: 15px;
|
||||
border: 1px solid #ddd;
|
||||
border-radius: 4px;
|
||||
background-color: #f9f9f9;
|
||||
}
|
||||
|
||||
.filter-group {
|
||||
margin-bottom: 15px;
|
||||
|
||||
h4 {
|
||||
margin-top: 0;
|
||||
margin-bottom: 10px;
|
||||
color: #333;
|
||||
font-weight: 600;
|
||||
}
|
||||
}
|
||||
|
||||
.filter-controls {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 15px;
|
||||
}
|
||||
|
||||
.filter-item {
|
||||
flex: 1 1 300px;
|
||||
min-width: 250px;
|
||||
padding: 10px;
|
||||
background: white;
|
||||
border: 1px solid #e0e0e0;
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
.filter-label {
|
||||
font-weight: 500;
|
||||
margin-bottom: 8px;
|
||||
color: #555;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.filter-input {
|
||||
width: 100%;
|
||||
|
||||
.filter-text-input,
|
||||
.filter-select,
|
||||
.filter-date {
|
||||
width: 100%;
|
||||
padding: 6px 12px;
|
||||
border: 1px solid #ccc;
|
||||
border-radius: 4px;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.filter-select {
|
||||
height: 34px;
|
||||
}
|
||||
}
|
||||
|
||||
.multiselect-container {
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.multiselect-display {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
padding: 6px 12px;
|
||||
border: 1px solid #ccc;
|
||||
border-radius: 4px;
|
||||
background: white;
|
||||
cursor: pointer;
|
||||
min-height: 34px;
|
||||
|
||||
.multiselect-label {
|
||||
flex: 1;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.multiselect-value {
|
||||
color: #666;
|
||||
font-size: 12px;
|
||||
margin-right: 8px;
|
||||
}
|
||||
|
||||
.dropdown-icon {
|
||||
flex-shrink: 0;
|
||||
transition: transform 0.2s ease;
|
||||
}
|
||||
|
||||
&:hover {
|
||||
border-color: #999;
|
||||
}
|
||||
}
|
||||
|
||||
.multiselect-dropdown {
|
||||
position: absolute;
|
||||
top: 100%;
|
||||
left: 0;
|
||||
right: 0;
|
||||
z-index: 1000;
|
||||
background: white;
|
||||
border: 1px solid #ccc;
|
||||
border-top: none;
|
||||
border-radius: 0 0 4px 4px;
|
||||
box-shadow: 0 2px 4px rgba(0,0,0,0.1);
|
||||
max-height: 200px;
|
||||
overflow-y: auto;
|
||||
|
||||
.checkbox-group {
|
||||
padding: 8px;
|
||||
|
||||
.checkbox-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
padding: 4px 0;
|
||||
|
||||
.checkbox-label {
|
||||
margin: 0;
|
||||
font-size: 14px;
|
||||
cursor: pointer;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.date-range {
|
||||
.date-input-group {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.date-separator {
|
||||
margin: 0 5px;
|
||||
color: #777;
|
||||
}
|
||||
}
|
||||
|
||||
.toggle {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
|
||||
.toggle-label {
|
||||
margin: 0;
|
||||
font-size: 14px;
|
||||
cursor: pointer;
|
||||
}
|
||||
}
|
||||
|
||||
.filter-actions {
|
||||
margin-top: 15px;
|
||||
padding-top: 15px;
|
||||
border-top: 1px solid #eee;
|
||||
|
||||
.btn {
|
||||
font-size: 13px;
|
||||
}
|
||||
}
|
||||
|
||||
// New header row styling
|
||||
.header-row {
|
||||
margin-bottom: 15px;
|
||||
padding-bottom: 10px;
|
||||
border-bottom: 1px solid #eee;
|
||||
|
||||
.chart-title {
|
||||
margin: 0;
|
||||
font-size: 18px;
|
||||
font-weight: 600;
|
||||
color: #333;
|
||||
}
|
||||
}
|
||||
|
||||
// Responsive design
|
||||
@media (max-width: 768px) {
|
||||
.filter-controls {
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.filter-item {
|
||||
min-width: 100%;
|
||||
}
|
||||
|
||||
.header-row {
|
||||
.chart-title {
|
||||
font-size: 16px;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,6 +1,8 @@
|
||||
import { Component, OnInit, Input, OnChanges, SimpleChanges } from '@angular/core';
|
||||
import { ChartData,ChartDataset } from 'chart.js';
|
||||
import { ChartData,ChartDataset,ChartOptions } from 'chart.js';
|
||||
import { Dashboard3Service } from 'src/app/services/builder/dashboard3.service';
|
||||
import { FilterService } from '../../common-filter/filter.service';
|
||||
import { Subscription } from 'rxjs';
|
||||
|
||||
@Component({
|
||||
selector: 'app-scatter-chart',
|
||||
@@ -34,9 +36,20 @@ export class ScatterChartComponent implements OnInit, OnChanges {
|
||||
// Multi-layer drilldown configuration inputs
|
||||
@Input() drilldownLayers: any[] = []; // Array of drilldown layer configurations
|
||||
|
||||
constructor(private dashboardService: Dashboard3Service) { }
|
||||
constructor(
|
||||
private dashboardService: Dashboard3Service,
|
||||
private filterService: FilterService
|
||||
) { }
|
||||
|
||||
ngOnInit(): void {
|
||||
// Subscribe to filter changes
|
||||
this.subscriptions.push(
|
||||
this.filterService.filterState$.subscribe(filters => {
|
||||
// When filters change, refresh the chart data
|
||||
this.fetchChartData();
|
||||
})
|
||||
);
|
||||
|
||||
// Initialize with default data
|
||||
this.fetchChartData();
|
||||
}
|
||||
@@ -44,6 +57,12 @@ export class ScatterChartComponent implements OnInit, OnChanges {
|
||||
ngOnChanges(changes: SimpleChanges): void {
|
||||
console.log('ScatterChartComponent input changes:', changes);
|
||||
|
||||
// Initialize filter values if they haven't been initialized yet
|
||||
if (!this.filtersInitialized && (changes.baseFilters || changes.drilldownFilters || changes.drilldownLayers)) {
|
||||
this.initializeFilterValues();
|
||||
this.filtersInitialized = true;
|
||||
}
|
||||
|
||||
// Check if any of the key properties have changed
|
||||
const xAxisChanged = changes.xAxis && !changes.xAxis.firstChange;
|
||||
const yAxisChanged = changes.yAxis && !changes.yAxis.firstChange;
|
||||
@@ -94,6 +113,89 @@ export class ScatterChartComponent implements OnInit, OnChanges {
|
||||
],
|
||||
},
|
||||
];
|
||||
|
||||
public scatterChartOptions: any = {
|
||||
responsive: true,
|
||||
maintainAspectRatio: false,
|
||||
scales: {
|
||||
x: {
|
||||
type: 'linear',
|
||||
position: 'bottom',
|
||||
title: {
|
||||
display: true,
|
||||
text: 'X Axis'
|
||||
},
|
||||
ticks: {
|
||||
autoSkip: true,
|
||||
maxTicksLimit: 10,
|
||||
callback: function(value: any) {
|
||||
if (typeof value === 'number') {
|
||||
// Format large numbers for better readability
|
||||
if (Math.abs(value) >= 1000000) {
|
||||
return (value / 1000000).toFixed(1) + 'M';
|
||||
} else if (Math.abs(value) >= 1000) {
|
||||
return (value / 1000).toFixed(1) + 'K';
|
||||
}
|
||||
return value.toString();
|
||||
}
|
||||
return value;
|
||||
}
|
||||
},
|
||||
grid: {
|
||||
display: true,
|
||||
color: 'rgba(0, 0, 0, 0.1)'
|
||||
}
|
||||
},
|
||||
y: {
|
||||
title: {
|
||||
display: true,
|
||||
text: 'Y Axis'
|
||||
},
|
||||
ticks: {
|
||||
autoSkip: true,
|
||||
maxTicksLimit: 10,
|
||||
callback: function(value: any) {
|
||||
if (typeof value === 'number') {
|
||||
// Format large numbers for better readability
|
||||
if (Math.abs(value) >= 1000000) {
|
||||
return (value / 1000000).toFixed(1) + 'M';
|
||||
} else if (Math.abs(value) >= 1000) {
|
||||
return (value / 1000).toFixed(1) + 'K';
|
||||
}
|
||||
return value.toString();
|
||||
}
|
||||
return value;
|
||||
}
|
||||
},
|
||||
grid: {
|
||||
display: true,
|
||||
color: 'rgba(0, 0, 0, 0.1)'
|
||||
}
|
||||
}
|
||||
},
|
||||
plugins: {
|
||||
legend: {
|
||||
display: true,
|
||||
position: 'top',
|
||||
},
|
||||
tooltip: {
|
||||
callbacks: {
|
||||
label: function(context: any) {
|
||||
return `(${context.parsed.x}, ${context.parsed.y})`;
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
layout: {
|
||||
padding: {
|
||||
left: 15,
|
||||
right: 15,
|
||||
top: 15,
|
||||
bottom: 60 // Add padding at the bottom to ensure X-axis visibility
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
public scatterChartType: string = 'scatter';
|
||||
|
||||
// Multi-layer drilldown state tracking
|
||||
@@ -107,6 +209,417 @@ export class ScatterChartComponent implements OnInit, OnChanges {
|
||||
// Flag to prevent infinite loops
|
||||
private isFetchingData: boolean = false;
|
||||
|
||||
// Subscriptions to unsubscribe on destroy
|
||||
private subscriptions: Subscription[] = [];
|
||||
|
||||
// Add properties for filter functionality
|
||||
private openMultiselects: Map<string, string> = new Map(); // Map of filterId -> context
|
||||
private documentClickHandler: ((event: MouseEvent) => void) | null = null;
|
||||
private filtersInitialized: boolean = false;
|
||||
|
||||
// Initialize filter values with proper default values based on type
|
||||
private initializeFilterValues(): void {
|
||||
console.log('Initializing filter values');
|
||||
|
||||
// Initialize base filters
|
||||
if (this.baseFilters) {
|
||||
this.baseFilters.forEach(filter => {
|
||||
if (filter.value === undefined || filter.value === null) {
|
||||
switch (filter.type) {
|
||||
case 'multiselect':
|
||||
filter.value = [];
|
||||
break;
|
||||
case 'date-range':
|
||||
filter.value = { start: null, end: null };
|
||||
break;
|
||||
case 'toggle':
|
||||
filter.value = false;
|
||||
break;
|
||||
default:
|
||||
filter.value = '';
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// Initialize drilldown filters
|
||||
if (this.drilldownFilters) {
|
||||
this.drilldownFilters.forEach(filter => {
|
||||
if (filter.value === undefined || filter.value === null) {
|
||||
switch (filter.type) {
|
||||
case 'multiselect':
|
||||
filter.value = [];
|
||||
break;
|
||||
case 'date-range':
|
||||
filter.value = { start: null, end: null };
|
||||
break;
|
||||
case 'toggle':
|
||||
filter.value = false;
|
||||
break;
|
||||
default:
|
||||
filter.value = '';
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// Initialize layer filters
|
||||
if (this.drilldownLayers) {
|
||||
this.drilldownLayers.forEach(layer => {
|
||||
if (layer.filters) {
|
||||
layer.filters.forEach((filter: any) => {
|
||||
if (filter.value === undefined || filter.value === null) {
|
||||
switch (filter.type) {
|
||||
case 'multiselect':
|
||||
filter.value = [];
|
||||
break;
|
||||
case 'date-range':
|
||||
filter.value = { start: null, end: null };
|
||||
break;
|
||||
case 'toggle':
|
||||
filter.value = false;
|
||||
break;
|
||||
default:
|
||||
filter.value = '';
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
console.log('Filter values initialized:', {
|
||||
baseFilters: this.baseFilters,
|
||||
drilldownFilters: this.drilldownFilters,
|
||||
drilldownLayers: this.drilldownLayers
|
||||
});
|
||||
}
|
||||
|
||||
// Check if there are active filters
|
||||
hasActiveFilters(): boolean {
|
||||
return (this.baseFilters && this.baseFilters.length > 0) ||
|
||||
(this.drilldownFilters && this.drilldownFilters.length > 0) ||
|
||||
this.hasActiveLayerFilters();
|
||||
}
|
||||
|
||||
// Check if there are active layer filters for current drilldown level
|
||||
hasActiveLayerFilters(): boolean {
|
||||
if (this.currentDrilldownLevel > 1 && this.drilldownLayers && this.drilldownLayers.length > 0) {
|
||||
const layerIndex = this.currentDrilldownLevel - 2; // -2 because level 1 is base drilldown
|
||||
return layerIndex < this.drilldownLayers.length &&
|
||||
this.drilldownLayers[layerIndex].filters &&
|
||||
this.drilldownLayers[layerIndex].filters.length > 0;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
// Get active layer filters for current drilldown level
|
||||
getActiveLayerFilters(): any[] {
|
||||
if (this.currentDrilldownLevel > 1 && this.drilldownLayers && this.drilldownLayers.length > 0) {
|
||||
const layerIndex = this.currentDrilldownLevel - 2; // -2 because level 1 is base drilldown
|
||||
if (layerIndex < this.drilldownLayers.length &&
|
||||
this.drilldownLayers[layerIndex].filters) {
|
||||
return this.drilldownLayers[layerIndex].filters;
|
||||
}
|
||||
}
|
||||
return [];
|
||||
}
|
||||
|
||||
// Get filter options for dropdown/multiselect filters
|
||||
getFilterOptions(filter: any): string[] {
|
||||
if (filter.options) {
|
||||
if (Array.isArray(filter.options)) {
|
||||
return filter.options;
|
||||
} else if (typeof filter.options === 'string') {
|
||||
return filter.options.split(',').map(opt => opt.trim()).filter(opt => opt);
|
||||
}
|
||||
}
|
||||
return [];
|
||||
}
|
||||
|
||||
// Check if an option is selected for multiselect filters
|
||||
isOptionSelected(filter: any, option: string): boolean {
|
||||
if (!filter.value) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (Array.isArray(filter.value)) {
|
||||
return filter.value.includes(option);
|
||||
}
|
||||
|
||||
return filter.value === option;
|
||||
}
|
||||
|
||||
// Handle base filter changes
|
||||
onBaseFilterChange(filter: any): void {
|
||||
console.log('Base filter changed:', filter);
|
||||
// Refresh data when filter changes
|
||||
this.fetchChartData();
|
||||
}
|
||||
|
||||
// Handle drilldown filter changes
|
||||
onDrilldownFilterChange(filter: any): void {
|
||||
console.log('Drilldown filter changed:', filter);
|
||||
// Refresh data when filter changes
|
||||
this.fetchChartData();
|
||||
}
|
||||
|
||||
// Handle layer filter changes
|
||||
onLayerFilterChange(filter: any): void {
|
||||
console.log('Layer filter changed:', filter);
|
||||
// Refresh data when filter changes
|
||||
this.fetchChartData();
|
||||
}
|
||||
|
||||
// Handle multiselect changes
|
||||
onMultiSelectChange(filter: any, option: string, event: any): void {
|
||||
const checked = event.target.checked;
|
||||
|
||||
// Initialize filter.value as array if it's not already
|
||||
if (!Array.isArray(filter.value)) {
|
||||
filter.value = [];
|
||||
}
|
||||
|
||||
if (checked) {
|
||||
// Add option to array if not already present
|
||||
if (!filter.value.includes(option)) {
|
||||
filter.value.push(option);
|
||||
}
|
||||
} else {
|
||||
// Remove option from array
|
||||
filter.value = filter.value.filter((item: string) => item !== option);
|
||||
}
|
||||
|
||||
// Refresh data when filter changes
|
||||
this.fetchChartData();
|
||||
}
|
||||
|
||||
// Handle date range changes
|
||||
onDateRangeChange(filter: any, dateRange: { start: string | null, end: string | null }): void {
|
||||
filter.value = dateRange;
|
||||
// Refresh data when filter changes
|
||||
this.fetchChartData();
|
||||
}
|
||||
|
||||
// Handle toggle changes
|
||||
onToggleChange(filter: any, checked: boolean): void {
|
||||
filter.value = checked;
|
||||
// Refresh data when filter changes
|
||||
this.fetchChartData();
|
||||
}
|
||||
|
||||
// Toggle multiselect dropdown visibility
|
||||
toggleMultiselect(filter: any, context: string): void {
|
||||
const filterId = `${context}-${filter.field}`;
|
||||
if (this.isMultiselectOpen(filter, context)) {
|
||||
this.openMultiselects.delete(filterId);
|
||||
} else {
|
||||
// Close all other multiselects first
|
||||
this.openMultiselects.clear();
|
||||
this.openMultiselects.set(filterId, context);
|
||||
|
||||
// Add document click handler to close dropdown when clicking outside
|
||||
this.addDocumentClickHandler();
|
||||
}
|
||||
}
|
||||
|
||||
// Add document click handler to close dropdowns when clicking outside
|
||||
private addDocumentClickHandler(): void {
|
||||
if (!this.documentClickHandler) {
|
||||
this.documentClickHandler = (event: MouseEvent) => {
|
||||
const target = event.target as HTMLElement;
|
||||
// Check if click is outside any multiselect dropdown
|
||||
if (!target.closest('.multiselect-container')) {
|
||||
this.openMultiselects.clear();
|
||||
this.removeDocumentClickHandler();
|
||||
}
|
||||
};
|
||||
|
||||
// Use setTimeout to ensure the click event that opened the dropdown doesn't immediately close it
|
||||
setTimeout(() => {
|
||||
document.addEventListener('click', this.documentClickHandler!);
|
||||
}, 0);
|
||||
}
|
||||
}
|
||||
|
||||
// Remove document click handler
|
||||
private removeDocumentClickHandler(): void {
|
||||
if (this.documentClickHandler) {
|
||||
document.removeEventListener('click', this.documentClickHandler);
|
||||
this.documentClickHandler = null;
|
||||
}
|
||||
}
|
||||
|
||||
// Check if multiselect dropdown is open
|
||||
isMultiselectOpen(filter: any, context: string): boolean {
|
||||
const filterId = `${context}-${filter.field}`;
|
||||
return this.openMultiselects.has(filterId);
|
||||
}
|
||||
|
||||
// Get count of selected options for a multiselect filter
|
||||
getSelectedOptionsCount(filter: any): number {
|
||||
if (!filter.value) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
if (Array.isArray(filter.value)) {
|
||||
return filter.value.length;
|
||||
}
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
// Clear all filters
|
||||
clearAllFilters(): void {
|
||||
// Clear base filters
|
||||
if (this.baseFilters) {
|
||||
this.baseFilters.forEach(filter => {
|
||||
if (filter.type === 'multiselect') {
|
||||
filter.value = [];
|
||||
} else if (filter.type === 'date-range') {
|
||||
filter.value = { start: null, end: null };
|
||||
} else if (filter.type === 'toggle') {
|
||||
filter.value = false;
|
||||
} else {
|
||||
filter.value = '';
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// Clear drilldown filters
|
||||
if (this.drilldownFilters) {
|
||||
this.drilldownFilters.forEach(filter => {
|
||||
if (filter.type === 'multiselect') {
|
||||
filter.value = [];
|
||||
} else if (filter.type === 'date-range') {
|
||||
filter.value = { start: null, end: null };
|
||||
} else if (filter.type === 'toggle') {
|
||||
filter.value = false;
|
||||
} else {
|
||||
filter.value = '';
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// Clear layer filters
|
||||
if (this.drilldownLayers) {
|
||||
this.drilldownLayers.forEach(layer => {
|
||||
if (layer.filters) {
|
||||
layer.filters.forEach((filter: any) => {
|
||||
if (filter.type === 'multiselect') {
|
||||
filter.value = [];
|
||||
} else if (filter.type === 'date-range') {
|
||||
filter.value = { start: null, end: null };
|
||||
} else if (filter.type === 'toggle') {
|
||||
filter.value = false;
|
||||
} else {
|
||||
filter.value = '';
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// Close all multiselect dropdowns
|
||||
this.openMultiselects.clear();
|
||||
|
||||
// Refresh data
|
||||
this.fetchChartData();
|
||||
}
|
||||
|
||||
// Transform data to scatter chart format
|
||||
private transformToScatterData(labels: any[], data: any[]): ChartDataset[] {
|
||||
// For scatter charts, we need to transform the data into scatter format
|
||||
// Scatter charts expect data in the format: {x: number, y: number}
|
||||
console.log('Transforming data to scatter format:', { labels, data });
|
||||
|
||||
// If we have the expected scatter data format, return it as is
|
||||
if (data && data.length > 0 && data[0].data && data[0].data.length > 0 &&
|
||||
typeof data[0].data[0] === 'object' && data[0].data[0].hasOwnProperty('x') &&
|
||||
data[0].data[0].hasOwnProperty('y')) {
|
||||
return data;
|
||||
}
|
||||
|
||||
// Transform the data properly for scatter chart
|
||||
// Assuming labels are x-values and data[0].data are y-values
|
||||
if (labels && data && data.length > 0 && data[0].data) {
|
||||
const yValues = data[0].data;
|
||||
const label = data[0].label || 'Dataset 1';
|
||||
|
||||
// Create scatter points from labels (x) and data (y)
|
||||
const scatterPoints = [];
|
||||
const minLength = Math.min(labels.length, yValues.length);
|
||||
|
||||
for (let i = 0; i < minLength; i++) {
|
||||
// Convert to numbers if they're strings
|
||||
const x = typeof labels[i] === 'string' ? parseFloat(labels[i]) : labels[i];
|
||||
const y = typeof yValues[i] === 'string' ? parseFloat(yValues[i]) : yValues[i];
|
||||
|
||||
// Only add valid points
|
||||
if (!isNaN(x) && !isNaN(y)) {
|
||||
scatterPoints.push({ x, y });
|
||||
}
|
||||
}
|
||||
|
||||
// Generate different colors for each point to avoid all points showing the same color
|
||||
const backgroundColors = [];
|
||||
const borderColors = [];
|
||||
|
||||
for (let i = 0; i < scatterPoints.length; i++) {
|
||||
// Generate a color based on the point index for variety
|
||||
const hue = (i * 137.508) % 360; // Use golden angle to spread colors
|
||||
backgroundColors.push(`hsla(${hue}, 70%, 50%, 0.6)`);
|
||||
borderColors.push(`hsla(${hue}, 70%, 40%, 1)`);
|
||||
}
|
||||
|
||||
// Create a single dataset with all scatter points
|
||||
const scatterDatasets: ChartDataset[] = [
|
||||
{
|
||||
data: scatterPoints,
|
||||
label: label,
|
||||
pointRadius: 8,
|
||||
pointHoverRadius: 10,
|
||||
backgroundColor: backgroundColors,
|
||||
borderColor: borderColors,
|
||||
borderWidth: 1,
|
||||
pointHoverBackgroundColor: 'rgba(255, 99, 132, 1)',
|
||||
}
|
||||
];
|
||||
|
||||
console.log('Transformed scatter data:', scatterDatasets);
|
||||
return scatterDatasets;
|
||||
}
|
||||
|
||||
// Otherwise, create a default scatter dataset
|
||||
const scatterDatasets: ChartDataset[] = [
|
||||
{
|
||||
data: [
|
||||
{ x: 1, y: 1 },
|
||||
{ x: 2, y: 3 },
|
||||
{ x: 3, y: -2 },
|
||||
{ x: 4, y: 4 },
|
||||
{ x: 5, y: -3 },
|
||||
],
|
||||
label: 'Dataset 1',
|
||||
pointRadius: 10,
|
||||
backgroundColor: [
|
||||
'red',
|
||||
'green',
|
||||
'blue',
|
||||
'purple',
|
||||
'yellow',
|
||||
'brown',
|
||||
'magenta',
|
||||
'cyan',
|
||||
'orange',
|
||||
'pink'
|
||||
],
|
||||
}
|
||||
];
|
||||
|
||||
return scatterDatasets;
|
||||
}
|
||||
|
||||
fetchChartData(): void {
|
||||
// Set flag to prevent recursive calls
|
||||
this.isFetchingData = true;
|
||||
@@ -139,7 +652,49 @@ export class ScatterChartComponent implements OnInit, OnChanges {
|
||||
filterParams = JSON.stringify(filterObj);
|
||||
}
|
||||
}
|
||||
console.log('Base filter parameters:', filterParams);
|
||||
|
||||
// Add common filters to filter parameters
|
||||
const commonFilters = this.filterService.getFilterValues();
|
||||
console.log('Common filters from service:', commonFilters);
|
||||
|
||||
if (Object.keys(commonFilters).length > 0) {
|
||||
// Merge common filters with base filters
|
||||
const mergedFilterObj = {};
|
||||
|
||||
// Add base filters first
|
||||
if (filterParams) {
|
||||
try {
|
||||
const baseFilterObj = JSON.parse(filterParams);
|
||||
Object.assign(mergedFilterObj, baseFilterObj);
|
||||
} catch (e) {
|
||||
console.warn('Failed to parse base filter parameters:', e);
|
||||
}
|
||||
}
|
||||
|
||||
// Add common filters using the field name as the key, not the filter id
|
||||
Object.keys(commonFilters).forEach(filterId => {
|
||||
const filterValue = commonFilters[filterId];
|
||||
// Find the filter definition to get the field name
|
||||
const filterDef = this.filterService.getFilters().find(f => f.id === filterId);
|
||||
if (filterDef && filterDef.field) {
|
||||
const fieldName = filterDef.field;
|
||||
if (filterValue !== undefined && filterValue !== null && filterValue !== '') {
|
||||
mergedFilterObj[fieldName] = filterValue;
|
||||
}
|
||||
} else {
|
||||
// Fallback to using filterId as field name if no field is defined
|
||||
if (filterValue !== undefined && filterValue !== null && filterValue !== '') {
|
||||
mergedFilterObj[filterId] = filterValue;
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
if (Object.keys(mergedFilterObj).length > 0) {
|
||||
filterParams = JSON.stringify(mergedFilterObj);
|
||||
}
|
||||
}
|
||||
|
||||
console.log('Final filter parameters:', filterParams);
|
||||
|
||||
// Log the URL that will be called
|
||||
const url = `chart/getdashjson/scatter?tableName=${this.table}&xAxis=${this.xAxis}&yAxes=${yAxisString}${this.connection ? `&sureId=${this.connection}` : ''}`;
|
||||
@@ -165,11 +720,19 @@ export class ScatterChartComponent implements OnInit, OnChanges {
|
||||
// Scatter charts expect data in the format: {x: number, y: number}
|
||||
this.noDataAvailable = data.chartLabels.length === 0;
|
||||
this.scatterChartData = this.transformToScatterData(data.chartLabels, data.chartData);
|
||||
|
||||
// Update chart options with axis titles
|
||||
this.updateChartOptionsWithAxisTitles();
|
||||
|
||||
console.log('Updated scatter chart with data:', this.scatterChartData);
|
||||
} else if (data && data.labels && data.datasets) {
|
||||
// Handle the original expected format as fallback
|
||||
this.noDataAvailable = data.labels.length === 0;
|
||||
this.scatterChartData = data.datasets;
|
||||
|
||||
// Update chart options with axis titles
|
||||
this.updateChartOptionsWithAxisTitles();
|
||||
|
||||
console.log('Updated scatter chart with legacy data format:', this.scatterChartData);
|
||||
} else {
|
||||
console.warn('Scatter chart received data does not have expected structure', data);
|
||||
@@ -197,6 +760,20 @@ export class ScatterChartComponent implements OnInit, OnChanges {
|
||||
}
|
||||
}
|
||||
|
||||
// Update chart options with axis titles
|
||||
private updateChartOptionsWithAxisTitles(): void {
|
||||
// Update X axis title
|
||||
if (this.scatterChartOptions.scales && this.scatterChartOptions.scales.x) {
|
||||
this.scatterChartOptions.scales.x.title.text = this.xAxis || 'X Axis';
|
||||
}
|
||||
|
||||
// Update Y axis title
|
||||
if (this.scatterChartOptions.scales && this.scatterChartOptions.scales.y) {
|
||||
const yAxisLabel = Array.isArray(this.yAxis) ? this.yAxis[0] : this.yAxis;
|
||||
this.scatterChartOptions.scales.y.title.text = yAxisLabel || 'Y Axis';
|
||||
}
|
||||
}
|
||||
|
||||
// Fetch drilldown data based on current drilldown level
|
||||
fetchDrilldownData(): void {
|
||||
console.log('Fetching drilldown data, current level:', this.currentDrilldownLevel);
|
||||
@@ -287,24 +864,34 @@ export class ScatterChartComponent implements OnInit, OnChanges {
|
||||
}
|
||||
}
|
||||
|
||||
// Convert drilldownFilters to filter parameters for drilldown level
|
||||
let drilldownFilterParams = '';
|
||||
if (this.drilldownFilters && this.drilldownFilters.length > 0) {
|
||||
const filterObj = {};
|
||||
this.drilldownFilters.forEach(filter => {
|
||||
if (filter.field && filter.value) {
|
||||
filterObj[filter.field] = filter.value;
|
||||
// Add common filters to drilldown filter parameters
|
||||
const commonFilters = this.filterService.getFilterValues();
|
||||
if (Object.keys(commonFilters).length > 0) {
|
||||
// Merge common filters with drilldown filters
|
||||
const mergedFilterObj = {};
|
||||
|
||||
// Add drilldown filters first
|
||||
if (filterParams) {
|
||||
try {
|
||||
const drilldownFilterObj = JSON.parse(filterParams);
|
||||
Object.assign(mergedFilterObj, drilldownFilterObj);
|
||||
} catch (e) {
|
||||
console.warn('Failed to parse drilldown filter parameters:', e);
|
||||
}
|
||||
}
|
||||
|
||||
// Add common filters
|
||||
Object.keys(commonFilters).forEach(key => {
|
||||
const value = commonFilters[key];
|
||||
if (value !== undefined && value !== null && value !== '') {
|
||||
mergedFilterObj[key] = value;
|
||||
}
|
||||
});
|
||||
if (Object.keys(filterObj).length > 0) {
|
||||
drilldownFilterParams = JSON.stringify(filterObj);
|
||||
|
||||
if (Object.keys(mergedFilterObj).length > 0) {
|
||||
filterParams = JSON.stringify(mergedFilterObj);
|
||||
}
|
||||
}
|
||||
console.log('Drilldown filter parameters:', drilldownFilterParams);
|
||||
|
||||
// Use drilldown filters if available, otherwise use layer filters
|
||||
const finalFilterParams = drilldownFilterParams || filterParams;
|
||||
console.log('Final filter parameters:', finalFilterParams);
|
||||
|
||||
// Log the URL that will be called
|
||||
const url = `chart/getdashjson/scatter?tableName=${actualApiUrl}&xAxis=${drilldownConfig.xAxis}&yAxes=${drilldownConfig.yAxis}${this.connection ? `&sureId=${this.connection}` : ''}`;
|
||||
@@ -312,7 +899,7 @@ export class ScatterChartComponent implements OnInit, OnChanges {
|
||||
|
||||
// Fetch data from the dashboard service with parameter field and value
|
||||
// Backend handles filtering, we just pass the parameter field and value
|
||||
this.dashboardService.getChartData(actualApiUrl, 'scatter', drilldownConfig.xAxis, drilldownConfig.yAxis, this.connection, parameterField, parameterValue, finalFilterParams).subscribe(
|
||||
this.dashboardService.getChartData(actualApiUrl, 'scatter', drilldownConfig.xAxis, drilldownConfig.yAxis, this.connection, parameterField, parameterValue, filterParams).subscribe(
|
||||
(data: any) => {
|
||||
console.log('Received drilldown data:', data);
|
||||
if (data === null) {
|
||||
@@ -325,7 +912,6 @@ export class ScatterChartComponent implements OnInit, OnChanges {
|
||||
// Handle the actual data structure returned by the API
|
||||
if (data && data.chartLabels && data.chartData) {
|
||||
// For scatter charts, we need to transform the data into scatter format
|
||||
// Scatter charts expect data in the format: {x: number, y: number}
|
||||
this.noDataAvailable = data.chartLabels.length === 0;
|
||||
this.scatterChartData = this.transformToScatterData(data.chartLabels, data.chartData);
|
||||
console.log('Updated scatter chart with drilldown data:', this.scatterChartData);
|
||||
@@ -349,33 +935,6 @@ export class ScatterChartComponent implements OnInit, OnChanges {
|
||||
);
|
||||
}
|
||||
|
||||
// Transform chart data to scatter chart format
|
||||
private transformToScatterData(labels: string[], datasets: any[]): ChartDataset[] {
|
||||
// For scatter charts, we need to transform the data into scatter format
|
||||
// Scatter charts expect data in the format: {x: number, y: number}
|
||||
|
||||
// This is a simple transformation - in a real implementation, you might want to
|
||||
// create a more sophisticated mapping based on your data structure
|
||||
return datasets.map((dataset, index) => {
|
||||
// Create scatter data points
|
||||
const scatterData = labels.map((label, i) => {
|
||||
// Use x-axis data as x coordinate, y-axis data as y coordinate
|
||||
const xValue = dataset.data[i] || 0;
|
||||
const yValue = i < datasets.length ? (datasets[i].data[index] || 0) : 0;
|
||||
|
||||
return { x: xValue, y: yValue };
|
||||
});
|
||||
|
||||
return {
|
||||
data: scatterData,
|
||||
label: dataset.label || `Dataset ${index + 1}`,
|
||||
backgroundColor: dataset.backgroundColor || `rgba(${Math.floor(Math.random() * 255)}, ${Math.floor(Math.random() * 255)}, ${Math.floor(Math.random() * 255)}, 0.6)`,
|
||||
borderColor: dataset.borderColor || 'rgba(0, 0, 0, 1)',
|
||||
pointRadius: dataset.pointRadius || 5
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
// Reset to original data (go back to base level)
|
||||
resetToOriginalData(): void {
|
||||
console.log('Resetting to original data');
|
||||
@@ -436,16 +995,18 @@ export class ScatterChartComponent implements OnInit, OnChanges {
|
||||
// Get the index of the clicked element
|
||||
const clickedIndex = e.active[0].index;
|
||||
|
||||
// Get the label of the clicked element
|
||||
// For scatter charts, we might not have labels in the same way as other charts
|
||||
const clickedLabel = `Point ${clickedIndex}`;
|
||||
// Get the dataset index
|
||||
const datasetIndex = e.active[0].datasetIndex;
|
||||
|
||||
console.log('Clicked on scatter point:', { index: clickedIndex, label: clickedLabel });
|
||||
// Get the data point
|
||||
const dataPoint = this.scatterChartData[datasetIndex].data[clickedIndex];
|
||||
|
||||
console.log('Clicked on scatter point:', { datasetIndex, clickedIndex, dataPoint });
|
||||
|
||||
// If we're not at the base level, store original data
|
||||
if (this.currentDrilldownLevel === 0) {
|
||||
// Store original data before entering drilldown mode
|
||||
this.originalScatterChartData = [...this.scatterChartData];
|
||||
this.originalScatterChartData = JSON.parse(JSON.stringify(this.scatterChartData));
|
||||
console.log('Stored original data for drilldown');
|
||||
}
|
||||
|
||||
@@ -487,9 +1048,10 @@ export class ScatterChartComponent implements OnInit, OnChanges {
|
||||
// Add this click to the drilldown stack
|
||||
const stackEntry = {
|
||||
level: nextDrilldownLevel,
|
||||
datasetIndex: datasetIndex,
|
||||
clickedIndex: clickedIndex,
|
||||
clickedLabel: clickedLabel,
|
||||
clickedValue: clickedLabel // Using label as value for now
|
||||
dataPoint: dataPoint,
|
||||
clickedValue: dataPoint // Using data point as value for now
|
||||
};
|
||||
|
||||
this.drilldownStack.push(stackEntry);
|
||||
@@ -513,6 +1075,12 @@ export class ScatterChartComponent implements OnInit, OnChanges {
|
||||
}
|
||||
|
||||
public chartHovered(e: any): void {
|
||||
console.log(e);
|
||||
console.log('Scatter chart hovered:', e);
|
||||
}
|
||||
|
||||
ngOnDestroy(): void {
|
||||
this.subscriptions.forEach(sub => sub.unsubscribe());
|
||||
// Clean up document click handler
|
||||
this.removeDocumentClickHandler();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,125 @@
|
||||
# Shield Dashboard
|
||||
|
||||
A professional analytics dashboard UI similar to the "Shield Overall Dashboard" design.
|
||||
|
||||
## Components
|
||||
|
||||
### Layout
|
||||
- **Left Sidebar**: Contains application name, KPI metrics, and filter controls
|
||||
- **Main Dashboard Area**: Grid-based responsive layout with various data visualization components
|
||||
|
||||
### Components
|
||||
|
||||
1. **Sidebar Filters**
|
||||
- Application header with title
|
||||
- KPI metrics display (Total Leads, Total Deals)
|
||||
- Filter controls (Sales Rep, Partner, Traction Channel, Sub Product Line)
|
||||
- Reset filters button
|
||||
|
||||
2. **Bar Chart**
|
||||
- Title: "Deal Stage Wise Progress"
|
||||
- Visualizes deal progress across different stages
|
||||
- Responsive design with loading shimmer effect
|
||||
|
||||
3. **Donut Charts**
|
||||
- End Customer Stage Split
|
||||
- Segment Penetration
|
||||
- Interactive with tooltips
|
||||
|
||||
4. **Map Chart**
|
||||
- Dealer locations with colored markers
|
||||
- Interactive hover tooltips showing dealer status
|
||||
- Legend for status colors
|
||||
|
||||
5. **Data Table**
|
||||
- Cross/Up Selling Scope
|
||||
- Scrollable with sticky header
|
||||
- Alternating row colors
|
||||
- Sortable columns
|
||||
- Probability bars with color coding
|
||||
|
||||
6. **Deal Details Card**
|
||||
- Company Name, Stage, Description, Amount, Stage Days
|
||||
- Color-coded deal stages
|
||||
- Responsive grid layout
|
||||
|
||||
7. **Quarterwise Flow**
|
||||
- Placeholder for chart configuration
|
||||
- "Chart configuration incomplete" message
|
||||
|
||||
## Features
|
||||
|
||||
### Responsiveness
|
||||
- Grid-based layout adapts to different screen sizes
|
||||
- Mobile-friendly design with stacked layout on small screens
|
||||
- Flexible components that resize appropriately
|
||||
|
||||
### Filter Functionality
|
||||
- Global filters that affect all dashboard components
|
||||
- Shared state management using BehaviorSubject
|
||||
- Real-time updates when filters change
|
||||
- Reset filters functionality
|
||||
|
||||
### Visual Design
|
||||
- Dark Navy Blue for headers and bars
|
||||
- Bright Orange for highlights and KPI boxes
|
||||
- White background for charts and tables
|
||||
- Modern color scheme with consistent styling
|
||||
|
||||
### Interactive Elements
|
||||
- Chart tooltips on hover
|
||||
- Interactive map with dealer status information
|
||||
- Sortable data table
|
||||
- Loading shimmer effects during data updates
|
||||
|
||||
## Technical Implementation
|
||||
|
||||
### Technologies Used
|
||||
- Angular 16
|
||||
- Chart.js with ng2-charts
|
||||
- Clarity Design System
|
||||
- SCSS for styling
|
||||
|
||||
### State Management
|
||||
- Shared service using BehaviorSubject for filter state
|
||||
- Reactive components that update based on filter changes
|
||||
- Simulated data updates (no backend integration)
|
||||
|
||||
### Responsive Design
|
||||
- CSS Grid and Flexbox layouts
|
||||
- Media queries for different screen sizes
|
||||
- Relative units for scalable components
|
||||
|
||||
## Component Structure
|
||||
|
||||
```
|
||||
shield-dashboard/
|
||||
├── shield-dashboard.component.ts|.html|.scss
|
||||
├── services/
|
||||
│ └── dashboard-filter.service.ts
|
||||
├── components/
|
||||
│ ├── sidebar-filters/
|
||||
│ │ ├── sidebar-filters.component.ts|.html|.scss
|
||||
│ ├── bar-chart/
|
||||
│ │ ├── bar-chart.component.ts|.html|.scss
|
||||
│ ├── donut-chart/
|
||||
│ │ ├── donut-chart.component.ts|.html|.scss
|
||||
│ ├── map-chart/
|
||||
│ │ ├── map-chart.component.ts|.html|.scss
|
||||
│ ├── data-table/
|
||||
│ │ ├── data-table.component.ts|.html|.scss
|
||||
│ ├── deal-details-card/
|
||||
│ │ ├── deal-details-card.component.ts|.html|.scss
|
||||
│ ├── quarterwise-flow/
|
||||
│ │ ├── quarterwise-flow.component.ts|.html|.scss
|
||||
│ └── loading-shimmer/
|
||||
│ ├── loading-shimmer.component.ts|.html|.scss
|
||||
└── shield-dashboard-routing.module.ts
|
||||
```
|
||||
|
||||
## Usage
|
||||
|
||||
To navigate to the Shield Dashboard, visit:
|
||||
`/cns-portal/shield-dashboard`
|
||||
|
||||
The dashboard is fully responsive and will adapt to different screen sizes. All components are interconnected through the shared filter service, so changing any filter will update all visualizations in real-time.
|
||||
@@ -0,0 +1,20 @@
|
||||
<div class="chart-container">
|
||||
<div class="chart-header">
|
||||
<h3>Deal Stage Wise Progress</h3>
|
||||
</div>
|
||||
<div class="chart-wrapper">
|
||||
<div class="chart-content" [class.loading]="isLoading">
|
||||
<canvas
|
||||
baseChart
|
||||
[data]="barChartData"
|
||||
[options]="barChartOptions"
|
||||
[type]="barChartType"
|
||||
(chartClick)="chartClicked($event)"
|
||||
(chartHover)="chartHovered($event)">
|
||||
</canvas>
|
||||
<div class="loading-overlay" *ngIf="isLoading">
|
||||
<div class="shimmer-bar"></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -0,0 +1,70 @@
|
||||
.chart-container {
|
||||
height: 100%;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
|
||||
.chart-header {
|
||||
margin-bottom: 20px;
|
||||
|
||||
h3 {
|
||||
font-size: 18px;
|
||||
font-weight: 600;
|
||||
color: #0a192f;
|
||||
margin: 0;
|
||||
}
|
||||
}
|
||||
|
||||
.chart-wrapper {
|
||||
flex: 1;
|
||||
position: relative;
|
||||
|
||||
.chart-content {
|
||||
position: relative;
|
||||
height: 100%;
|
||||
|
||||
&.loading {
|
||||
opacity: 0.7;
|
||||
|
||||
canvas {
|
||||
filter: blur(2px);
|
||||
}
|
||||
}
|
||||
|
||||
canvas {
|
||||
max-width: 100%;
|
||||
max-height: 100%;
|
||||
transition: filter 0.3s ease;
|
||||
}
|
||||
|
||||
.loading-overlay {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
background: rgba(255, 255, 255, 0.8);
|
||||
|
||||
.shimmer-bar {
|
||||
width: 80%;
|
||||
height: 20px;
|
||||
background: linear-gradient(90deg, #f0f0f0 25%, #e0e0e0 50%, #f0f0f0 75%);
|
||||
background-size: 200% 100%;
|
||||
animation: shimmer 1.5s infinite;
|
||||
border-radius: 4px;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes shimmer {
|
||||
0% {
|
||||
background-position: -200% 0;
|
||||
}
|
||||
100% {
|
||||
background-position: 200% 0;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,140 @@
|
||||
import { Component, OnInit, ViewChild, AfterViewInit, OnDestroy } from '@angular/core';
|
||||
import { BaseChartDirective } from 'ng2-charts';
|
||||
import { ChartConfiguration, ChartData, ChartEvent, ChartType } from 'chart.js';
|
||||
import { DashboardFilterService } from '../../services/dashboard-filter.service';
|
||||
import { Subscription } from 'rxjs';
|
||||
|
||||
@Component({
|
||||
selector: 'app-shield-bar-chart',
|
||||
templateUrl: './bar-chart.component.html',
|
||||
styleUrls: ['./bar-chart.component.scss']
|
||||
})
|
||||
export class BarChartComponent implements OnInit, AfterViewInit, OnDestroy {
|
||||
@ViewChild(BaseChartDirective) chart?: BaseChartDirective;
|
||||
|
||||
private filterSubscription: Subscription = new Subscription();
|
||||
|
||||
// Loading state
|
||||
isLoading: boolean = false;
|
||||
|
||||
public barChartOptions: ChartConfiguration['options'] = {
|
||||
responsive: true,
|
||||
maintainAspectRatio: false,
|
||||
scales: {
|
||||
x: {
|
||||
grid: {
|
||||
color: 'rgba(0, 0, 0, 0.05)'
|
||||
},
|
||||
ticks: {
|
||||
color: '#64748b'
|
||||
}
|
||||
},
|
||||
y: {
|
||||
beginAtZero: true,
|
||||
grid: {
|
||||
color: 'rgba(0, 0, 0, 0.05)'
|
||||
},
|
||||
ticks: {
|
||||
color: '#64748b'
|
||||
}
|
||||
}
|
||||
},
|
||||
plugins: {
|
||||
legend: {
|
||||
display: false
|
||||
},
|
||||
tooltip: {
|
||||
backgroundColor: 'rgba(10, 25, 47, 0.9)',
|
||||
titleColor: '#ff6b35',
|
||||
bodyColor: '#ffffff',
|
||||
borderColor: 'rgba(255, 107, 53, 0.3)',
|
||||
borderWidth: 1,
|
||||
cornerRadius: 6
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
public barChartType: ChartType = 'bar';
|
||||
public barChartPlugins = [];
|
||||
|
||||
public barChartData: ChartData<'bar'> = {
|
||||
labels: ['Prospecting', 'Qualification', 'Needs Analysis', 'Value Proposition', 'Decision Making', 'Negotiation'],
|
||||
datasets: [
|
||||
{
|
||||
data: [65, 59, 80, 81, 56, 55],
|
||||
label: 'Deal Progress',
|
||||
backgroundColor: '#0a192f',
|
||||
borderColor: '#0a192f',
|
||||
borderWidth: 1,
|
||||
borderRadius: 4
|
||||
}
|
||||
]
|
||||
};
|
||||
|
||||
constructor(private filterService: DashboardFilterService) { }
|
||||
|
||||
ngOnInit(): void {
|
||||
// Subscribe to filter changes
|
||||
this.filterSubscription.add(
|
||||
this.filterService.filterState$.subscribe(filters => {
|
||||
this.updateChartData(filters);
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
ngAfterViewInit(): void {
|
||||
// Initial chart render
|
||||
this.updateChart();
|
||||
}
|
||||
|
||||
ngOnDestroy(): void {
|
||||
// Unsubscribe from all subscriptions
|
||||
this.filterSubscription.unsubscribe();
|
||||
}
|
||||
|
||||
// Update chart data based on filters
|
||||
updateChartData(filters: any): void {
|
||||
// Show loading state
|
||||
this.isLoading = true;
|
||||
|
||||
// Simulate data change based on filters
|
||||
// In a real implementation, this would fetch new data from an API
|
||||
const baseData = [65, 59, 80, 81, 56, 55];
|
||||
|
||||
// Apply filter effects (simplified logic)
|
||||
let multiplier = 1;
|
||||
if (filters.salesRep) multiplier *= 0.9;
|
||||
if (filters.partner) multiplier *= 0.85;
|
||||
if (filters.tractionChannel) multiplier *= 0.95;
|
||||
if (filters.subProductLine) multiplier *= 0.8;
|
||||
|
||||
// Add a small delay to simulate loading
|
||||
setTimeout(() => {
|
||||
this.barChartData.datasets[0].data = baseData.map(value =>
|
||||
Math.floor(value * multiplier)
|
||||
);
|
||||
|
||||
// Update chart
|
||||
this.updateChart();
|
||||
|
||||
// Hide loading state
|
||||
this.isLoading = false;
|
||||
}, 300);
|
||||
}
|
||||
|
||||
// Update chart with new data
|
||||
updateChart(): void {
|
||||
if (this.chart) {
|
||||
this.chart.update();
|
||||
}
|
||||
}
|
||||
|
||||
// events
|
||||
public chartClicked({ event, active }: { event?: ChartEvent, active?: {}[] }): void {
|
||||
console.log(event, active);
|
||||
}
|
||||
|
||||
public chartHovered({ event, active }: { event?: ChartEvent, active?: {}[] }): void {
|
||||
console.log(event, active);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,78 @@
|
||||
<div class="table-container">
|
||||
<div class="table-header">
|
||||
<h3>Cross/Up Selling Scope</h3>
|
||||
</div>
|
||||
<div class="table-wrapper">
|
||||
<table class="data-table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th (click)="sortTable('companyName')" class="sortable">
|
||||
Company Name
|
||||
<span class="sort-indicator" *ngIf="sortColumn === 'companyName'">
|
||||
<clr-icon *ngIf="sortDirection === 'asc'" shape="arrow up"></clr-icon>
|
||||
<clr-icon *ngIf="sortDirection === 'desc'" shape="arrow down"></clr-icon>
|
||||
</span>
|
||||
</th>
|
||||
<th (click)="sortTable('contactPerson')" class="sortable">
|
||||
Contact Person
|
||||
<span class="sort-indicator" *ngIf="sortColumn === 'contactPerson'">
|
||||
<clr-icon *ngIf="sortDirection === 'asc'" shape="arrow up"></clr-icon>
|
||||
<clr-icon *ngIf="sortDirection === 'desc'" shape="arrow down"></clr-icon>
|
||||
</span>
|
||||
</th>
|
||||
<th (click)="sortTable('product')" class="sortable">
|
||||
Product
|
||||
<span class="sort-indicator" *ngIf="sortColumn === 'product'">
|
||||
<clr-icon *ngIf="sortDirection === 'asc'" shape="arrow up"></clr-icon>
|
||||
<clr-icon *ngIf="sortDirection === 'desc'" shape="arrow down"></clr-icon>
|
||||
</span>
|
||||
</th>
|
||||
<th (click)="sortTable('potentialValue')" class="sortable">
|
||||
Potential Value
|
||||
<span class="sort-indicator" *ngIf="sortColumn === 'potentialValue'">
|
||||
<clr-icon *ngIf="sortDirection === 'asc'" shape="arrow up"></clr-icon>
|
||||
<clr-icon *ngIf="sortDirection === 'desc'" shape="arrow down"></clr-icon>
|
||||
</span>
|
||||
</th>
|
||||
<th (click)="sortTable('probability')" class="sortable">
|
||||
Probability
|
||||
<span class="sort-indicator" *ngIf="sortColumn === 'probability'">
|
||||
<clr-icon *ngIf="sortDirection === 'asc'" shape="arrow up"></clr-icon>
|
||||
<clr-icon *ngIf="sortDirection === 'desc'" shape="arrow down"></clr-icon>
|
||||
</span>
|
||||
</th>
|
||||
<th (click)="sortTable('nextAction')" class="sortable">
|
||||
Next Action
|
||||
<span class="sort-indicator" *ngIf="sortColumn === 'nextAction'">
|
||||
<clr-icon *ngIf="sortDirection === 'asc'" shape="arrow up"></clr-icon>
|
||||
<clr-icon *ngIf="sortDirection === 'desc'" shape="arrow down"></clr-icon>
|
||||
</span>
|
||||
</th>
|
||||
<th (click)="sortTable('actionDate')" class="sortable">
|
||||
Action Date
|
||||
<span class="sort-indicator" *ngIf="sortColumn === 'actionDate'">
|
||||
<clr-icon *ngIf="sortDirection === 'asc'" shape="arrow up"></clr-icon>
|
||||
<clr-icon *ngIf="sortDirection === 'desc'" shape="arrow down"></clr-icon>
|
||||
</span>
|
||||
</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr *ngFor="let item of crossSellingData; let i = index" [class.even]="i % 2 === 0">
|
||||
<td>{{ item.companyName }}</td>
|
||||
<td>{{ item.contactPerson }}</td>
|
||||
<td>{{ item.product }}</td>
|
||||
<td>{{ formatCurrency(item.potentialValue) }}</td>
|
||||
<td>
|
||||
<div class="probability-bar">
|
||||
<div class="probability-fill" [style.width.%]="item.probability" [style.background-color]="item.probability > 70 ? '#0a192f' : item.probability > 50 ? '#ff6b35' : '#64748b'"></div>
|
||||
<span class="probability-text">{{ formatProbability(item.probability) }}</span>
|
||||
</div>
|
||||
</td>
|
||||
<td>{{ item.nextAction }}</td>
|
||||
<td>{{ item.actionDate }}</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
@@ -0,0 +1,107 @@
|
||||
.table-container {
|
||||
height: 100%;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
|
||||
.table-header {
|
||||
margin-bottom: 20px;
|
||||
|
||||
h3 {
|
||||
font-size: 18px;
|
||||
font-weight: 600;
|
||||
color: #0a192f;
|
||||
margin: 0;
|
||||
}
|
||||
}
|
||||
|
||||
.table-wrapper {
|
||||
flex: 1;
|
||||
overflow: auto;
|
||||
|
||||
.data-table {
|
||||
width: 100%;
|
||||
border-collapse: collapse;
|
||||
background: white;
|
||||
|
||||
thead {
|
||||
background: #f1f5f9;
|
||||
position: sticky;
|
||||
top: 0;
|
||||
z-index: 5;
|
||||
|
||||
th {
|
||||
padding: 12px 15px;
|
||||
text-align: left;
|
||||
font-weight: 600;
|
||||
color: #0a192f;
|
||||
border-bottom: 2px solid #e2e8f0;
|
||||
cursor: pointer;
|
||||
user-select: none;
|
||||
position: relative;
|
||||
|
||||
&:hover {
|
||||
background: #e2e8f0;
|
||||
}
|
||||
|
||||
&.sortable {
|
||||
padding-right: 30px;
|
||||
}
|
||||
|
||||
.sort-indicator {
|
||||
position: absolute;
|
||||
right: 10px;
|
||||
top: 50%;
|
||||
transform: translateY(-50%);
|
||||
color: #ff6b35;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
tbody {
|
||||
tr {
|
||||
border-bottom: 1px solid #e2e8f0;
|
||||
|
||||
&.even {
|
||||
background: #f8fafc;
|
||||
}
|
||||
|
||||
&:hover {
|
||||
background: #f1f5f9;
|
||||
}
|
||||
|
||||
td {
|
||||
padding: 12px 15px;
|
||||
color: #334155;
|
||||
font-size: 14px;
|
||||
|
||||
.probability-bar {
|
||||
position: relative;
|
||||
width: 100%;
|
||||
height: 20px;
|
||||
background: #e2e8f0;
|
||||
border-radius: 10px;
|
||||
overflow: hidden;
|
||||
|
||||
.probability-fill {
|
||||
height: 100%;
|
||||
border-radius: 10px;
|
||||
transition: width 0.3s ease;
|
||||
}
|
||||
|
||||
.probability-text {
|
||||
position: absolute;
|
||||
top: 50%;
|
||||
left: 50%;
|
||||
transform: translate(-50%, -50%);
|
||||
font-size: 12px;
|
||||
font-weight: 500;
|
||||
color: white;
|
||||
text-shadow: 0 1px 1px rgba(0, 0, 0, 0.5);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,117 @@
|
||||
import { Component, OnInit, OnDestroy } from '@angular/core';
|
||||
import { DashboardFilterService } from '../../services/dashboard-filter.service';
|
||||
import { Subscription } from 'rxjs';
|
||||
|
||||
interface CrossSellingItem {
|
||||
id: number;
|
||||
companyName: string;
|
||||
contactPerson: string;
|
||||
product: string;
|
||||
potentialValue: number;
|
||||
probability: number;
|
||||
nextAction: string;
|
||||
actionDate: string;
|
||||
}
|
||||
|
||||
@Component({
|
||||
selector: 'app-shield-data-table',
|
||||
templateUrl: './data-table.component.html',
|
||||
styleUrls: ['./data-table.component.scss']
|
||||
})
|
||||
export class DataTableComponent implements OnInit, OnDestroy {
|
||||
private filterSubscription: Subscription = new Subscription();
|
||||
|
||||
// Mock data for cross/up selling scope
|
||||
originalCrossSellingData: CrossSellingItem[] = [
|
||||
{ id: 1, companyName: 'Tech Solutions Inc', contactPerson: 'John Smith', product: 'Product A', potentialValue: 50000, probability: 75, nextAction: 'Follow-up call', actionDate: '2023-06-15' },
|
||||
{ id: 2, companyName: 'Global Enterprises', contactPerson: 'Sarah Johnson', product: 'Product B', potentialValue: 75000, probability: 60, nextAction: 'Send proposal', actionDate: '2023-06-18' },
|
||||
{ id: 3, companyName: 'Innovative Systems', contactPerson: 'Mike Brown', product: 'Product C', potentialValue: 30000, probability: 85, nextAction: 'Demo scheduled', actionDate: '2023-06-20' },
|
||||
{ id: 4, companyName: 'Future Tech Ltd', contactPerson: 'Emily Davis', product: 'Product A', potentialValue: 45000, probability: 70, nextAction: 'Send quote', actionDate: '2023-06-22' },
|
||||
{ id: 5, companyName: 'Digital Dynamics', contactPerson: 'Robert Wilson', product: 'Product B', potentialValue: 60000, probability: 55, nextAction: 'Meeting scheduled', actionDate: '2023-06-25' },
|
||||
{ id: 6, companyName: 'Alpha Solutions', contactPerson: 'Lisa Miller', product: 'Product C', potentialValue: 40000, probability: 80, nextAction: 'Follow-up email', actionDate: '2023-06-28' },
|
||||
{ id: 7, companyName: 'Beta Innovations', contactPerson: 'David Taylor', product: 'Product A', potentialValue: 55000, probability: 65, nextAction: 'Product demo', actionDate: '2023-06-28' },
|
||||
{ id: 8, companyName: 'Gamma Technologies', contactPerson: 'Jennifer Anderson', product: 'Product B', potentialValue: 70000, probability: 50, nextAction: 'Proposal review', actionDate: '2023-07-05' }
|
||||
];
|
||||
|
||||
crossSellingData: CrossSellingItem[] = [...this.originalCrossSellingData];
|
||||
|
||||
// Sorting properties
|
||||
sortColumn: keyof CrossSellingItem = 'companyName';
|
||||
sortDirection: 'asc' | 'desc' = 'asc';
|
||||
|
||||
constructor(private filterService: DashboardFilterService) { }
|
||||
|
||||
ngOnInit(): void {
|
||||
// Subscribe to filter changes
|
||||
this.filterSubscription.add(
|
||||
this.filterService.filterState$.subscribe(filters => {
|
||||
this.updateTableData(filters);
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
ngOnDestroy(): void {
|
||||
// Unsubscribe from all subscriptions
|
||||
this.filterSubscription.unsubscribe();
|
||||
}
|
||||
|
||||
// Update table data based on filters
|
||||
updateTableData(filters: any): void {
|
||||
// Simulate data change based on filters
|
||||
// In a real implementation, this would fetch new data from an API
|
||||
|
||||
if (filters.salesRep || filters.partner || filters.tractionChannel || filters.subProductLine) {
|
||||
// Apply filter effects (simplified logic)
|
||||
const filteredData = this.originalCrossSellingData.filter(item => {
|
||||
// Simple filtering logic - in a real app, this would be more sophisticated
|
||||
return Math.random() > 0.3; // Randomly filter out some items
|
||||
});
|
||||
|
||||
this.crossSellingData = filteredData;
|
||||
} else {
|
||||
// No filters applied, show all data
|
||||
this.crossSellingData = [...this.originalCrossSellingData];
|
||||
}
|
||||
|
||||
// Re-apply current sorting
|
||||
this.sortTable(this.sortColumn);
|
||||
}
|
||||
|
||||
// Sort table by column
|
||||
sortTable(column: keyof CrossSellingItem): void {
|
||||
if (this.sortColumn === column) {
|
||||
this.sortDirection = this.sortDirection === 'asc' ? 'desc' : 'asc';
|
||||
} else {
|
||||
this.sortColumn = column;
|
||||
this.sortDirection = 'asc';
|
||||
}
|
||||
|
||||
this.crossSellingData.sort((a, b) => {
|
||||
const aValue = a[column];
|
||||
const bValue = b[column];
|
||||
|
||||
if (aValue < bValue) {
|
||||
return this.sortDirection === 'asc' ? -1 : 1;
|
||||
}
|
||||
if (aValue > bValue) {
|
||||
return this.sortDirection === 'asc' ? 1 : -1;
|
||||
}
|
||||
return 0;
|
||||
});
|
||||
}
|
||||
|
||||
// Format currency
|
||||
formatCurrency(value: number): string {
|
||||
return new Intl.NumberFormat('en-US', {
|
||||
style: 'currency',
|
||||
currency: 'USD',
|
||||
minimumFractionDigits: 0,
|
||||
maximumFractionDigits: 0
|
||||
}).format(value);
|
||||
}
|
||||
|
||||
// Format probability as percentage
|
||||
formatProbability(probability: number): string {
|
||||
return `${probability}%`;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,36 @@
|
||||
<div class="deal-details-container">
|
||||
<div class="deal-details-header">
|
||||
<h3>Deal Details</h3>
|
||||
</div>
|
||||
<div class="deal-cards">
|
||||
<div class="deal-card" *ngFor="let deal of dealDetails">
|
||||
<div class="deal-header">
|
||||
<div class="company-name">{{ deal.companyName }}</div>
|
||||
<div class="deal-stage" [style.background-color]="getStageColor(deal.stage)">
|
||||
{{ deal.stage }}
|
||||
</div>
|
||||
</div>
|
||||
<div class="deal-description">
|
||||
{{ deal.description }}
|
||||
</div>
|
||||
<div class="deal-info">
|
||||
<div class="info-item">
|
||||
<div class="info-label">Amount</div>
|
||||
<div class="info-value">{{ formatCurrency(deal.amount) }}</div>
|
||||
</div>
|
||||
<div class="info-item">
|
||||
<div class="info-label">Stage Days</div>
|
||||
<div class="info-value">{{ deal.stageDays }} days</div>
|
||||
</div>
|
||||
<div class="info-item">
|
||||
<div class="info-label">Contact</div>
|
||||
<div class="info-value">{{ deal.contactPerson }}</div>
|
||||
</div>
|
||||
<div class="info-item">
|
||||
<div class="info-label">Last Contact</div>
|
||||
<div class="info-value">{{ deal.lastContact }}</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -0,0 +1,102 @@
|
||||
.deal-details-container {
|
||||
height: 100%;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
|
||||
.deal-details-header {
|
||||
margin-bottom: 20px;
|
||||
|
||||
h3 {
|
||||
font-size: 18px;
|
||||
font-weight: 600;
|
||||
color: #0a192f;
|
||||
margin: 0;
|
||||
}
|
||||
}
|
||||
|
||||
.deal-cards {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 15px;
|
||||
overflow-y: auto;
|
||||
|
||||
.deal-card {
|
||||
background: white;
|
||||
border-radius: 10px;
|
||||
padding: 20px;
|
||||
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.05);
|
||||
border: 1px solid #e2e8f0;
|
||||
transition: transform 0.2s ease, box-shadow 0.2s ease;
|
||||
|
||||
&:hover {
|
||||
transform: translateY(-2px);
|
||||
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1);
|
||||
}
|
||||
|
||||
.deal-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: flex-start;
|
||||
margin-bottom: 15px;
|
||||
|
||||
.company-name {
|
||||
font-size: 18px;
|
||||
font-weight: 600;
|
||||
color: #0a192f;
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.deal-stage {
|
||||
padding: 6px 12px;
|
||||
border-radius: 20px;
|
||||
font-size: 12px;
|
||||
font-weight: 500;
|
||||
color: white;
|
||||
text-align: center;
|
||||
min-width: 120px;
|
||||
}
|
||||
}
|
||||
|
||||
.deal-description {
|
||||
color: #64748b;
|
||||
font-size: 14px;
|
||||
line-height: 1.5;
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
.deal-info {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(2, 1fr);
|
||||
gap: 15px;
|
||||
|
||||
.info-item {
|
||||
.info-label {
|
||||
font-size: 12px;
|
||||
color: #94a3b8;
|
||||
margin-bottom: 4px;
|
||||
}
|
||||
|
||||
.info-value {
|
||||
font-size: 14px;
|
||||
font-weight: 500;
|
||||
color: #0a192f;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Responsive design
|
||||
@media (max-width: 768px) {
|
||||
.deal-details-container {
|
||||
.deal-cards {
|
||||
.deal-card {
|
||||
.deal-info {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,119 @@
|
||||
import { Component, OnInit, OnDestroy } from '@angular/core';
|
||||
import { DashboardFilterService } from '../../services/dashboard-filter.service';
|
||||
import { Subscription } from 'rxjs';
|
||||
|
||||
interface DealDetail {
|
||||
id: number;
|
||||
companyName: string;
|
||||
stage: string;
|
||||
description: string;
|
||||
amount: number;
|
||||
stageDays: number;
|
||||
contactPerson: string;
|
||||
lastContact: string;
|
||||
}
|
||||
|
||||
@Component({
|
||||
selector: 'app-shield-deal-details-card',
|
||||
templateUrl: './deal-details-card.component.html',
|
||||
styleUrls: ['./deal-details-card.component.scss']
|
||||
})
|
||||
export class DealDetailsCardComponent implements OnInit, OnDestroy {
|
||||
private filterSubscription: Subscription = new Subscription();
|
||||
|
||||
// Mock deal details data
|
||||
originalDealDetails: DealDetail[] = [
|
||||
{
|
||||
id: 1,
|
||||
companyName: 'Tech Solutions Inc',
|
||||
stage: 'Negotiation',
|
||||
description: 'Enterprise software solution for HR management',
|
||||
amount: 125000,
|
||||
stageDays: 15,
|
||||
contactPerson: 'John Smith',
|
||||
lastContact: '2023-06-10'
|
||||
},
|
||||
{
|
||||
id: 2,
|
||||
companyName: 'Global Enterprises',
|
||||
stage: 'Decision Making',
|
||||
description: 'Cloud infrastructure services migration',
|
||||
amount: 85000,
|
||||
stageDays: 8,
|
||||
contactPerson: 'Sarah Johnson',
|
||||
lastContact: '2023-06-12'
|
||||
},
|
||||
{
|
||||
id: 3,
|
||||
companyName: 'Innovative Systems',
|
||||
stage: 'Value Proposition',
|
||||
description: 'Custom AI implementation for logistics',
|
||||
amount: 210000,
|
||||
stageDays: 22,
|
||||
contactPerson: 'Mike Brown',
|
||||
lastContact: '2023-06-05'
|
||||
}
|
||||
];
|
||||
|
||||
dealDetails: DealDetail[] = [...this.originalDealDetails];
|
||||
|
||||
// Stage colors
|
||||
stageColors: { [key: string]: string } = {
|
||||
'Prospecting': '#93c5fd',
|
||||
'Qualification': '#60a5fa',
|
||||
'Needs Analysis': '#3b82f6',
|
||||
'Value Proposition': '#1d4ed8',
|
||||
'Decision Making': '#0a192f',
|
||||
'Negotiation': '#ff6b35'
|
||||
};
|
||||
|
||||
constructor(private filterService: DashboardFilterService) { }
|
||||
|
||||
ngOnInit(): void {
|
||||
// Subscribe to filter changes
|
||||
this.filterSubscription.add(
|
||||
this.filterService.filterState$.subscribe(filters => {
|
||||
this.updateDealData(filters);
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
ngOnDestroy(): void {
|
||||
// Unsubscribe from all subscriptions
|
||||
this.filterSubscription.unsubscribe();
|
||||
}
|
||||
|
||||
// Update deal data based on filters
|
||||
updateDealData(filters: any): void {
|
||||
// Simulate data change based on filters
|
||||
// In a real implementation, this would fetch new data from an API
|
||||
|
||||
if (filters.salesRep || filters.partner || filters.tractionChannel || filters.subProductLine) {
|
||||
// Apply filter effects (simplified logic)
|
||||
const filteredData = this.originalDealDetails.filter(item => {
|
||||
// Simple filtering logic - in a real app, this would be more sophisticated
|
||||
return Math.random() > 0.2; // Randomly filter out some items
|
||||
});
|
||||
|
||||
this.dealDetails = filteredData;
|
||||
} else {
|
||||
// No filters applied, show all data
|
||||
this.dealDetails = [...this.originalDealDetails];
|
||||
}
|
||||
}
|
||||
|
||||
// Get stage color
|
||||
getStageColor(stage: string): string {
|
||||
return this.stageColors[stage] || '#64748b';
|
||||
}
|
||||
|
||||
// Format currency
|
||||
formatCurrency(value: number): string {
|
||||
return new Intl.NumberFormat('en-US', {
|
||||
style: 'currency',
|
||||
currency: 'USD',
|
||||
minimumFractionDigits: 0,
|
||||
maximumFractionDigits: 0
|
||||
}).format(value);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,21 @@
|
||||
<div class="chart-container">
|
||||
<div class="chart-header">
|
||||
<h3>{{ chartTitle }}</h3>
|
||||
</div>
|
||||
<div class="chart-wrapper">
|
||||
<div class="chart-content" [class.loading]="isLoading">
|
||||
<canvas
|
||||
baseChart
|
||||
[datasets]="chartData.datasets"
|
||||
[labels]="chartData.labels"
|
||||
[options]="donutChartOptions"
|
||||
[type]="donutChartType"
|
||||
(chartClick)="chartClicked($event)"
|
||||
(chartHover)="chartHovered($event)">
|
||||
</canvas>
|
||||
<div class="loading-overlay" *ngIf="isLoading">
|
||||
<div class="shimmer-donut"></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -0,0 +1,70 @@
|
||||
.chart-container {
|
||||
height: 100%;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
|
||||
.chart-header {
|
||||
margin-bottom: 20px;
|
||||
|
||||
h3 {
|
||||
font-size: 18px;
|
||||
font-weight: 600;
|
||||
color: #0a192f;
|
||||
margin: 0;
|
||||
}
|
||||
}
|
||||
|
||||
.chart-wrapper {
|
||||
flex: 1;
|
||||
position: relative;
|
||||
|
||||
.chart-content {
|
||||
position: relative;
|
||||
height: 100%;
|
||||
|
||||
&.loading {
|
||||
opacity: 0.7;
|
||||
|
||||
canvas {
|
||||
filter: blur(2px);
|
||||
}
|
||||
}
|
||||
|
||||
canvas {
|
||||
max-width: 100%;
|
||||
max-height: 100%;
|
||||
transition: filter 0.3s ease;
|
||||
}
|
||||
|
||||
.loading-overlay {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
background: rgba(255, 255, 255, 0.8);
|
||||
|
||||
.shimmer-donut {
|
||||
width: 120px;
|
||||
height: 120px;
|
||||
border-radius: 50%;
|
||||
background: linear-gradient(90deg, #f0f0f0 25%, #e0e0e0 50%, #f0f0f0 75%);
|
||||
background-size: 200% 100%;
|
||||
animation: shimmer 1.5s infinite;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes shimmer {
|
||||
0% {
|
||||
background-position: -200% 0;
|
||||
}
|
||||
100% {
|
||||
background-position: 200% 0;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,195 @@
|
||||
import { Component, Input, OnInit, ViewChild, OnDestroy } from '@angular/core';
|
||||
import { BaseChartDirective } from 'ng2-charts';
|
||||
import { ChartConfiguration, ChartData, ChartEvent, ChartType } from 'chart.js';
|
||||
import { DashboardFilterService } from '../../services/dashboard-filter.service';
|
||||
import { Subscription } from 'rxjs';
|
||||
|
||||
@Component({
|
||||
selector: 'app-shield-donut-chart',
|
||||
templateUrl: './donut-chart.component.html',
|
||||
styleUrls: ['./donut-chart.component.scss']
|
||||
})
|
||||
export class DonutChartComponent implements OnInit, OnDestroy {
|
||||
@Input() chartType: 'endCustomer' | 'segmentPenetration' = 'endCustomer';
|
||||
|
||||
@ViewChild(BaseChartDirective) chart?: BaseChartDirective;
|
||||
|
||||
private filterSubscription: Subscription = new Subscription();
|
||||
|
||||
// Loading state
|
||||
isLoading: boolean = false;
|
||||
|
||||
public donutChartOptions: ChartConfiguration['options'] = {
|
||||
responsive: true,
|
||||
maintainAspectRatio: false,
|
||||
plugins: {
|
||||
legend: {
|
||||
display: true,
|
||||
position: 'bottom',
|
||||
labels: {
|
||||
color: '#64748b',
|
||||
font: {
|
||||
size: 12
|
||||
},
|
||||
padding: 20
|
||||
}
|
||||
},
|
||||
tooltip: {
|
||||
backgroundColor: 'rgba(10, 25, 47, 0.9)',
|
||||
titleColor: '#ff6b35',
|
||||
bodyColor: '#ffffff',
|
||||
borderColor: 'rgba(255, 107, 53, 0.3)',
|
||||
borderWidth: 1,
|
||||
cornerRadius: 6,
|
||||
callbacks: {
|
||||
label: function(context) {
|
||||
const label = context.label || '';
|
||||
const value = context.parsed;
|
||||
return `${label}: ${value}`;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
public donutChartType: ChartType = 'doughnut';
|
||||
public donutChartPlugins = [];
|
||||
|
||||
// Data for End Customer Stage Split
|
||||
public endCustomerData: ChartData<'doughnut'> = {
|
||||
labels: ['Prospecting', 'Qualification', 'Needs Analysis', 'Value Proposition', 'Decision Making'],
|
||||
datasets: [
|
||||
{
|
||||
data: [30, 25, 20, 15, 10],
|
||||
backgroundColor: [
|
||||
'#0a192f',
|
||||
'#1e3a8a',
|
||||
'#3b82f6',
|
||||
'#60a5fa',
|
||||
'#93c5fd'
|
||||
],
|
||||
hoverBackgroundColor: [
|
||||
'#1e3a8a',
|
||||
'#3b82f6',
|
||||
'#60a5fa',
|
||||
'#93c5fd',
|
||||
'#bfdbfe'
|
||||
],
|
||||
borderWidth: 0
|
||||
}
|
||||
]
|
||||
};
|
||||
|
||||
// Data for Segment Penetration
|
||||
public segmentPenetrationData: ChartData<'doughnut'> = {
|
||||
labels: ['Enterprise', 'Mid-Market', 'SMB', 'Startup'],
|
||||
datasets: [
|
||||
{
|
||||
data: [40, 30, 20, 10],
|
||||
backgroundColor: [
|
||||
'#ff6b35',
|
||||
'#ff8c66',
|
||||
'#ffb099',
|
||||
'#ffd6cc'
|
||||
],
|
||||
hoverBackgroundColor: [
|
||||
'#ff8c66',
|
||||
'#ffb099',
|
||||
'#ffd6cc',
|
||||
'#ffffff'
|
||||
],
|
||||
borderWidth: 0
|
||||
}
|
||||
]
|
||||
};
|
||||
|
||||
constructor(private filterService: DashboardFilterService) { }
|
||||
|
||||
ngOnInit(): void {
|
||||
// Subscribe to filter changes
|
||||
this.filterSubscription.add(
|
||||
this.filterService.filterState$.subscribe(filters => {
|
||||
this.updateChartData(filters);
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
ngOnDestroy(): void {
|
||||
// Unsubscribe from all subscriptions
|
||||
this.filterSubscription.unsubscribe();
|
||||
}
|
||||
|
||||
// Get chart data based on chart type
|
||||
get chartData(): ChartData<'doughnut'> {
|
||||
return this.chartType === 'endCustomer' ? this.endCustomerData : this.segmentPenetrationData;
|
||||
}
|
||||
|
||||
// Get chart title based on chart type
|
||||
get chartTitle(): string {
|
||||
return this.chartType === 'endCustomer'
|
||||
? 'End Customer Stage Split'
|
||||
: 'Segment Penetration';
|
||||
}
|
||||
|
||||
// Update chart data based on filters
|
||||
updateChartData(filters: any): void {
|
||||
// Show loading state
|
||||
this.isLoading = true;
|
||||
|
||||
// Simulate data change based on filters
|
||||
// In a real implementation, this would fetch new data from an API
|
||||
|
||||
// Add a small delay to simulate loading
|
||||
setTimeout(() => {
|
||||
if (this.chartType === 'endCustomer') {
|
||||
const baseData = [30, 25, 20, 15, 10];
|
||||
|
||||
// Apply filter effects (simplified logic)
|
||||
let multiplier = 1;
|
||||
if (filters.salesRep) multiplier *= 0.9;
|
||||
if (filters.partner) multiplier *= 0.85;
|
||||
if (filters.tractionChannel) multiplier *= 0.95;
|
||||
if (filters.subProductLine) multiplier *= 0.8;
|
||||
|
||||
this.endCustomerData.datasets[0].data = baseData.map(value =>
|
||||
Math.floor(value * multiplier)
|
||||
);
|
||||
} else {
|
||||
const baseData = [40, 30, 20, 10];
|
||||
|
||||
// Apply filter effects (simplified logic)
|
||||
let multiplier = 1;
|
||||
if (filters.salesRep) multiplier *= 0.85;
|
||||
if (filters.partner) multiplier *= 0.9;
|
||||
if (filters.tractionChannel) multiplier *= 0.95;
|
||||
if (filters.subProductLine) multiplier *= 0.75;
|
||||
|
||||
this.segmentPenetrationData.datasets[0].data = baseData.map(value =>
|
||||
Math.floor(value * multiplier)
|
||||
);
|
||||
}
|
||||
|
||||
// Update chart
|
||||
this.updateChart();
|
||||
|
||||
// Hide loading state
|
||||
this.isLoading = false;
|
||||
}, 300);
|
||||
}
|
||||
|
||||
// Update chart with new data
|
||||
updateChart(): void {
|
||||
if (this.chart) {
|
||||
this.chart.update();
|
||||
}
|
||||
}
|
||||
|
||||
// events
|
||||
public chartClicked({ event, active }: { event?: ChartEvent, active?: {}[] }): void {
|
||||
console.log(event, active);
|
||||
}
|
||||
|
||||
public chartHovered({ event, active }: { event?: ChartEvent, active?: {}[] }): void {
|
||||
console.log(event, active);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,45 @@
|
||||
.shimmer-container {
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
|
||||
.shimmer-content {
|
||||
transition: opacity 0.3s ease;
|
||||
}
|
||||
|
||||
.shimmer-overlay {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
background: rgba(255, 255, 255, 0.7);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
z-index: 100;
|
||||
|
||||
.shimmer-animation {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
background: linear-gradient(90deg, #f0f0f0 25%, #e0e0e0 50%, #f0f0f0 75%);
|
||||
background-size: 200% 100%;
|
||||
animation: shimmer 1.5s infinite;
|
||||
border-radius: 8px;
|
||||
}
|
||||
}
|
||||
|
||||
&.active {
|
||||
.shimmer-content {
|
||||
opacity: 0.5;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes shimmer {
|
||||
0% {
|
||||
background-position: -200% 0;
|
||||
}
|
||||
100% {
|
||||
background-position: 200% 0;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,19 @@
|
||||
import { Component, Input } from '@angular/core';
|
||||
|
||||
@Component({
|
||||
selector: 'app-shield-loading-shimmer',
|
||||
template: `
|
||||
<div class="shimmer-container" [ngClass]="{ 'active': loading }">
|
||||
<div class="shimmer-content">
|
||||
<ng-content></ng-content>
|
||||
</div>
|
||||
<div class="shimmer-overlay" *ngIf="loading">
|
||||
<div class="shimmer-animation"></div>
|
||||
</div>
|
||||
</div>
|
||||
`,
|
||||
styleUrls: ['./loading-shimmer.component.scss']
|
||||
})
|
||||
export class LoadingShimmerComponent {
|
||||
@Input() loading: boolean = false;
|
||||
}
|
||||
@@ -0,0 +1,43 @@
|
||||
<div class="map-container">
|
||||
<div class="map-header">
|
||||
<h3>Dealer Locations</h3>
|
||||
</div>
|
||||
<div class="map-wrapper">
|
||||
<!-- Simple map representation with dealer markers -->
|
||||
<div class="map-placeholder">
|
||||
<div class="india-map">
|
||||
<!-- Simplified India map outline -->
|
||||
<div class="map-outline"></div>
|
||||
|
||||
<!-- Dealer markers -->
|
||||
<div
|
||||
*ngFor="let dealer of dealerLocations"
|
||||
class="dealer-marker"
|
||||
[style.left]="dealer.lng * 0.8 + 10 + '%'"
|
||||
[style.top]="100 - (dealer.lat * 1.2) + '%'"
|
||||
[style.background-color]="getStatusColor(dealer.status)"
|
||||
(mouseenter)="onDealerHover(dealer)"
|
||||
(mouseleave)="onDealerLeave()"
|
||||
[attr.data-tooltip]="dealer.name + ' - ' + dealer.city + ' (' + getStatusLabel(dealer.status) + ')'">
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Legend -->
|
||||
<div class="map-legend">
|
||||
<div class="legend-item" *ngFor="let status of ['active', 'inactive', 'training', 'onboarding']">
|
||||
<div class="legend-color" [style.background-color]="getStatusColor(status)"></div>
|
||||
<div class="legend-label">{{ getStatusLabel(status) }}</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Dealer info tooltip -->
|
||||
<div class="dealer-tooltip" *ngIf="hoveredDealer" [style.left]="hoveredDealer.lng * 0.8 + 12 + '%'" [style.top]="100 - (hoveredDealer.lat * 1.2) - 5 + '%'">
|
||||
<div class="tooltip-content">
|
||||
<div class="dealer-name">{{ hoveredDealer.name }}</div>
|
||||
<div class="dealer-location">{{ hoveredDealer.city }}, {{ hoveredDealer.state }}</div>
|
||||
<div class="dealer-status" [style.color]="getStatusColor(hoveredDealer.status)">{{ getStatusLabel(hoveredDealer.status) }}</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -0,0 +1,135 @@
|
||||
.map-container {
|
||||
height: 100%;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
|
||||
.map-header {
|
||||
margin-bottom: 20px;
|
||||
|
||||
h3 {
|
||||
font-size: 18px;
|
||||
font-weight: 600;
|
||||
color: #0a192f;
|
||||
margin: 0;
|
||||
}
|
||||
}
|
||||
|
||||
.map-wrapper {
|
||||
flex: 1;
|
||||
position: relative;
|
||||
|
||||
.map-placeholder {
|
||||
height: 100%;
|
||||
background: #f8fafc;
|
||||
border-radius: 8px;
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
|
||||
.india-map {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
position: relative;
|
||||
background: #e2e8f0;
|
||||
|
||||
.map-outline {
|
||||
position: absolute;
|
||||
top: 10%;
|
||||
left: 10%;
|
||||
right: 10%;
|
||||
bottom: 10%;
|
||||
background: #cbd5e1;
|
||||
border-radius: 50% 40% 45% 50% / 40% 50% 40% 45%;
|
||||
box-shadow: inset 0 0 20px rgba(0, 0, 0, 0.1);
|
||||
}
|
||||
|
||||
.dealer-marker {
|
||||
position: absolute;
|
||||
width: 16px;
|
||||
height: 16px;
|
||||
border-radius: 50%;
|
||||
cursor: pointer;
|
||||
transform: translate(-50%, -50%);
|
||||
box-shadow: 0 2px 6px rgba(0, 0, 0, 0.3);
|
||||
transition: transform 0.2s ease, box-shadow 0.2s ease;
|
||||
|
||||
&:hover {
|
||||
transform: translate(-50%, -50%) scale(1.3);
|
||||
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.4);
|
||||
z-index: 10;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.map-legend {
|
||||
position: absolute;
|
||||
bottom: 20px;
|
||||
left: 20px;
|
||||
background: rgba(255, 255, 255, 0.9);
|
||||
border-radius: 8px;
|
||||
padding: 12px;
|
||||
box-shadow: 0 2px 10px rgba(0, 0, 0, 0.1);
|
||||
display: flex;
|
||||
gap: 15px;
|
||||
|
||||
.legend-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
|
||||
.legend-color {
|
||||
width: 12px;
|
||||
height: 12px;
|
||||
border-radius: 50%;
|
||||
}
|
||||
|
||||
.legend-label {
|
||||
font-size: 12px;
|
||||
color: #64748b;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.dealer-tooltip {
|
||||
position: absolute;
|
||||
background: rgba(10, 25, 47, 0.95);
|
||||
color: white;
|
||||
border-radius: 6px;
|
||||
padding: 10px 15px;
|
||||
transform: translate(-50%, -100%);
|
||||
margin-top: -10px;
|
||||
z-index: 100;
|
||||
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.2);
|
||||
min-width: 180px;
|
||||
|
||||
&::after {
|
||||
content: '';
|
||||
position: absolute;
|
||||
top: 100%;
|
||||
left: 50%;
|
||||
margin-left: -5px;
|
||||
border-width: 5px;
|
||||
border-style: solid;
|
||||
border-color: rgba(10, 25, 47, 0.95) transparent transparent transparent;
|
||||
}
|
||||
|
||||
.tooltip-content {
|
||||
.dealer-name {
|
||||
font-weight: 600;
|
||||
margin-bottom: 4px;
|
||||
}
|
||||
|
||||
.dealer-location {
|
||||
font-size: 13px;
|
||||
color: #cbd5e1;
|
||||
margin-bottom: 4px;
|
||||
}
|
||||
|
||||
.dealer-status {
|
||||
font-size: 12px;
|
||||
font-weight: 500;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,109 @@
|
||||
import { Component, OnInit, OnDestroy } from '@angular/core';
|
||||
import { DashboardFilterService } from '../../services/dashboard-filter.service';
|
||||
import { Subscription } from 'rxjs';
|
||||
|
||||
interface DealerLocation {
|
||||
id: number;
|
||||
name: string;
|
||||
lat: number;
|
||||
lng: number;
|
||||
status: 'active' | 'inactive' | 'training' | 'onboarding';
|
||||
city: string;
|
||||
state: string;
|
||||
}
|
||||
|
||||
@Component({
|
||||
selector: 'app-shield-map-chart',
|
||||
templateUrl: './map-chart.component.html',
|
||||
styleUrls: ['./map-chart.component.scss']
|
||||
})
|
||||
export class MapChartComponent implements OnInit, OnDestroy {
|
||||
private filterSubscription: Subscription = new Subscription();
|
||||
|
||||
// Mock dealer location data
|
||||
originalDealerLocations: DealerLocation[] = [
|
||||
{ id: 1, name: 'ABC Motors', lat: 28.6139, lng: 77.2090, status: 'active', city: 'New Delhi', state: 'Delhi' },
|
||||
{ id: 2, name: 'XYZ Auto', lat: 19.0760, lng: 72.8777, status: 'active', city: 'Mumbai', state: 'Maharashtra' },
|
||||
{ id: 3, name: 'PQR Dealers', lat: 13.0827, lng: 80.2707, status: 'training', city: 'Chennai', state: 'Tamil Nadu' },
|
||||
{ id: 4, name: 'LMN Enterprises', lat: 12.9716, lng: 77.5946, status: 'inactive', city: 'Bangalore', state: 'Karnataka' },
|
||||
{ id: 5, name: 'DEF Solutions', lat: 22.5726, lng: 88.3639, status: 'active', city: 'Kolkata', state: 'West Bengal' },
|
||||
{ id: 6, name: 'GHI Services', lat: 25.3176, lng: 82.9739, status: 'onboarding', city: 'Varanasi', state: 'Uttar Pradesh' },
|
||||
{ id: 7, name: 'JKL Group', lat: 23.0225, lng: 72.5714, status: 'active', city: 'Ahmedabad', state: 'Gujarat' },
|
||||
{ id: 8, name: 'MNO Corp', lat: 18.5204, lng: 73.8567, status: 'training', city: 'Pune', state: 'Maharashtra' }
|
||||
];
|
||||
|
||||
dealerLocations: DealerLocation[] = [...this.originalDealerLocations];
|
||||
|
||||
// Status colors
|
||||
statusColors = {
|
||||
active: '#0a192f',
|
||||
inactive: '#64748b',
|
||||
training: '#ff6b35',
|
||||
onboarding: '#f59e0b'
|
||||
};
|
||||
|
||||
// Status labels
|
||||
statusLabels = {
|
||||
active: 'Active',
|
||||
inactive: 'Inactive',
|
||||
training: 'Training',
|
||||
onboarding: 'Onboarding'
|
||||
};
|
||||
|
||||
hoveredDealer: DealerLocation | null = null;
|
||||
|
||||
constructor(private filterService: DashboardFilterService) { }
|
||||
|
||||
ngOnInit(): void {
|
||||
// Subscribe to filter changes
|
||||
this.filterSubscription.add(
|
||||
this.filterService.filterState$.subscribe(filters => {
|
||||
this.updateMapData(filters);
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
ngOnDestroy(): void {
|
||||
// Unsubscribe from all subscriptions
|
||||
this.filterSubscription.unsubscribe();
|
||||
}
|
||||
|
||||
// Update map data based on filters
|
||||
updateMapData(filters: any): void {
|
||||
// Simulate data change based on filters
|
||||
// In a real implementation, this would fetch new data from an API
|
||||
|
||||
if (filters.salesRep || filters.partner || filters.tractionChannel || filters.subProductLine) {
|
||||
// Apply filter effects (simplified logic)
|
||||
const filteredData = this.originalDealerLocations.filter(location => {
|
||||
// Simple filtering logic - in a real app, this would be more sophisticated
|
||||
return Math.random() > 0.25; // Randomly filter out some locations
|
||||
});
|
||||
|
||||
this.dealerLocations = filteredData;
|
||||
} else {
|
||||
// No filters applied, show all data
|
||||
this.dealerLocations = [...this.originalDealerLocations];
|
||||
}
|
||||
}
|
||||
|
||||
// Get status color based on dealer status
|
||||
getStatusColor(status: string): string {
|
||||
return (this.statusColors as any)[status] || '#64748b';
|
||||
}
|
||||
|
||||
// Get status label based on dealer status
|
||||
getStatusLabel(status: string): string {
|
||||
return (this.statusLabels as any)[status] || status;
|
||||
}
|
||||
|
||||
// Handle dealer hover
|
||||
onDealerHover(dealer: DealerLocation): void {
|
||||
this.hoveredDealer = dealer;
|
||||
}
|
||||
|
||||
// Handle dealer leave
|
||||
onDealerLeave(): void {
|
||||
this.hoveredDealer = null;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,20 @@
|
||||
<div class="quarterwise-flow-container">
|
||||
<div class="quarterwise-flow-header">
|
||||
<h3>Quarterwise Flow</h3>
|
||||
</div>
|
||||
<div class="quarterwise-flow-content">
|
||||
<div class="placeholder-message">
|
||||
<div class="message-icon">
|
||||
<i class="info-icon">ℹ️</i>
|
||||
</div>
|
||||
<div class="message-text">
|
||||
<h4>Chart Configuration Incomplete</h4>
|
||||
<p>Please configure the quarterwise flow chart settings to visualize the data.</p>
|
||||
</div>
|
||||
<button class="configure-button">
|
||||
<i class="config-icon">⚙️</i>
|
||||
Configure Chart
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -0,0 +1,70 @@
|
||||
.quarterwise-flow-container {
|
||||
height: 100%;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
|
||||
.quarterwise-flow-header {
|
||||
margin-bottom: 20px;
|
||||
|
||||
h3 {
|
||||
font-size: 18px;
|
||||
font-weight: 600;
|
||||
color: #0a192f;
|
||||
margin: 0;
|
||||
}
|
||||
}
|
||||
|
||||
.quarterwise-flow-content {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
|
||||
.placeholder-message {
|
||||
text-align: center;
|
||||
padding: 30px;
|
||||
background: #f8fafc;
|
||||
border-radius: 12px;
|
||||
border: 1px dashed #cbd5e1;
|
||||
|
||||
.message-icon {
|
||||
color: #94a3b8;
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
.message-text {
|
||||
h4 {
|
||||
color: #0a192f;
|
||||
margin: 0 0 10px 0;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
p {
|
||||
color: #64748b;
|
||||
margin: 0 0 20px 0;
|
||||
font-size: 14px;
|
||||
line-height: 1.5;
|
||||
}
|
||||
}
|
||||
|
||||
.configure-button {
|
||||
padding: 10px 20px;
|
||||
background: #0a192f;
|
||||
color: white;
|
||||
border: none;
|
||||
border-radius: 6px;
|
||||
font-size: 14px;
|
||||
font-weight: 500;
|
||||
cursor: pointer;
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
transition: background-color 0.2s ease;
|
||||
|
||||
&:hover {
|
||||
background: #1e3a8a;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,15 @@
|
||||
import { Component, OnInit } from '@angular/core';
|
||||
|
||||
@Component({
|
||||
selector: 'app-shield-quarterwise-flow',
|
||||
templateUrl: './quarterwise-flow.component.html',
|
||||
styleUrls: ['./quarterwise-flow.component.scss']
|
||||
})
|
||||
export class QuarterwiseFlowComponent implements OnInit {
|
||||
|
||||
constructor() { }
|
||||
|
||||
ngOnInit(): void {
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,3 @@
|
||||
<div class="sidebar-filters">
|
||||
<!-- Component Palette Button and List have been moved to the main shield dashboard component -->
|
||||
</div>
|
||||
@@ -0,0 +1,25 @@
|
||||
<div class="sidebar-filters">
|
||||
<!-- Component Palette Button -->
|
||||
<div class="component-palette-section">
|
||||
<button class="component-palette-button" (click)="toggleComponentPalette()">
|
||||
<clr-icon shape="plus-circle"></clr-icon>
|
||||
Add Components
|
||||
</button>
|
||||
|
||||
<!-- Component Palette (hidden by default) -->
|
||||
<div class="component-palette" *ngIf="showComponentPalette">
|
||||
<h3 class="palette-title">Available Components</h3>
|
||||
<div class="component-list">
|
||||
<div
|
||||
*ngFor="let component of availableComponents"
|
||||
class="component-item"
|
||||
draggable="true"
|
||||
(dragstart)="onComponentDragStart($event, component)"
|
||||
(dragend)="onComponentDragEnd($event)">
|
||||
<clr-icon shape="drag-handle" class="drag-icon"></clr-icon>
|
||||
{{ component.name }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -0,0 +1,216 @@
|
||||
.sidebar-filters {
|
||||
height: 100%;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
|
||||
.componentbtn {
|
||||
margin: 10px;
|
||||
width: calc(100% - 20px);
|
||||
}
|
||||
|
||||
.nav-list {
|
||||
padding: 0;
|
||||
margin: 0 10px;
|
||||
|
||||
.nav-link {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
padding: 8px 12px;
|
||||
margin-bottom: 5px;
|
||||
background: #f8f9fa;
|
||||
border: 1px solid #e9ecef;
|
||||
border-radius: 4px;
|
||||
cursor: move;
|
||||
|
||||
&:hover {
|
||||
background: #e9ecef;
|
||||
}
|
||||
|
||||
.has-badge {
|
||||
margin-left: auto;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.app-header {
|
||||
margin-bottom: 30px;
|
||||
|
||||
.app-name {
|
||||
font-size: 24px;
|
||||
font-weight: 700;
|
||||
margin: 0 0 5px 0;
|
||||
color: #ff6b35;
|
||||
}
|
||||
|
||||
.dashboard-title {
|
||||
font-size: 18px;
|
||||
font-weight: 500;
|
||||
margin: 0;
|
||||
color: #cbd5e1;
|
||||
}
|
||||
}
|
||||
|
||||
.kpi-section {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 15px;
|
||||
margin-bottom: 30px;
|
||||
|
||||
.kpi-card {
|
||||
background: rgba(255, 255, 255, 0.1);
|
||||
border-radius: 10px;
|
||||
padding: 15px;
|
||||
text-align: center;
|
||||
|
||||
.kpi-title {
|
||||
font-size: 14px;
|
||||
color: #94a3b8;
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
|
||||
.kpi-value {
|
||||
font-size: 24px;
|
||||
font-weight: 700;
|
||||
color: white;
|
||||
}
|
||||
}
|
||||
|
||||
.total-leads {
|
||||
border-left: 3px solid #ff6b35;
|
||||
}
|
||||
|
||||
.total-deals {
|
||||
border-left: 3px solid #0a192f;
|
||||
}
|
||||
}
|
||||
|
||||
.component-palette-section {
|
||||
margin: 20px;
|
||||
|
||||
.component-palette-button {
|
||||
width: 100%;
|
||||
padding: 12px;
|
||||
background: rgba(255, 107, 53, 0.2);
|
||||
color: white;
|
||||
border: 1px solid rgba(255, 107, 53, 0.5);
|
||||
border-radius: 8px;
|
||||
font-size: 16px;
|
||||
font-weight: 500;
|
||||
cursor: pointer;
|
||||
transition: all 0.2s ease;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: 8px;
|
||||
|
||||
&:hover {
|
||||
background: rgba(255, 107, 53, 0.3);
|
||||
border-color: #ff6b35;
|
||||
}
|
||||
}
|
||||
|
||||
.component-palette {
|
||||
margin-top: 15px;
|
||||
background: rgba(255, 255, 255, 0.05);
|
||||
border-radius: 8px;
|
||||
padding: 15px;
|
||||
|
||||
.palette-title {
|
||||
font-size: 16px;
|
||||
color: white;
|
||||
margin: 0 0 15px 0;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.component-list {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 10px;
|
||||
|
||||
.component-item {
|
||||
padding: 10px 15px;
|
||||
background: rgba(255, 255, 255, 0.1);
|
||||
border: 1px solid rgba(255, 255, 255, 0.2);
|
||||
border-radius: 6px;
|
||||
color: white;
|
||||
cursor: move;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
transition: all 0.2s ease;
|
||||
|
||||
&:hover {
|
||||
background: rgba(255, 107, 53, 0.2);
|
||||
border-color: #ff6b35;
|
||||
}
|
||||
|
||||
.drag-icon {
|
||||
font-size: 16px;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.filters-section {
|
||||
flex: 1;
|
||||
|
||||
.filters-title {
|
||||
font-size: 18px;
|
||||
color: white;
|
||||
margin: 0 0 20px 0;
|
||||
padding-bottom: 10px;
|
||||
border-bottom: 1px solid rgba(255, 255, 255, 0.1);
|
||||
}
|
||||
|
||||
.filter-group {
|
||||
margin-bottom: 20px;
|
||||
|
||||
label {
|
||||
display: block;
|
||||
font-size: 14px;
|
||||
color: #cbd5e1;
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
|
||||
.filter-select {
|
||||
width: 100%;
|
||||
padding: 10px 15px;
|
||||
background: rgba(255, 255, 255, 0.1);
|
||||
border: 1px solid rgba(255, 255, 255, 0.2);
|
||||
border-radius: 8px;
|
||||
color: white;
|
||||
font-size: 14px;
|
||||
|
||||
&:focus {
|
||||
outline: none;
|
||||
border-color: #ff6b35;
|
||||
box-shadow: 0 0 0 2px rgba(255, 107, 53, 0.2);
|
||||
}
|
||||
|
||||
option {
|
||||
background: #0a192f;
|
||||
color: white;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.reset-button {
|
||||
width: 100%;
|
||||
padding: 12px;
|
||||
background: rgba(255, 255, 255, 0.1);
|
||||
color: white;
|
||||
border: 1px solid rgba(255, 255, 255, 0.2);
|
||||
border-radius: 8px;
|
||||
font-size: 16px;
|
||||
font-weight: 500;
|
||||
cursor: pointer;
|
||||
transition: all 0.2s ease;
|
||||
|
||||
&:hover {
|
||||
background: rgba(255, 107, 53, 0.2);
|
||||
border-color: #ff6b35;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,16 @@
|
||||
import { Component, OnInit } from '@angular/core';
|
||||
import { DashboardFilterService } from '../../services/dashboard-filter.service';
|
||||
|
||||
@Component({
|
||||
selector: 'app-shield-sidebar-filters',
|
||||
templateUrl: './sidebar-filters.component.html',
|
||||
styleUrls: ['./sidebar-filters.component.scss']
|
||||
})
|
||||
export class SidebarFiltersComponent implements OnInit {
|
||||
|
||||
constructor(private filterService: DashboardFilterService) { }
|
||||
|
||||
ngOnInit(): void {
|
||||
// Component initialization
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,16 @@
|
||||
import { Component, OnInit } from '@angular/core';
|
||||
import { DashboardFilterService } from '../../services/dashboard-filter.service';
|
||||
|
||||
@Component({
|
||||
selector: 'app-shield-sidebar-filters',
|
||||
templateUrl: './sidebar-filters.component.html',
|
||||
styleUrls: ['./sidebar-filters.component.scss']
|
||||
})
|
||||
export class SidebarFiltersComponent implements OnInit {
|
||||
|
||||
constructor(private filterService: DashboardFilterService) { }
|
||||
|
||||
ngOnInit(): void {
|
||||
// Component initialization
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,10 @@
|
||||
export * from './shield-dashboard.component';
|
||||
|
||||
// Export all components
|
||||
export * from './components/sidebar-filters/sidebar-filters.component';
|
||||
export * from './components/bar-chart/bar-chart.component';
|
||||
export * from './components/donut-chart/donut-chart.component';
|
||||
export * from './components/map-chart/map-chart.component';
|
||||
export * from './components/data-table/data-table.component';
|
||||
export * from './components/deal-details-card/deal-details-card.component';
|
||||
export * from './components/quarterwise-flow/quarterwise-flow.component';
|
||||
@@ -0,0 +1,84 @@
|
||||
import { Injectable } from '@angular/core';
|
||||
import { BehaviorSubject, Observable } from 'rxjs';
|
||||
|
||||
// Define the filter state interface
|
||||
export interface FilterState {
|
||||
salesRep: string;
|
||||
partner: string;
|
||||
tractionChannel: string;
|
||||
subProductLine: string;
|
||||
}
|
||||
|
||||
@Injectable({
|
||||
providedIn: 'root'
|
||||
})
|
||||
export class DashboardFilterService {
|
||||
// Shared filter state using BehaviorSubject
|
||||
private filterStateSubject = new BehaviorSubject<FilterState>({
|
||||
salesRep: '',
|
||||
partner: '',
|
||||
tractionChannel: '',
|
||||
subProductLine: ''
|
||||
});
|
||||
|
||||
public filterState$ = this.filterStateSubject.asObservable();
|
||||
|
||||
// KPI data
|
||||
private kpiDataSubject = new BehaviorSubject<{ totalLeads: number; totalDeals: number }>({
|
||||
totalLeads: 1248,
|
||||
totalDeals: 842
|
||||
});
|
||||
|
||||
public kpiData$ = this.kpiDataSubject.asObservable();
|
||||
|
||||
constructor() { }
|
||||
|
||||
// Update filter state
|
||||
updateFilter(filterType: keyof FilterState, value: string): void {
|
||||
const currentState = this.filterStateSubject.value;
|
||||
const newState = { ...currentState, [filterType]: value };
|
||||
this.filterStateSubject.next(newState);
|
||||
|
||||
// Simulate KPI data change based on filters
|
||||
this.updateKpiData(newState);
|
||||
}
|
||||
|
||||
// Reset all filters to default values
|
||||
resetFilters(): void {
|
||||
this.filterStateSubject.next({
|
||||
salesRep: '',
|
||||
partner: '',
|
||||
tractionChannel: '',
|
||||
subProductLine: ''
|
||||
});
|
||||
|
||||
// Reset KPI data to default values
|
||||
this.kpiDataSubject.next({
|
||||
totalLeads: 1248,
|
||||
totalDeals: 842
|
||||
});
|
||||
}
|
||||
|
||||
// Update KPI data based on filters (simulated)
|
||||
private updateKpiData(filters: FilterState): void {
|
||||
// This is a simplified simulation - in a real app, this would come from an API
|
||||
let totalLeads = 1248;
|
||||
let totalDeals = 842;
|
||||
|
||||
// Apply filter effects (simplified logic)
|
||||
if (filters.salesRep) totalLeads = Math.floor(totalLeads * 0.8);
|
||||
if (filters.partner) totalDeals = Math.floor(totalDeals * 0.9);
|
||||
if (filters.tractionChannel) totalLeads = Math.floor(totalLeads * 0.85);
|
||||
if (filters.subProductLine) totalDeals = Math.floor(totalDeals * 0.95);
|
||||
|
||||
this.kpiDataSubject.next({
|
||||
totalLeads,
|
||||
totalDeals
|
||||
});
|
||||
}
|
||||
|
||||
// Get current filter values
|
||||
getCurrentFilters(): FilterState {
|
||||
return this.filterStateSubject.value;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,16 @@
|
||||
import { NgModule } from '@angular/core';
|
||||
import { RouterModule, Routes } from '@angular/router';
|
||||
import { ShieldDashboardComponent } from './shield-dashboard.component';
|
||||
|
||||
const routes: Routes = [
|
||||
{
|
||||
path: '',
|
||||
component: ShieldDashboardComponent
|
||||
}
|
||||
];
|
||||
|
||||
@NgModule({
|
||||
imports: [RouterModule.forChild(routes)],
|
||||
exports: [RouterModule]
|
||||
})
|
||||
export class ShieldDashboardRoutingModule { }
|
||||
@@ -0,0 +1,493 @@
|
||||
<div class="shield-dashboard">
|
||||
<div class="dashboard-container">
|
||||
<!-- Sidebar Filters -->
|
||||
<div class="sidebar">
|
||||
<button class="btn componentbtn" (click)="toggleComponentPalette()">
|
||||
<clr-icon shape="plus"></clr-icon> Component
|
||||
</button>
|
||||
|
||||
<ul class="nav-list" style="list-style-type: none;" *ngIf="showComponentPalette">
|
||||
<li *ngFor="let widget of WidgetsMock">
|
||||
<!--
|
||||
Draggable widget from store using vanilla javascript event (dragstart)
|
||||
onDrag() is call, it take $event and a widget identifier as parameters
|
||||
-->
|
||||
<a draggable="true" class="nav-link" (dragstart)="onDrag($event, widget.identifier)">
|
||||
<clr-icon shape="drag-handle" style="margin-right: 10px;"></clr-icon>
|
||||
{{ widget.name }}
|
||||
<clr-icon shape="plugin" class="has-badge"></clr-icon>
|
||||
</a>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
<!-- Main Dashboard Content -->
|
||||
<div class="main-content">
|
||||
<!-- Deleted Items Section -->
|
||||
<div class="deleted-items-section" *ngIf="deletedItems.length > 0">
|
||||
<h3>Deleted Items</h3>
|
||||
<div class="deleted-items-list">
|
||||
<div *ngFor="let item of deletedItems" class="deleted-item">
|
||||
<span>{{ item.name }}</span>
|
||||
<button class="btn btn-sm btn-primary" (click)="restoreItem(item)">
|
||||
<clr-icon shape="undo"></clr-icon> Restore
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<button class="btn btn-sm btn-danger" (click)="clearDeletedItems()">
|
||||
<clr-icon shape="trash"></clr-icon> Clear All
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<!-- Dashboard Grid with Drag and Drop -->
|
||||
<gridster [options]="options" (drop)="onDrop($event)" style="background-color: transparent; min-height: 500px;">
|
||||
<gridster-item [item]="item" *ngFor="let item of dashboard">
|
||||
<!-- Remove Button -->
|
||||
<button class="btn btn-icon btn-danger" style="margin-left: 10px; margin-top: 10px;" (click)="removeItem(item)">
|
||||
<clr-icon shape="trash"></clr-icon>
|
||||
</button>
|
||||
|
||||
<!-- Edit Button -->
|
||||
<button class="btn btn-icon" style="margin-top: 10px; float: right;" (click)="editGadget(item)">
|
||||
<clr-icon shape="pencil"></clr-icon>
|
||||
</button>
|
||||
|
||||
<!-- Drag Handle -->
|
||||
<button class="btn btn-icon drag-handler" style="margin-left: 10px; margin-top: 10px;">
|
||||
<clr-icon shape="drag-handle"></clr-icon>
|
||||
</button>
|
||||
|
||||
<!-- Chart Components -->
|
||||
<div class="grid-item-content">
|
||||
<h4 style="margin-top: 0px; margin-left: 10px;">{{item.name}}</h4>
|
||||
<div *ngIf="item.chartType === 'bar-chart'">
|
||||
<app-shield-bar-chart></app-shield-bar-chart>
|
||||
</div>
|
||||
<div *ngIf="item.chartType === 'donut-chart' && item.name === 'End Customer Donut'">
|
||||
<app-shield-donut-chart chartType="endCustomer"></app-shield-donut-chart>
|
||||
</div>
|
||||
<div *ngIf="item.chartType === 'donut-chart' && item.name === 'Segment Penetration Donut'">
|
||||
<app-shield-donut-chart chartType="segmentPenetration"></app-shield-donut-chart>
|
||||
</div>
|
||||
<div *ngIf="item.chartType === 'map-chart'">
|
||||
<app-shield-map-chart></app-shield-map-chart>
|
||||
</div>
|
||||
<div *ngIf="item.chartType === 'data-table'">
|
||||
<app-shield-data-table></app-shield-data-table>
|
||||
</div>
|
||||
<div *ngIf="item.chartType === 'deal-details'">
|
||||
<app-shield-deal-details-card></app-shield-deal-details-card>
|
||||
</div>
|
||||
<div *ngIf="item.chartType === 'quarterwise-flow'">
|
||||
<app-shield-quarterwise-flow></app-shield-quarterwise-flow>
|
||||
</div>
|
||||
</div>
|
||||
</gridster-item>
|
||||
</gridster>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Configuration Modal -->
|
||||
<clr-modal [(clrModalOpen)]="modeledit" [clrModalStaticBackdrop]="true" clrModalSize="lg">
|
||||
<h3 class="modal-title">Configure Chart</h3>
|
||||
<div class="modal-body" *ngIf="selectedItem">
|
||||
<form [formGroup]="configForm" class="clr-form-horizontal">
|
||||
<!-- Chart Title -->
|
||||
<div class="clr-row">
|
||||
<div class="clr-col-12">
|
||||
<clr-input-container>
|
||||
<label for="charttitle">Chart Title</label>
|
||||
<input id="charttitle" type="text" formControlName="charttitle" clrInput />
|
||||
<clr-control-helper>Enter a descriptive title for your chart</clr-control-helper>
|
||||
</clr-input-container>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Connection Selection Field -->
|
||||
<div class="clr-row">
|
||||
<div class="clr-col-12">
|
||||
<clr-select-container>
|
||||
<label for="connection">Connection</label>
|
||||
<select id="connection" formControlName="connection" clrSelect>
|
||||
<option value="">Select Connection</option>
|
||||
<option *ngFor="let conn of sureconnectData" [value]="conn.id">
|
||||
{{conn.connection_name || conn.id}}
|
||||
</option>
|
||||
</select>
|
||||
<clr-control-helper>Select a SureConnect connection to use for this chart</clr-control-helper>
|
||||
</clr-select-container>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Chart Options (except for Data Table and Deal Details) -->
|
||||
<div class="clr-row" *ngIf="selectedItem?.name !== 'Data Table' && selectedItem?.name !== 'Deal Details'">
|
||||
<div class="clr-col-12">
|
||||
<div class="clr-form-control clr-row">
|
||||
<div class="clr-col-12">
|
||||
<clr-checkbox-container>
|
||||
<clr-checkbox-wrapper>
|
||||
<input type="checkbox" id="chartlegend" formControlName="chartlegend" clrCheckbox />
|
||||
<label for="chartlegend">Show Chart Legend</label>
|
||||
</clr-checkbox-wrapper>
|
||||
<clr-checkbox-wrapper>
|
||||
<input type="checkbox" id="showlabel" formControlName="showlabel" clrCheckbox />
|
||||
<label for="showlabel">Show Chart Label</label>
|
||||
</clr-checkbox-wrapper>
|
||||
</clr-checkbox-container>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- API URL -->
|
||||
<div class="clr-row">
|
||||
<div class="clr-col-12">
|
||||
<clr-input-container>
|
||||
<label for="table">API URL</label>
|
||||
<div class="clr-input-group">
|
||||
<input type="text" id="table" formControlName="table" clrInput />
|
||||
<button class="btn btn-icon btn-primary" (click)="getColumns(configForm.value.connection, configForm.value.table)" type="button">
|
||||
<clr-icon shape="refresh"></clr-icon>
|
||||
</button>
|
||||
</div>
|
||||
<clr-control-helper>Enter the API endpoint for your data source</clr-control-helper>
|
||||
</clr-input-container>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- X-Axis (except for Data Table and Deal Details) -->
|
||||
<div class="clr-row" *ngIf="selectedItem?.name !== 'Data Table' && selectedItem?.name !== 'Deal Details'">
|
||||
<div class="clr-col-12">
|
||||
<clr-select-container>
|
||||
<label for="xAxis">X-Axis</label>
|
||||
<select id="xAxis" formControlName="xAxis" clrSelect>
|
||||
<option value="">Select Column</option>
|
||||
<option *ngFor="let data of columnData" [value]="data">{{data}}</option>
|
||||
</select>
|
||||
<clr-control-helper>Select the column to use for the X-axis</clr-control-helper>
|
||||
</clr-select-container>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Y-Axis (except for Data Table and Deal Details) -->
|
||||
<div class="clr-row" *ngIf="selectedItem?.name !== 'Data Table' && selectedItem?.name !== 'Deal Details'">
|
||||
<div class="clr-col-12">
|
||||
<clr-combobox-container>
|
||||
<label for="yAxis">Y-Axis (Numeric)</label>
|
||||
<clr-combobox id="yAxis" formControlName="yAxis" clrMulti="true" required>
|
||||
<ng-container *clrOptionSelected="let selected">
|
||||
{{selected}}
|
||||
</ng-container>
|
||||
<clr-options>
|
||||
<clr-option *clrOptionItems="let state of columnData" [clrValue]="state">
|
||||
{{state}}
|
||||
</clr-option>
|
||||
</clr-options>
|
||||
</clr-combobox>
|
||||
<clr-control-helper>Select one or more columns for the Y-axis</clr-control-helper>
|
||||
</clr-combobox-container>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Base API Filters Section -->
|
||||
<div class="clr-row section-divider">
|
||||
<div class="clr-col-12">
|
||||
<h4>Base API Filters</h4>
|
||||
<p class="clr-subtext">Configure filters for the main API (applied regardless of drilldown settings)</p>
|
||||
|
||||
<!-- Add Base Filter Button -->
|
||||
<button class="btn btn-sm btn-primary" (click)="addBaseFilter()" type="button">
|
||||
<clr-icon shape="plus"></clr-icon> Add Filter
|
||||
</button>
|
||||
|
||||
<!-- Base Filter Fields List -->
|
||||
<div *ngFor="let filter of selectedItem?.baseFilters; let i = index" class="filter-item">
|
||||
<div class="filter-header">
|
||||
<span>Filter {{i + 1}}</span>
|
||||
<button class="btn btn-icon btn-danger btn-sm" (click)="removeBaseFilter(i)" type="button">
|
||||
<clr-icon shape="trash"></clr-icon>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div class="clr-row filter-content">
|
||||
<div class="clr-col-5">
|
||||
<clr-input-container>
|
||||
<label [attr.for]="'baseFilterField' + i">Field Name</label>
|
||||
<input type="text" [id]="'baseFilterField' + i" [(ngModel)]="filter.field" [ngModelOptions]="{standalone: true}" clrInput />
|
||||
</clr-input-container>
|
||||
</div>
|
||||
|
||||
<div class="clr-col-5">
|
||||
<clr-input-container>
|
||||
<label [attr.for]="'baseFilterValue' + i">Filter Value</label>
|
||||
<input type="text" [id]="'baseFilterValue' + i" [(ngModel)]="filter.value" [ngModelOptions]="{standalone: true}" clrInput />
|
||||
</clr-input-container>
|
||||
</div>
|
||||
|
||||
<div class="clr-col-2">
|
||||
<button class="btn btn-icon btn-danger btn-sm" (click)="removeBaseFilter(i)" type="button">
|
||||
<clr-icon shape="trash"></clr-icon>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Base Drilldown Configuration Section -->
|
||||
<div class="clr-row section-divider">
|
||||
<div class="clr-col-12">
|
||||
<h4>Base Drilldown Configuration</h4>
|
||||
<clr-checkbox-container>
|
||||
<clr-checkbox-wrapper>
|
||||
<input type="checkbox" id="drilldownEnabled" [(ngModel)]="selectedItem.drilldownEnabled" [ngModelOptions]="{standalone: true}" clrCheckbox />
|
||||
<label for="drilldownEnabled">Enable Base Drilldown</label>
|
||||
</clr-checkbox-wrapper>
|
||||
</clr-checkbox-container>
|
||||
<p class="clr-subtext">Enable drilldown functionality for this chart</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Drilldown API URL -->
|
||||
<div class="clr-row" *ngIf="selectedItem?.drilldownEnabled">
|
||||
<div class="clr-col-12">
|
||||
<clr-input-container>
|
||||
<label for="drilldownApiUrl">Base Drilldown API URL</label>
|
||||
<div class="clr-input-group">
|
||||
<input type="text" id="drilldownApiUrl" [(ngModel)]="selectedItem.drilldownApiUrl" [ngModelOptions]="{standalone: true}" clrInput />
|
||||
<button class="btn btn-icon btn-primary" (click)="refreshDrilldownColumns()" [disabled]="!selectedItem.drilldownApiUrl" type="button">
|
||||
<clr-icon shape="refresh"></clr-icon>
|
||||
</button>
|
||||
</div>
|
||||
<clr-control-helper>Enter the API URL for base drilldown data. Use angle brackets for parameters, e.g., http://api.example.com/data/<country></clr-control-helper>
|
||||
</clr-input-container>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Drilldown X-Axis -->
|
||||
<div class="clr-row" *ngIf="selectedItem?.drilldownEnabled">
|
||||
<div class="clr-col-12">
|
||||
<clr-select-container>
|
||||
<label for="drilldownXAxis">Base Drilldown X-Axis</label>
|
||||
<select id="drilldownXAxis" [(ngModel)]="selectedItem.drilldownXAxis" [ngModelOptions]="{standalone: true}" clrSelect>
|
||||
<option value="">Select X-Axis Column</option>
|
||||
<option *ngFor="let column of drilldownColumnData" [value]="column">{{column}}</option>
|
||||
</select>
|
||||
<clr-control-helper>Select the column to use for X-axis in base drilldown view</clr-control-helper>
|
||||
</clr-select-container>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Drilldown Y-Axis (except for Deal Details) -->
|
||||
<div class="clr-row" *ngIf="selectedItem?.drilldownEnabled && selectedItem?.name !== 'Deal Details'">
|
||||
<div class="clr-col-12">
|
||||
<clr-select-container>
|
||||
<label for="drilldownYAxis">Base Drilldown Y-Axis</label>
|
||||
<select id="drilldownYAxis" [(ngModel)]="selectedItem.drilldownYAxis" [ngModelOptions]="{standalone: true}" clrSelect>
|
||||
<option value="">Select Y-Axis Column</option>
|
||||
<option *ngFor="let column of drilldownColumnData" [value]="column">{{column}}</option>
|
||||
</select>
|
||||
<clr-control-helper>Select the column to use for Y-axis in base drilldown view</clr-control-helper>
|
||||
</clr-select-container>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Drilldown Parameter -->
|
||||
<div class="clr-row" *ngIf="selectedItem?.drilldownEnabled">
|
||||
<div class="clr-col-12">
|
||||
<clr-select-container>
|
||||
<label for="drilldownParameter">Base Drilldown Parameter</label>
|
||||
<select id="drilldownParameter" [(ngModel)]="selectedItem.drilldownParameter" [ngModelOptions]="{standalone: true}" clrSelect>
|
||||
<option value="">Select Parameter Column</option>
|
||||
<option *ngFor="let column of drilldownColumnData" [value]="column">{{column}}</option>
|
||||
</select>
|
||||
<clr-control-helper>Select the column to use as parameter for URL template replacement in base drilldown</clr-control-helper>
|
||||
</clr-select-container>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Base Drilldown Filter Configuration -->
|
||||
<div class="clr-row section-divider" *ngIf="selectedItem?.drilldownEnabled">
|
||||
<div class="clr-col-12">
|
||||
<h5>Base Drilldown Filters</h5>
|
||||
<p class="clr-subtext">Configure filters for the base drilldown level</p>
|
||||
|
||||
<!-- Add Drilldown Filter Button -->
|
||||
<button class="btn btn-sm btn-primary" (click)="addDrilldownFilter()" type="button">
|
||||
<clr-icon shape="plus"></clr-icon> Add Filter
|
||||
</button>
|
||||
|
||||
<!-- Drilldown Filter Fields List -->
|
||||
<div *ngFor="let filter of selectedItem?.drilldownFilters; let i = index" class="filter-item">
|
||||
<div class="filter-header">
|
||||
<span>Filter {{i + 1}}</span>
|
||||
<button class="btn btn-icon btn-danger btn-sm" (click)="removeDrilldownFilter(i)" type="button">
|
||||
<clr-icon shape="trash"></clr-icon>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div class="clr-row filter-content">
|
||||
<div class="clr-col-5">
|
||||
<clr-input-container>
|
||||
<label [attr.for]="'drilldownFilterField' + i">Field Name</label>
|
||||
<input type="text" [id]="'drilldownFilterField' + i" [(ngModel)]="filter.field" [ngModelOptions]="{standalone: true}" clrInput />
|
||||
</clr-input-container>
|
||||
</div>
|
||||
|
||||
<div class="clr-col-5">
|
||||
<clr-input-container>
|
||||
<label [attr.for]="'drilldownFilterValue' + i">Filter Value</label>
|
||||
<input type="text" [id]="'drilldownFilterValue' + i" [(ngModel)]="filter.value" [ngModelOptions]="{standalone: true}" clrInput />
|
||||
</clr-input-container>
|
||||
</div>
|
||||
|
||||
<div class="clr-col-2">
|
||||
<button class="btn btn-icon btn-danger btn-sm" (click)="removeDrilldownFilter(i)" type="button">
|
||||
<clr-icon shape="trash"></clr-icon>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Multi-Layer Drilldown Configurations -->
|
||||
<div class="clr-row section-divider" *ngIf="selectedItem?.drilldownEnabled">
|
||||
<div class="clr-col-12">
|
||||
<h4>Multi-Layer Drilldown Configurations</h4>
|
||||
<button class="btn btn-sm btn-primary" (click)="addDrilldownLayer()" type="button">
|
||||
<clr-icon shape="plus"></clr-icon> Add Drilldown Layer
|
||||
</button>
|
||||
<p class="clr-subtext">Add additional drilldown layers for multi-level navigation</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Dynamic Drilldown Layers -->
|
||||
<div class="clr-row" *ngFor="let layer of selectedItem?.drilldownLayers; let i = index">
|
||||
<div class="clr-col-12 drilldown-layer">
|
||||
<div class="layer-header">
|
||||
<h5>Drilldown Layer {{i + 1}}</h5>
|
||||
<button class="btn btn-icon btn-danger btn-sm" (click)="removeDrilldownLayer(i)" type="button">
|
||||
<clr-icon shape="trash"></clr-icon>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<clr-checkbox-container>
|
||||
<clr-checkbox-wrapper>
|
||||
<input type="checkbox" [id]="'layerEnabled' + i" [(ngModel)]="layer.enabled" [ngModelOptions]="{standalone: true}" clrCheckbox />
|
||||
<label [for]="'layerEnabled' + i">Enable Layer {{i + 1}} Drilldown</label>
|
||||
</clr-checkbox-wrapper>
|
||||
</clr-checkbox-container>
|
||||
|
||||
<!-- Layer API URL -->
|
||||
<div class="clr-row">
|
||||
<div class="clr-col-12">
|
||||
<clr-input-container>
|
||||
<label [for]="'layerApiUrl' + i">Layer {{i + 1}} API URL</label>
|
||||
<div class="clr-input-group">
|
||||
<input type="text" [id]="'layerApiUrl' + i" [(ngModel)]="layer.apiUrl" clrInput [ngModelOptions]="{standalone: true}" />
|
||||
<button class="btn btn-icon btn-primary" (click)="refreshDrilldownLayerColumns(i)" [disabled]="!layer.apiUrl" type="button">
|
||||
<clr-icon shape="refresh"></clr-icon>
|
||||
</button>
|
||||
</div>
|
||||
<clr-control-helper>Enter the API URL for layer {{i + 1}} drilldown data. Use angle brackets for parameters, e.g., http://api.example.com/data/<state></clr-control-helper>
|
||||
</clr-input-container>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Layer X-Axis -->
|
||||
<div class="clr-row">
|
||||
<div class="clr-col-12">
|
||||
<clr-select-container>
|
||||
<label [for]="'layerXAxis' + i">Layer {{i + 1}} X-Axis</label>
|
||||
<select [id]="'layerXAxis' + i" [(ngModel)]="layer.xAxis" [ngModelOptions]="{standalone: true}" clrSelect>
|
||||
<option value="">Select X-Axis Column</option>
|
||||
<option *ngFor="let column of layerColumnData[i] || []" [value]="column">{{column}}</option>
|
||||
</select>
|
||||
<clr-control-helper>Select the column to use for X-axis in layer {{i + 1}} drilldown view</clr-control-helper>
|
||||
</clr-select-container>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Layer Y-Axis (except for Deal Details) -->
|
||||
<div class="clr-row" *ngIf="selectedItem?.name !== 'Deal Details'">
|
||||
<div class="clr-col-12">
|
||||
<clr-select-container>
|
||||
<label [for]="'layerYAxis' + i">Layer {{i + 1}} Y-Axis</label>
|
||||
<select [id]="'layerYAxis' + i" [(ngModel)]="layer.yAxis" [ngModelOptions]="{standalone: true}" clrSelect>
|
||||
<option value="">Select Y-Axis Column</option>
|
||||
<option *ngFor="let column of layerColumnData[i] || []" [value]="column">{{column}}</option>
|
||||
</select>
|
||||
<clr-control-helper>Select the column to use for Y-axis in layer {{i + 1}} drilldown view</clr-control-helper>
|
||||
</clr-select-container>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Parameter Selection for Drilldown Layer -->
|
||||
<div class="clr-row">
|
||||
<div class="clr-col-12">
|
||||
<clr-select-container>
|
||||
<label [for]="'layerParameter' + i">Layer {{i + 1}} Parameter</label>
|
||||
<select [id]="'layerParameter' + i" [(ngModel)]="layer.parameter" [ngModelOptions]="{standalone: true}" clrSelect>
|
||||
<option value="">Select Parameter Column</option>
|
||||
<option *ngFor="let column of layerColumnData[i] || []" [value]="column">{{column}}</option>
|
||||
</select>
|
||||
<clr-control-helper>Select the column to use as parameter for URL template replacement in layer {{i + 1}} drilldown</clr-control-helper>
|
||||
</clr-select-container>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Layer Filter Configuration -->
|
||||
<div class="clr-row section-divider">
|
||||
<div class="clr-col-12">
|
||||
<h5>Layer {{i + 1}} Filters</h5>
|
||||
<p class="clr-subtext">Configure filters for this drilldown layer</p>
|
||||
|
||||
<!-- Add Layer Filter Button -->
|
||||
<button class="btn btn-sm btn-primary" (click)="addLayerFilter(i)" type="button">
|
||||
<clr-icon shape="plus"></clr-icon> Add Filter
|
||||
</button>
|
||||
|
||||
<!-- Layer Filter Fields List -->
|
||||
<div *ngFor="let filter of layer.filters; let j = index" class="filter-item">
|
||||
<div class="filter-header">
|
||||
<span>Filter {{j + 1}}</span>
|
||||
<button class="btn btn-icon btn-danger btn-sm" (click)="removeLayerFilter(i, j)" type="button">
|
||||
<clr-icon shape="trash"></clr-icon>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div class="clr-row filter-content">
|
||||
<div class="clr-col-5">
|
||||
<clr-input-container>
|
||||
<label [attr.for]="'layerFilterField' + i + '_' + j">Field Name</label>
|
||||
<input type="text" [id]="'layerFilterField' + i + '_' + j" [(ngModel)]="filter.field" [ngModelOptions]="{standalone: true}" clrInput />
|
||||
</clr-input-container>
|
||||
</div>
|
||||
|
||||
<div class="clr-col-5">
|
||||
<clr-input-container>
|
||||
<label [attr.for]="'layerFilterValue' + i + '_' + j">Filter Value</label>
|
||||
<input type="text" [id]="'layerFilterValue' + i + '_' + j" [(ngModel)]="filter.value" [ngModelOptions]="{standalone: true}" clrInput />
|
||||
</clr-input-container>
|
||||
</div>
|
||||
|
||||
<div class="clr-col-2">
|
||||
<button class="btn btn-icon btn-danger btn-sm" (click)="removeLayerFilter(i, j)" type="button">
|
||||
<clr-icon shape="trash"></clr-icon>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<button type="button" class="btn btn-outline" (click)="cancelConfiguration()">Cancel</button>
|
||||
<button type="button" class="btn btn-primary" (click)="saveConfiguration()">Save</button>
|
||||
</div>
|
||||
</clr-modal>
|
||||
@@ -0,0 +1,220 @@
|
||||
.shield-dashboard {
|
||||
padding: 20px;
|
||||
background-color: #f5f5f5;
|
||||
min-height: 100vh;
|
||||
}
|
||||
|
||||
.dashboard-container {
|
||||
display: flex;
|
||||
gap: 20px;
|
||||
}
|
||||
|
||||
.sidebar {
|
||||
flex: 0 0 250px;
|
||||
background: white;
|
||||
border-radius: 4px;
|
||||
box-shadow: 0 2px 4px rgba(0,0,0,0.1);
|
||||
padding: 15px;
|
||||
}
|
||||
|
||||
.main-content {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 20px;
|
||||
}
|
||||
|
||||
.kpi-section {
|
||||
display: flex;
|
||||
gap: 20px;
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
.kpi-card {
|
||||
flex: 1;
|
||||
background: white;
|
||||
border-radius: 4px;
|
||||
box-shadow: 0 2px 4px rgba(0,0,0,0.1);
|
||||
padding: 20px;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.kpi-title {
|
||||
font-size: 14px;
|
||||
color: #666;
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
|
||||
.kpi-value {
|
||||
font-size: 24px;
|
||||
font-weight: bold;
|
||||
color: #333;
|
||||
}
|
||||
|
||||
.total-leads {
|
||||
border-top: 4px solid #4285f4;
|
||||
}
|
||||
|
||||
.total-deals {
|
||||
border-top: 4px solid #0f9d58;
|
||||
}
|
||||
|
||||
.deleted-items-section {
|
||||
background: white;
|
||||
border-radius: 4px;
|
||||
box-shadow: 0 2px 4px rgba(0,0,0,0.1);
|
||||
padding: 20px;
|
||||
|
||||
h3 {
|
||||
margin-top: 0;
|
||||
color: #333;
|
||||
border-bottom: 1px solid #eee;
|
||||
padding-bottom: 10px;
|
||||
}
|
||||
|
||||
.deleted-items-list {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 10px;
|
||||
margin-bottom: 15px;
|
||||
|
||||
.deleted-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
background: #f8f9fa;
|
||||
border: 1px solid #e9ecef;
|
||||
border-radius: 4px;
|
||||
padding: 8px 12px;
|
||||
|
||||
span {
|
||||
font-size: 14px;
|
||||
color: #495057;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.dashboard-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fill, minmax(400px, 1fr));
|
||||
gap: 20px;
|
||||
}
|
||||
|
||||
.grid-item {
|
||||
background: white;
|
||||
border-radius: 4px;
|
||||
box-shadow: 0 2px 4px rgba(0,0,0,0.1);
|
||||
padding: 15px;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.grid-item-content {
|
||||
margin-top: 30px;
|
||||
}
|
||||
|
||||
.drag-handler {
|
||||
cursor: move;
|
||||
}
|
||||
|
||||
.drop-zone-indicator {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
min-height: 500px;
|
||||
background-color: #f8f9fa;
|
||||
border: 2px dashed #dee2e6;
|
||||
border-radius: 4px;
|
||||
color: #6c757d;
|
||||
font-size: 18px;
|
||||
text-align: center;
|
||||
|
||||
p {
|
||||
margin: 0;
|
||||
}
|
||||
}
|
||||
|
||||
/* Gridster specific styles */
|
||||
gridster {
|
||||
background: transparent !important;
|
||||
}
|
||||
|
||||
gridster-item {
|
||||
background: white;
|
||||
border-radius: 4px;
|
||||
box-shadow: 0 2px 4px rgba(0,0,0,0.1);
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
/* Configuration Modal Styles */
|
||||
.section-divider {
|
||||
margin-top: 20px;
|
||||
padding-top: 15px;
|
||||
border-top: 1px solid #eee;
|
||||
}
|
||||
|
||||
.section-divider:first-child {
|
||||
border-top: none;
|
||||
padding-top: 0;
|
||||
margin-top: 0;
|
||||
}
|
||||
|
||||
.filter-item {
|
||||
margin-bottom: 15px;
|
||||
padding: 12px;
|
||||
border: 1px solid #e9ecef;
|
||||
border-radius: 4px;
|
||||
background-color: #f8f9fa;
|
||||
}
|
||||
|
||||
.filter-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
|
||||
.filter-content {
|
||||
margin-top: 10px;
|
||||
}
|
||||
|
||||
.drilldown-layer {
|
||||
margin-top: 20px;
|
||||
padding: 15px;
|
||||
border: 1px solid #dee2e6;
|
||||
border-radius: 4px;
|
||||
background-color: #ffffff;
|
||||
}
|
||||
|
||||
.layer-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
margin-bottom: 15px;
|
||||
padding-bottom: 10px;
|
||||
border-bottom: 1px solid #eee;
|
||||
}
|
||||
|
||||
.layer-header h5 {
|
||||
margin: 0;
|
||||
color: #495057;
|
||||
}
|
||||
|
||||
/* Responsive design */
|
||||
@media (max-width: 768px) {
|
||||
.dashboard-container {
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.sidebar {
|
||||
flex: 0 0 auto;
|
||||
}
|
||||
|
||||
.kpi-section {
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.filter-item .clr-row > div {
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,596 @@
|
||||
import { Component, OnInit } from '@angular/core';
|
||||
import { GridsterConfig, GridsterItem } from 'angular-gridster2';
|
||||
import { FormBuilder, FormGroup } from '@angular/forms';
|
||||
import { DatastoreService } from 'src/app/services/fnd/datastore.service';
|
||||
import { AlertsService } from 'src/app/services/fnd/alerts.service';
|
||||
import { SureconnectService } from '../../sureconnect/sureconnect.service';
|
||||
|
||||
interface ShieldDashboardItem extends GridsterItem {
|
||||
chartType: string;
|
||||
name: string;
|
||||
id: number;
|
||||
component?: any;
|
||||
// Configuration properties
|
||||
charttitle?: string;
|
||||
connection?: string;
|
||||
table?: string;
|
||||
xAxis?: string;
|
||||
yAxis?: string[];
|
||||
chartlegend?: boolean;
|
||||
showlabel?: boolean;
|
||||
baseFilters?: any[];
|
||||
drilldownEnabled?: boolean;
|
||||
drilldownApiUrl?: string;
|
||||
drilldownXAxis?: string;
|
||||
drilldownYAxis?: string;
|
||||
drilldownParameter?: string;
|
||||
drilldownFilters?: any[];
|
||||
drilldownLayers?: any[];
|
||||
}
|
||||
|
||||
interface WidgetModel {
|
||||
name: string;
|
||||
identifier: string;
|
||||
}
|
||||
|
||||
@Component({
|
||||
selector: 'app-shield-dashboard',
|
||||
templateUrl: './shield-dashboard.component.html',
|
||||
styleUrls: ['./shield-dashboard.component.scss']
|
||||
})
|
||||
export class ShieldDashboardComponent implements OnInit {
|
||||
options: GridsterConfig;
|
||||
dashboard: Array<ShieldDashboardItem>;
|
||||
|
||||
// Configuration modal
|
||||
configModalOpen = false;
|
||||
modeledit = false; // Add this to match the main dashboard pattern
|
||||
configForm: FormGroup;
|
||||
selectedItem: ShieldDashboardItem | null = null;
|
||||
|
||||
// Component palette
|
||||
showComponentPalette = false;
|
||||
WidgetsMock: WidgetModel[] = [
|
||||
{
|
||||
name: 'Bar Chart',
|
||||
identifier: 'bar_chart'
|
||||
},
|
||||
{
|
||||
name: 'Doughnut Chart',
|
||||
identifier: 'doughnut_chart'
|
||||
},
|
||||
{
|
||||
name: 'Map Chart',
|
||||
identifier: 'map_chart'
|
||||
},
|
||||
{
|
||||
name: 'Data Table',
|
||||
identifier: 'grid_view'
|
||||
},
|
||||
{
|
||||
name: 'Deal Details',
|
||||
identifier: 'to_do_chart'
|
||||
},
|
||||
{
|
||||
name: 'Quarterwise Flow',
|
||||
identifier: 'line_chart'
|
||||
},
|
||||
{
|
||||
name: 'Compact Filter',
|
||||
identifier: 'compact_filter'
|
||||
}
|
||||
];
|
||||
|
||||
// Services data
|
||||
storedata: any[] = [];
|
||||
columnData: any[] = [];
|
||||
sureconnectData: any[] = [];
|
||||
drilldownColumnData: any[] = [];
|
||||
layerColumnData: { [key: number]: any[] } = {};
|
||||
|
||||
// Keep track of deleted items
|
||||
deletedItems: Array<ShieldDashboardItem> = [];
|
||||
|
||||
constructor(
|
||||
private _fb: FormBuilder,
|
||||
private datastoreService: DatastoreService,
|
||||
private alertService: AlertsService,
|
||||
private sureconnectService: SureconnectService
|
||||
) { }
|
||||
|
||||
ngOnInit(): void {
|
||||
this.options = {
|
||||
gridType: 'fit',
|
||||
enableEmptyCellDrop: true,
|
||||
emptyCellDropCallback: this.onDrop,
|
||||
pushItems: true,
|
||||
swap: true,
|
||||
pushDirections: { north: true, east: true, south: true, west: true },
|
||||
resizable: { enabled: true },
|
||||
itemChangeCallback: this.itemChange.bind(this),
|
||||
draggable: {
|
||||
enabled: true,
|
||||
ignoreContent: true,
|
||||
dropOverItems: true,
|
||||
dragHandleClass: 'drag-handler',
|
||||
ignoreContentClass: 'no-drag',
|
||||
},
|
||||
displayGrid: 'always',
|
||||
minCols: 10,
|
||||
minRows: 10,
|
||||
itemResizeCallback: this.itemResize.bind(this)
|
||||
};
|
||||
|
||||
// Initialize the dashboard with empty canvas
|
||||
this.dashboard = [];
|
||||
|
||||
// Initialize form
|
||||
this.configForm = this._fb.group({
|
||||
charttitle: [''],
|
||||
connection: [''],
|
||||
table: [''],
|
||||
xAxis: [''],
|
||||
yAxis: [''],
|
||||
chartlegend: [true],
|
||||
showlabel: [true]
|
||||
});
|
||||
|
||||
// Load service data
|
||||
this.loadServicesData();
|
||||
}
|
||||
|
||||
// Load initial data from services
|
||||
loadServicesData() {
|
||||
// Load sureconnect data
|
||||
this.sureconnectService.getAll().subscribe((data: any[]) => {
|
||||
this.sureconnectData = data;
|
||||
});
|
||||
|
||||
// Load datastore data
|
||||
this.datastoreService.getAll().subscribe((data) => {
|
||||
this.storedata = data as any[];
|
||||
});
|
||||
}
|
||||
|
||||
// Toggle component palette visibility
|
||||
toggleComponentPalette(): void {
|
||||
this.showComponentPalette = !this.showComponentPalette;
|
||||
}
|
||||
|
||||
// Handle drag start event for components - matching the working implementation
|
||||
onDrag(event: DragEvent, identifier: string): void {
|
||||
console.log("on drag", identifier);
|
||||
console.log("on drag ", event);
|
||||
if (event.dataTransfer) {
|
||||
event.dataTransfer.setData('widgetIdentifier', identifier);
|
||||
}
|
||||
}
|
||||
|
||||
onDrop(ev: any) {
|
||||
// Handle dropping new components onto the dashboard
|
||||
console.log('Item dropped:', ev);
|
||||
|
||||
// Get the component identifier from the drag event
|
||||
const componentType = ev.dataTransfer ? ev.dataTransfer.getData('widgetIdentifier') : '';
|
||||
console.log('Component type dropped:', componentType);
|
||||
|
||||
if (componentType) {
|
||||
this.addComponentToDashboard(componentType);
|
||||
} else {
|
||||
console.log('No component type found in drag data');
|
||||
}
|
||||
}
|
||||
|
||||
// Add a new component to the dashboard
|
||||
addComponentToDashboard(componentType: string) {
|
||||
// Generate a new ID for the component
|
||||
const newId = this.dashboard.length > 0 ? Math.max(...this.dashboard.map(item => item.id), 0) + 1 : 1;
|
||||
|
||||
let newItem: ShieldDashboardItem;
|
||||
|
||||
switch (componentType) {
|
||||
case "bar_chart":
|
||||
newItem = {
|
||||
cols: 5,
|
||||
rows: 6,
|
||||
y: 0,
|
||||
x: 0,
|
||||
chartType: 'bar-chart',
|
||||
name: 'Bar Chart',
|
||||
id: newId,
|
||||
chartlegend: true,
|
||||
showlabel: true,
|
||||
baseFilters: [],
|
||||
drilldownEnabled: false,
|
||||
drilldownLayers: []
|
||||
};
|
||||
break;
|
||||
case "doughnut_chart":
|
||||
// For doughnut charts, we'll need to determine which one based on existing items
|
||||
const donutCount = this.dashboard.filter(item => item.chartType === 'donut-chart').length;
|
||||
if (donutCount % 2 === 0) {
|
||||
newItem = {
|
||||
cols: 5,
|
||||
rows: 6,
|
||||
y: 0,
|
||||
x: 0,
|
||||
chartType: 'donut-chart',
|
||||
name: 'End Customer Donut',
|
||||
id: newId,
|
||||
chartlegend: true,
|
||||
showlabel: true,
|
||||
baseFilters: [],
|
||||
drilldownEnabled: false,
|
||||
drilldownLayers: []
|
||||
};
|
||||
} else {
|
||||
newItem = {
|
||||
cols: 5,
|
||||
rows: 6,
|
||||
y: 0,
|
||||
x: 0,
|
||||
chartType: 'donut-chart',
|
||||
name: 'Segment Penetration Donut',
|
||||
id: newId,
|
||||
chartlegend: true,
|
||||
showlabel: true,
|
||||
baseFilters: [],
|
||||
drilldownEnabled: false,
|
||||
drilldownLayers: []
|
||||
};
|
||||
}
|
||||
break;
|
||||
case "map_chart":
|
||||
newItem = {
|
||||
cols: 5,
|
||||
rows: 6,
|
||||
y: 0,
|
||||
x: 0,
|
||||
chartType: 'map-chart',
|
||||
name: 'Map Chart',
|
||||
id: newId,
|
||||
chartlegend: true,
|
||||
showlabel: true,
|
||||
baseFilters: [],
|
||||
drilldownEnabled: false,
|
||||
drilldownLayers: []
|
||||
};
|
||||
break;
|
||||
case "grid_view":
|
||||
newItem = {
|
||||
cols: 10,
|
||||
rows: 6,
|
||||
y: 0,
|
||||
x: 0,
|
||||
chartType: 'data-table',
|
||||
name: 'Data Table',
|
||||
id: newId,
|
||||
chartlegend: true,
|
||||
showlabel: true,
|
||||
baseFilters: [],
|
||||
drilldownEnabled: false,
|
||||
drilldownLayers: []
|
||||
};
|
||||
break;
|
||||
case "to_do_chart":
|
||||
newItem = {
|
||||
cols: 5,
|
||||
rows: 6,
|
||||
y: 0,
|
||||
x: 0,
|
||||
chartType: 'deal-details',
|
||||
name: 'Deal Details',
|
||||
id: newId,
|
||||
chartlegend: true,
|
||||
showlabel: true,
|
||||
baseFilters: [],
|
||||
drilldownEnabled: false,
|
||||
drilldownLayers: []
|
||||
};
|
||||
break;
|
||||
case "line_chart":
|
||||
newItem = {
|
||||
cols: 5,
|
||||
rows: 6,
|
||||
y: 0,
|
||||
x: 0,
|
||||
chartType: 'quarterwise-flow',
|
||||
name: 'Quarterwise Flow',
|
||||
id: newId,
|
||||
chartlegend: true,
|
||||
showlabel: true,
|
||||
baseFilters: [],
|
||||
drilldownEnabled: false,
|
||||
drilldownLayers: []
|
||||
};
|
||||
break;
|
||||
case "compact_filter":
|
||||
newItem = {
|
||||
cols: 3,
|
||||
rows: 2,
|
||||
y: 0,
|
||||
x: 0,
|
||||
chartType: 'compact-filter',
|
||||
name: 'Compact Filter',
|
||||
id: newId,
|
||||
baseFilters: [],
|
||||
drilldownEnabled: false,
|
||||
drilldownLayers: []
|
||||
};
|
||||
break;
|
||||
default:
|
||||
newItem = {
|
||||
cols: 5,
|
||||
rows: 6,
|
||||
y: 0,
|
||||
x: 0,
|
||||
chartType: componentType,
|
||||
name: componentType,
|
||||
id: newId,
|
||||
chartlegend: true,
|
||||
showlabel: true,
|
||||
baseFilters: [],
|
||||
drilldownEnabled: false,
|
||||
drilldownLayers: []
|
||||
};
|
||||
}
|
||||
|
||||
// Add the new item to the dashboard
|
||||
this.dashboard.push(newItem);
|
||||
}
|
||||
|
||||
removeItem(item: ShieldDashboardItem) {
|
||||
// Add the item to deleted items list before removing
|
||||
this.deletedItems.push({...item});
|
||||
|
||||
// Remove the item from the dashboard
|
||||
this.dashboard.splice(this.dashboard.indexOf(item), 1);
|
||||
}
|
||||
|
||||
// Restore a deleted item
|
||||
restoreItem(item: ShieldDashboardItem) {
|
||||
// Remove from deleted items
|
||||
this.deletedItems.splice(this.deletedItems.indexOf(item), 1);
|
||||
|
||||
// Add back to dashboard
|
||||
this.dashboard.push(item);
|
||||
}
|
||||
|
||||
// Clear all deleted items
|
||||
clearDeletedItems() {
|
||||
this.deletedItems = [];
|
||||
}
|
||||
|
||||
itemChange() {
|
||||
console.log('Item changed:', this.dashboard);
|
||||
}
|
||||
|
||||
itemResize(item: any, itemComponent: any) {
|
||||
console.log('Item resized:', item);
|
||||
// Trigger a window resize event to notify charts to resize
|
||||
window.dispatchEvent(new Event('resize'));
|
||||
}
|
||||
|
||||
/**
|
||||
* Extract only the relevant chart configuration properties to pass to chart components
|
||||
* This prevents errors when trying to set properties that don't exist on the components
|
||||
*/
|
||||
getChartInputs(item: any): any {
|
||||
// Only pass properties that are relevant to chart components
|
||||
const chartInputs = {
|
||||
chartType: item.chartType,
|
||||
name: item.name,
|
||||
charttitle: item.charttitle,
|
||||
connection: item.connection,
|
||||
table: item.table,
|
||||
xAxis: item.xAxis,
|
||||
yAxis: item.yAxis,
|
||||
chartlegend: item.chartlegend,
|
||||
showlabel: item.showlabel,
|
||||
baseFilters: item.baseFilters || [],
|
||||
drilldownEnabled: item.drilldownEnabled,
|
||||
drilldownApiUrl: item.drilldownApiUrl,
|
||||
drilldownXAxis: item.drilldownXAxis,
|
||||
drilldownYAxis: item.drilldownYAxis,
|
||||
drilldownParameter: item.drilldownParameter,
|
||||
drilldownFilters: item.drilldownFilters || [],
|
||||
drilldownLayers: item.drilldownLayers || []
|
||||
};
|
||||
|
||||
// Remove undefined properties to avoid passing unnecessary data
|
||||
Object.keys(chartInputs).forEach(key => {
|
||||
if (chartInputs[key] === undefined) {
|
||||
delete chartInputs[key];
|
||||
}
|
||||
});
|
||||
|
||||
return chartInputs;
|
||||
}
|
||||
|
||||
// Open configuration modal for a chart
|
||||
editGadget(item: ShieldDashboardItem) {
|
||||
console.log('Opening configuration modal for item:', item);
|
||||
this.selectedItem = item;
|
||||
this.modeledit = true; // Use modeledit instead of configModalOpen
|
||||
|
||||
// Initialize form with item data
|
||||
this.configForm.patchValue({
|
||||
charttitle: item.charttitle || '',
|
||||
connection: item.connection || '',
|
||||
table: item.table || '',
|
||||
xAxis: item.xAxis || '',
|
||||
yAxis: item.yAxis || [],
|
||||
chartlegend: item.chartlegend !== undefined ? item.chartlegend : true,
|
||||
showlabel: item.showlabel !== undefined ? item.showlabel : true
|
||||
});
|
||||
|
||||
console.log('Form values after patch:', this.configForm.value);
|
||||
|
||||
// Load columns if table is set
|
||||
if (item.table) {
|
||||
this.getColumns(item.connection, item.table);
|
||||
}
|
||||
|
||||
// Load drilldown columns if drilldown API URL is set
|
||||
if (item.drilldownApiUrl) {
|
||||
this.refreshDrilldownColumns();
|
||||
}
|
||||
}
|
||||
|
||||
// Save configuration changes
|
||||
saveConfiguration() {
|
||||
if (this.selectedItem) {
|
||||
const formData = this.configForm.value;
|
||||
console.log('Saving configuration:', formData);
|
||||
|
||||
// Update the selected item with form data
|
||||
Object.assign(this.selectedItem, formData);
|
||||
|
||||
// Close the modal
|
||||
this.modeledit = false; // Use modeledit instead of configModalOpen
|
||||
this.selectedItem = null;
|
||||
}
|
||||
}
|
||||
|
||||
// Cancel configuration changes
|
||||
cancelConfiguration() {
|
||||
console.log('Canceling configuration');
|
||||
this.modeledit = false; // Use modeledit instead of configModalOpen
|
||||
this.selectedItem = null;
|
||||
}
|
||||
|
||||
// Get tables from datastore
|
||||
getTables(id: string) {
|
||||
this.alertService.getTablefromstore(parseInt(id, 10)).subscribe(gateway => {
|
||||
console.log(gateway);
|
||||
// Handle table data
|
||||
}, (error) => {
|
||||
console.log(error);
|
||||
});
|
||||
}
|
||||
|
||||
// Get columns from API
|
||||
getColumns(connectionId: string, table: string) {
|
||||
const connId = connectionId ? parseInt(connectionId, 10) : undefined;
|
||||
this.alertService.getColumnfromurl(table, connId).subscribe(data => {
|
||||
console.log('Column data:', data);
|
||||
this.columnData = data;
|
||||
}, (error) => {
|
||||
console.log(error);
|
||||
this.columnData = [];
|
||||
});
|
||||
}
|
||||
|
||||
// Refresh drilldown columns
|
||||
refreshDrilldownColumns() {
|
||||
if (this.selectedItem && this.selectedItem.drilldownApiUrl) {
|
||||
const connId = this.selectedItem.connection ? parseInt(this.selectedItem.connection, 10) : undefined;
|
||||
this.alertService.getColumnfromurl(this.selectedItem.drilldownApiUrl, connId).subscribe(data => {
|
||||
console.log('Drilldown column data:', data);
|
||||
this.drilldownColumnData = data;
|
||||
}, (error) => {
|
||||
console.log('Error fetching drilldown columns:', error);
|
||||
this.drilldownColumnData = [];
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// Add base filter
|
||||
addBaseFilter() {
|
||||
if (this.selectedItem) {
|
||||
if (!this.selectedItem.baseFilters) {
|
||||
this.selectedItem.baseFilters = [];
|
||||
}
|
||||
this.selectedItem.baseFilters.push({ field: '', value: '' });
|
||||
}
|
||||
}
|
||||
|
||||
// Remove base filter
|
||||
removeBaseFilter(index: number) {
|
||||
if (this.selectedItem && this.selectedItem.baseFilters) {
|
||||
this.selectedItem.baseFilters.splice(index, 1);
|
||||
}
|
||||
}
|
||||
|
||||
// Add drilldown filter
|
||||
addDrilldownFilter() {
|
||||
if (this.selectedItem) {
|
||||
if (!this.selectedItem.drilldownFilters) {
|
||||
this.selectedItem.drilldownFilters = [];
|
||||
}
|
||||
this.selectedItem.drilldownFilters.push({ field: '', value: '' });
|
||||
}
|
||||
}
|
||||
|
||||
// Remove drilldown filter
|
||||
removeDrilldownFilter(index: number) {
|
||||
if (this.selectedItem && this.selectedItem.drilldownFilters) {
|
||||
this.selectedItem.drilldownFilters.splice(index, 1);
|
||||
}
|
||||
}
|
||||
|
||||
// Add drilldown layer
|
||||
addDrilldownLayer() {
|
||||
if (this.selectedItem) {
|
||||
if (!this.selectedItem.drilldownLayers) {
|
||||
this.selectedItem.drilldownLayers = [];
|
||||
}
|
||||
this.selectedItem.drilldownLayers.push({
|
||||
enabled: false,
|
||||
apiUrl: '',
|
||||
xAxis: '',
|
||||
yAxis: '',
|
||||
parameter: '',
|
||||
filters: []
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// Remove drilldown layer
|
||||
removeDrilldownLayer(index: number) {
|
||||
if (this.selectedItem && this.selectedItem.drilldownLayers) {
|
||||
this.selectedItem.drilldownLayers.splice(index, 1);
|
||||
}
|
||||
}
|
||||
|
||||
// Add layer filter
|
||||
addLayerFilter(layerIndex: number) {
|
||||
if (this.selectedItem && this.selectedItem.drilldownLayers) {
|
||||
const layer = this.selectedItem.drilldownLayers[layerIndex];
|
||||
if (layer) {
|
||||
if (!layer.filters) {
|
||||
layer.filters = [];
|
||||
}
|
||||
layer.filters.push({ field: '', value: '' });
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Refresh drilldown columns for a specific layer
|
||||
refreshDrilldownLayerColumns(layerIndex: number) {
|
||||
if (this.selectedItem && this.selectedItem.drilldownLayers && this.selectedItem.drilldownLayers[layerIndex]) {
|
||||
const layer = this.selectedItem.drilldownLayers[layerIndex];
|
||||
if (layer && layer.apiUrl) {
|
||||
const connId = this.selectedItem.connection ? parseInt(this.selectedItem.connection, 10) : undefined;
|
||||
this.alertService.getColumnfromurl(layer.apiUrl, connId).subscribe(data => {
|
||||
console.log(`Drilldown layer ${layerIndex} column data:`, data);
|
||||
// Store layer column data in the layerColumnData property
|
||||
this.layerColumnData[layerIndex] = data;
|
||||
}, (error) => {
|
||||
console.log(`Error fetching drilldown layer ${layerIndex} columns:`, error);
|
||||
this.layerColumnData[layerIndex] = [];
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Remove layer filter
|
||||
removeLayerFilter(layerIndex: number, filterIndex: number) {
|
||||
if (this.selectedItem && this.selectedItem.drilldownLayers) {
|
||||
const layer = this.selectedItem.drilldownLayers[layerIndex];
|
||||
if (layer && layer.filters) {
|
||||
layer.filters.splice(filterIndex, 1);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,223 @@
|
||||
import { Component, OnInit } from '@angular/core';
|
||||
import { GridsterConfig, GridsterItem } from 'angular-gridster2';
|
||||
|
||||
interface ShieldDashboardItem extends GridsterItem {
|
||||
chartType: string;
|
||||
name: string;
|
||||
id: number;
|
||||
component?: any;
|
||||
}
|
||||
|
||||
@Component({
|
||||
selector: 'app-shield-dashboard',
|
||||
templateUrl: './shield-dashboard.component.html',
|
||||
styleUrls: ['./shield-dashboard.component.scss']
|
||||
})
|
||||
export class ShieldDashboardComponent implements OnInit {
|
||||
options: GridsterConfig;
|
||||
dashboard: Array<ShieldDashboardItem>;
|
||||
|
||||
// Keep track of deleted items
|
||||
deletedItems: Array<ShieldDashboardItem> = [];
|
||||
|
||||
constructor() { }
|
||||
|
||||
ngOnInit(): void {
|
||||
this.options = {
|
||||
gridType: 'fit',
|
||||
enableEmptyCellDrop: true,
|
||||
emptyCellDropCallback: this.onDrop,
|
||||
pushItems: true,
|
||||
swap: true,
|
||||
pushDirections: { north: true, east: true, south: true, west: true },
|
||||
resizable: { enabled: true },
|
||||
itemChangeCallback: this.itemChange.bind(this),
|
||||
draggable: {
|
||||
enabled: true,
|
||||
ignoreContent: true,
|
||||
dropOverItems: true,
|
||||
dragHandleClass: 'drag-handler',
|
||||
ignoreContentClass: 'no-drag',
|
||||
},
|
||||
displayGrid: 'always',
|
||||
minCols: 10,
|
||||
minRows: 10,
|
||||
itemResizeCallback: this.itemResize.bind(this)
|
||||
};
|
||||
|
||||
// Initialize the dashboard with empty canvas
|
||||
this.dashboard = [];
|
||||
}
|
||||
|
||||
onDrop = (event: any) => {
|
||||
// Handle dropping new components onto the dashboard
|
||||
console.log('Item dropped:', event);
|
||||
|
||||
// Get the component identifier from the drag event
|
||||
const componentType = event.dataTransfer ? event.dataTransfer.getData('widgetIdentifier') : '';
|
||||
console.log('Component type dropped:', componentType);
|
||||
|
||||
if (componentType) {
|
||||
this.addComponentToDashboard(componentType);
|
||||
} else {
|
||||
console.log('No component type found in drag data');
|
||||
}
|
||||
}
|
||||
|
||||
// Add a new component to the dashboard
|
||||
addComponentToDashboard(componentType: string) {
|
||||
// Generate a new ID for the component
|
||||
const newId = this.dashboard.length > 0 ? Math.max(...this.dashboard.map(item => item.id), 0) + 1 : 1;
|
||||
|
||||
let newItem: ShieldDashboardItem;
|
||||
|
||||
switch (componentType) {
|
||||
case "bar_chart":
|
||||
newItem = {
|
||||
cols: 5,
|
||||
rows: 6,
|
||||
y: 0,
|
||||
x: 0,
|
||||
chartType: 'bar-chart',
|
||||
name: 'Bar Chart',
|
||||
id: newId
|
||||
};
|
||||
break;
|
||||
case "doughnut_chart":
|
||||
// For doughnut charts, we'll need to determine which one based on existing items
|
||||
const donutCount = this.dashboard.filter(item => item.chartType === 'donut-chart').length;
|
||||
if (donutCount % 2 === 0) {
|
||||
newItem = {
|
||||
cols: 5,
|
||||
rows: 6,
|
||||
y: 0,
|
||||
x: 0,
|
||||
chartType: 'donut-chart',
|
||||
name: 'End Customer Donut',
|
||||
id: newId
|
||||
};
|
||||
} else {
|
||||
newItem = {
|
||||
cols: 5,
|
||||
rows: 6,
|
||||
y: 0,
|
||||
x: 0,
|
||||
chartType: 'donut-chart',
|
||||
name: 'Segment Penetration Donut',
|
||||
id: newId
|
||||
};
|
||||
}
|
||||
break;
|
||||
case "map_chart":
|
||||
newItem = {
|
||||
cols: 5,
|
||||
rows: 6,
|
||||
y: 0,
|
||||
x: 0,
|
||||
chartType: 'map-chart',
|
||||
name: 'Map Chart',
|
||||
id: newId
|
||||
};
|
||||
break;
|
||||
case "grid_view":
|
||||
newItem = {
|
||||
cols: 10,
|
||||
rows: 6,
|
||||
y: 0,
|
||||
x: 0,
|
||||
chartType: 'data-table',
|
||||
name: 'Data Table',
|
||||
id: newId
|
||||
};
|
||||
break;
|
||||
case "to_do_chart":
|
||||
newItem = {
|
||||
cols: 5,
|
||||
rows: 6,
|
||||
y: 0,
|
||||
x: 0,
|
||||
chartType: 'deal-details',
|
||||
name: 'Deal Details',
|
||||
id: newId
|
||||
};
|
||||
break;
|
||||
case "line_chart":
|
||||
newItem = {
|
||||
cols: 5,
|
||||
rows: 6,
|
||||
y: 0,
|
||||
x: 0,
|
||||
chartType: 'quarterwise-flow',
|
||||
name: 'Quarterwise Flow',
|
||||
id: newId
|
||||
};
|
||||
break;
|
||||
default:
|
||||
newItem = {
|
||||
cols: 5,
|
||||
rows: 6,
|
||||
y: 0,
|
||||
x: 0,
|
||||
chartType: componentType,
|
||||
name: componentType,
|
||||
id: newId
|
||||
};
|
||||
}
|
||||
|
||||
// Add the new item to the dashboard
|
||||
this.dashboard.push(newItem);
|
||||
}
|
||||
|
||||
removeItem(item: ShieldDashboardItem) {
|
||||
// Add the item to deleted items list before removing
|
||||
this.deletedItems.push({...item});
|
||||
|
||||
// Remove the item from the dashboard
|
||||
this.dashboard.splice(this.dashboard.indexOf(item), 1);
|
||||
}
|
||||
|
||||
// Restore a deleted item
|
||||
restoreItem(item: ShieldDashboardItem) {
|
||||
// Remove from deleted items
|
||||
this.deletedItems.splice(this.deletedItems.indexOf(item), 1);
|
||||
|
||||
// Add back to dashboard
|
||||
this.dashboard.push(item);
|
||||
}
|
||||
|
||||
// Clear all deleted items
|
||||
clearDeletedItems() {
|
||||
this.deletedItems = [];
|
||||
}
|
||||
|
||||
itemChange() {
|
||||
console.log('Item changed:', this.dashboard);
|
||||
}
|
||||
|
||||
itemResize(item: any, itemComponent: any) {
|
||||
console.log('Item resized:', item);
|
||||
// Trigger a window resize event to notify charts to resize
|
||||
window.dispatchEvent(new Event('resize'));
|
||||
}
|
||||
|
||||
/**
|
||||
* Extract only the relevant chart configuration properties to pass to chart components
|
||||
* This prevents errors when trying to set properties that don't exist on the components
|
||||
*/
|
||||
getChartInputs(item: any): any {
|
||||
// Only pass properties that are relevant to chart components
|
||||
const chartInputs = {
|
||||
chartType: item.chartType,
|
||||
name: item.name
|
||||
};
|
||||
|
||||
// Remove undefined properties to avoid passing unnecessary data
|
||||
Object.keys(chartInputs).forEach(key => {
|
||||
if (chartInputs[key] === undefined) {
|
||||
delete chartInputs[key];
|
||||
}
|
||||
});
|
||||
|
||||
return chartInputs;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,44 @@
|
||||
import { NgModule, CUSTOM_ELEMENTS_SCHEMA, NO_ERRORS_SCHEMA } from '@angular/core';
|
||||
import { CommonModule } from '@angular/common';
|
||||
import { FormsModule, ReactiveFormsModule } from '@angular/forms';
|
||||
import { ClarityModule } from '@clr/angular';
|
||||
import { GridsterModule } from 'angular-gridster2';
|
||||
import { NgChartsModule } from 'ng2-charts';
|
||||
import { DynamicModule } from 'ng-dynamic-component';
|
||||
|
||||
import { ShieldDashboardRoutingModule } from './shield-dashboard-routing.module';
|
||||
import { ShieldDashboardComponent } from './shield-dashboard.component';
|
||||
import { SidebarFiltersComponent } from './components/sidebar-filters/sidebar-filters.component';
|
||||
import { BarChartComponent } from './components/bar-chart/bar-chart.component';
|
||||
import { DonutChartComponent } from './components/donut-chart/donut-chart.component';
|
||||
import { MapChartComponent } from './components/map-chart/map-chart.component';
|
||||
import { DataTableComponent } from './components/data-table/data-table.component';
|
||||
import { DealDetailsCardComponent } from './components/deal-details-card/deal-details-card.component';
|
||||
import { QuarterwiseFlowComponent } from './components/quarterwise-flow/quarterwise-flow.component';
|
||||
import { LoadingShimmerComponent } from './components/loading-shimmer/loading-shimmer.component';
|
||||
|
||||
@NgModule({
|
||||
declarations: [
|
||||
ShieldDashboardComponent,
|
||||
SidebarFiltersComponent,
|
||||
BarChartComponent,
|
||||
DonutChartComponent,
|
||||
MapChartComponent,
|
||||
DataTableComponent,
|
||||
DealDetailsCardComponent,
|
||||
QuarterwiseFlowComponent,
|
||||
LoadingShimmerComponent
|
||||
],
|
||||
imports: [
|
||||
CommonModule,
|
||||
FormsModule,
|
||||
ReactiveFormsModule,
|
||||
ClarityModule,
|
||||
GridsterModule,
|
||||
NgChartsModule,
|
||||
DynamicModule,
|
||||
ShieldDashboardRoutingModule
|
||||
],
|
||||
schemas: [CUSTOM_ELEMENTS_SCHEMA, NO_ERRORS_SCHEMA]
|
||||
})
|
||||
export class ShieldDashboardModule { }
|
||||
@@ -1,27 +1,307 @@
|
||||
<table class="table">
|
||||
<thead>
|
||||
<th class="c-col">#</th>
|
||||
<th>Item</th>
|
||||
<th></th>
|
||||
</thead>
|
||||
<tr class="ui basic segment" *ngFor="let todo of todoList; let i = index">
|
||||
<td class="c-col">{{i + 1}}</td>
|
||||
<td>{{todo}}</td>
|
||||
<td style="text-align:right">
|
||||
<a routerLink="." (click)="removeTodo(i)">
|
||||
<clr-icon shape="times"></clr-icon>
|
||||
</a>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td></td>
|
||||
<td>
|
||||
<input [(ngModel)]="todo" placeholder="Add Todo" class="clr-input">
|
||||
</td>
|
||||
<td style="text-align:right">
|
||||
<a routerLink="." color='primary' (click)="addTodo(todo)">
|
||||
<clr-icon shape="plus"></clr-icon>
|
||||
</a>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
<div class="to-do-chart-container">
|
||||
<!-- Filter Controls Section -->
|
||||
<div class="filter-section" *ngIf="hasActiveFilters()">
|
||||
<!-- Base Filters -->
|
||||
<div class="filter-group" *ngIf="baseFilters && baseFilters.length > 0">
|
||||
<h4>Base Filters</h4>
|
||||
<div class="filter-controls">
|
||||
<div *ngFor="let filter of baseFilters" class="filter-item">
|
||||
<div class="filter-label">{{ filter.field }} ({{ filter.type || 'text' }})</div>
|
||||
|
||||
<!-- Text Filter -->
|
||||
<div *ngIf="!filter.type || filter.type === 'text'" class="filter-input">
|
||||
<input type="text"
|
||||
[(ngModel)]="filter.value"
|
||||
(ngModelChange)="onBaseFilterChange(filter)"
|
||||
[placeholder]="filter.field"
|
||||
class="clr-input filter-text-input">
|
||||
</div>
|
||||
|
||||
<!-- Dropdown Filter -->
|
||||
<div *ngIf="filter.type === 'dropdown'" class="filter-input">
|
||||
<select [(ngModel)]="filter.value"
|
||||
(ngModelChange)="onBaseFilterChange(filter)"
|
||||
class="clr-select filter-select">
|
||||
<option value="">Select {{ filter.field }}</option>
|
||||
<option *ngFor="let option of getFilterOptions(filter)" [value]="option">{{ option }}</option>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<!-- Multi-Select Filter - Updated to show key first, then dropdown on click -->
|
||||
<div *ngIf="filter.type === 'multiselect'" class="filter-input multiselect-container">
|
||||
<div class="multiselect-display" (click)="toggleMultiselect(filter, 'base')">
|
||||
<span class="multiselect-label">{{ filter.field }}</span>
|
||||
<span class="multiselect-value" *ngIf="getSelectedOptionsCount(filter) > 0">
|
||||
({{ getSelectedOptionsCount(filter) }} selected)
|
||||
</span>
|
||||
<clr-icon shape="caret down" class="dropdown-icon"></clr-icon>
|
||||
</div>
|
||||
<div class="multiselect-dropdown" *ngIf="isMultiselectOpen(filter, 'base')">
|
||||
<div class="checkbox-group">
|
||||
<div *ngFor="let option of getFilterOptions(filter); let i = index" class="checkbox-item">
|
||||
<input type="checkbox"
|
||||
[checked]="isOptionSelected(filter, option)"
|
||||
(change)="onMultiSelectChange(filter, option, $event)"
|
||||
[id]="'base-' + filter.field + '-' + i"
|
||||
class="clr-checkbox">
|
||||
<label [for]="'base-' + filter.field + '-' + i" class="checkbox-label">{{ option }}</label>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Date Range Filter -->
|
||||
<div *ngIf="filter.type === 'date-range'" class="filter-input date-range">
|
||||
<div class="date-input-group">
|
||||
<input type="date"
|
||||
[(ngModel)]="filter.value.start"
|
||||
(ngModelChange)="onDateRangeChange(filter, { start: $event, end: filter.value.end })"
|
||||
placeholder="Start Date"
|
||||
class="clr-input filter-date">
|
||||
<span class="date-separator">to</span>
|
||||
<input type="date"
|
||||
[(ngModel)]="filter.value.end"
|
||||
(ngModelChange)="onDateRangeChange(filter, { start: filter.value.start, end: $event })"
|
||||
placeholder="End Date"
|
||||
class="clr-input filter-date">
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Toggle Filter -->
|
||||
<div *ngIf="filter.type === 'toggle'" class="filter-input toggle">
|
||||
<input type="checkbox"
|
||||
[(ngModel)]="filter.value"
|
||||
(ngModelChange)="onToggleChange(filter, $event)"
|
||||
clrToggle
|
||||
class="clr-toggle">
|
||||
<label class="toggle-label">{{ filter.field }}</label>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Drilldown Filters -->
|
||||
<div class="filter-group" *ngIf="drilldownFilters && drilldownFilters.length > 0 && currentDrilldownLevel > 0">
|
||||
<h4>Drilldown Filters</h4>
|
||||
<div class="filter-controls">
|
||||
<div *ngFor="let filter of drilldownFilters" class="filter-item">
|
||||
<div class="filter-label">{{ filter.field }} ({{ filter.type || 'text' }})</div>
|
||||
|
||||
<!-- Text Filter -->
|
||||
<div *ngIf="!filter.type || filter.type === 'text'" class="filter-input">
|
||||
<input type="text"
|
||||
[(ngModel)]="filter.value"
|
||||
(ngModelChange)="onDrilldownFilterChange(filter)"
|
||||
[placeholder]="filter.field"
|
||||
class="clr-input filter-text-input">
|
||||
</div>
|
||||
|
||||
<!-- Dropdown Filter -->
|
||||
<div *ngIf="filter.type === 'dropdown'" class="filter-input">
|
||||
<select [(ngModel)]="filter.value"
|
||||
(ngModelChange)="onDrilldownFilterChange(filter)"
|
||||
class="clr-select filter-select">
|
||||
<option value="">Select {{ filter.field }}</option>
|
||||
<option *ngFor="let option of getFilterOptions(filter)" [value]="option">{{ option }}</option>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<!-- Multi-Select Filter - Updated to show key first, then dropdown on click -->
|
||||
<div *ngIf="filter.type === 'multiselect'" class="filter-input multiselect-container">
|
||||
<div class="multiselect-display" (click)="toggleMultiselect(filter, 'drilldown')">
|
||||
<span class="multiselect-label">{{ filter.field }}</span>
|
||||
<span class="multiselect-value" *ngIf="getSelectedOptionsCount(filter) > 0">
|
||||
({{ getSelectedOptionsCount(filter) }} selected)
|
||||
</span>
|
||||
<clr-icon shape="caret down" class="dropdown-icon"></clr-icon>
|
||||
</div>
|
||||
<div class="multiselect-dropdown" *ngIf="isMultiselectOpen(filter, 'drilldown')">
|
||||
<div class="checkbox-group">
|
||||
<div *ngFor="let option of getFilterOptions(filter); let i = index" class="checkbox-item">
|
||||
<input type="checkbox"
|
||||
[checked]="isOptionSelected(filter, option)"
|
||||
(change)="onMultiSelectChange(filter, option, $event)"
|
||||
[id]="'drilldown-' + filter.field + '-' + i"
|
||||
class="clr-checkbox">
|
||||
<label [for]="'drilldown-' + filter.field + '-' + i" class="checkbox-label">{{ option }}</label>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Date Range Filter -->
|
||||
<div *ngIf="filter.type === 'date-range'" class="filter-input date-range">
|
||||
<div class="date-input-group">
|
||||
<input type="date"
|
||||
[(ngModel)]="filter.value.start"
|
||||
(ngModelChange)="onDateRangeChange(filter, { start: $event, end: filter.value.end })"
|
||||
placeholder="Start Date"
|
||||
class="clr-input filter-date">
|
||||
<span class="date-separator">to</span>
|
||||
<input type="date"
|
||||
[(ngModel)]="filter.value.end"
|
||||
(ngModelChange)="onDateRangeChange(filter, { start: filter.value.start, end: $event })"
|
||||
placeholder="End Date"
|
||||
class="clr-input filter-date">
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Toggle Filter -->
|
||||
<div *ngIf="filter.type === 'toggle'" class="filter-input toggle">
|
||||
<input type="checkbox"
|
||||
[(ngModel)]="filter.value"
|
||||
(ngModelChange)="onToggleChange(filter, $event)"
|
||||
clrToggle
|
||||
class="clr-toggle">
|
||||
<label class="toggle-label">{{ filter.field }}</label>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Layer Filters -->
|
||||
<div class="filter-group" *ngIf="hasActiveLayerFilters()">
|
||||
<h4>Layer Filters</h4>
|
||||
<div class="filter-controls">
|
||||
<div *ngFor="let filter of getActiveLayerFilters()" class="filter-item">
|
||||
<div class="filter-label">{{ filter.field }} ({{ filter.type || 'text' }})</div>
|
||||
|
||||
<!-- Text Filter -->
|
||||
<div *ngIf="!filter.type || filter.type === 'text'" class="filter-input">
|
||||
<input type="text"
|
||||
[(ngModel)]="filter.value"
|
||||
(ngModelChange)="onLayerFilterChange(filter)"
|
||||
[placeholder]="filter.field"
|
||||
class="clr-input filter-text-input">
|
||||
</div>
|
||||
|
||||
<!-- Dropdown Filter -->
|
||||
<div *ngIf="filter.type === 'dropdown'" class="filter-input">
|
||||
<select [(ngModel)]="filter.value"
|
||||
(ngModelChange)="onLayerFilterChange(filter)"
|
||||
class="clr-select filter-select">
|
||||
<option value="">Select {{ filter.field }}</option>
|
||||
<option *ngFor="let option of getFilterOptions(filter)" [value]="option">{{ option }}</option>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<!-- Multi-Select Filter - Updated to show key first, then dropdown on click -->
|
||||
<div *ngIf="filter.type === 'multiselect'" class="filter-input multiselect-container">
|
||||
<div class="multiselect-display" (click)="toggleMultiselect(filter, 'layer')">
|
||||
<span class="multiselect-label">{{ filter.field }}</span>
|
||||
<span class="multiselect-value" *ngIf="getSelectedOptionsCount(filter) > 0">
|
||||
({{ getSelectedOptionsCount(filter) }} selected)
|
||||
</span>
|
||||
<clr-icon shape="caret down" class="dropdown-icon"></clr-icon>
|
||||
</div>
|
||||
<div class="multiselect-dropdown" *ngIf="isMultiselectOpen(filter, 'layer')">
|
||||
<div class="checkbox-group">
|
||||
<div *ngFor="let option of getFilterOptions(filter); let i = index" class="checkbox-item">
|
||||
<input type="checkbox"
|
||||
[checked]="isOptionSelected(filter, option)"
|
||||
(change)="onMultiSelectChange(filter, option, $event)"
|
||||
[id]="'layer-' + filter.field + '-' + i"
|
||||
class="clr-checkbox">
|
||||
<label [for]="'layer-' + filter.field + '-' + i" class="checkbox-label">{{ option }}</label>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Date Range Filter -->
|
||||
<div *ngIf="filter.type === 'date-range'" class="filter-input date-range">
|
||||
<div class="date-input-group">
|
||||
<input type="date"
|
||||
[(ngModel)]="filter.value.start"
|
||||
(ngModelChange)="onDateRangeChange(filter, { start: $event, end: filter.value.end })"
|
||||
placeholder="Start Date"
|
||||
class="clr-input filter-date">
|
||||
<span class="date-separator">to</span>
|
||||
<input type="date"
|
||||
[(ngModel)]="filter.value.end"
|
||||
(ngModelChange)="onDateRangeChange(filter, { start: filter.value.start, end: $event })"
|
||||
placeholder="End Date"
|
||||
class="clr-input filter-date">
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Toggle Filter -->
|
||||
<div *ngIf="filter.type === 'toggle'" class="filter-input toggle">
|
||||
<input type="checkbox"
|
||||
[(ngModel)]="filter.value"
|
||||
(ngModelChange)="onToggleChange(filter, $event)"
|
||||
clrToggle
|
||||
class="clr-toggle">
|
||||
<label class="toggle-label">{{ filter.field }}</label>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Clear Filters Button -->
|
||||
<div class="filter-actions">
|
||||
<button class="btn btn-sm btn-outline" (click)="clearAllFilters()">Clear All Filters</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Header row with chart title and drilldown navigation -->
|
||||
<div class="clr-row header-row">
|
||||
<div class="clr-col-6">
|
||||
<h3 class="chart-title">{{charttitle || 'To Do Chart'}}</h3>
|
||||
</div>
|
||||
<div class="clr-col-6" style="text-align: right;">
|
||||
<!-- Add drilldown navigation controls -->
|
||||
<button class="btn btn-sm btn-link" *ngIf="drilldownEnabled && drilldownStack.length > 0" (click)="navigateBack()">
|
||||
<cds-icon shape="arrow" direction="left"></cds-icon>
|
||||
Back to {{drilldownStack.length > 1 ? 'Previous Level' : 'Main Data'}}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Show current drilldown level -->
|
||||
<div class="clr-row" *ngIf="drilldownEnabled && drilldownStack.length > 0">
|
||||
<div class="clr-col-12">
|
||||
<div class="alert alert-info" style="padding: 8px 12px; margin-bottom: 12px;">
|
||||
<div class="alert-items">
|
||||
<div class="alert-item static">
|
||||
<div class="alert-icon-wrapper">
|
||||
<cds-icon class="alert-icon" shape="info-circle"></cds-icon>
|
||||
</div>
|
||||
<span class="alert-text">
|
||||
Drilldown Level: {{currentDrilldownLevel}}
|
||||
<span *ngIf="drilldownStack.length > 0">
|
||||
(Clicked on: {{drilldownStack[drilldownStack.length - 1].clickedLabel}})
|
||||
</span>
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Existing content -->
|
||||
<div class="todo-table-container">
|
||||
<table class="table todo-table">
|
||||
<thead>
|
||||
<th class="c-col">#</th>
|
||||
<th>Item</th>
|
||||
<th></th>
|
||||
</thead>
|
||||
<tr class="ui basic segment" *ngFor="let todo of todoList; let i = index">
|
||||
<td class="c-col">{{i + 1}}</td>
|
||||
<td>{{todo}}</td>
|
||||
<td style="text-align:right">
|
||||
<a (click)="removeTodo(i)" class="remove-button">
|
||||
<clr-icon shape="times"></clr-icon>
|
||||
</a>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
</div>
|
||||
<div class="add-todo-section">
|
||||
<input [(ngModel)]="todo" (keyup.enter)="addTodo(todo)" placeholder="Add Todo" class="clr-input todo-input">
|
||||
<button (click)="addTodo(todo)" class="btn btn-primary add-button">
|
||||
<clr-icon shape="plus"></clr-icon> Add
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
@@ -0,0 +1,289 @@
|
||||
// To Do Chart specific styles
|
||||
.to-do-chart-container {
|
||||
padding: 20px;
|
||||
height: 100%;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.todo-table-container {
|
||||
flex: 1;
|
||||
overflow-y: auto;
|
||||
max-height: 400px;
|
||||
border: 1px solid #ddd;
|
||||
border-radius: 4px;
|
||||
margin-bottom: 20px; // Add margin at the bottom for spacing
|
||||
|
||||
.todo-table {
|
||||
width: 100%;
|
||||
border-collapse: collapse;
|
||||
margin: 0;
|
||||
|
||||
th, td {
|
||||
padding: 12px;
|
||||
text-align: left;
|
||||
border-bottom: 1px solid #ddd;
|
||||
}
|
||||
|
||||
th {
|
||||
background-color: #f2f2f2;
|
||||
font-weight: bold;
|
||||
position: sticky;
|
||||
top: 0;
|
||||
z-index: 10;
|
||||
}
|
||||
|
||||
tr:hover {
|
||||
background-color: #f5f5f5;
|
||||
}
|
||||
|
||||
.c-col {
|
||||
width: 50px;
|
||||
}
|
||||
|
||||
// Add padding at the bottom of the table body
|
||||
tbody {
|
||||
tr:last-child td {
|
||||
padding-bottom: 20px; // Extra padding for the last row
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.add-todo-section {
|
||||
display: flex;
|
||||
gap: 10px;
|
||||
margin-top: 15px;
|
||||
padding: 20px; // Increased padding all around
|
||||
border-top: 1px solid #eee;
|
||||
border: 1px solid #ddd;
|
||||
border-radius: 4px;
|
||||
background-color: #f9f9f9;
|
||||
|
||||
.todo-input {
|
||||
flex: 1;
|
||||
padding: 12px; // Increased padding for better touch targets
|
||||
border: 1px solid #ccc;
|
||||
border-radius: 4px;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.add-button {
|
||||
white-space: nowrap;
|
||||
padding: 12px 20px; // Increased padding for better touch targets
|
||||
}
|
||||
}
|
||||
|
||||
.remove-button {
|
||||
background: none;
|
||||
border: none;
|
||||
cursor: pointer;
|
||||
padding: 8px; // Increased padding for better touch targets
|
||||
border-radius: 3px;
|
||||
color: #dc3545;
|
||||
|
||||
&:hover {
|
||||
background-color: #e0e0e0;
|
||||
}
|
||||
}
|
||||
|
||||
.filter-section {
|
||||
margin-bottom: 20px;
|
||||
padding: 15px;
|
||||
border: 1px solid #ddd;
|
||||
border-radius: 4px;
|
||||
background-color: #f9f9f9;
|
||||
}
|
||||
|
||||
.filter-group {
|
||||
margin-bottom: 15px;
|
||||
|
||||
h4 {
|
||||
margin-top: 0;
|
||||
margin-bottom: 10px;
|
||||
color: #333;
|
||||
font-weight: 600;
|
||||
}
|
||||
}
|
||||
|
||||
.filter-controls {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 15px;
|
||||
}
|
||||
|
||||
.filter-item {
|
||||
flex: 1 1 300px;
|
||||
min-width: 250px;
|
||||
padding: 10px;
|
||||
background: white;
|
||||
border: 1px solid #e0e0e0;
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
.filter-label {
|
||||
font-weight: 500;
|
||||
margin-bottom: 8px;
|
||||
color: #555;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.filter-input {
|
||||
width: 100%;
|
||||
|
||||
.filter-text-input,
|
||||
.filter-select,
|
||||
.filter-date {
|
||||
width: 100%;
|
||||
padding: 6px 12px;
|
||||
border: 1px solid #ccc;
|
||||
border-radius: 4px;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.filter-select {
|
||||
height: 34px;
|
||||
}
|
||||
}
|
||||
|
||||
.multiselect-container {
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.multiselect-display {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
padding: 6px 12px;
|
||||
border: 1px solid #ccc;
|
||||
border-radius: 4px;
|
||||
background: white;
|
||||
cursor: pointer;
|
||||
min-height: 34px;
|
||||
|
||||
.multiselect-label {
|
||||
flex: 1;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.multiselect-value {
|
||||
color: #666;
|
||||
font-size: 12px;
|
||||
margin-right: 8px;
|
||||
}
|
||||
|
||||
.dropdown-icon {
|
||||
flex-shrink: 0;
|
||||
transition: transform 0.2s ease;
|
||||
}
|
||||
|
||||
&:hover {
|
||||
border-color: #999;
|
||||
}
|
||||
}
|
||||
|
||||
.multiselect-dropdown {
|
||||
position: absolute;
|
||||
top: 100%;
|
||||
left: 0;
|
||||
right: 0;
|
||||
z-index: 1000;
|
||||
background: white;
|
||||
border: 1px solid #ccc;
|
||||
border-top: none;
|
||||
border-radius: 0 0 4px 4px;
|
||||
box-shadow: 0 2px 4px rgba(0,0,0,0.1);
|
||||
max-height: 200px;
|
||||
overflow-y: auto;
|
||||
|
||||
.checkbox-group {
|
||||
padding: 8px;
|
||||
|
||||
.checkbox-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
padding: 4px 0;
|
||||
|
||||
.checkbox-label {
|
||||
margin: 0;
|
||||
font-size: 14px;
|
||||
cursor: pointer;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.date-range {
|
||||
.date-input-group {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.date-separator {
|
||||
margin: 0 5px;
|
||||
color: #777;
|
||||
}
|
||||
}
|
||||
|
||||
.toggle {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
|
||||
.toggle-label {
|
||||
margin: 0;
|
||||
font-size: 14px;
|
||||
cursor: pointer;
|
||||
}
|
||||
}
|
||||
|
||||
.filter-actions {
|
||||
margin-top: 15px;
|
||||
padding-top: 15px;
|
||||
border-top: 1px solid #eee;
|
||||
|
||||
.btn {
|
||||
font-size: 13px;
|
||||
}
|
||||
}
|
||||
|
||||
// New header row styling
|
||||
.header-row {
|
||||
margin-bottom: 15px;
|
||||
padding-bottom: 10px;
|
||||
border-bottom: 1px solid #eee;
|
||||
|
||||
.chart-title {
|
||||
margin: 0;
|
||||
font-size: 18px;
|
||||
font-weight: 600;
|
||||
color: #333;
|
||||
}
|
||||
}
|
||||
|
||||
// Responsive design
|
||||
@media (max-width: 768px) {
|
||||
.filter-controls {
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.filter-item {
|
||||
min-width: 100%;
|
||||
}
|
||||
|
||||
.header-row {
|
||||
.chart-title {
|
||||
font-size: 16px;
|
||||
}
|
||||
}
|
||||
|
||||
.add-todo-section {
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.to-do-chart-container {
|
||||
padding: 15px; // Adjust padding for mobile
|
||||
}
|
||||
}
|
||||
@@ -1,4 +1,7 @@
|
||||
import { Component, OnInit, Input, OnChanges, SimpleChanges } from '@angular/core';
|
||||
import { Dashboard3Service } from 'src/app/services/builder/dashboard3.service';
|
||||
import { FilterService } from '../../common-filter/filter.service';
|
||||
import { Subscription } from 'rxjs';
|
||||
|
||||
@Component({
|
||||
selector: 'app-to-do-chart',
|
||||
@@ -21,15 +24,49 @@ export class ToDoChartComponent implements OnInit, OnChanges {
|
||||
@Input() datasource: string;
|
||||
@Input() fieldName: string;
|
||||
@Input() connection: number; // Add connection input
|
||||
// Drilldown configuration inputs
|
||||
@Input() drilldownEnabled: boolean = false;
|
||||
@Input() drilldownApiUrl: string;
|
||||
@Input() drilldownXAxis: string;
|
||||
@Input() drilldownYAxis: string;
|
||||
@Input() drilldownParameter: string; // Add drilldown parameter input
|
||||
@Input() baseFilters: any[] = []; // Add base filters input
|
||||
@Input() drilldownFilters: any[] = []; // Add drilldown filters input
|
||||
// Multi-layer drilldown configuration inputs
|
||||
@Input() drilldownLayers: any[] = []; // Array of drilldown layer configurations
|
||||
|
||||
constructor() { }
|
||||
// Multi-layer drilldown state tracking
|
||||
drilldownStack: any[] = []; // Stack to track drilldown navigation history
|
||||
currentDrilldownLevel: number = 0; // Current drilldown level (0 = base level)
|
||||
originalTodoList: string[] = [];
|
||||
|
||||
constructor(
|
||||
private dashboardService: Dashboard3Service,
|
||||
private filterService: FilterService
|
||||
) { }
|
||||
|
||||
ngOnInit(): void {
|
||||
// Subscribe to filter changes
|
||||
this.subscriptions.push(
|
||||
this.filterService.filterState$.subscribe(filters => {
|
||||
// When filters change, refresh the chart data
|
||||
this.fetchToDoData();
|
||||
})
|
||||
);
|
||||
|
||||
// Initialize with default data
|
||||
this.fetchToDoData();
|
||||
}
|
||||
|
||||
ngOnChanges(changes: SimpleChanges): void {
|
||||
console.log('ToDoChartComponent input changes:', changes);
|
||||
|
||||
// Initialize filter values if they haven't been initialized yet
|
||||
if (!this.filtersInitialized && (changes.baseFilters || changes.drilldownFilters || changes.drilldownLayers)) {
|
||||
this.initializeFilterValues();
|
||||
this.filtersInitialized = true;
|
||||
}
|
||||
|
||||
// Check if any of the key properties have changed
|
||||
const xAxisChanged = changes.xAxis && !changes.xAxis.firstChange;
|
||||
const yAxisChanged = changes.yAxis && !changes.yAxis.firstChange;
|
||||
@@ -46,26 +83,119 @@ export class ToDoChartComponent implements OnInit, OnChanges {
|
||||
|
||||
data: any;
|
||||
todo: string;
|
||||
todoList = ['todo 1'];
|
||||
todoList: string[] = [];
|
||||
|
||||
// Add properties for filter functionality
|
||||
private openMultiselects: Map<string, string> = new Map(); // Map of filterId -> context
|
||||
private documentClickHandler: ((event: MouseEvent) => void) | null = null;
|
||||
private filtersInitialized: boolean = false;
|
||||
|
||||
// Subscriptions to unsubscribe on destroy
|
||||
private subscriptions: Subscription[] = [];
|
||||
|
||||
fetchToDoData(): void {
|
||||
// If we have the necessary data, fetch to-do data from the service
|
||||
if (this.table) {
|
||||
console.log('Fetching to-do data for:', { table: this.table });
|
||||
if (this.table && this.xAxis) {
|
||||
console.log('Fetching to-do data for:', { table: this.table, xAxis: this.xAxis, connection: this.connection });
|
||||
|
||||
// For to-do chart, we might want to fetch data differently
|
||||
// This is a placeholder implementation - you may need to adjust based on your API
|
||||
console.log('To-do chart would fetch data from table:', this.table);
|
||||
// Convert baseFilters to filter parameters
|
||||
let filterParams = '';
|
||||
if (this.baseFilters && this.baseFilters.length > 0) {
|
||||
const filterObj = {};
|
||||
this.baseFilters.forEach(filter => {
|
||||
if (filter.field && filter.value) {
|
||||
filterObj[filter.field] = filter.value;
|
||||
}
|
||||
});
|
||||
if (Object.keys(filterObj).length > 0) {
|
||||
filterParams = JSON.stringify(filterObj);
|
||||
}
|
||||
}
|
||||
|
||||
// In a real implementation, you would connect to your service here
|
||||
// For now, we'll just keep the default to-do list
|
||||
// Add common filters to filter parameters
|
||||
const commonFilters = this.filterService.getFilterValues();
|
||||
console.log('Common filters from service:', commonFilters);
|
||||
|
||||
if (Object.keys(commonFilters).length > 0) {
|
||||
// Merge common filters with base filters
|
||||
const mergedFilterObj = {};
|
||||
|
||||
// Add base filters first
|
||||
if (filterParams) {
|
||||
try {
|
||||
const baseFilterObj = JSON.parse(filterParams);
|
||||
Object.assign(mergedFilterObj, baseFilterObj);
|
||||
} catch (e) {
|
||||
console.warn('Failed to parse base filter parameters:', e);
|
||||
}
|
||||
}
|
||||
|
||||
// Add common filters using the field name as the key, not the filter id
|
||||
Object.keys(commonFilters).forEach(filterId => {
|
||||
const filterValue = commonFilters[filterId];
|
||||
// Find the filter definition to get the field name
|
||||
const filterDef = this.filterService.getFilters().find(f => f.id === filterId);
|
||||
if (filterDef && filterDef.field) {
|
||||
const fieldName = filterDef.field;
|
||||
if (filterValue !== undefined && filterValue !== null && filterValue !== '') {
|
||||
mergedFilterObj[fieldName] = filterValue;
|
||||
}
|
||||
} else {
|
||||
// Fallback to using filterId as field name if no field is defined
|
||||
if (filterValue !== undefined && filterValue !== null && filterValue !== '') {
|
||||
mergedFilterObj[filterId] = filterValue;
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
if (Object.keys(mergedFilterObj).length > 0) {
|
||||
filterParams = JSON.stringify(mergedFilterObj);
|
||||
}
|
||||
}
|
||||
|
||||
console.log('Final filter parameters:', filterParams);
|
||||
|
||||
// Fetch data from the dashboard service
|
||||
this.dashboardService.getChartData(this.table, 'todo', this.xAxis, '', this.connection, '', '', filterParams).subscribe(
|
||||
(data: any) => {
|
||||
console.log('Received to-do chart data:', data);
|
||||
if (data === null) {
|
||||
console.warn('To-do chart API returned null data. Check if the API endpoint is working correctly.');
|
||||
this.todoList = [];
|
||||
return;
|
||||
}
|
||||
|
||||
// Handle the actual data structure returned by the API
|
||||
if (data && data.chartLabels) {
|
||||
// Use chartLabels as the todo items
|
||||
this.todoList = data.chartLabels;
|
||||
} else if (data && data.labels) {
|
||||
// Fallback to labels if chartLabels is not available
|
||||
this.todoList = data.labels;
|
||||
} else {
|
||||
console.warn('To-do chart received data does not have expected structure', data);
|
||||
this.todoList = [];
|
||||
}
|
||||
},
|
||||
(error) => {
|
||||
console.error('Error fetching to-do chart data:', error);
|
||||
this.todoList = [];
|
||||
}
|
||||
);
|
||||
} else {
|
||||
console.log('Missing required data for to-do chart:', { table: this.table });
|
||||
console.log('Missing required data for to-do chart:', { table: this.table, xAxis: this.xAxis });
|
||||
// Initialize with default data if no table is specified
|
||||
if (this.todoList.length === 0) {
|
||||
this.todoList = ['Sample Task 1', 'Sample Task 2', 'Sample Task 3'];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public addTodo(todo: string) {
|
||||
this.todoList.push(todo);
|
||||
if (todo && todo.trim() !== '') {
|
||||
this.todoList.push(todo.trim());
|
||||
this.todo = ''; // Clear the input field
|
||||
}
|
||||
}
|
||||
|
||||
public removeTodo(todoIx: number) {
|
||||
@@ -73,4 +203,353 @@ export class ToDoChartComponent implements OnInit, OnChanges {
|
||||
this.todoList.splice(todoIx, 1);
|
||||
}
|
||||
}
|
||||
|
||||
// Navigate back to previous drilldown level
|
||||
navigateBack(): void {
|
||||
console.log('Navigating back, current stack:', this.drilldownStack);
|
||||
console.log('Current level:', this.currentDrilldownLevel);
|
||||
|
||||
if (this.drilldownStack.length > 0) {
|
||||
// Remove the last entry from the stack
|
||||
const removedEntry = this.drilldownStack.pop();
|
||||
console.log('Removed entry from stack:', removedEntry);
|
||||
|
||||
// Update the current drilldown level
|
||||
this.currentDrilldownLevel = this.drilldownStack.length;
|
||||
console.log('New level after pop:', this.currentDrilldownLevel);
|
||||
console.log('Stack after pop:', this.drilldownStack);
|
||||
|
||||
if (this.drilldownStack.length > 0) {
|
||||
// Fetch data for the previous level
|
||||
console.log('Fetching data for previous level');
|
||||
this.fetchToDoData();
|
||||
} else {
|
||||
// Back to base level
|
||||
console.log('Back to base level, resetting to original data');
|
||||
this.todoList = [...this.originalTodoList];
|
||||
}
|
||||
} else {
|
||||
// Already at base level, reset to original data
|
||||
console.log('Already at base level, resetting to original data');
|
||||
this.todoList = [...this.originalTodoList];
|
||||
}
|
||||
}
|
||||
|
||||
// Initialize filter values with proper default values based on type
|
||||
private initializeFilterValues(): void {
|
||||
console.log('Initializing filter values');
|
||||
|
||||
// Initialize base filters
|
||||
if (this.baseFilters) {
|
||||
this.baseFilters.forEach(filter => {
|
||||
if (filter.value === undefined || filter.value === null) {
|
||||
switch (filter.type) {
|
||||
case 'multiselect':
|
||||
filter.value = [];
|
||||
break;
|
||||
case 'date-range':
|
||||
filter.value = { start: null, end: null };
|
||||
break;
|
||||
case 'toggle':
|
||||
filter.value = false;
|
||||
break;
|
||||
default:
|
||||
filter.value = '';
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// Initialize drilldown filters
|
||||
if (this.drilldownFilters) {
|
||||
this.drilldownFilters.forEach(filter => {
|
||||
if (filter.value === undefined || filter.value === null) {
|
||||
switch (filter.type) {
|
||||
case 'multiselect':
|
||||
filter.value = [];
|
||||
break;
|
||||
case 'date-range':
|
||||
filter.value = { start: null, end: null };
|
||||
break;
|
||||
case 'toggle':
|
||||
filter.value = false;
|
||||
break;
|
||||
default:
|
||||
filter.value = '';
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// Initialize layer filters
|
||||
if (this.drilldownLayers) {
|
||||
this.drilldownLayers.forEach(layer => {
|
||||
if (layer.filters) {
|
||||
layer.filters.forEach((filter: any) => {
|
||||
if (filter.value === undefined || filter.value === null) {
|
||||
switch (filter.type) {
|
||||
case 'multiselect':
|
||||
filter.value = [];
|
||||
break;
|
||||
case 'date-range':
|
||||
filter.value = { start: null, end: null };
|
||||
break;
|
||||
case 'toggle':
|
||||
filter.value = false;
|
||||
break;
|
||||
default:
|
||||
filter.value = '';
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
console.log('Filter values initialized:', {
|
||||
baseFilters: this.baseFilters,
|
||||
drilldownFilters: this.drilldownFilters,
|
||||
drilldownLayers: this.drilldownLayers
|
||||
});
|
||||
}
|
||||
|
||||
// Check if there are active filters
|
||||
hasActiveFilters(): boolean {
|
||||
return (this.baseFilters && this.baseFilters.length > 0) ||
|
||||
(this.drilldownFilters && this.drilldownFilters.length > 0) ||
|
||||
this.hasActiveLayerFilters();
|
||||
}
|
||||
|
||||
// Check if there are active layer filters for current drilldown level
|
||||
hasActiveLayerFilters(): boolean {
|
||||
if (this.currentDrilldownLevel > 1 && this.drilldownLayers && this.drilldownLayers.length > 0) {
|
||||
const layerIndex = this.currentDrilldownLevel - 2; // -2 because level 1 is base drilldown
|
||||
return layerIndex < this.drilldownLayers.length &&
|
||||
this.drilldownLayers[layerIndex].filters &&
|
||||
this.drilldownLayers[layerIndex].filters.length > 0;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
// Get active layer filters for current drilldown level
|
||||
getActiveLayerFilters(): any[] {
|
||||
if (this.currentDrilldownLevel > 1 && this.drilldownLayers && this.drilldownLayers.length > 0) {
|
||||
const layerIndex = this.currentDrilldownLevel - 2; // -2 because level 1 is base drilldown
|
||||
if (layerIndex < this.drilldownLayers.length &&
|
||||
this.drilldownLayers[layerIndex].filters) {
|
||||
return this.drilldownLayers[layerIndex].filters;
|
||||
}
|
||||
}
|
||||
return [];
|
||||
}
|
||||
|
||||
// Get filter options for dropdown/multiselect filters
|
||||
getFilterOptions(filter: any): string[] {
|
||||
if (filter.options) {
|
||||
if (Array.isArray(filter.options)) {
|
||||
return filter.options;
|
||||
} else if (typeof filter.options === 'string') {
|
||||
return filter.options.split(',').map(opt => opt.trim()).filter(opt => opt);
|
||||
}
|
||||
}
|
||||
return [];
|
||||
}
|
||||
|
||||
// Check if an option is selected for multiselect filters
|
||||
isOptionSelected(filter: any, option: string): boolean {
|
||||
if (!filter.value) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (Array.isArray(filter.value)) {
|
||||
return filter.value.includes(option);
|
||||
}
|
||||
|
||||
return filter.value === option;
|
||||
}
|
||||
|
||||
// Handle base filter changes
|
||||
onBaseFilterChange(filter: any): void {
|
||||
console.log('Base filter changed:', filter);
|
||||
// Refresh data when filter changes
|
||||
this.fetchToDoData();
|
||||
}
|
||||
|
||||
// Handle drilldown filter changes
|
||||
onDrilldownFilterChange(filter: any): void {
|
||||
console.log('Drilldown filter changed:', filter);
|
||||
// Refresh data when filter changes
|
||||
this.fetchToDoData();
|
||||
}
|
||||
|
||||
// Handle layer filter changes
|
||||
onLayerFilterChange(filter: any): void {
|
||||
console.log('Layer filter changed:', filter);
|
||||
// Refresh data when filter changes
|
||||
this.fetchToDoData();
|
||||
}
|
||||
|
||||
// Handle multiselect changes
|
||||
onMultiSelectChange(filter: any, option: string, event: any): void {
|
||||
const checked = event.target.checked;
|
||||
|
||||
// Initialize filter.value as array if it's not already
|
||||
if (!Array.isArray(filter.value)) {
|
||||
filter.value = [];
|
||||
}
|
||||
|
||||
if (checked) {
|
||||
// Add option to array if not already present
|
||||
if (!filter.value.includes(option)) {
|
||||
filter.value.push(option);
|
||||
}
|
||||
} else {
|
||||
// Remove option from array
|
||||
filter.value = filter.value.filter((item: string) => item !== option);
|
||||
}
|
||||
|
||||
// Refresh data when filter changes
|
||||
this.fetchToDoData();
|
||||
}
|
||||
|
||||
// Handle date range changes
|
||||
onDateRangeChange(filter: any, dateRange: { start: string | null, end: string | null }): void {
|
||||
filter.value = dateRange;
|
||||
// Refresh data when filter changes
|
||||
this.fetchToDoData();
|
||||
}
|
||||
|
||||
// Handle toggle changes
|
||||
onToggleChange(filter: any, checked: boolean): void {
|
||||
filter.value = checked;
|
||||
// Refresh data when filter changes
|
||||
this.fetchToDoData();
|
||||
}
|
||||
|
||||
// Toggle multiselect dropdown visibility
|
||||
toggleMultiselect(filter: any, context: string): void {
|
||||
const filterId = `${context}-${filter.field}`;
|
||||
if (this.isMultiselectOpen(filter, context)) {
|
||||
this.openMultiselects.delete(filterId);
|
||||
} else {
|
||||
// Close all other multiselects first
|
||||
this.openMultiselects.clear();
|
||||
this.openMultiselects.set(filterId, context);
|
||||
|
||||
// Add document click handler to close dropdown when clicking outside
|
||||
this.addDocumentClickHandler();
|
||||
}
|
||||
}
|
||||
|
||||
// Add document click handler to close dropdowns when clicking outside
|
||||
private addDocumentClickHandler(): void {
|
||||
if (!this.documentClickHandler) {
|
||||
this.documentClickHandler = (event: MouseEvent) => {
|
||||
const target = event.target as HTMLElement;
|
||||
// Check if click is outside any multiselect dropdown
|
||||
if (!target.closest('.multiselect-container')) {
|
||||
this.openMultiselects.clear();
|
||||
this.removeDocumentClickHandler();
|
||||
}
|
||||
};
|
||||
|
||||
// Use setTimeout to ensure the click event that opened the dropdown doesn't immediately close it
|
||||
setTimeout(() => {
|
||||
document.addEventListener('click', this.documentClickHandler!);
|
||||
}, 0);
|
||||
}
|
||||
}
|
||||
|
||||
// Remove document click handler
|
||||
private removeDocumentClickHandler(): void {
|
||||
if (this.documentClickHandler) {
|
||||
document.removeEventListener('click', this.documentClickHandler);
|
||||
this.documentClickHandler = null;
|
||||
}
|
||||
}
|
||||
|
||||
// Check if multiselect dropdown is open
|
||||
isMultiselectOpen(filter: any, context: string): boolean {
|
||||
const filterId = `${context}-${filter.field}`;
|
||||
return this.openMultiselects.has(filterId);
|
||||
}
|
||||
|
||||
// Get count of selected options for a multiselect filter
|
||||
getSelectedOptionsCount(filter: any): number {
|
||||
if (!filter.value) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
if (Array.isArray(filter.value)) {
|
||||
return filter.value.length;
|
||||
}
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
// Clear all filters
|
||||
clearAllFilters(): void {
|
||||
// Clear base filters
|
||||
if (this.baseFilters) {
|
||||
this.baseFilters.forEach(filter => {
|
||||
if (filter.type === 'multiselect') {
|
||||
filter.value = [];
|
||||
} else if (filter.type === 'date-range') {
|
||||
filter.value = { start: null, end: null };
|
||||
} else if (filter.type === 'toggle') {
|
||||
filter.value = false;
|
||||
} else {
|
||||
filter.value = '';
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// Clear drilldown filters
|
||||
if (this.drilldownFilters) {
|
||||
this.drilldownFilters.forEach(filter => {
|
||||
if (filter.type === 'multiselect') {
|
||||
filter.value = [];
|
||||
} else if (filter.type === 'date-range') {
|
||||
filter.value = { start: null, end: null };
|
||||
} else if (filter.type === 'toggle') {
|
||||
filter.value = false;
|
||||
} else {
|
||||
filter.value = '';
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// Clear layer filters
|
||||
if (this.drilldownLayers) {
|
||||
this.drilldownLayers.forEach(layer => {
|
||||
if (layer.filters) {
|
||||
layer.filters.forEach((filter: any) => {
|
||||
if (filter.type === 'multiselect') {
|
||||
filter.value = [];
|
||||
} else if (filter.type === 'date-range') {
|
||||
filter.value = { start: null, end: null };
|
||||
} else if (filter.type === 'toggle') {
|
||||
filter.value = false;
|
||||
} else {
|
||||
filter.value = '';
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// Close all multiselect dropdowns
|
||||
this.openMultiselects.clear();
|
||||
|
||||
// Refresh data
|
||||
this.fetchToDoData();
|
||||
}
|
||||
|
||||
ngOnDestroy(): void {
|
||||
// Unsubscribe from all subscriptions to prevent memory leaks
|
||||
this.subscriptions.forEach(subscription => subscription.unsubscribe());
|
||||
|
||||
// Remove document click handler if it exists
|
||||
this.removeDocumentClickHandler();
|
||||
}
|
||||
}
|
||||
@@ -11,6 +11,9 @@
|
||||
<h3>{{ 'all_dashboard' | translate }}</h3>
|
||||
</div>
|
||||
<div class="clr-col-4" style="text-align: right;">
|
||||
<button class="btn btn-success" [routerLink]="['/cns-portal/shield-dashboard']">
|
||||
<clr-icon shape="shield"></clr-icon>Shield Dashboard
|
||||
</button>
|
||||
<button id="add" class="btn btn-primary" (click)="gotoadd()">
|
||||
<clr-icon shape="plus"></clr-icon>{{ 'dashboard_builder' | translate }}
|
||||
</button>
|
||||
@@ -112,6 +115,3 @@
|
||||
</div>
|
||||
</div>
|
||||
</clr-modal>
|
||||
|
||||
|
||||
|
||||
@@ -12,23 +12,23 @@ import { ModulesetupService } from 'src/app/services/builder/modulesetup.service
|
||||
styleUrls: ['./dashrunnerall.component.scss']
|
||||
})
|
||||
export class DashrunnerallComponent implements OnInit {
|
||||
addModall:boolean = false;
|
||||
selected:any[] = [];
|
||||
addModall: boolean = false;
|
||||
selected: any[] = [];
|
||||
loading = false;
|
||||
data:any;
|
||||
id:any;
|
||||
moduleId:any;
|
||||
data: any;
|
||||
id: any;
|
||||
moduleId: any;
|
||||
modalDelete = false;
|
||||
rowSelected :any= {};
|
||||
rowSelected: any = {};
|
||||
rows: any[];
|
||||
projectname;
|
||||
projectId;
|
||||
error;
|
||||
constructor(
|
||||
private router : Router,
|
||||
private route: ActivatedRoute,private dashboardService : Dashboard3Service,
|
||||
private router: Router,
|
||||
private route: ActivatedRoute, private dashboardService: Dashboard3Service,
|
||||
// private wireframeservice : WireframeService,
|
||||
private excel: ExcelService,private mainService: ModulesetupService,
|
||||
private excel: ExcelService, private mainService: ModulesetupService,
|
||||
private toastr: ToastrService,) { }
|
||||
|
||||
ngOnInit(): void {
|
||||
@@ -42,41 +42,46 @@ export class DashrunnerallComponent implements OnInit {
|
||||
// this.getprojectName(this.projectId);
|
||||
}
|
||||
|
||||
getprojectName(id){
|
||||
getprojectName(id) {
|
||||
this.mainService.getProjectModules(id).subscribe((data) => {
|
||||
console.log(data);
|
||||
this.projectname=data.items[0]['projectName'];
|
||||
this.projectname = data.items[0]['projectName'];
|
||||
console.log(this.projectname);
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
getdashboard()
|
||||
{
|
||||
this.dashboardService.getAllDash().subscribe((data) =>{
|
||||
getdashboard() {
|
||||
this.dashboardService.getAllDash().subscribe((data) => {
|
||||
this.data = data;
|
||||
this.rows = this.data;
|
||||
console.log(data);
|
||||
this.error="No data Available";
|
||||
this.error = "No data Available";
|
||||
console.log(this.error);
|
||||
});
|
||||
}
|
||||
|
||||
openModal()
|
||||
{
|
||||
openModal() {
|
||||
this.addModall = true;
|
||||
}
|
||||
gotoadd()
|
||||
{
|
||||
this.router.navigate(['../../dashboardbuilder'],{relativeTo:this.route});
|
||||
gotoadd() {
|
||||
this.router.navigate(['../../dashboardbuilder'], { relativeTo: this.route });
|
||||
}
|
||||
goToEdit(id:number)
|
||||
{
|
||||
this.router.navigate(['../dashrunner/'+id],{relativeTo:this.route});
|
||||
// for runner line navigation
|
||||
// goToEditData(id: number){
|
||||
// this.router.navigate(['../editdata/'+id],{relativeTo:this.route});
|
||||
// }
|
||||
goToEdit(id: number) {
|
||||
// Navigate to editnewdash component instead of dashrunnerline
|
||||
// Pass a query parameter to indicate this is from dashboard runner
|
||||
this.router.navigate(['../../dashboardbuilder/editdashn/' + id], {
|
||||
relativeTo: this.route,
|
||||
queryParams: { fromRunner: true }
|
||||
});
|
||||
}
|
||||
|
||||
goToEditData(id: number){
|
||||
this.router.navigate(['../editdata/'+id],{relativeTo:this.route});
|
||||
goToEditData(id: number) {
|
||||
this.router.navigate(['../editdata/' + id], { relativeTo: this.route });
|
||||
}
|
||||
|
||||
onExport() {
|
||||
@@ -84,29 +89,28 @@ export class DashrunnerallComponent implements OnInit {
|
||||
moment().format('YYYYMMDD_HHmmss'))
|
||||
}
|
||||
|
||||
gotoAction(){
|
||||
this.router.navigate(["../../actions"], { relativeTo: this.route, queryParams: { m_id: this.moduleId,pname:this.projectname } });
|
||||
gotoAction() {
|
||||
this.router.navigate(["../../actions"], { relativeTo: this.route, queryParams: { m_id: this.moduleId, pname: this.projectname } });
|
||||
}
|
||||
gotoRepo(){
|
||||
gotoRepo() {
|
||||
this.router.navigate(["../../modulecard"], { relativeTo: this.route, queryParams: { p_id: this.projectId } });
|
||||
}
|
||||
|
||||
onDelete(row){
|
||||
onDelete(row) {
|
||||
this.rowSelected = row;
|
||||
console.log(this.rowSelected);
|
||||
this.modalDelete = true;
|
||||
}
|
||||
delete(id)
|
||||
{
|
||||
this.modalDelete = false;
|
||||
console.log("in delete "+id);
|
||||
this.dashboardService.deleteField(id).subscribe((data)=>{
|
||||
delete(id) {
|
||||
this.modalDelete = false;
|
||||
console.log("in delete " + id);
|
||||
this.dashboardService.deleteField(id).subscribe((data) => {
|
||||
console.log(data);
|
||||
this.ngOnInit();
|
||||
});
|
||||
if (id) {
|
||||
});
|
||||
if (id) {
|
||||
this.toastr.success('Deleted successfully');
|
||||
}
|
||||
}
|
||||
}
|
||||
// openModal()
|
||||
// {
|
||||
|
||||
@@ -3,6 +3,10 @@ import { DashrunnerService } from '../dashrunner.service';
|
||||
import { ActivatedRoute, Router } from '@angular/router';
|
||||
import { Dashboard3Service } from 'src/app/services/builder/dashboard3.service';
|
||||
import { DashboardContentModel } from 'src/app/models/builder/dashboard';
|
||||
// Add FilterService import
|
||||
import { FilterService } from '../../../dashboardnew/common-filter/filter.service';
|
||||
// Add Subscription import
|
||||
import { Subscription } from 'rxjs';
|
||||
|
||||
@Component({
|
||||
selector: 'app-bar-runner',
|
||||
@@ -24,8 +28,19 @@ export class BarRunnerComponent implements OnInit {
|
||||
JsonData;
|
||||
|
||||
barData;
|
||||
constructor(private Dashtestservive:DashrunnerService,private route: ActivatedRoute,private dashboardService: Dashboard3Service,
|
||||
private router : Router,) { }
|
||||
ConnectionId: number; // Add ConnectionId property
|
||||
|
||||
// Add subscriptions to unsubscribe on destroy
|
||||
private subscriptions: Subscription[] = [];
|
||||
|
||||
constructor(
|
||||
private Dashtestservive:DashrunnerService,
|
||||
private route: ActivatedRoute,
|
||||
private dashboardService: Dashboard3Service,
|
||||
private router : Router,
|
||||
// Add FilterService to constructor
|
||||
private filterService: FilterService
|
||||
) { }
|
||||
|
||||
barChartLabels: any[] = [];
|
||||
barChartType: string = 'bar';
|
||||
@@ -47,6 +62,13 @@ export class BarRunnerComponent implements OnInit {
|
||||
this.editId = this.route.snapshot.params.id;
|
||||
console.log(this.editId);
|
||||
|
||||
// Subscribe to filter changes
|
||||
this.subscriptions.push(
|
||||
this.filterService.filterState$.subscribe(filters => {
|
||||
// When filters change, refresh the chart data
|
||||
this.fetchChartData();
|
||||
})
|
||||
);
|
||||
|
||||
this.dashboardService.getById(this.editId).subscribe((data)=>{
|
||||
console.log(data);
|
||||
@@ -74,22 +96,62 @@ export class BarRunnerComponent implements OnInit {
|
||||
this.YAxis = ChartObject[i].yAxis;
|
||||
this.showlabel = ChartObject[i].showlabel;
|
||||
this.barChartLegend = ChartObject[i].chartlegend;
|
||||
this.ConnectionId = ChartObject[i].connection; // Add connection ID
|
||||
console.log(this.TableName);
|
||||
this.Dashtestservive.getChartData(this.TableName,"Bar Chart",this.XAxis,this.YAxis).subscribe((Ldata) => {
|
||||
console.log(Ldata);
|
||||
this.JsonData = Ldata;
|
||||
this.barChartData = this.JsonData.barChartData;
|
||||
this.barChartLabels = this.JsonData.barChartLabels;
|
||||
|
||||
},(error) => {
|
||||
console.log(error);
|
||||
});
|
||||
// Fetch data with filters
|
||||
this.fetchChartData();
|
||||
break; // No need to continue the loop once the correct placeholder is found
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// Fetch chart data with filter support
|
||||
fetchChartData(): void {
|
||||
if (this.TableName && this.XAxis && this.YAxis) {
|
||||
// Convert YAxis to string if it's an array
|
||||
const yAxisString = Array.isArray(this.YAxis) ? this.YAxis.join(',') : this.YAxis;
|
||||
|
||||
// Get filter parameters from common filters
|
||||
const commonFilters = this.filterService.getFilterValues();
|
||||
const filterDefinitions = this.filterService.getFilters();
|
||||
|
||||
// Build filter object using field names as keys
|
||||
const filterObj = {};
|
||||
Object.keys(commonFilters).forEach(filterId => {
|
||||
const filterValue = commonFilters[filterId];
|
||||
|
||||
// Find the filter definition to get the field name
|
||||
const filterDef = this.filterService.getFilters().find(f => f.id === filterId);
|
||||
|
||||
if (filterDef && filterDef.field) {
|
||||
const fieldName = filterDef.field;
|
||||
if (filterValue !== undefined && filterValue !== null && filterValue !== '') {
|
||||
filterObj[fieldName] = filterValue;
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
// Convert to JSON string for API call
|
||||
let filterParams = '';
|
||||
if (Object.keys(filterObj).length > 0) {
|
||||
filterParams = JSON.stringify(filterObj);
|
||||
}
|
||||
|
||||
console.log('BarRunner: Final filter object to send to API:', filterObj);
|
||||
|
||||
// Fetch data from the dashboard service with filters
|
||||
this.Dashtestservive.getChartDataWithFilters(this.TableName, "Bar Chart", this.XAxis, this.YAxis, this.ConnectionId, '', '', filterParams).subscribe((Ldata) => {
|
||||
console.log(Ldata);
|
||||
this.JsonData = Ldata;
|
||||
this.barChartData = this.JsonData.barChartData;
|
||||
this.barChartLabels = this.JsonData.barChartLabels;
|
||||
},(error) => {
|
||||
console.log(error);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
generatePDFFile(){
|
||||
this.buttonClicked.emit();
|
||||
const content = this.contentContainerRef.nativeElement;
|
||||
@@ -98,4 +160,16 @@ export class BarRunnerComponent implements OnInit {
|
||||
this.Dashtestservive.generatePDF(content, filename);
|
||||
}
|
||||
|
||||
ngOnDestroy() {
|
||||
// Unsubscribe from all subscriptions to prevent memory leaks
|
||||
console.log('BarRunnerComponent ngOnDestroy called, unsubscribing from', this.subscriptions.length, 'subscriptions');
|
||||
this.subscriptions.forEach(subscription => {
|
||||
if (subscription && !subscription.closed) {
|
||||
subscription.unsubscribe();
|
||||
}
|
||||
});
|
||||
this.subscriptions = [];
|
||||
|
||||
console.log('BarRunnerComponent destroyed and cleaned up');
|
||||
}
|
||||
}
|
||||
@@ -5,6 +5,10 @@ import { ChartConfiguration, ChartDataset, ChartOptions } from 'chart.js';
|
||||
import { DashboardContentModel } from 'src/app/models/builder/dashboard';
|
||||
import { DashrunnerService } from '../dashrunner.service';
|
||||
import { Dashboard3Service } from 'src/app/services/builder/dashboard3.service';
|
||||
// Add FilterService import
|
||||
import { FilterService } from '../../../dashboardnew/common-filter/filter.service';
|
||||
// Add Subscription import
|
||||
import { Subscription } from 'rxjs';
|
||||
|
||||
@Component({
|
||||
selector: 'app-bubble-runner',
|
||||
@@ -25,9 +29,15 @@ export class BubbleRunnerComponent implements OnInit {
|
||||
JsonData;
|
||||
lineChartNoLabels: [] = [];
|
||||
ChartLegend = false;
|
||||
ConnectionId: number; // Add ConnectionId property
|
||||
|
||||
// Add subscriptions to unsubscribe on destroy
|
||||
private subscriptions: Subscription[] = [];
|
||||
|
||||
constructor(private Dashtestservive:DashrunnerService,private route: ActivatedRoute,private dashboardService: Dashboard3Service,
|
||||
private router : Router,) { }
|
||||
private router : Router,
|
||||
// Add FilterService to constructor
|
||||
private filterService: FilterService) { }
|
||||
|
||||
public bubbleChartOptions: ChartConfiguration['options'] = {
|
||||
// scales: {
|
||||
@@ -87,6 +97,13 @@ export class BubbleRunnerComponent implements OnInit {
|
||||
this.editId = this.route.snapshot.params.id;
|
||||
console.log(this.editId);
|
||||
|
||||
// Subscribe to filter changes
|
||||
this.subscriptions.push(
|
||||
this.filterService.filterState$.subscribe(filters => {
|
||||
// When filters change, refresh the chart data
|
||||
this.fetchChartData();
|
||||
})
|
||||
);
|
||||
|
||||
this.dashboardService.getById(this.editId).subscribe((data)=>{
|
||||
console.log(data);
|
||||
@@ -112,22 +129,62 @@ export class BubbleRunnerComponent implements OnInit {
|
||||
this.YAxis = ChartObject[i].yAxis;
|
||||
this.showlabel = ChartObject[i].showlabel;
|
||||
this.ChartLegend = ChartObject[i].chartlegend;
|
||||
this.ConnectionId = ChartObject[i].connection; // Add connection ID
|
||||
console.log(this.TableName);
|
||||
this.Dashtestservive.getChartData(this.TableName,"Bubble Chart",this.XAxis,this.YAxis).subscribe((Ldata) => {
|
||||
console.log(Ldata);
|
||||
this.JsonData = Ldata;
|
||||
this.bubbleChartData = this.JsonData.bubbleChartData;
|
||||
// this.radarChartLabels = this.JsonData.radarChartLabels;
|
||||
|
||||
},(error) => {
|
||||
console.log(error);
|
||||
});
|
||||
// Fetch data with filters
|
||||
this.fetchChartData();
|
||||
break; // No need to continue the loop once the correct placeholder is found
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// Fetch chart data with filter support
|
||||
fetchChartData(): void {
|
||||
if (this.TableName && this.XAxis && this.YAxis) {
|
||||
// Convert YAxis to string if it's an array
|
||||
const yAxisString = Array.isArray(this.YAxis) ? this.YAxis.join(',') : this.YAxis;
|
||||
|
||||
// Get filter parameters from common filters
|
||||
const commonFilters = this.filterService.getFilterValues();
|
||||
const filterDefinitions = this.filterService.getFilters();
|
||||
|
||||
// Build filter object using field names as keys
|
||||
const filterObj = {};
|
||||
Object.keys(commonFilters).forEach(filterId => {
|
||||
const filterValue = commonFilters[filterId];
|
||||
|
||||
// Find the filter definition to get the field name
|
||||
const filterDef = this.filterService.getFilters().find(f => f.id === filterId);
|
||||
|
||||
if (filterDef && filterDef.field) {
|
||||
const fieldName = filterDef.field;
|
||||
if (filterValue !== undefined && filterValue !== null && filterValue !== '') {
|
||||
filterObj[fieldName] = filterValue;
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
// Convert to JSON string for API call
|
||||
let filterParams = '';
|
||||
if (Object.keys(filterObj).length > 0) {
|
||||
filterParams = JSON.stringify(filterObj);
|
||||
}
|
||||
|
||||
console.log('BubbleRunner: Final filter object to send to API:', filterObj);
|
||||
|
||||
// Fetch data from the dashboard service with filters
|
||||
this.Dashtestservive.getChartDataWithFilters(this.TableName, "Bubble Chart", this.XAxis, this.YAxis, this.ConnectionId, '', '', filterParams).subscribe((Ldata) => {
|
||||
console.log(Ldata);
|
||||
this.JsonData = Ldata;
|
||||
this.bubbleChartData = this.JsonData.bubbleChartData;
|
||||
// this.radarChartLabels = this.JsonData.radarChartLabels;
|
||||
},(error) => {
|
||||
console.log(error);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
generatePDFFile(){
|
||||
this.buttonClicked.emit();
|
||||
const content = this.contentContainerRef.nativeElement;
|
||||
@@ -136,5 +193,18 @@ export class BubbleRunnerComponent implements OnInit {
|
||||
this.Dashtestservive.generatePDF(content, filename);
|
||||
}
|
||||
|
||||
ngOnDestroy() {
|
||||
// Unsubscribe from all subscriptions to prevent memory leaks
|
||||
console.log('BubbleRunnerComponent ngOnDestroy called, unsubscribing from', this.subscriptions.length, 'subscriptions');
|
||||
this.subscriptions.forEach(subscription => {
|
||||
if (subscription && !subscription.closed) {
|
||||
subscription.unsubscribe();
|
||||
}
|
||||
});
|
||||
this.subscriptions = [];
|
||||
|
||||
console.log('BubbleRunnerComponent destroyed and cleaned up');
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
||||
@@ -0,0 +1,73 @@
|
||||
<div class="compact-filter">
|
||||
<div class="filter-header" (click)="toggleFilter()">
|
||||
<span class="filter-label" *ngIf="filterLabel">{{ filterLabel }}</span>
|
||||
<span class="filter-key" *ngIf="!filterLabel && filterKey">{{ filterKey }}</span>
|
||||
<span class="filter-type">({{ filterType }})</span>
|
||||
<clr-icon shape="caret down" class="expand-icon" *ngIf="!isExpanded"></clr-icon>
|
||||
<clr-icon shape="caret up" class="expand-icon" *ngIf="isExpanded"></clr-icon>
|
||||
</div>
|
||||
|
||||
<div class="filter-content" *ngIf="isExpanded">
|
||||
<!-- Text Filter -->
|
||||
<div class="filter-control" *ngIf="filterType === 'text'">
|
||||
<input type="text"
|
||||
[(ngModel)]="filterValue"
|
||||
(ngModelChange)="onFilterValueChange($event)"
|
||||
[placeholder]="filterLabel || filterKey"
|
||||
class="clr-input compact-input">
|
||||
</div>
|
||||
|
||||
<!-- Dropdown Filter -->
|
||||
<div class="filter-control" *ngIf="filterType === 'dropdown'">
|
||||
<select [(ngModel)]="filterValue"
|
||||
(ngModelChange)="onFilterValueChange($event)"
|
||||
class="clr-select compact-select">
|
||||
<option value="">{{ filterLabel || filterKey }}</option>
|
||||
<option *ngFor="let option of filterOptions; let i = index" [value]="option">{{ option }}</option>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<!-- Multi-Select Filter -->
|
||||
<div class="filter-control" *ngIf="filterType === 'multiselect'">
|
||||
<div class="multiselect-container">
|
||||
<div class="checkbox-group">
|
||||
<div *ngFor="let option of filterOptions; let i = index" class="checkbox-item">
|
||||
<input type="checkbox"
|
||||
[checked]="isOptionSelected(option)"
|
||||
(change)="onMultiSelectChange(option, $event)"
|
||||
[id]="'checkbox-' + filterKey + '-' + i"
|
||||
class="clr-checkbox">
|
||||
<label [for]="'checkbox-' + filterKey + '-' + i" class="checkbox-label">{{ option }}</label>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Date Range Filter -->
|
||||
<div class="filter-control date-range" *ngIf="filterType === 'date-range'">
|
||||
<div class="date-input-group">
|
||||
<input type="date"
|
||||
[(ngModel)]="filterValue.start"
|
||||
(ngModelChange)="onDateRangeChange({ start: $event, end: filterValue.end })"
|
||||
placeholder="Start Date"
|
||||
class="clr-input compact-date">
|
||||
<span class="date-separator">to</span>
|
||||
<input type="date"
|
||||
[(ngModel)]="filterValue.end"
|
||||
(ngModelChange)="onDateRangeChange({ start: filterValue.start, end: $event })"
|
||||
placeholder="End Date"
|
||||
class="clr-input compact-date">
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Toggle Filter -->
|
||||
<div class="filter-control toggle" *ngIf="filterType === 'toggle'">
|
||||
<input type="checkbox"
|
||||
[(ngModel)]="filterValue"
|
||||
(ngModelChange)="onToggleChange($event)"
|
||||
clrToggle
|
||||
class="clr-toggle">
|
||||
<label class="toggle-label">{{ filterLabel || filterKey }}</label>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -0,0 +1,149 @@
|
||||
.compact-filter {
|
||||
display: block;
|
||||
min-width: 200px;
|
||||
max-width: 300px;
|
||||
margin: 8px;
|
||||
padding: 0;
|
||||
background: #ffffff;
|
||||
border: 1px solid #d7d7d7;
|
||||
border-radius: 4px;
|
||||
font-size: 14px;
|
||||
box-shadow: 0 1px 2px rgba(0, 0, 0, 0.05);
|
||||
|
||||
.filter-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
padding: 12px 15px;
|
||||
cursor: pointer;
|
||||
background: #f8f8f8;
|
||||
border-bottom: 1px solid #eaeaea;
|
||||
border-radius: 4px 4px 0 0;
|
||||
|
||||
&:hover {
|
||||
background: #f0f0f0;
|
||||
}
|
||||
|
||||
.filter-label, .filter-key {
|
||||
font-weight: 500;
|
||||
color: #333333;
|
||||
flex-grow: 1;
|
||||
}
|
||||
|
||||
.filter-type {
|
||||
font-size: 12px;
|
||||
color: #666666;
|
||||
margin: 0 8px;
|
||||
background: #eaeaea;
|
||||
padding: 2px 8px;
|
||||
border-radius: 12px;
|
||||
}
|
||||
|
||||
.expand-icon {
|
||||
width: 16px;
|
||||
height: 16px;
|
||||
color: #666666;
|
||||
}
|
||||
}
|
||||
|
||||
.filter-content {
|
||||
padding: 15px;
|
||||
|
||||
.filter-control {
|
||||
margin-bottom: 12px;
|
||||
|
||||
&:last-child {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
&.date-range {
|
||||
.date-input-group {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
|
||||
.date-separator {
|
||||
font-size: 14px;
|
||||
color: #666666;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
&.toggle {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.compact-input,
|
||||
.compact-select,
|
||||
.compact-date {
|
||||
width: 100%;
|
||||
padding: 8px 12px;
|
||||
font-size: 14px;
|
||||
border: 1px solid #d7d7d7;
|
||||
border-radius: 4px;
|
||||
background: #ffffff;
|
||||
|
||||
&:focus {
|
||||
outline: none;
|
||||
border-color: #0072ce;
|
||||
box-shadow: 0 0 0 1px #0072ce;
|
||||
}
|
||||
}
|
||||
|
||||
.compact-select {
|
||||
height: 36px;
|
||||
}
|
||||
|
||||
.multiselect-container {
|
||||
max-height: 200px;
|
||||
overflow-y: auto;
|
||||
border: 1px solid #d7d7d7;
|
||||
border-radius: 4px;
|
||||
padding: 10px;
|
||||
background: #ffffff;
|
||||
}
|
||||
|
||||
.checkbox-group {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.checkbox-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
|
||||
.clr-checkbox {
|
||||
width: 16px;
|
||||
height: 16px;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.checkbox-label {
|
||||
font-size: 14px;
|
||||
margin: 0;
|
||||
cursor: pointer;
|
||||
color: #333333;
|
||||
}
|
||||
}
|
||||
|
||||
.toggle-label {
|
||||
margin: 0;
|
||||
font-size: 14px;
|
||||
color: #333333;
|
||||
}
|
||||
|
||||
.clr-toggle {
|
||||
margin: 0;
|
||||
}
|
||||
}
|
||||
|
||||
// Host styling
|
||||
:host {
|
||||
display: block;
|
||||
}
|
||||
@@ -0,0 +1,245 @@
|
||||
import { Component, OnInit, Input, Output, EventEmitter, OnChanges, SimpleChanges } from '@angular/core';
|
||||
import { FilterService, Filter } from '../../../dashboardnew/common-filter/filter.service';
|
||||
|
||||
@Component({
|
||||
selector: 'app-compact-filter-runner',
|
||||
templateUrl: './compact-filter-runner.component.html',
|
||||
styleUrls: ['./compact-filter-runner.component.scss']
|
||||
})
|
||||
export class CompactFilterRunnerComponent implements OnInit, OnChanges {
|
||||
@Input() filterKey: string = '';
|
||||
@Input() filterType: string = 'text';
|
||||
@Input() filterOptions: string[] = [];
|
||||
@Input() filterLabel: string = '';
|
||||
@Input() apiUrl: string = '';
|
||||
@Input() connection: number | undefined;
|
||||
@Output() filterChange = new EventEmitter<any>();
|
||||
|
||||
selectedFilter: Filter | null = null;
|
||||
filterValue: any = '';
|
||||
availableFilters: Filter[] = [];
|
||||
availableKeys: string[] = [];
|
||||
availableValues: string[] = [];
|
||||
isExpanded: boolean = false; // Add expansion state
|
||||
|
||||
constructor(
|
||||
private filterService: FilterService
|
||||
) {
|
||||
console.log('=== COMPACT FILTER RUNNER CONSTRUCTOR CALLED ===');
|
||||
}
|
||||
|
||||
ngOnInit(): void {
|
||||
console.log('=== COMPACT FILTER RUNNER DEBUG INFO ===');
|
||||
console.log('Component initialized with inputs:');
|
||||
console.log('- filterKey:', this.filterKey);
|
||||
console.log('- filterType:', this.filterType);
|
||||
console.log('- filterOptions:', this.filterOptions);
|
||||
console.log('- filterLabel:', this.filterLabel);
|
||||
console.log('- apiUrl:', this.apiUrl);
|
||||
console.log('- connection:', this.connection);
|
||||
console.log('========================================');
|
||||
|
||||
// Register this filter with the filter service
|
||||
this.registerFilter();
|
||||
|
||||
// Subscribe to filter definitions to get available filters
|
||||
this.filterService.filters$.subscribe(filters => {
|
||||
this.availableFilters = filters;
|
||||
console.log('Available filters updated:', filters);
|
||||
this.updateSelectedFilter();
|
||||
});
|
||||
|
||||
// Subscribe to filter state changes
|
||||
this.filterService.filterState$.subscribe(state => {
|
||||
console.log('Filter state updated:', state);
|
||||
if (this.selectedFilter && state.hasOwnProperty(this.selectedFilter.id)) {
|
||||
this.filterValue = state[this.selectedFilter.id];
|
||||
console.log('Filter value updated for', this.selectedFilter.id, ':', this.filterValue);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
ngOnChanges(changes: SimpleChanges): void {
|
||||
console.log('=== COMPACT FILTER RUNNER CHANGES DEBUG ===');
|
||||
console.log('Component inputs changed:', changes);
|
||||
|
||||
// If filterKey or filterType changes, re-register the filter
|
||||
if (changes.filterKey || changes.filterType || changes.filterOptions) {
|
||||
console.log('Re-registering filter due to input changes');
|
||||
this.registerFilter();
|
||||
}
|
||||
console.log('==========================================');
|
||||
}
|
||||
|
||||
// Toggle filter expansion
|
||||
toggleFilter(): void {
|
||||
this.isExpanded = !this.isExpanded;
|
||||
}
|
||||
|
||||
// Register this filter with the filter service
|
||||
registerFilter(): void {
|
||||
console.log('Registering filter with key:', this.filterKey, 'type:', this.filterType);
|
||||
|
||||
if (this.filterKey) {
|
||||
// Get current filter values from the service
|
||||
const currentFilterValues = this.filterService.getFilterValues();
|
||||
console.log('Current filter values from service:', currentFilterValues);
|
||||
|
||||
// Create a filter definition for this compact filter
|
||||
const filterDef: Filter = {
|
||||
id: `${this.filterKey}`,
|
||||
field: this.filterKey,
|
||||
label: this.filterLabel || this.filterKey,
|
||||
type: this.filterType as any,
|
||||
options: this.filterOptions,
|
||||
value: this.filterValue // Use the current filter value
|
||||
};
|
||||
|
||||
console.log('Created filter definition:', filterDef);
|
||||
|
||||
// Get current filters
|
||||
const currentFilters = this.filterService.getFilters();
|
||||
console.log('Current filters from service:', currentFilters);
|
||||
|
||||
// Check if this filter is already registered
|
||||
const existingFilterIndex = currentFilters.findIndex(f => f.id === filterDef.id);
|
||||
console.log('Existing filter index:', existingFilterIndex);
|
||||
|
||||
if (existingFilterIndex >= 0) {
|
||||
// Preserve the existing filter configuration
|
||||
const existingFilter = currentFilters[existingFilterIndex];
|
||||
console.log('Found existing filter:', existingFilter);
|
||||
|
||||
// Preserve the existing filter value if it exists in the service
|
||||
if (currentFilterValues.hasOwnProperty(existingFilter.id)) {
|
||||
filterDef.value = currentFilterValues[existingFilter.id];
|
||||
this.filterValue = filterDef.value; // Update local value
|
||||
console.log('Using value from service:', filterDef.value);
|
||||
} else if (existingFilter.value !== undefined) {
|
||||
// Fallback to existing filter's value if no service value
|
||||
filterDef.value = existingFilter.value;
|
||||
this.filterValue = filterDef.value;
|
||||
console.log('Using value from existing filter:', filterDef.value);
|
||||
}
|
||||
|
||||
// Preserve other configuration properties
|
||||
filterDef.label = existingFilter.label;
|
||||
filterDef.options = existingFilter.options || this.filterOptions;
|
||||
|
||||
// Update existing filter
|
||||
currentFilters[existingFilterIndex] = filterDef;
|
||||
console.log('Updated existing filter:', filterDef);
|
||||
} else {
|
||||
// For new filters, check if there's already a value in the service
|
||||
if (currentFilterValues.hasOwnProperty(filterDef.id)) {
|
||||
filterDef.value = currentFilterValues[filterDef.id];
|
||||
this.filterValue = filterDef.value; // Update local value
|
||||
console.log('Using value from service for new filter:', filterDef.value);
|
||||
}
|
||||
|
||||
// Add new filter
|
||||
currentFilters.push(filterDef);
|
||||
console.log('Added new filter:', filterDef);
|
||||
}
|
||||
|
||||
// Update the filter service with the new filter list
|
||||
this.filterService.setFilters(currentFilters);
|
||||
|
||||
// Update the selected filter reference
|
||||
this.selectedFilter = filterDef;
|
||||
console.log('Selected filter set to:', this.selectedFilter);
|
||||
} else {
|
||||
console.log('No filterKey provided, skipping filter registration');
|
||||
}
|
||||
}
|
||||
|
||||
updateSelectedFilter(): void {
|
||||
console.log('Updating selected filter. Filter key:', this.filterKey, 'Available filters:', this.availableFilters);
|
||||
if (this.filterKey && this.availableFilters.length > 0) {
|
||||
this.selectedFilter = this.availableFilters.find(f => f.field === this.filterKey) || null;
|
||||
console.log('Found selected filter:', this.selectedFilter);
|
||||
if (this.selectedFilter) {
|
||||
// Get current value for this filter from the service
|
||||
const currentState = this.filterService.getFilterValues();
|
||||
console.log('Current state from service:', currentState);
|
||||
const filterValue = currentState[this.selectedFilter.id];
|
||||
if (filterValue !== undefined) {
|
||||
this.filterValue = filterValue;
|
||||
} else if (this.selectedFilter.value !== undefined) {
|
||||
// Use the filter's default value if no service value
|
||||
this.filterValue = this.selectedFilter.value;
|
||||
} else {
|
||||
// Use the current filter value as fallback
|
||||
this.filterValue = this.filterValue || '';
|
||||
}
|
||||
|
||||
console.log('Updated selected filter value:', this.filterValue);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
onFilterValueChange(value: any): void {
|
||||
console.log('Filter value changed:', value);
|
||||
|
||||
if (this.selectedFilter) {
|
||||
this.filterValue = value;
|
||||
this.filterService.updateFilterValue(this.selectedFilter.id, value);
|
||||
this.filterChange.emit({ filterId: this.selectedFilter.id, value: value });
|
||||
|
||||
// Update the filter definition in the service to reflect the new value
|
||||
const currentFilters = this.filterService.getFilters();
|
||||
const filterIndex = currentFilters.findIndex(f => f.id === this.selectedFilter.id);
|
||||
if (filterIndex >= 0) {
|
||||
currentFilters[filterIndex].value = value;
|
||||
this.filterService.setFilters(currentFilters);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
onToggleChange(checked: boolean): void {
|
||||
this.onFilterValueChange(checked);
|
||||
}
|
||||
|
||||
onDateRangeChange(dateRange: { start: string | null, end: string | null }): void {
|
||||
this.onFilterValueChange(dateRange);
|
||||
}
|
||||
|
||||
// Handle multi-select changes
|
||||
onMultiSelectChange(option: string, event: any): void {
|
||||
const checked = event.target.checked;
|
||||
|
||||
// Initialize filterValue as array if it's not already
|
||||
if (!Array.isArray(this.filterValue)) {
|
||||
this.filterValue = [];
|
||||
}
|
||||
|
||||
if (checked) {
|
||||
// Add option to array if not already present
|
||||
if (!this.filterValue.includes(option)) {
|
||||
this.filterValue.push(option);
|
||||
}
|
||||
} else {
|
||||
// Remove option from array
|
||||
this.filterValue = this.filterValue.filter((item: string) => item !== option);
|
||||
}
|
||||
|
||||
// Emit the change
|
||||
this.onFilterValueChange(this.filterValue);
|
||||
}
|
||||
|
||||
// Add method to check if an option is selected for checkboxes (needed for proper UI rendering)
|
||||
isOptionSelected(option: string): boolean {
|
||||
console.log('Checking if option is selected:', option, 'Current filter value:', this.filterValue);
|
||||
if (!this.filterValue) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Ensure filterValue is an array for multiselect
|
||||
if (!Array.isArray(this.filterValue)) {
|
||||
this.filterValue = [];
|
||||
return false;
|
||||
}
|
||||
|
||||
return this.filterValue.includes(option);
|
||||
}
|
||||
}
|
||||
@@ -160,7 +160,27 @@ getlinechart(): any[] {
|
||||
return this._http.get(url);
|
||||
}
|
||||
|
||||
// New method to support filters
|
||||
public getChartDataWithFilters(tableName: string, jobType: string, xAxis:any, yAxes:any, sureId: number | undefined, parameterField: string, parameterValue: string, filterParams: string): Observable<any> {
|
||||
let url = `${baseUrl}/chart/getdashjson/${jobType}?tableName=${tableName}&xAxis=${xAxis}&yAxes=${yAxes}`;
|
||||
|
||||
// Add sureId if provided
|
||||
if (sureId) {
|
||||
url += `&sureId=${sureId}`;
|
||||
}
|
||||
|
||||
// Add parameter field and value if provided
|
||||
if (parameterField && parameterValue) {
|
||||
url += `¶meter=${encodeURIComponent(parameterField)}¶meterValue=${encodeURIComponent(parameterValue)}`;
|
||||
}
|
||||
|
||||
// Add filter parameters if provided
|
||||
if (filterParams) {
|
||||
url += `&filters=${encodeURIComponent(filterParams)}`;
|
||||
}
|
||||
|
||||
return this._http.get(url);
|
||||
}
|
||||
|
||||
//////////////////////////////////////////////
|
||||
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user