Compare commits
15 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
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;
|
||||
|
||||
|
||||
@@ -1,20 +0,0 @@
|
||||
<!DOCTYPE html>
|
||||
|
||||
<html>
|
||||
<head>
|
||||
<title>gaurav</title>
|
||||
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<h1>this is h1</h1>
|
||||
<h2>this is h1</h2>
|
||||
<h3>this is h1</h3>
|
||||
<h4>this is h1</h4>
|
||||
<p>Lorem ipsum, dolor sit amet consectetur adipisicing elit. Ipsa fuga, asperiores mollitia iste vitae repellendus adipisci atque eum corrupti ad placeat unde voluptatum quia perferendis neque expedita, sequi iure quo. Ut error adipisci ex cum sint, suscipit, voluptatem repellat nemo dolorum unde dolores quasi aut. A earum quo mollitia voluptatibus!</p>
|
||||
|
||||
|
||||
</body>
|
||||
|
||||
</html>
|
||||
|
||||
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,117 @@
|
||||
<!-- 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-checkboxes" style="max-height: 200px; overflow-y: auto; border: 1px solid #ddd; padding: 10px;">
|
||||
<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,378 @@
|
||||
import { Component, OnInit, Input, Output, EventEmitter, OnChanges, SimpleChanges } from '@angular/core';
|
||||
import { FilterService, Filter } from './filter.service';
|
||||
import { AlertsService } from 'src/app/services/fnd/alerts.service';
|
||||
|
||||
@Component({
|
||||
selector: 'app-compact-filter',
|
||||
templateUrl: './compact-filter.component.html',
|
||||
styleUrls: ['./compact-filter.component.scss']
|
||||
})
|
||||
export class CompactFilterComponent implements OnInit, OnChanges {
|
||||
@Input() filterKey: string = '';
|
||||
@Input() filterType: string = 'text';
|
||||
@Input() filterOptions: string[] = [];
|
||||
@Input() filterLabel: string = '';
|
||||
@Input() apiUrl: string = '';
|
||||
@Input() connectionId: number | undefined;
|
||||
@Output() filterChange = new EventEmitter<any>();
|
||||
@Output() configChange = new EventEmitter<any>();
|
||||
|
||||
selectedFilter: Filter | null = null;
|
||||
filterValue: any = '';
|
||||
availableFilters: Filter[] = [];
|
||||
availableKeys: string[] = [];
|
||||
availableValues: string[] = [];
|
||||
|
||||
// Configuration properties
|
||||
isConfigMode: boolean = false;
|
||||
configFilterKey: string = '';
|
||||
configFilterType: string = 'text';
|
||||
configFilterOptions: string = '';
|
||||
configFilterLabel: string = '';
|
||||
configApiUrl: string = '';
|
||||
configConnectionId: number | undefined;
|
||||
|
||||
constructor(
|
||||
private filterService: FilterService,
|
||||
private alertService: AlertsService
|
||||
) { }
|
||||
|
||||
ngOnInit(): void {
|
||||
// Initialize configuration from inputs
|
||||
this.configFilterKey = this.filterKey;
|
||||
this.configFilterType = this.filterType;
|
||||
this.configFilterLabel = this.filterLabel;
|
||||
this.configFilterOptions = this.filterOptions.join(',');
|
||||
this.configApiUrl = this.apiUrl;
|
||||
this.configConnectionId = this.connectionId;
|
||||
|
||||
// Load available keys and values if API URL and filter key are provided
|
||||
if (this.apiUrl) {
|
||||
this.loadAvailableKeys();
|
||||
// Load available values for the current filter key if it's a dropdown or multiselect
|
||||
if ((this.filterType === 'dropdown' || this.filterType === 'multiselect') && this.filterKey) {
|
||||
this.loadAvailableValues(this.filterKey);
|
||||
}
|
||||
}
|
||||
|
||||
// Register this filter with the filter service
|
||||
this.registerFilter();
|
||||
|
||||
// Subscribe to filter definitions to get available filters
|
||||
this.filterService.filters$.subscribe(filters => {
|
||||
this.availableFilters = filters;
|
||||
this.updateSelectedFilter();
|
||||
});
|
||||
|
||||
// Subscribe to filter state changes
|
||||
this.filterService.filterState$.subscribe(state => {
|
||||
if (this.selectedFilter && state.hasOwnProperty(this.selectedFilter.id)) {
|
||||
this.filterValue = state[this.selectedFilter.id];
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
ngOnChanges(changes: SimpleChanges): void {
|
||||
// If filterKey or filterType changes, re-register the filter
|
||||
if (changes.filterKey || changes.filterType) {
|
||||
// Load available values for the current filter key if it's a dropdown or multiselect
|
||||
if ((this.filterType === 'dropdown' || this.filterType === 'multiselect') && this.filterKey) {
|
||||
this.loadAvailableValues(this.filterKey);
|
||||
}
|
||||
this.registerFilter();
|
||||
}
|
||||
|
||||
// Handle API URL changes
|
||||
if (changes.apiUrl && !changes.apiUrl.firstChange) {
|
||||
if (this.apiUrl) {
|
||||
this.loadAvailableKeys();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Register this filter with the filter service
|
||||
registerFilter(): void {
|
||||
if (this.filterKey) {
|
||||
// Get current filter values from the service
|
||||
const currentFilterValues = this.filterService.getFilterValues();
|
||||
|
||||
// Create a filter definition for this compact filter
|
||||
const filterDef: Filter = {
|
||||
id: `${this.filterKey}`,
|
||||
field: this.filterKey,
|
||||
label: this.filterLabel || this.filterKey,
|
||||
type: this.filterType as any,
|
||||
options: this.filterOptions,
|
||||
value: this.filterValue // Use the current filter value
|
||||
};
|
||||
|
||||
// Get current filters
|
||||
const currentFilters = this.filterService.getFilters();
|
||||
|
||||
// Check if this filter is already registered
|
||||
const existingFilterIndex = currentFilters.findIndex(f => f.id === filterDef.id);
|
||||
|
||||
if (existingFilterIndex >= 0) {
|
||||
// Preserve the existing filter configuration
|
||||
const existingFilter = currentFilters[existingFilterIndex];
|
||||
|
||||
// Preserve the existing filter value if it exists in the service
|
||||
if (currentFilterValues.hasOwnProperty(existingFilter.id)) {
|
||||
filterDef.value = currentFilterValues[existingFilter.id];
|
||||
this.filterValue = filterDef.value; // Update local value
|
||||
} else if (existingFilter.value !== undefined) {
|
||||
// Fallback to existing filter's value if no service value
|
||||
filterDef.value = existingFilter.value;
|
||||
this.filterValue = filterDef.value;
|
||||
}
|
||||
|
||||
// Preserve other configuration properties
|
||||
filterDef.label = existingFilter.label;
|
||||
filterDef.options = existingFilter.options || this.filterOptions;
|
||||
|
||||
// Update existing filter
|
||||
currentFilters[existingFilterIndex] = filterDef;
|
||||
} else {
|
||||
// For new filters, check if there's already a value in the service
|
||||
if (currentFilterValues.hasOwnProperty(filterDef.id)) {
|
||||
filterDef.value = currentFilterValues[filterDef.id];
|
||||
this.filterValue = filterDef.value; // Update local value
|
||||
}
|
||||
|
||||
// Add new filter
|
||||
currentFilters.push(filterDef);
|
||||
}
|
||||
|
||||
// Update the filter service with the new filter list
|
||||
this.filterService.setFilters(currentFilters);
|
||||
|
||||
// Update the selected filter reference
|
||||
this.selectedFilter = filterDef;
|
||||
}
|
||||
}
|
||||
|
||||
updateSelectedFilter(): void {
|
||||
if (this.filterKey && this.availableFilters.length > 0) {
|
||||
this.selectedFilter = this.availableFilters.find(f => f.field === this.filterKey) || null;
|
||||
if (this.selectedFilter) {
|
||||
// Get current value for this filter from the service
|
||||
const currentState = this.filterService.getFilterValues();
|
||||
const filterValue = currentState[this.selectedFilter.id];
|
||||
if (filterValue !== undefined) {
|
||||
this.filterValue = filterValue;
|
||||
} else if (this.selectedFilter.value !== undefined) {
|
||||
// Use the filter's default value if no service value
|
||||
this.filterValue = this.selectedFilter.value;
|
||||
} else {
|
||||
// Use the current filter value as fallback
|
||||
this.filterValue = this.filterValue || '';
|
||||
}
|
||||
|
||||
// Also update configuration properties from the selected filter
|
||||
this.configFilterKey = this.selectedFilter.field;
|
||||
this.configFilterType = this.selectedFilter.type;
|
||||
this.configFilterLabel = this.selectedFilter.label;
|
||||
this.configFilterOptions = (this.selectedFilter.options || []).join(',');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
onFilterValueChange(value: any): void {
|
||||
if (this.selectedFilter) {
|
||||
this.filterValue = value;
|
||||
this.filterService.updateFilterValue(this.selectedFilter.id, value);
|
||||
this.filterChange.emit({ filterId: this.selectedFilter.id, value: value });
|
||||
|
||||
// Update the filter definition in the service to reflect the new value
|
||||
const currentFilters = this.filterService.getFilters();
|
||||
const filterIndex = currentFilters.findIndex(f => f.id === this.selectedFilter.id);
|
||||
if (filterIndex >= 0) {
|
||||
currentFilters[filterIndex].value = value;
|
||||
this.filterService.setFilters(currentFilters);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
onToggleChange(checked: boolean): void {
|
||||
this.onFilterValueChange(checked);
|
||||
}
|
||||
|
||||
onDateRangeChange(dateRange: { start: string | null, end: string | null }): void {
|
||||
this.onFilterValueChange(dateRange);
|
||||
}
|
||||
|
||||
// Load available keys from API
|
||||
loadAvailableKeys(): void {
|
||||
if (this.apiUrl) {
|
||||
this.alertService.getColumnfromurl(this.apiUrl, this.connectionId).subscribe(
|
||||
(keys: string[]) => {
|
||||
this.availableKeys = keys;
|
||||
},
|
||||
(error) => {
|
||||
console.error('Error loading available keys:', error);
|
||||
this.availableKeys = [];
|
||||
}
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// Load available values for a specific key
|
||||
loadAvailableValues(key: string): void {
|
||||
if (this.apiUrl && key) {
|
||||
this.alertService.getValuesFromUrl(this.apiUrl, this.connectionId, key).subscribe(
|
||||
(values: string[]) => {
|
||||
this.availableValues = values;
|
||||
// Update filter options if this is a dropdown or multiselect
|
||||
if (this.filterType === 'dropdown' || this.filterType === 'multiselect') {
|
||||
this.filterOptions = values;
|
||||
}
|
||||
},
|
||||
(error) => {
|
||||
console.error('Error loading available values:', error);
|
||||
this.availableValues = [];
|
||||
}
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// Configuration methods
|
||||
toggleConfigMode(): void {
|
||||
this.isConfigMode = !this.isConfigMode;
|
||||
if (this.isConfigMode) {
|
||||
// Initialize config values from current filter if available
|
||||
if (this.selectedFilter) {
|
||||
this.configFilterKey = this.selectedFilter.field;
|
||||
this.configFilterType = this.selectedFilter.type;
|
||||
this.configFilterLabel = this.selectedFilter.label;
|
||||
this.configFilterOptions = (this.selectedFilter.options || []).join(',');
|
||||
} else {
|
||||
// Fallback to current properties
|
||||
this.configFilterKey = this.filterKey;
|
||||
this.configFilterType = this.filterType;
|
||||
this.configFilterLabel = this.filterLabel;
|
||||
this.configFilterOptions = this.filterOptions.join(',');
|
||||
}
|
||||
this.configApiUrl = this.apiUrl;
|
||||
this.configConnectionId = this.connectionId;
|
||||
}
|
||||
}
|
||||
|
||||
saveConfiguration(): void {
|
||||
const config = {
|
||||
filterKey: this.configFilterKey,
|
||||
filterType: this.configFilterType,
|
||||
filterLabel: this.configFilterLabel,
|
||||
filterOptions: this.configFilterOptions.split(',').map(opt => opt.trim()).filter(opt => opt),
|
||||
apiUrl: this.configApiUrl,
|
||||
connectionId: this.configConnectionId
|
||||
};
|
||||
|
||||
// Emit configuration change
|
||||
this.configChange.emit(config);
|
||||
|
||||
// Update local properties
|
||||
this.filterKey = config.filterKey;
|
||||
this.filterType = config.filterType;
|
||||
this.filterLabel = config.filterLabel;
|
||||
this.filterOptions = config.filterOptions;
|
||||
this.apiUrl = config.apiUrl;
|
||||
this.connectionId = config.connectionId;
|
||||
|
||||
// Load available keys if API URL is provided
|
||||
if (this.apiUrl) {
|
||||
this.loadAvailableKeys();
|
||||
}
|
||||
|
||||
// Load available values for the selected key if it's a dropdown or multiselect
|
||||
if ((this.configFilterType === 'dropdown' || this.configFilterType === 'multiselect') && this.configFilterKey) {
|
||||
this.loadAvailableValues(this.configFilterKey);
|
||||
}
|
||||
|
||||
// Register the updated filter with the filter service
|
||||
this.registerFilter();
|
||||
|
||||
// Update selected filter
|
||||
this.updateSelectedFilter();
|
||||
|
||||
// Exit config mode
|
||||
this.isConfigMode = false;
|
||||
}
|
||||
|
||||
cancelConfiguration(): void {
|
||||
this.isConfigMode = false;
|
||||
}
|
||||
|
||||
// Handle filter key change in configuration
|
||||
onFilterKeyChange(key: string): void {
|
||||
this.configFilterKey = key;
|
||||
// Load available values for the selected key if it's a dropdown or multiselect
|
||||
if ((this.configFilterType === 'dropdown' || this.configFilterType === 'multiselect') && key) {
|
||||
this.loadAvailableValues(key);
|
||||
}
|
||||
}
|
||||
|
||||
// Handle API URL change in configuration
|
||||
onApiUrlChange(url: string): void {
|
||||
this.configApiUrl = url;
|
||||
// Load available keys when API URL changes
|
||||
if (url) {
|
||||
this.loadAvailableKeys();
|
||||
// Also clear available values since the API has changed
|
||||
this.availableValues = [];
|
||||
this.filterOptions = [];
|
||||
}
|
||||
}
|
||||
|
||||
// Handle filter type change in configuration
|
||||
onFilterTypeChange(type: string): void {
|
||||
this.configFilterType = type;
|
||||
// If changing to dropdown or multiselect and we have a key selected, load values
|
||||
if ((type === 'dropdown' || type === 'multiselect') && this.configFilterKey) {
|
||||
this.loadAvailableValues(this.configFilterKey);
|
||||
}
|
||||
}
|
||||
|
||||
// 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);
|
||||
}
|
||||
}
|
||||
@@ -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';
|
||||
@@ -90,11 +90,106 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
<!-- Compact Filter Configuration (shown only for Compact Filter components) -->
|
||||
<div class="clr-row" *ngIf="gadgetsEditdata?.fieldName === 'Compact Filter'">
|
||||
<div class="clr-col-sm-12">
|
||||
<h4>Compact Filter Configuration</h4>
|
||||
|
||||
<div class="clr-row">
|
||||
<div class="clr-col-sm-12">
|
||||
<label for="compactFilterConnection">Connection</label>
|
||||
<select id="compactFilterConnection" class="clr-select" [(ngModel)]="gadgetsEditdata.connection"
|
||||
(ngModelChange)="onCompactFilterConnectionChange($event)" [ngModelOptions]="{standalone: true}">
|
||||
<option value="">Select Connection</option>
|
||||
<option *ngFor="let conn of sureconnectData" [value]="conn.id">
|
||||
{{conn.connection_name || conn.id}}
|
||||
</option>
|
||||
</select>
|
||||
<div class="clr-subtext">Select a connection for this compact filter</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="clr-row" style="margin-top: 10px;">
|
||||
<div class="clr-col-sm-12">
|
||||
<label for="compactFilterApiUrl">API URL</label>
|
||||
<div>
|
||||
<input type="text" id="compactFilterApiUrl" class="clr-input" [(ngModel)]="gadgetsEditdata.table"
|
||||
(ngModelChange)="onCompactFilterApiUrlChange($event)" [ngModelOptions]="{standalone: true}"
|
||||
placeholder="Enter API URL">
|
||||
<span>
|
||||
<button class="btn btn-icon btn-primary" style="margin: 0px;"
|
||||
(click)="loadAvailableKeys(gadgetsEditdata.table, gadgetsEditdata.connection)"
|
||||
[disabled]="!gadgetsEditdata.table">
|
||||
<clr-icon shape="redo"></clr-icon>
|
||||
</button>
|
||||
</span>
|
||||
</div>
|
||||
<div class="clr-subtext">Enter the API URL to fetch data for this filter</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="clr-row" style="margin-top: 10px;">
|
||||
<div class="clr-col-sm-12">
|
||||
<label for="filterKey">Filter Key</label>
|
||||
<select id="filterKey" class="clr-select" [(ngModel)]="gadgetsEditdata.filterKey"
|
||||
(ngModelChange)="onFilterKeyChange($event)" [ngModelOptions]="{standalone: true}">
|
||||
<option value="">Select Filter Key</option>
|
||||
<option *ngFor="let key of availableKeys" [value]="key">{{ key }}</option>
|
||||
</select>
|
||||
<div class="clr-subtext">Select the field name to filter on</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="clr-row" style="margin-top: 10px;">
|
||||
<div class="clr-col-sm-12">
|
||||
<label for="filterType">Filter Type</label>
|
||||
<select id="filterType" class="clr-select" [(ngModel)]="gadgetsEditdata.filterType"
|
||||
(ngModelChange)="onFilterTypeChange($event)" [ngModelOptions]="{standalone: true}">
|
||||
<option value="text">Text</option>
|
||||
<option value="dropdown">Dropdown</option>
|
||||
<option value="multiselect">Multi-Select</option>
|
||||
<option value="date-range">Date Range</option>
|
||||
<option value="toggle">Toggle</option>
|
||||
</select>
|
||||
<div class="clr-subtext">Select the type of filter control to display</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="clr-row" style="margin-top: 10px;">
|
||||
<div class="clr-col-sm-12">
|
||||
<label for="filterLabel">Filter Label (Optional)</label>
|
||||
<input type="text" id="filterLabel" class="clr-input" [(ngModel)]="gadgetsEditdata.filterLabel"
|
||||
[ngModelOptions]="{standalone: true}" placeholder="Enter filter label">
|
||||
<div class="clr-subtext">Label to display for this filter in the UI (if not provided, filter key will be
|
||||
used)</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="clr-row" style="margin-top: 10px;"
|
||||
*ngIf="gadgetsEditdata.filterType === 'dropdown' || gadgetsEditdata.filterType === 'multiselect'">
|
||||
<div class="clr-col-sm-12">
|
||||
<label for="filterOptions">Filter Options (comma separated)</label>
|
||||
<input type="text" id="filterOptions" class="clr-input" [(ngModel)]="filterOptionsString"
|
||||
[ngModelOptions]="{standalone: true}" placeholder="Option1,Option2,Option3">
|
||||
<div class="clr-subtext">Comma-separated list of options for dropdown/multiselect filters</div>
|
||||
<div class="clr-subtext" *ngIf="gadgetsEditdata.filterKey">
|
||||
<strong>Available values for "{{ gadgetsEditdata.filterKey }}":</strong> {{ filterOptionsString }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="clr-row" *ngIf="gadgetsEditdata?.fieldName !== 'Compact Filter'"
|
||||
style="margin-top: 20px; padding-top: 15px; border-top: 1px solid #ddd;">
|
||||
|
||||
<!-- Add Connection Selection Field -->
|
||||
<div class="clr-row">
|
||||
<div class="clr-col-sm-12">
|
||||
<label for="connection">Connection</label>
|
||||
<select id="connection" formControlName="connection" [(ngModel)]="gadgetsEditdata.connection" class="clr-select">
|
||||
<select id="connection" formControlName="connection" [(ngModel)]="gadgetsEditdata.connection"
|
||||
class="clr-select">
|
||||
<option value="">Select Connection</option>
|
||||
<option *ngFor="let conn of sureconnectData" [value]="conn.id">
|
||||
{{conn.connection_name || conn.id}}
|
||||
@@ -149,8 +244,8 @@
|
||||
<div class="clr-col-sm-12">
|
||||
<label for="table">Api Url</label>
|
||||
<div><input type="urk" id="table" formControlName="table" class="clr-input"
|
||||
[(ngModel)]="gadgetsEditdata.table" style="width:90%"> <span><button class="btn btn-icon btn-primary"
|
||||
style="margin: 0px;" (click)="callApi(gadgetsEditdata.table)">
|
||||
[(ngModel)]="gadgetsEditdata.table" style="width:90%"> <span><button
|
||||
class="btn btn-icon btn-primary" style="margin: 0px;" (click)="callApi(gadgetsEditdata.table)">
|
||||
<clr-icon shape="redo"></clr-icon> </button></span></div>
|
||||
<!-- <select id="table" formControlName="table" [(ngModel)]="gadgetsEditdata.table" (change)="tablename($event.target.value)">
|
||||
<option value="null">choose Table</option>
|
||||
@@ -223,8 +318,8 @@
|
||||
</div>
|
||||
|
||||
<!-- Add Base Filter Button -->
|
||||
<button class="btn btn-sm btn-primary" (click)="addBaseFilter()" style="margin-top: 10px; margin-bottom: 10px;"
|
||||
[disabled]="gadgetsEditdata.commonFilterEnabled">
|
||||
<button class="btn btn-sm btn-primary" (click)="addBaseFilter()"
|
||||
style="margin-top: 10px; margin-bottom: 10px;" [disabled]="gadgetsEditdata.commonFilterEnabled">
|
||||
<clr-icon shape="plus"></clr-icon> Add Filter
|
||||
</button>
|
||||
|
||||
@@ -245,14 +340,14 @@
|
||||
[disabled]="gadgetsEditdata.commonFilterEnabled">
|
||||
<option value="">Select Field</option>
|
||||
<!-- Base API filters should always use columnData, not drilldownColumnData -->
|
||||
<option *ngFor="let column of getAvailableFields(gadgetsEditdata.baseFilters, i, columnData)" [value]="column">{{column}}</option>
|
||||
<option *ngFor="let column of getAvailableFields(gadgetsEditdata.baseFilters, i, columnData)"
|
||||
[value]="column">{{column}}</option>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<div class="clr-col-sm-5">
|
||||
<input type="text" [(ngModel)]="filter.value" [ngModelOptions]="{standalone: true}"
|
||||
class="clr-input" placeholder="Filter Value"
|
||||
[disabled]="gadgetsEditdata.commonFilterEnabled"/>
|
||||
<input type="text" [(ngModel)]="filter.value" [ngModelOptions]="{standalone: true}" class="clr-input"
|
||||
placeholder="Filter Value" [disabled]="gadgetsEditdata.commonFilterEnabled" />
|
||||
</div>
|
||||
|
||||
<div class="clr-col-sm-2">
|
||||
@@ -288,22 +383,25 @@
|
||||
<label for="drilldownApiUrl">Base Drilldown API URL</label>
|
||||
<div>
|
||||
<input type="text" id="drilldownApiUrl" formControlName="drilldownApiUrl" class="clr-input"
|
||||
[(ngModel)]="gadgetsEditdata.drilldownApiUrl" style="width:90%" [ngModelOptions]="{standalone: true}">
|
||||
[(ngModel)]="gadgetsEditdata.drilldownApiUrl" style="width:90%"
|
||||
[ngModelOptions]="{standalone: true}">
|
||||
<span>
|
||||
<button class="btn btn-icon btn-primary" style="margin: 0px;"
|
||||
(click)="refreshBaseDrilldownColumns()" [disabled]="!gadgetsEditdata.drilldownApiUrl">
|
||||
<button class="btn btn-icon btn-primary" style="margin: 0px;" (click)="refreshBaseDrilldownColumns()"
|
||||
[disabled]="!gadgetsEditdata.drilldownApiUrl">
|
||||
<clr-icon shape="redo"></clr-icon>
|
||||
</button>
|
||||
</span>
|
||||
</div>
|
||||
<div class="clr-subtext">Enter the API URL for base drilldown data. Use angle brackets for parameters, e.g., http://api.example.com/data/<country></div>
|
||||
<div class="clr-subtext">Enter the API URL for base drilldown data. Use angle brackets for parameters, e.g.,
|
||||
http://api.example.com/data/<country></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="clr-row" *ngIf="gadgetsEditdata.drilldownEnabled">
|
||||
<div class="clr-col-sm-12">
|
||||
<label for="drilldownXAxis">Base Drilldown X-Axis</label>
|
||||
<select id="drilldownXAxis" formControlName="drilldownXAxis" [(ngModel)]="gadgetsEditdata.drilldownXAxis" [ngModelOptions]="{standalone: true}">
|
||||
<select id="drilldownXAxis" formControlName="drilldownXAxis" [(ngModel)]="gadgetsEditdata.drilldownXAxis"
|
||||
[ngModelOptions]="{standalone: true}">
|
||||
<option value="">Select X-Axis Column</option>
|
||||
<option *ngFor="let column of drilldownColumnData" [value]="column">{{column}}</option>
|
||||
</select>
|
||||
@@ -317,7 +415,8 @@
|
||||
gadgetsEditdata?.fieldName !== 'To Do Chart'">
|
||||
<div class="clr-col-sm-12">
|
||||
<label for="drilldownYAxis">Base Drilldown Y-Axis</label>
|
||||
<select id="drilldownYAxis" formControlName="drilldownYAxis" [(ngModel)]="gadgetsEditdata.drilldownYAxis" [ngModelOptions]="{standalone: true}">
|
||||
<select id="drilldownYAxis" formControlName="drilldownYAxis" [(ngModel)]="gadgetsEditdata.drilldownYAxis"
|
||||
[ngModelOptions]="{standalone: true}">
|
||||
<option value="">Select Y-Axis Column</option>
|
||||
<option *ngFor="let column of drilldownColumnData" [value]="column">{{column}}</option>
|
||||
</select>
|
||||
@@ -329,11 +428,14 @@
|
||||
<div class="clr-row" *ngIf="gadgetsEditdata.drilldownEnabled">
|
||||
<div class="clr-col-sm-12">
|
||||
<label for="drilldownParameter">Base Drilldown Parameter</label>
|
||||
<select id="drilldownParameter" [(ngModel)]="gadgetsEditdata.drilldownParameter" [ngModelOptions]="{standalone: true}">
|
||||
<select id="drilldownParameter" [(ngModel)]="gadgetsEditdata.drilldownParameter"
|
||||
[ngModelOptions]="{standalone: true}">
|
||||
<option value="">Select Parameter Column</option>
|
||||
<option *ngFor="let column of drilldownColumnData" [value]="column">{{column}}</option>
|
||||
</select>
|
||||
<div class="clr-subtext">Select the column to use as parameter for URL template replacement in base drilldown</div>
|
||||
<div class="clr-subtext">Select the column to use as parameter for URL template replacement in base
|
||||
drilldown
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -347,16 +449,18 @@
|
||||
<div class="clr-form-control" style="margin-top: 10px;">
|
||||
<div class="clr-control-container">
|
||||
<div class="clr-checkbox-wrapper">
|
||||
<input type="checkbox" id="commonFilterToggleDrilldown" [(ngModel)]="gadgetsEditdata.commonFilterEnabledDrilldown"
|
||||
(change)="onCommonFilterToggleDrilldown()" [ngModelOptions]="{standalone: true}" class="clr-checkbox" />
|
||||
<input type="checkbox" id="commonFilterToggleDrilldown"
|
||||
[(ngModel)]="gadgetsEditdata.commonFilterEnabledDrilldown"
|
||||
(change)="onCommonFilterToggleDrilldown()" [ngModelOptions]="{standalone: true}"
|
||||
class="clr-checkbox" />
|
||||
<label for="commonFilterToggleDrilldown" class="clr-control-label">Use Common Filter</label>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Add Drilldown Filter Button -->
|
||||
<button class="btn btn-sm btn-primary" (click)="addDrilldownFilter()" style="margin-top: 10px; margin-bottom: 10px;"
|
||||
[disabled]="gadgetsEditdata.commonFilterEnabledDrilldown">
|
||||
<button class="btn btn-sm btn-primary" (click)="addDrilldownFilter()"
|
||||
style="margin-top: 10px; margin-bottom: 10px;" [disabled]="gadgetsEditdata.commonFilterEnabledDrilldown">
|
||||
<clr-icon shape="plus"></clr-icon> Add Filter
|
||||
</button>
|
||||
|
||||
@@ -376,14 +480,15 @@
|
||||
<select [(ngModel)]="filter.field" [ngModelOptions]="{standalone: true}" class="clr-select"
|
||||
[disabled]="gadgetsEditdata.commonFilterEnabledDrilldown">
|
||||
<option value="">Select Field</option>
|
||||
<option *ngFor="let column of getAvailableFields(gadgetsEditdata.drilldownFilters, i, drilldownColumnData)" [value]="column">{{column}}</option>
|
||||
<option
|
||||
*ngFor="let column of getAvailableFields(gadgetsEditdata.drilldownFilters, i, drilldownColumnData)"
|
||||
[value]="column">{{column}}</option>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<div class="clr-col-sm-5">
|
||||
<input type="text" [(ngModel)]="filter.value" [ngModelOptions]="{standalone: true}"
|
||||
class="clr-input" placeholder="Filter Value"
|
||||
[disabled]="gadgetsEditdata.commonFilterEnabledDrilldown"/>
|
||||
<input type="text" [(ngModel)]="filter.value" [ngModelOptions]="{standalone: true}" class="clr-input"
|
||||
placeholder="Filter Value" [disabled]="gadgetsEditdata.commonFilterEnabledDrilldown" />
|
||||
</div>
|
||||
|
||||
<div class="clr-col-sm-2">
|
||||
@@ -400,7 +505,8 @@
|
||||
|
||||
|
||||
<!-- Multi-Layer Drilldown Configurations -->
|
||||
<div class="clr-row" *ngIf="gadgetsEditdata.drilldownEnabled" style="margin-top: 20px; padding-top: 15px; border-top: 1px solid #ddd;">
|
||||
<div class="clr-row" *ngIf="gadgetsEditdata.drilldownEnabled"
|
||||
style="margin-top: 20px; padding-top: 15px; border-top: 1px solid #ddd;">
|
||||
<div class="clr-col-sm-12">
|
||||
<h4>Multi-Layer Drilldown Configurations</h4>
|
||||
<button class="btn btn-sm btn-primary" (click)="addDrilldownLayer()">
|
||||
@@ -412,7 +518,8 @@
|
||||
|
||||
<!-- Dynamic Drilldown Layers -->
|
||||
<div class="clr-row" *ngFor="let layer of gadgetsEditdata.drilldownLayers; let i = index">
|
||||
<div class="clr-col-sm-12" style="margin-top: 15px; padding: 10px; border: 1px solid #eee; border-radius: 4px;">
|
||||
<div class="clr-col-sm-12"
|
||||
style="margin-top: 15px; padding: 10px; border: 1px solid #eee; border-radius: 4px;">
|
||||
<div style="display: flex; justify-content: space-between; align-items: center;">
|
||||
<h5>Drilldown Layer {{i + 1}}</h5>
|
||||
<button class="btn btn-icon btn-danger btn-sm" (click)="removeDrilldownLayer(i)">
|
||||
@@ -423,7 +530,8 @@
|
||||
<div class="clr-form-control">
|
||||
<div class="clr-control-container">
|
||||
<div class="clr-checkbox-wrapper">
|
||||
<input type="checkbox" [id]="'layerEnabled' + i" [(ngModel)]="layer.enabled" class="clr-checkbox" [ngModelOptions]="{standalone: true}" />
|
||||
<input type="checkbox" [id]="'layerEnabled' + i" [(ngModel)]="layer.enabled" class="clr-checkbox"
|
||||
[ngModelOptions]="{standalone: true}" />
|
||||
<label [for]="'layerEnabled' + i" class="clr-control-label">Enable Layer {{i + 1}} Drilldown</label>
|
||||
</div>
|
||||
</div>
|
||||
@@ -433,8 +541,8 @@
|
||||
<div class="clr-col-sm-12">
|
||||
<label [for]="'layerApiUrl' + i">Layer {{i + 1}} API URL</label>
|
||||
<div>
|
||||
<input type="text" [id]="'layerApiUrl' + i" class="clr-input"
|
||||
[(ngModel)]="layer.apiUrl" style="width:90%" [ngModelOptions]="{standalone: true}">
|
||||
<input type="text" [id]="'layerApiUrl' + i" class="clr-input" [(ngModel)]="layer.apiUrl"
|
||||
style="width:90%" [ngModelOptions]="{standalone: true}">
|
||||
<span>
|
||||
<button class="btn btn-icon btn-primary" style="margin: 0px;"
|
||||
(click)="refreshDrilldownLayerColumns(i)" [disabled]="!layer.apiUrl">
|
||||
@@ -442,7 +550,8 @@
|
||||
</button>
|
||||
</span>
|
||||
</div>
|
||||
<div class="clr-subtext">Enter the API URL for layer {{i + 1}} drilldown data. Use angle brackets for parameters, e.g., http://api.example.com/data/<state></div>
|
||||
<div class="clr-subtext">Enter the API URL for layer {{i + 1}} drilldown data. Use angle brackets for
|
||||
parameters, e.g., http://api.example.com/data/<state></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -478,7 +587,9 @@
|
||||
<option value="">Select Parameter Column</option>
|
||||
<option *ngFor="let column of layerColumnData[i] || []" [value]="column">{{column}}</option>
|
||||
</select>
|
||||
<div class="clr-subtext">Select the column to use as parameter for URL template replacement in layer {{i + 1}} drilldown</div>
|
||||
<div class="clr-subtext">Select the column to use as parameter for URL template replacement in layer {{i
|
||||
+
|
||||
1}} drilldown</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -492,16 +603,17 @@
|
||||
<div class="clr-form-control" style="margin-top: 10px;">
|
||||
<div class="clr-control-container">
|
||||
<div class="clr-checkbox-wrapper">
|
||||
<input type="checkbox" [id]="'commonFilterToggleLayer' + i" [(ngModel)]="layer.commonFilterEnabled"
|
||||
(change)="onCommonFilterToggleLayer(i)" [ngModelOptions]="{standalone: true}" class="clr-checkbox" />
|
||||
<input type="checkbox" [id]="'commonFilterToggleLayer' + i"
|
||||
[(ngModel)]="layer.commonFilterEnabled" (change)="onCommonFilterToggleLayer(i)"
|
||||
[ngModelOptions]="{standalone: true}" class="clr-checkbox" />
|
||||
<label [for]="'commonFilterToggleLayer' + i" class="clr-control-label">Use Common Filter</label>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Add Layer Filter Button -->
|
||||
<button class="btn btn-sm btn-primary" (click)="addLayerFilter(i)" style="margin-top: 10px; margin-bottom: 10px;"
|
||||
[disabled]="layer.commonFilterEnabled">
|
||||
<button class="btn btn-sm btn-primary" (click)="addLayerFilter(i)"
|
||||
style="margin-top: 10px; margin-bottom: 10px;" [disabled]="layer.commonFilterEnabled">
|
||||
<clr-icon shape="plus"></clr-icon> Add Filter
|
||||
</button>
|
||||
|
||||
@@ -521,14 +633,14 @@
|
||||
<select [(ngModel)]="filter.field" [ngModelOptions]="{standalone: true}" class="clr-select"
|
||||
[disabled]="layer.commonFilterEnabled">
|
||||
<option value="">Select Field</option>
|
||||
<option *ngFor="let column of getAvailableFields(layer.filters, j, layerColumnData[i] || [])" [value]="column">{{column}}</option>
|
||||
<option *ngFor="let column of getAvailableFields(layer.filters, j, layerColumnData[i] || [])"
|
||||
[value]="column">{{column}}</option>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<div class="clr-col-sm-5">
|
||||
<input type="text" [(ngModel)]="filter.value" [ngModelOptions]="{standalone: true}"
|
||||
class="clr-input" placeholder="Filter Value"
|
||||
[disabled]="layer.commonFilterEnabled"/>
|
||||
class="clr-input" placeholder="Filter Value" [disabled]="layer.commonFilterEnabled" />
|
||||
</div>
|
||||
|
||||
<div class="clr-col-sm-2">
|
||||
@@ -550,6 +662,9 @@
|
||||
<input id="chartparameter" type="text" formControlName="chartparameter" class="clr-input" [(ngModel)]="gadgetsEditdata.chartparameter">
|
||||
</div>
|
||||
</div> -->
|
||||
|
||||
</div>
|
||||
|
||||
<div class="modal-footer">
|
||||
<button type="button" class="btn btn-outline" (click)="modeledit = false">Cancel</button>
|
||||
<button type="button" class="btn btn-primary" (click)="applyChanges(modelid)">Apply</button>
|
||||
@@ -568,7 +683,8 @@
|
||||
<div class="clr-row">
|
||||
<div class="clr-col-sm-12">
|
||||
<label for="commonFilterConnection">Connection</label>
|
||||
<select id="commonFilterConnection" formControlName="connection" [(ngModel)]="commonFilterData.connection" class="clr-select">
|
||||
<select id="commonFilterConnection" formControlName="connection" [(ngModel)]="commonFilterData.connection"
|
||||
class="clr-select">
|
||||
<option value="">Select Connection</option>
|
||||
<option *ngFor="let conn of sureconnectData" [value]="conn.id">
|
||||
{{conn.connection_name || conn.id}}
|
||||
@@ -584,8 +700,8 @@
|
||||
<input type="text" id="commonFilterApiUrl" formControlName="apiUrl" class="clr-input"
|
||||
[(ngModel)]="commonFilterData.apiUrl" style="width:90%">
|
||||
<span>
|
||||
<button class="btn btn-icon btn-primary" style="margin: 0px;"
|
||||
(click)="refreshCommonFilterColumns()" [disabled]="!commonFilterData.apiUrl">
|
||||
<button class="btn btn-icon btn-primary" style="margin: 0px;" (click)="refreshCommonFilterColumns()"
|
||||
[disabled]="!commonFilterData.apiUrl">
|
||||
<clr-icon shape="redo"></clr-icon>
|
||||
</button>
|
||||
</span>
|
||||
@@ -599,7 +715,8 @@
|
||||
<h5>Common Filters</h5>
|
||||
|
||||
<!-- Add Common Filter Button -->
|
||||
<button class="btn btn-sm btn-primary" (click)="addCommonFilter()" style="margin-top: 10px; margin-bottom: 10px;">
|
||||
<button class="btn btn-sm btn-primary" (click)="addCommonFilter()"
|
||||
style="margin-top: 10px; margin-bottom: 10px;">
|
||||
<clr-icon shape="plus"></clr-icon> Add Filter
|
||||
</button>
|
||||
|
||||
@@ -622,8 +739,8 @@
|
||||
</div>
|
||||
|
||||
<div class="clr-col-sm-5">
|
||||
<input type="text" [(ngModel)]="filter.value" [ngModelOptions]="{standalone: true}"
|
||||
class="clr-input" placeholder="Filter Value" />
|
||||
<input type="text" [(ngModel)]="filter.value" [ngModelOptions]="{standalone: true}" class="clr-input"
|
||||
placeholder="Filter Value" />
|
||||
</div>
|
||||
|
||||
<div class="clr-col-sm-2">
|
||||
|
||||
@@ -22,6 +22,10 @@ import { AlertsService } from 'src/app/services/fnd/alerts.service';
|
||||
import { isArray } from 'highcharts';
|
||||
// Add the SureconnectService import
|
||||
import { SureconnectService } from '../sureconnect/sureconnect.service';
|
||||
// Add the CommonFilterComponent import
|
||||
import { CommonFilterComponent } from '../common-filter/common-filter.component';
|
||||
// Add the CompactFilterComponent import
|
||||
import { CompactFilterComponent } from '../common-filter';
|
||||
|
||||
function isNullArray(arr) {
|
||||
return !Array.isArray(arr) || arr.length === 0;
|
||||
@@ -44,7 +48,17 @@ export class EditnewdashComponent implements OnInit {
|
||||
public entryForm: FormGroup;
|
||||
public commonFilterForm: FormGroup; // Add common filter form
|
||||
|
||||
// Add filterOptionsString property for compact filter
|
||||
filterOptionsString: string = '';
|
||||
|
||||
// Add availableKeys property for compact filter
|
||||
availableKeys: string[] = [];
|
||||
|
||||
WidgetsMock: WidgetModel[] = [
|
||||
{
|
||||
name: 'Common Filter',
|
||||
identifier: 'common_filter'
|
||||
},
|
||||
{
|
||||
name: 'Radar Chart',
|
||||
identifier: 'radar_chart'
|
||||
@@ -92,6 +106,10 @@ export class EditnewdashComponent implements OnInit {
|
||||
{
|
||||
name: 'Grid View',
|
||||
identifier: 'grid_view'
|
||||
},
|
||||
{
|
||||
name: 'Compact Filter',
|
||||
identifier: 'compact_filter'
|
||||
}
|
||||
]
|
||||
|
||||
@@ -104,6 +122,7 @@ export class EditnewdashComponent implements OnInit {
|
||||
public dashArr: [];
|
||||
|
||||
protected componentCollection = [
|
||||
{ name: "Common Filter", componentInstance: CommonFilterComponent },
|
||||
{ name: "Line Chart", componentInstance: LineChartComponent },
|
||||
{ name: "Doughnut Chart", componentInstance: DoughnutChartComponent },
|
||||
{ name: "Radar Chart", componentInstance: RadarChartComponent },
|
||||
@@ -116,6 +135,7 @@ export class EditnewdashComponent implements OnInit {
|
||||
{ name: "Financial Chart", componentInstance: FinancialChartComponent },
|
||||
{ name: "To Do Chart", componentInstance: ToDoChartComponent },
|
||||
{ name: "Grid View", componentInstance: GridViewComponent },
|
||||
{ name: "Compact Filter", componentInstance: CompactFilterComponent }, // Add this line
|
||||
];
|
||||
model: any;
|
||||
linesdata: any;
|
||||
@@ -161,7 +181,12 @@ export class EditnewdashComponent implements OnInit {
|
||||
drilldownLayers: [] as any[],
|
||||
// Common filter properties
|
||||
commonFilterEnabled: false,
|
||||
commonFilterEnabledDrilldown: false
|
||||
commonFilterEnabledDrilldown: false,
|
||||
// Compact filter properties
|
||||
filterKey: '',
|
||||
filterType: 'text',
|
||||
filterLabel: '',
|
||||
filterOptions: [] as string[]
|
||||
};
|
||||
|
||||
// Add sureconnect data property
|
||||
@@ -201,7 +226,9 @@ export class EditnewdashComponent implements OnInit {
|
||||
},
|
||||
displayGrid: "always",
|
||||
minCols: 10,
|
||||
minRows: 10
|
||||
minRows: 10,
|
||||
// Add resize callback to handle chart resizing
|
||||
itemResizeCallback: this.itemResize.bind(this)
|
||||
};
|
||||
|
||||
this.editId = this.route.snapshot.params.id;
|
||||
@@ -339,6 +366,16 @@ export class EditnewdashComponent implements OnInit {
|
||||
dashboard.component = component.componentInstance;
|
||||
}
|
||||
});
|
||||
|
||||
// Ensure compact filter configuration properties are properly initialized
|
||||
if (dashboard.component === 'Compact Filter' || dashboard.name === 'Compact Filter') {
|
||||
// Make sure all compact filter properties exist
|
||||
if (dashboard.filterKey === undefined) dashboard.filterKey = '';
|
||||
if (dashboard.filterType === undefined) dashboard.filterType = 'text';
|
||||
if (dashboard.filterLabel === undefined) dashboard.filterLabel = '';
|
||||
if (dashboard.filterOptions === undefined) dashboard.filterOptions = [];
|
||||
// table and connection properties should already exist for all components
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@@ -353,6 +390,16 @@ export class EditnewdashComponent implements OnInit {
|
||||
dashboard.component = component.name;
|
||||
}
|
||||
});
|
||||
|
||||
// Ensure compact filter configuration properties are preserved
|
||||
if (dashboard.name === 'Compact Filter') {
|
||||
// Make sure all compact filter properties exist
|
||||
if (dashboard.filterKey === undefined) dashboard.filterKey = '';
|
||||
if (dashboard.filterType === undefined) dashboard.filterType = 'text';
|
||||
if (dashboard.filterLabel === undefined) dashboard.filterLabel = '';
|
||||
if (dashboard.filterOptions === undefined) dashboard.filterOptions = [];
|
||||
// table and connection properties should already exist for all components
|
||||
}
|
||||
});
|
||||
}
|
||||
// Add method to get available fields for a filter dropdown (excluding already selected fields)
|
||||
@@ -500,6 +547,31 @@ export class EditnewdashComponent implements OnInit {
|
||||
component: ToDoChartComponent,
|
||||
name: "To Do Chart"
|
||||
});
|
||||
case "common_filter":
|
||||
return this.dashboardArray.push({
|
||||
cols: 10,
|
||||
rows: 3,
|
||||
x: 0,
|
||||
y: 0,
|
||||
chartid: maxChartId + 1,
|
||||
component: CommonFilterComponent,
|
||||
name: "Common Filter"
|
||||
});
|
||||
case "compact_filter":
|
||||
return this.dashboardArray.push({
|
||||
cols: 3,
|
||||
rows: 2,
|
||||
x: 0,
|
||||
y: 0,
|
||||
chartid: maxChartId + 1,
|
||||
component: CompactFilterComponent,
|
||||
name: "Compact Filter",
|
||||
// Add default configuration for compact filter
|
||||
filterKey: '',
|
||||
filterType: 'text',
|
||||
filterLabel: '',
|
||||
filterOptions: []
|
||||
});
|
||||
case "grid_view":
|
||||
return this.dashboardArray.push({
|
||||
cols: 5,
|
||||
@@ -542,6 +614,31 @@ export class EditnewdashComponent implements OnInit {
|
||||
if (item['commonFilterEnabledDrilldown'] === undefined) {
|
||||
this.gadgetsEditdata['commonFilterEnabledDrilldown'] = false;
|
||||
}
|
||||
// Initialize compact filter properties if not present
|
||||
if (item['filterKey'] === undefined) {
|
||||
this.gadgetsEditdata['filterKey'] = '';
|
||||
}
|
||||
if (item['filterType'] === undefined) {
|
||||
this.gadgetsEditdata['filterType'] = 'text';
|
||||
}
|
||||
if (item['filterLabel'] === undefined) {
|
||||
this.gadgetsEditdata['filterLabel'] = '';
|
||||
}
|
||||
if (item['filterOptions'] === undefined) {
|
||||
this.gadgetsEditdata['filterOptions'] = [];
|
||||
}
|
||||
|
||||
// Initialize filterOptionsString for compact filter
|
||||
if (item.name === 'Compact Filter') {
|
||||
this.filterOptionsString = this.gadgetsEditdata['filterOptions'].join(', ');
|
||||
// Load available keys when editing a compact filter
|
||||
if (this.gadgetsEditdata['table']) {
|
||||
this.loadAvailableKeys(this.gadgetsEditdata['table'], this.gadgetsEditdata['connection']);
|
||||
}
|
||||
} else {
|
||||
this.filterOptionsString = '';
|
||||
}
|
||||
|
||||
this.getStores();
|
||||
|
||||
// Set default connection if none is set and we have connections
|
||||
@@ -640,6 +737,9 @@ export class EditnewdashComponent implements OnInit {
|
||||
|
||||
//https://www.w3schools.com/js/tryit.asp?filename=tryjson_stringify_function_tostring
|
||||
|
||||
// First serialize the dashboard collection to ensure component names are properly set
|
||||
this.serialize(this.dashboardCollection.dashboard);
|
||||
|
||||
let cmp = this.dashboardCollection.dashboard.forEach(dashboard => {
|
||||
this.componentCollection.forEach(component => {
|
||||
if (dashboard.name === component.name) {
|
||||
@@ -660,8 +760,6 @@ export class EditnewdashComponent implements OnInit {
|
||||
//console.log(merged);
|
||||
console.log("temp data", typeof tmp);
|
||||
console.log(tmp);
|
||||
let parsed = JSON.parse(tmp);
|
||||
this.serialize(parsed.dashboard);
|
||||
this.dashbord1_Line.model = tmp;
|
||||
|
||||
// let obj = this.dashboardCollection;
|
||||
@@ -717,6 +815,21 @@ export class EditnewdashComponent implements OnInit {
|
||||
xyz.drilldownLayers = this.gadgetsEditdata.drilldownLayers;
|
||||
xyz.commonFilterEnabled = this.gadgetsEditdata.commonFilterEnabled; // Add common filter property
|
||||
|
||||
// For compact filter, preserve filter configuration properties
|
||||
if (item.name === 'Compact Filter') {
|
||||
xyz.filterKey = this.gadgetsEditdata.filterKey || '';
|
||||
xyz.filterType = this.gadgetsEditdata.filterType || 'text';
|
||||
xyz.filterLabel = this.gadgetsEditdata.filterLabel || '';
|
||||
// Convert filterOptionsString to array
|
||||
if (this.gadgetsEditdata.fieldName === 'Compact Filter') {
|
||||
xyz.filterOptions = this.filterOptionsString.split(',').map(opt => opt.trim()).filter(opt => opt);
|
||||
} else {
|
||||
xyz.filterOptions = this.gadgetsEditdata.filterOptions || [];
|
||||
}
|
||||
xyz.table = this.gadgetsEditdata.table || '';
|
||||
xyz.connection = this.gadgetsEditdata.connection || undefined;
|
||||
}
|
||||
|
||||
console.log(xyz);
|
||||
return xyz;
|
||||
}
|
||||
@@ -751,7 +864,97 @@ export class EditnewdashComponent implements OnInit {
|
||||
* 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
|
||||
// For CompactFilterComponent, pass only filter configuration properties
|
||||
if (item.name === 'Compact Filter') {
|
||||
const filterInputs = {
|
||||
filterKey: item['filterKey'] || '',
|
||||
filterType: item['filterType'] || 'text',
|
||||
filterLabel: item['filterLabel'] || '',
|
||||
filterOptions: item['filterOptions'] || [],
|
||||
apiUrl: item['table'] || '', // Use table as API URL
|
||||
connectionId: item['connection'] ? parseInt(item['connection'], 10) : undefined
|
||||
};
|
||||
|
||||
// Preserve configuration in the item itself
|
||||
item['filterKey'] = filterInputs['filterKey'];
|
||||
item['filterType'] = filterInputs['filterType'];
|
||||
item['filterLabel'] = filterInputs['filterLabel'];
|
||||
item['filterOptions'] = filterInputs['filterOptions'];
|
||||
item['table'] = filterInputs['apiUrl'];
|
||||
item['connection'] = item['connection'];
|
||||
|
||||
// Remove undefined properties to avoid passing unnecessary data
|
||||
Object.keys(filterInputs).forEach(key => {
|
||||
if (filterInputs[key] === undefined) {
|
||||
delete filterInputs[key];
|
||||
}
|
||||
});
|
||||
|
||||
return filterInputs;
|
||||
}
|
||||
|
||||
// For CommonFilterComponent, pass only filter-related properties
|
||||
if (item.component && item.component.name === 'CommonFilterComponent') {
|
||||
const commonFilterInputs = {
|
||||
baseFilters: item['baseFilters'] || [],
|
||||
drilldownFilters: item['drilldownFilters'] || [],
|
||||
drilldownLayers: item['drilldownLayers'] || [],
|
||||
fieldName: item['name'] || '',
|
||||
connection: item['connection'] || undefined
|
||||
};
|
||||
|
||||
// Remove undefined properties to avoid passing unnecessary data
|
||||
Object.keys(commonFilterInputs).forEach(key => {
|
||||
if (commonFilterInputs[key] === undefined) {
|
||||
delete commonFilterInputs[key];
|
||||
}
|
||||
});
|
||||
|
||||
return commonFilterInputs;
|
||||
}
|
||||
|
||||
// For GridViewComponent, pass chart properties with drilldown support
|
||||
if (item.component && item.component.name === 'GridViewComponent') {
|
||||
const gridInputs = {
|
||||
xAxis: item.xAxis,
|
||||
yAxis: item.yAxis,
|
||||
table: item.table,
|
||||
datastore: item.datastore,
|
||||
charttitle: item.charttitle,
|
||||
chartlegend: item.chartlegend,
|
||||
showlabel: item.showlabel,
|
||||
chartcolor: item.chartcolor,
|
||||
slices: item.slices,
|
||||
donut: item.donut,
|
||||
charturl: item.charturl,
|
||||
chartparameter: item.chartparameter,
|
||||
datasource: item.datasource,
|
||||
fieldName: item.name, // Using item.name as fieldName
|
||||
connection: item['connection'], // Add connection field using bracket notation
|
||||
// Base drilldown configuration properties
|
||||
drilldownEnabled: item['drilldownEnabled'],
|
||||
drilldownApiUrl: item['drilldownApiUrl'],
|
||||
// Removed drilldownParameterKey since we're using URL templates
|
||||
drilldownXAxis: item['drilldownXAxis'],
|
||||
drilldownYAxis: item['drilldownYAxis'],
|
||||
drilldownParameter: item['drilldownParameter'], // Add drilldown parameter
|
||||
baseFilters: item['baseFilters'] || [], // Add base filters
|
||||
drilldownFilters: item['drilldownFilters'] || [], // Add drilldown filters
|
||||
// Multi-layer drilldown configurations
|
||||
drilldownLayers: item['drilldownLayers'] || []
|
||||
};
|
||||
|
||||
// Remove undefined properties to avoid passing unnecessary data
|
||||
Object.keys(gridInputs).forEach(key => {
|
||||
if (gridInputs[key] === undefined) {
|
||||
delete gridInputs[key];
|
||||
}
|
||||
});
|
||||
|
||||
return gridInputs;
|
||||
}
|
||||
|
||||
// For all other chart components, pass chart-specific properties
|
||||
const chartInputs = {
|
||||
xAxis: item.xAxis,
|
||||
yAxis: item.yAxis,
|
||||
@@ -835,6 +1038,27 @@ export class EditnewdashComponent implements OnInit {
|
||||
updatedItem.commonFilterEnabled = this.gadgetsEditdata.commonFilterEnabled; // Add common filter property
|
||||
updatedItem.commonFilterEnabledDrilldown = this.gadgetsEditdata.commonFilterEnabledDrilldown; // Add drilldown common filter property
|
||||
|
||||
// For compact filter, preserve filter configuration properties
|
||||
if (item.name === 'Compact Filter') {
|
||||
updatedItem.filterKey = this.gadgetsEditdata.filterKey || '';
|
||||
updatedItem.filterType = this.gadgetsEditdata.filterType || 'text';
|
||||
updatedItem.filterLabel = this.gadgetsEditdata.filterLabel || '';
|
||||
// Convert filterOptionsString to array
|
||||
if (this.gadgetsEditdata.fieldName === 'Compact Filter') {
|
||||
updatedItem.filterOptions = this.filterOptionsString.split(',').map(opt => opt.trim()).filter(opt => opt);
|
||||
} else {
|
||||
updatedItem.filterOptions = this.gadgetsEditdata.filterOptions || [];
|
||||
}
|
||||
updatedItem.table = this.gadgetsEditdata.table || ''; // API URL
|
||||
updatedItem.connection = this.gadgetsEditdata.connection || undefined; // Connection ID
|
||||
|
||||
// Also preserve these properties in gadgetsEditdata for consistency
|
||||
this.gadgetsEditdata.filterKey = updatedItem.filterKey;
|
||||
this.gadgetsEditdata.filterType = updatedItem.filterType;
|
||||
this.gadgetsEditdata.filterLabel = updatedItem.filterLabel;
|
||||
this.gadgetsEditdata.filterOptions = updatedItem.filterOptions;
|
||||
}
|
||||
|
||||
console.log('Updated item:', updatedItem);
|
||||
return updatedItem;
|
||||
}
|
||||
@@ -1247,4 +1471,93 @@ export class EditnewdashComponent implements OnInit {
|
||||
// When disabling, the user can edit the filters normally
|
||||
}
|
||||
}
|
||||
|
||||
// Add method to handle item resize events
|
||||
itemResize(item: any, itemComponent: any) {
|
||||
console.log('Item resized:', item);
|
||||
// Trigger a window resize event to notify charts to resize
|
||||
window.dispatchEvent(new Event('resize'));
|
||||
|
||||
// Also try to directly notify the chart component if possible
|
||||
if (itemComponent && itemComponent.item && itemComponent.item.component) {
|
||||
// If the resized item contains a chart, we could try to call its resize method directly
|
||||
// This would require the chart component to have a public resize method
|
||||
}
|
||||
}
|
||||
|
||||
// Add method to load available keys for compact filter
|
||||
loadAvailableKeys(apiUrl: string, connectionId: string | undefined) {
|
||||
if (apiUrl) {
|
||||
const connectionIdNum = connectionId ? parseInt(connectionId, 10) : undefined;
|
||||
this.alertService.getColumnfromurl(apiUrl, connectionIdNum).subscribe(
|
||||
(keys: string[]) => {
|
||||
this.availableKeys = keys;
|
||||
},
|
||||
(error) => {
|
||||
console.error('Error loading available keys:', error);
|
||||
this.availableKeys = [];
|
||||
}
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// Add method to load available values for a specific key
|
||||
loadAvailableValues(key: string) {
|
||||
if (key && this.gadgetsEditdata['table']) {
|
||||
const connectionIdNum = this.gadgetsEditdata['connection'] ?
|
||||
parseInt(this.gadgetsEditdata['connection'], 10) : undefined;
|
||||
this.alertService.getValuesFromUrl(this.gadgetsEditdata['table'], connectionIdNum, key).subscribe(
|
||||
(values: string[]) => {
|
||||
// Update filter options string for dropdown/multiselect
|
||||
if (this.gadgetsEditdata['filterType'] === 'dropdown' ||
|
||||
this.gadgetsEditdata['filterType'] === 'multiselect') {
|
||||
this.filterOptionsString = values.join(', ');
|
||||
// Also update the gadgetsEditdata filterOptions array
|
||||
this.gadgetsEditdata['filterOptions'] = values;
|
||||
}
|
||||
},
|
||||
(error) => {
|
||||
console.error('Error loading available values:', error);
|
||||
}
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// Add method to handle filter key change
|
||||
onFilterKeyChange(key: string) {
|
||||
this.gadgetsEditdata['filterKey'] = key;
|
||||
// Load available values when filter key changes
|
||||
if (key && (this.gadgetsEditdata['filterType'] === 'dropdown' ||
|
||||
this.gadgetsEditdata['filterType'] === 'multiselect')) {
|
||||
this.loadAvailableValues(key);
|
||||
}
|
||||
}
|
||||
|
||||
// Add method to handle filter type change
|
||||
onFilterTypeChange(type: string) {
|
||||
this.gadgetsEditdata['filterType'] = type;
|
||||
// Load available values when filter type changes to dropdown or multiselect
|
||||
if ((type === 'dropdown' || type === 'multiselect') && this.gadgetsEditdata['filterKey']) {
|
||||
this.loadAvailableValues(this.gadgetsEditdata['filterKey']);
|
||||
}
|
||||
}
|
||||
|
||||
// Add method to handle API URL change for compact filter
|
||||
onCompactFilterApiUrlChange(url: string) {
|
||||
this.gadgetsEditdata['table'] = url;
|
||||
// Load available keys when API URL changes
|
||||
if (url) {
|
||||
this.loadAvailableKeys(url, this.gadgetsEditdata['connection']);
|
||||
}
|
||||
}
|
||||
|
||||
// Add method to handle connection change for compact filter
|
||||
onCompactFilterConnectionChange(connectionId: string) {
|
||||
this.gadgetsEditdata['connection'] = connectionId;
|
||||
// Reload available keys when connection changes
|
||||
if (this.gadgetsEditdata['table']) {
|
||||
this.loadAvailableKeys(this.gadgetsEditdata['table'], connectionId);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
Binary file not shown.
@@ -1,4 +1,4 @@
|
||||
<div style="display: block">
|
||||
<div style="display: block; height: 100%; width: 100%;">
|
||||
<!-- No filter controls needed with the new simplified approach -->
|
||||
<!-- Filters are now configured at the drilldown level -->
|
||||
|
||||
@@ -19,11 +19,12 @@
|
||||
</div>
|
||||
|
||||
<!-- Chart display -->
|
||||
<div *ngIf="!noDataAvailable">
|
||||
<div *ngIf="!noDataAvailable" style="position: relative; height: calc(100% - 50px);">
|
||||
<canvas baseChart
|
||||
[datasets]="barChartData"
|
||||
[labels]="barChartLabels"
|
||||
[type]="barChartType"
|
||||
[options]="barChartOptions"
|
||||
(chartHover)="chartHovered($event)"
|
||||
(chartClick)="chartClicked($event)">
|
||||
</canvas>
|
||||
|
||||
@@ -0,0 +1,31 @@
|
||||
// Bar Chart Component Styles
|
||||
:host {
|
||||
display: block;
|
||||
height: 100%;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.bar-chart-container {
|
||||
position: relative;
|
||||
height: 100%;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
canvas {
|
||||
display: block;
|
||||
max-width: 100%;
|
||||
max-height: 100%;
|
||||
}
|
||||
|
||||
// Responsive design for chart container
|
||||
@media (max-width: 768px) {
|
||||
.bar-chart-container {
|
||||
height: 300px;
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 480px) {
|
||||
.bar-chart-container {
|
||||
height: 250px;
|
||||
}
|
||||
}
|
||||
@@ -1,6 +1,9 @@
|
||||
import { Component, Input, OnInit, OnChanges, OnDestroy, SimpleChanges } from '@angular/core';
|
||||
import { Component, Input, OnInit, OnChanges, OnDestroy, SimpleChanges, ViewChild } from '@angular/core';
|
||||
import { Dashboard3Service } from '../../../../../../services/builder/dashboard3.service';
|
||||
import { Subscription } from 'rxjs';
|
||||
import { FilterService } from '../../common-filter/filter.service';
|
||||
// Add BaseChartDirective import for chart resizing
|
||||
import { BaseChartDirective } from 'ng2-charts';
|
||||
|
||||
@Component({
|
||||
selector: 'app-bar-chart',
|
||||
@@ -34,6 +37,9 @@ export class BarChartComponent implements OnInit, OnChanges, OnDestroy {
|
||||
// Multi-layer drilldown configuration inputs
|
||||
@Input() drilldownLayers: any[] = []; // Array of drilldown layer configurations
|
||||
|
||||
// Add ViewChild to access the chart directive
|
||||
@ViewChild(BaseChartDirective) chart?: BaseChartDirective;
|
||||
|
||||
barChartLabels: string[] = ['Apple', 'Banana', 'Kiwifruit', 'Blueberry', 'Orange', 'Grapes'];
|
||||
barChartType: string = 'bar';
|
||||
barChartPlugins = [];
|
||||
@@ -42,6 +48,33 @@ export class BarChartComponent implements OnInit, OnChanges, OnDestroy {
|
||||
];
|
||||
barChartLegend: boolean = true;
|
||||
|
||||
// Add responsive chart options
|
||||
barChartOptions: any = {
|
||||
responsive: true,
|
||||
maintainAspectRatio: false,
|
||||
scales: {
|
||||
x: {
|
||||
ticks: {
|
||||
autoSkip: false,
|
||||
maxRotation: 45,
|
||||
minRotation: 45
|
||||
}
|
||||
},
|
||||
y: {
|
||||
beginAtZero: true
|
||||
}
|
||||
},
|
||||
plugins: {
|
||||
legend: {
|
||||
display: true,
|
||||
position: 'top',
|
||||
},
|
||||
tooltip: {
|
||||
enabled: true
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
// Multi-layer drilldown state tracking
|
||||
drilldownStack: any[] = []; // Stack to track drilldown navigation history
|
||||
currentDrilldownLevel: number = 0; // Current drilldown level (0 = base level)
|
||||
@@ -57,9 +90,20 @@ export class BarChartComponent implements OnInit, OnChanges, OnDestroy {
|
||||
// Subscriptions to unsubscribe on destroy
|
||||
private subscriptions: Subscription[] = [];
|
||||
|
||||
constructor(private dashboardService: Dashboard3Service) { }
|
||||
constructor(
|
||||
private dashboardService: Dashboard3Service,
|
||||
private filterService: FilterService
|
||||
) { }
|
||||
|
||||
ngOnInit(): void {
|
||||
// Subscribe to filter changes
|
||||
this.subscriptions.push(
|
||||
this.filterService.filterState$.subscribe(filters => {
|
||||
// When filters change, refresh the chart data
|
||||
this.fetchChartData();
|
||||
})
|
||||
);
|
||||
|
||||
// Initialize with default data
|
||||
this.fetchChartData();
|
||||
}
|
||||
@@ -92,6 +136,7 @@ export class BarChartComponent implements OnInit, OnChanges, OnDestroy {
|
||||
// Update legend visibility if it changed
|
||||
if (changes.chartlegend !== undefined) {
|
||||
this.barChartLegend = changes.chartlegend.currentValue;
|
||||
this.barChartOptions.plugins.legend.display = this.barChartLegend;
|
||||
console.log('Chart legend changed to:', this.barChartLegend);
|
||||
}
|
||||
}
|
||||
@@ -110,10 +155,15 @@ export class BarChartComponent implements OnInit, OnChanges, OnDestroy {
|
||||
|
||||
// If we have the necessary data, fetch chart data from the service
|
||||
if (this.table && this.xAxis && this.yAxis) {
|
||||
console.log('Fetching bar chart data for:', { table: this.table, xAxis: this.xAxis, yAxis: this.yAxis, connection: this.connection });
|
||||
console.log('=== BAR CHART DEBUG INFO ===');
|
||||
console.log('Table:', this.table);
|
||||
console.log('X-Axis:', this.xAxis);
|
||||
console.log('Y-Axis:', this.yAxis);
|
||||
console.log('Connection:', this.connection);
|
||||
|
||||
// Convert yAxis to string if it's an array
|
||||
const yAxisString = Array.isArray(this.yAxis) ? this.yAxis.join(',') : this.yAxis;
|
||||
console.log('Y-Axis String:', yAxisString);
|
||||
|
||||
// Get the parameter value from the drilldown stack for base level (should be empty)
|
||||
let parameterValue = '';
|
||||
@@ -123,23 +173,46 @@ export class BarChartComponent implements OnInit, OnChanges, OnDestroy {
|
||||
console.log('Bar chart data URL:', url);
|
||||
|
||||
// Convert baseFilters to filter parameters
|
||||
let filterParams = '';
|
||||
if (this.baseFilters && this.baseFilters.length > 0) {
|
||||
const filterObj = {};
|
||||
|
||||
// Add base filters
|
||||
if (this.baseFilters && this.baseFilters.length > 0) {
|
||||
this.baseFilters.forEach(filter => {
|
||||
if (filter.field && filter.value) {
|
||||
filterObj[filter.field] = filter.value;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// Add common filters
|
||||
const commonFilters = this.filterService.getFilterValues();
|
||||
const filterDefinitions = this.filterService.getFilters();
|
||||
Object.keys(commonFilters).forEach(filterId => {
|
||||
const filterValue = commonFilters[filterId];
|
||||
|
||||
// Find the filter definition to get the field name
|
||||
const filterDef = this.filterService.getFilters().find(f => f.id === filterId);
|
||||
|
||||
if (filterDef && filterDef.field) {
|
||||
const fieldName = filterDef.field;
|
||||
if (filterValue !== undefined && filterValue !== null && filterValue !== '') {
|
||||
filterObj[fieldName] = filterValue;
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
// Convert to JSON string for API call
|
||||
let filterParams = '';
|
||||
if (Object.keys(filterObj).length > 0) {
|
||||
filterParams = JSON.stringify(filterObj);
|
||||
}
|
||||
}
|
||||
console.log('Base filter parameters:', filterParams);
|
||||
|
||||
console.log('Final filter object:', filterObj);
|
||||
// Fetch data from the dashboard service with parameter field and value
|
||||
// For base level, we pass empty parameter and value, but now also pass filters
|
||||
const subscription = this.dashboardService.getChartData(this.table, 'bar', this.xAxis, yAxisString, this.connection, '', '', filterParams).subscribe(
|
||||
(data: any) => {
|
||||
console.log('=== BAR CHART DATA RESPONSE ===');
|
||||
console.log('Received bar chart data:', data);
|
||||
if (data === null) {
|
||||
console.warn('Bar chart API returned null data. Check if the API endpoint is working correctly.');
|
||||
@@ -160,6 +233,7 @@ export class BarChartComponent implements OnInit, OnChanges, OnDestroy {
|
||||
// Trigger change detection
|
||||
// this.barChartData = [...this.barChartData];
|
||||
console.log('Updated bar chart with data:', { labels: this.barChartLabels, data: this.barChartData });
|
||||
console.log('=== CHART UPDATED SUCCESSFULLY ===');
|
||||
} else if (data && data.labels && data.datasets) {
|
||||
// Backend has already filtered the data, just display it
|
||||
this.noDataAvailable = data.labels.length === 0;
|
||||
@@ -168,6 +242,7 @@ export class BarChartComponent implements OnInit, OnChanges, OnDestroy {
|
||||
// Trigger change detection
|
||||
// this.barChartData = [...this.barChartData];
|
||||
console.log('Updated bar chart with legacy data format:', { labels: this.barChartLabels, data: this.barChartData });
|
||||
console.log('=== CHART UPDATED SUCCESSFULLY (LEGACY) ===');
|
||||
} else {
|
||||
console.warn('Bar chart received data does not have expected structure', data);
|
||||
this.noDataAvailable = true;
|
||||
@@ -178,6 +253,7 @@ export class BarChartComponent implements OnInit, OnChanges, OnDestroy {
|
||||
this.isFetchingData = false;
|
||||
},
|
||||
(error) => {
|
||||
console.error('=== BAR CHART ERROR ===');
|
||||
console.error('Error fetching bar chart data:', error);
|
||||
this.noDataAvailable = true;
|
||||
this.barChartLabels = [];
|
||||
@@ -286,33 +362,49 @@ export class BarChartComponent implements OnInit, OnChanges, OnDestroy {
|
||||
console.log('Drilldown data URL:', url);
|
||||
|
||||
// Convert drilldown layer filters to filter parameters (if applicable)
|
||||
let filterParams = '';
|
||||
if (drilldownConfig.filters && drilldownConfig.filters.length > 0) {
|
||||
const filterObj = {};
|
||||
|
||||
// Add drilldown layer filters
|
||||
if (drilldownConfig.filters && drilldownConfig.filters.length > 0) {
|
||||
drilldownConfig.filters.forEach((filter: any) => {
|
||||
if (filter.field && filter.value) {
|
||||
filterObj[filter.field] = filter.value;
|
||||
}
|
||||
});
|
||||
if (Object.keys(filterObj).length > 0) {
|
||||
filterParams = JSON.stringify(filterObj);
|
||||
}
|
||||
}
|
||||
console.log('Drilldown layer filter parameters:', filterParams);
|
||||
|
||||
// Convert drilldownFilters to filter parameters for drilldown level
|
||||
let drilldownFilterParams = '';
|
||||
// Add drilldownFilters
|
||||
if (this.drilldownFilters && this.drilldownFilters.length > 0) {
|
||||
const filterObj = {};
|
||||
this.drilldownFilters.forEach(filter => {
|
||||
if (filter.field && filter.value) {
|
||||
filterObj[filter.field] = filter.value;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// Add common filters
|
||||
const commonFilters = this.filterService.getFilterValues();
|
||||
const filterDefinitions = this.filterService.getFilters();
|
||||
Object.keys(commonFilters).forEach(filterId => {
|
||||
const filterValue = commonFilters[filterId];
|
||||
|
||||
// Find the filter definition to get the field name
|
||||
const filterDef = this.filterService.getFilters().find(f => f.id === filterId);
|
||||
|
||||
if (filterDef && filterDef.field) {
|
||||
const fieldName = filterDef.field;
|
||||
if (filterValue !== undefined && filterValue !== null && filterValue !== '') {
|
||||
filterObj[fieldName] = filterValue;
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
// Convert to JSON string for API call
|
||||
let drilldownFilterParams = '';
|
||||
if (Object.keys(filterObj).length > 0) {
|
||||
drilldownFilterParams = JSON.stringify(filterObj);
|
||||
}
|
||||
}
|
||||
|
||||
console.log('Drilldown filter parameters:', drilldownFilterParams);
|
||||
|
||||
// For drilldown level, we pass the parameter value from the drilldown stack and drilldown filters
|
||||
@@ -426,6 +518,18 @@ export class BarChartComponent implements OnInit, OnChanges, OnDestroy {
|
||||
}
|
||||
}
|
||||
|
||||
// Public method to refresh data when filters change
|
||||
refreshData(): void {
|
||||
this.fetchChartData();
|
||||
}
|
||||
|
||||
// Method to handle window resize events
|
||||
onResize(): void {
|
||||
if (this.chart) {
|
||||
this.chart.chart?.resize();
|
||||
}
|
||||
}
|
||||
|
||||
// Ensure labels and data arrays have the same length
|
||||
private syncLabelAndDataArrays(): void {
|
||||
// For bar charts, we need to ensure all datasets have the same number of data points
|
||||
|
||||
@@ -1,23 +1,30 @@
|
||||
<div class="doughnut-chart-container">
|
||||
<!-- Compact Filters -->
|
||||
<div class="compact-filters-container" *ngIf="baseFilters && baseFilters.length > 0">
|
||||
<app-compact-filter
|
||||
*ngFor="let filter of baseFilters"
|
||||
[filterKey]="filter.field"
|
||||
(filterChange)="onFilterChange($event)">
|
||||
</app-compact-filter>
|
||||
</div>
|
||||
|
||||
<!-- Drilldown mode indicator -->
|
||||
<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;">
|
||||
<div *ngIf="currentDrilldownLevel > 0" class="drilldown-indicator">
|
||||
<span class="drilldown-text">Drilldown Level: {{currentDrilldownLevel}}</span>
|
||||
<button class="btn btn-secondary btn-sm" (click)="navigateBack()">
|
||||
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;">
|
||||
<button class="btn btn-danger btn-sm" (click)="resetToOriginalData()">
|
||||
Back to Main View
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div class="chart-header">
|
||||
<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-wrapper">
|
||||
<div class="chart-content" [class.loading]="doughnutChartLabels.length === 0 && doughnutChartData.length === 0 && !noDataAvailable">
|
||||
<!-- Show no data message -->
|
||||
<div class="no-data-message" *ngIf="noDataAvailable">
|
||||
<p>No chart data available</p>
|
||||
@@ -33,7 +40,14 @@
|
||||
(chartHover)="chartHovered($event)"
|
||||
(chartClick)="chartClicked($event)">
|
||||
</canvas>
|
||||
|
||||
<!-- Loading overlay -->
|
||||
<div class="loading-overlay" *ngIf="doughnutChartLabels.length === 0 && doughnutChartData.length === 0 && !noDataAvailable">
|
||||
<div class="shimmer-donut"></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="chart-legend" *ngIf="!noDataAvailable && 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>
|
||||
|
||||
@@ -17,17 +17,78 @@
|
||||
transform: translateY(-2px);
|
||||
}
|
||||
|
||||
.compact-filters-container {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 5px;
|
||||
margin-bottom: 10px;
|
||||
padding: 5px;
|
||||
background: #ffffff;
|
||||
border: 1px solid #e9ecef;
|
||||
border-radius: 6px;
|
||||
min-height: 40px;
|
||||
}
|
||||
|
||||
|
||||
.chart-title {
|
||||
font-size: 26px;
|
||||
font-weight: 700;
|
||||
color: #2c3e50;
|
||||
margin-bottom: 20px;
|
||||
.drilldown-indicator {
|
||||
background-color: #e0e0e0;
|
||||
padding: 10px;
|
||||
margin-bottom: 15px;
|
||||
border-radius: 8px;
|
||||
text-align: center;
|
||||
padding-bottom: 15px;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
|
||||
}
|
||||
|
||||
.drilldown-text {
|
||||
font-weight: bold;
|
||||
color: #333;
|
||||
font-size: 16px;
|
||||
}
|
||||
|
||||
.btn {
|
||||
padding: 6px 12px;
|
||||
border: none;
|
||||
border-radius: 4px;
|
||||
cursor: pointer;
|
||||
font-size: 14px;
|
||||
transition: all 0.3s ease;
|
||||
}
|
||||
|
||||
.btn:hover {
|
||||
transform: translateY(-2px);
|
||||
box-shadow: 0 4px 8px rgba(0, 0, 0, 0.2);
|
||||
}
|
||||
|
||||
.btn-sm {
|
||||
padding: 4px 8px;
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
.btn-secondary {
|
||||
background-color: #007cba;
|
||||
color: white;
|
||||
}
|
||||
|
||||
.btn-danger {
|
||||
background-color: #dc3545;
|
||||
color: white;
|
||||
}
|
||||
|
||||
.chart-header {
|
||||
margin-bottom: 20px;
|
||||
|
||||
.chart-title {
|
||||
font-size: 22px;
|
||||
font-weight: 600;
|
||||
color: #0a192f;
|
||||
margin: 0;
|
||||
text-align: center;
|
||||
padding-bottom: 10px;
|
||||
border-bottom: 2px solid #3498db;
|
||||
text-shadow: 0 1px 2px rgba(0, 0, 0, 0.1);
|
||||
}
|
||||
}
|
||||
|
||||
.chart-wrapper {
|
||||
@@ -56,6 +117,62 @@
|
||||
transition: all 0.3s ease;
|
||||
}
|
||||
|
||||
.chart-content {
|
||||
position: relative;
|
||||
height: 100%;
|
||||
width: 100%;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
|
||||
&.loading {
|
||||
opacity: 0.7;
|
||||
|
||||
canvas {
|
||||
filter: blur(2px);
|
||||
}
|
||||
}
|
||||
|
||||
.no-data-message {
|
||||
text-align: center;
|
||||
padding: 30px;
|
||||
color: #666;
|
||||
font-size: 18px;
|
||||
font-style: italic;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
height: 100%;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.no-data-message p {
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.loading-overlay {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
background: rgba(255, 255, 255, 0.8);
|
||||
|
||||
.shimmer-donut {
|
||||
width: 120px;
|
||||
height: 120px;
|
||||
border-radius: 50%;
|
||||
background: linear-gradient(90deg, #f0f0f0 25%, #e0e0e0 50%, #f0f0f0 75%);
|
||||
background-size: 200% 100%;
|
||||
animation: shimmer 1.5s infinite;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.chart-legend {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
@@ -119,36 +236,13 @@
|
||||
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;
|
||||
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); }
|
||||
@keyframes shimmer {
|
||||
0% {
|
||||
background-position: -200% 0;
|
||||
}
|
||||
100% {
|
||||
background-position: 200% 0;
|
||||
}
|
||||
}
|
||||
|
||||
/* Responsive design */
|
||||
@@ -157,9 +251,17 @@
|
||||
padding: 15px;
|
||||
}
|
||||
|
||||
.chart-title {
|
||||
font-size: 20px;
|
||||
margin-bottom: 15px;
|
||||
.chart-header .chart-title {
|
||||
font-size: 18px;
|
||||
}
|
||||
|
||||
.drilldown-indicator {
|
||||
flex-direction: column;
|
||||
gap: 5px;
|
||||
}
|
||||
|
||||
.drilldown-text {
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.chart-wrapper {
|
||||
@@ -181,4 +283,8 @@
|
||||
font-size: 16px;
|
||||
padding: 20px;
|
||||
}
|
||||
|
||||
.compact-filters-container {
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
}
|
||||
@@ -1,5 +1,7 @@
|
||||
import { Component, OnInit, Input, OnChanges, SimpleChanges, AfterViewChecked } from '@angular/core';
|
||||
import { Component, OnInit, Input, OnChanges, SimpleChanges, AfterViewChecked, OnDestroy } from '@angular/core';
|
||||
import { Dashboard3Service } from 'src/app/services/builder/dashboard3.service';
|
||||
import { FilterService } from '../../common-filter/filter.service';
|
||||
import { Subscription } from 'rxjs';
|
||||
|
||||
@Component({
|
||||
selector: 'app-doughnut-chart',
|
||||
@@ -97,9 +99,23 @@ export class DoughnutChartComponent implements OnInit, OnChanges, AfterViewCheck
|
||||
// Flag to prevent infinite loops
|
||||
private isFetchingData: boolean = false;
|
||||
|
||||
constructor(private dashboardService: Dashboard3Service) { }
|
||||
// Subscriptions to unsubscribe on destroy
|
||||
private subscriptions: Subscription[] = [];
|
||||
|
||||
constructor(
|
||||
private dashboardService: Dashboard3Service,
|
||||
private filterService: FilterService
|
||||
) { }
|
||||
|
||||
ngOnInit(): void {
|
||||
// Subscribe to filter changes
|
||||
this.subscriptions.push(
|
||||
this.filterService.filterState$.subscribe(filters => {
|
||||
// When filters change, refresh the chart data
|
||||
this.fetchChartData();
|
||||
})
|
||||
);
|
||||
|
||||
// Validate initial data
|
||||
this.validateChartData();
|
||||
this.fetchChartData();
|
||||
@@ -180,6 +196,21 @@ export class DoughnutChartComponent implements OnInit, OnChanges, AfterViewCheck
|
||||
});
|
||||
}
|
||||
|
||||
ngOnDestroy(): void {
|
||||
this.subscriptions.forEach(sub => sub.unsubscribe());
|
||||
}
|
||||
|
||||
// Handle filter changes from compact filters
|
||||
onFilterChange(event: { filterId: string, value: any }): void {
|
||||
console.log('Compact filter changed:', event);
|
||||
// The filter service will automatically trigger chart updates through the subscription
|
||||
}
|
||||
|
||||
// Public method to refresh data when filters change
|
||||
refreshData(): void {
|
||||
this.fetchChartData();
|
||||
}
|
||||
|
||||
fetchChartData(): void {
|
||||
// Set flag to prevent recursive calls
|
||||
this.isFetchingData = true;
|
||||
@@ -212,7 +243,49 @@ export class DoughnutChartComponent implements OnInit, OnChanges, AfterViewCheck
|
||||
filterParams = JSON.stringify(filterObj);
|
||||
}
|
||||
}
|
||||
console.log('Base filter parameters:', filterParams);
|
||||
|
||||
// Add common filters to filter parameters
|
||||
const commonFilters = this.filterService.getFilterValues();
|
||||
console.log('Common filters from service:', commonFilters);
|
||||
|
||||
if (Object.keys(commonFilters).length > 0) {
|
||||
// Merge common filters with base filters
|
||||
const mergedFilterObj = {};
|
||||
|
||||
// Add base filters first
|
||||
if (filterParams) {
|
||||
try {
|
||||
const baseFilterObj = JSON.parse(filterParams);
|
||||
Object.assign(mergedFilterObj, baseFilterObj);
|
||||
} catch (e) {
|
||||
console.warn('Failed to parse base filter parameters:', e);
|
||||
}
|
||||
}
|
||||
|
||||
// Add common filters using the field name as the key, not the filter id
|
||||
Object.keys(commonFilters).forEach(filterId => {
|
||||
const filterValue = commonFilters[filterId];
|
||||
// Find the filter definition to get the field name
|
||||
const filterDef = this.filterService.getFilters().find(f => f.id === filterId);
|
||||
if (filterDef && filterDef.field) {
|
||||
const fieldName = filterDef.field;
|
||||
if (filterValue !== undefined && filterValue !== null && filterValue !== '') {
|
||||
mergedFilterObj[fieldName] = filterValue;
|
||||
}
|
||||
} else {
|
||||
// Fallback to using filterId as field name if no field is defined
|
||||
if (filterValue !== undefined && filterValue !== null && filterValue !== '') {
|
||||
mergedFilterObj[filterId] = filterValue;
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
if (Object.keys(mergedFilterObj).length > 0) {
|
||||
filterParams = JSON.stringify(mergedFilterObj);
|
||||
}
|
||||
}
|
||||
|
||||
console.log('Final filter parameters:', filterParams);
|
||||
|
||||
// Log the URL that will be called
|
||||
const url = `chart/getdashjson/doughnut?tableName=${this.table}&xAxis=${this.xAxis}&yAxes=${yAxisString}${this.connection ? `&sureId=${this.connection}` : ''}`;
|
||||
|
||||
@@ -4,9 +4,41 @@
|
||||
<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 +51,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,28 @@
|
||||
@import '../../../../../../../styles1.scss';
|
||||
input.ng-invalid.ng-touched {
|
||||
border-color: red;
|
||||
// Add styles for drilldown navigation
|
||||
.alert-info {
|
||||
background-color: #dcedf7;
|
||||
border-color: #a3d4f5;
|
||||
color: #21333b;
|
||||
}
|
||||
|
||||
.error_mess {
|
||||
color: red;
|
||||
.alert-info .alert-icon {
|
||||
color: #0072a3;
|
||||
}
|
||||
clr-datagrid{
|
||||
height: 400px; /* Adjust the height as needed */
|
||||
overflow-y: auto;
|
||||
|
||||
.btn-link {
|
||||
color: #0072a3;
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
.btn-link:hover {
|
||||
color: #00567a;
|
||||
text-decoration: underline;
|
||||
}
|
||||
|
||||
.dg-wrapper {
|
||||
padding: 12px;
|
||||
}
|
||||
|
||||
.clr-row {
|
||||
margin-bottom: 12px;
|
||||
}
|
||||
@@ -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,37 @@ 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[] = [];
|
||||
|
||||
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,12 +93,21 @@ 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;
|
||||
|
||||
// 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();
|
||||
}
|
||||
}
|
||||
@@ -68,20 +115,87 @@ export class GridViewComponent implements OnInit, OnChanges {
|
||||
// Dynamic headers for the grid
|
||||
|
||||
fetchGridData(): void {
|
||||
// Set flag to prevent recursive calls
|
||||
this.isFetchingData = true;
|
||||
|
||||
// If we're in drilldown mode, fetch the appropriate drilldown data
|
||||
if (this.currentDrilldownLevel > 0 && this.drilldownStack.length > 0) {
|
||||
this.fetchDrilldownData();
|
||||
// Reset flag after fetching
|
||||
this.isFetchingData = false;
|
||||
return;
|
||||
}
|
||||
|
||||
// If we have the necessary data, fetch grid data from the service
|
||||
if (this.table && this.xAxis) {
|
||||
console.log('Fetching grid data for:', { table: this.table, xAxis: this.xAxis, yAxis: this.yAxis, connection: this.connection });
|
||||
// if (this.table && this.xAxis) {
|
||||
if (this.table) {
|
||||
|
||||
console.log('=== GRID VIEW DEBUG INFO ===');
|
||||
console.log('Table:', this.table);
|
||||
console.log('X-Axis:', this.xAxis);
|
||||
console.log('Y-Axis:', this.yAxis);
|
||||
console.log('Connection:', this.connection);
|
||||
|
||||
// Convert yAxis to string if it's an array
|
||||
const yAxisString = Array.isArray(this.yAxis) ? this.yAxis.join(',') : this.yAxis;
|
||||
|
||||
// Get the parameter value from the drilldown stack for base level (should be empty)
|
||||
let parameterValue = '';
|
||||
|
||||
// Log the URL that will be called
|
||||
let url = `chart/getdashjson/grid?tableName=${this.table}&xAxis=${this.xAxis}&yAxes=${yAxisString}${this.connection ? `&sureId=${this.connection}` : ''}`;
|
||||
console.log('Grid data URL:', url);
|
||||
|
||||
// Get filter parameters from base filters
|
||||
const filterObj = {};
|
||||
|
||||
// Add base filters
|
||||
if (this.baseFilters && this.baseFilters.length > 0) {
|
||||
this.baseFilters.forEach(filter => {
|
||||
if (filter.field && filter.value) {
|
||||
filterObj[filter.field] = filter.value;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// Add common filters directly as key-value pairs
|
||||
const commonFilters = this.filterService.getFilterValues();
|
||||
const filterDefinitions = this.filterService.getFilters();
|
||||
|
||||
// Add common filters using the field name as the key
|
||||
Object.keys(commonFilters).forEach(filterId => {
|
||||
const filterValue = commonFilters[filterId];
|
||||
|
||||
// Find the filter definition to get the field name
|
||||
const filterDef = this.filterService.getFilters().find(f => f.id === filterId);
|
||||
|
||||
if (filterDef && filterDef.field) {
|
||||
const fieldName = filterDef.field;
|
||||
if (filterValue !== undefined && filterValue !== null && filterValue !== '') {
|
||||
filterObj[fieldName] = filterValue;
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
// Convert to JSON string for API call
|
||||
let filterParams = '';
|
||||
if (Object.keys(filterObj).length > 0) {
|
||||
filterParams = JSON.stringify(filterObj);
|
||||
}
|
||||
|
||||
console.log('GridView: Final filter object to send to API:', filterObj);
|
||||
|
||||
// Fetch data from the dashboard service, similar to other chart components
|
||||
this.dashboardService.getChartData(this.table, 'grid', this.xAxis, yAxisString, this.connection).subscribe(
|
||||
this.dashboardService.getChartData(this.table, 'grid', this.xAxis, yAxisString, this.connection, '', '', filterParams).subscribe(
|
||||
(data: any) => {
|
||||
console.log('=== GRID VIEW DATA RESPONSE ===');
|
||||
console.log('Received grid data:', data);
|
||||
if (data === null) {
|
||||
console.warn('Grid API returned null data. Check if the API endpoint is working correctly.');
|
||||
this.error = "No data Available";
|
||||
this.noDataAvailable = true;
|
||||
// Reset flag after fetching
|
||||
this.isFetchingData = false;
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -90,27 +204,36 @@ export class GridViewComponent implements OnInit, OnChanges {
|
||||
this.givendata = data.chartData;
|
||||
this.extractDynamicHeaders(data.chartData);
|
||||
this.error = this.givendata.length === 0 ? "No data Available" : undefined;
|
||||
this.noDataAvailable = this.givendata.length === 0;
|
||||
console.log('Updated grid with data:', this.givendata);
|
||||
} else if (data && data.data) {
|
||||
// Handle the original expected format as fallback
|
||||
this.givendata = data.data;
|
||||
this.extractDynamicHeaders(data.data);
|
||||
this.error = this.givendata.length === 0 ? "No data Available" : undefined;
|
||||
this.noDataAvailable = this.givendata.length === 0;
|
||||
console.log('Updated grid with legacy data format:', this.givendata);
|
||||
} else if (Array.isArray(data)) {
|
||||
// Handle case where data is directly an array
|
||||
this.givendata = data;
|
||||
this.extractDynamicHeaders(data);
|
||||
this.error = this.givendata.length === 0 ? "No data Available" : undefined;
|
||||
this.noDataAvailable = this.givendata.length === 0;
|
||||
console.log('Updated grid with array data:', this.givendata);
|
||||
} else {
|
||||
console.warn('Grid received data does not have expected structure', data);
|
||||
this.error = "No valid data received";
|
||||
this.givendata = [];
|
||||
this.noDataAvailable = true;
|
||||
}
|
||||
// Reset flag after fetching
|
||||
this.isFetchingData = false;
|
||||
}, (error) => {
|
||||
console.log('Error fetching grid data:', error);
|
||||
this.error = "Server Error";
|
||||
this.noDataAvailable = true;
|
||||
// Reset flag after fetching
|
||||
this.isFetchingData = false;
|
||||
});
|
||||
} else if (this.table) {
|
||||
console.log('Missing xAxis, falling back to default data fetching');
|
||||
@@ -126,13 +249,342 @@ export class GridViewComponent implements OnInit, OnChanges {
|
||||
this.givendata = Array.isArray(data) ? data : [];
|
||||
this.extractDynamicHeaders(data);
|
||||
this.error = this.givendata && this.givendata.length === 0 ? "No data Available" : undefined;
|
||||
this.noDataAvailable = this.givendata && this.givendata.length === 0;
|
||||
// Reset flag after fetching
|
||||
this.isFetchingData = false;
|
||||
}, (error) => {
|
||||
console.log(error);
|
||||
this.error = "Server Error";
|
||||
this.noDataAvailable = true;
|
||||
// Reset flag after fetching
|
||||
this.isFetchingData = false;
|
||||
});
|
||||
} else {
|
||||
console.log('Missing required data for grid:', { table: this.table });
|
||||
this.error = "Table name is required";
|
||||
this.noDataAvailable = true;
|
||||
// Reset flag after fetching
|
||||
this.isFetchingData = false;
|
||||
}
|
||||
}
|
||||
|
||||
// Fetch drilldown data based on current drilldown level
|
||||
fetchDrilldownData(): void {
|
||||
console.log('Fetching drilldown data, current level:', this.currentDrilldownLevel);
|
||||
console.log('Drilldown stack:', this.drilldownStack);
|
||||
|
||||
// Get the current drilldown configuration based on the current level
|
||||
let drilldownConfig;
|
||||
if (this.currentDrilldownLevel === 1) {
|
||||
// Base drilldown level
|
||||
drilldownConfig = {
|
||||
apiUrl: this.drilldownApiUrl,
|
||||
xAxis: this.drilldownXAxis,
|
||||
yAxis: this.drilldownYAxis,
|
||||
parameter: this.drilldownParameter
|
||||
};
|
||||
} else {
|
||||
// Multi-layer drilldown level
|
||||
const layerIndex = this.currentDrilldownLevel - 2; // -2 because level 1 is base drilldown
|
||||
if (layerIndex >= 0 && layerIndex < this.drilldownLayers.length) {
|
||||
drilldownConfig = this.drilldownLayers[layerIndex];
|
||||
} else {
|
||||
console.warn('Invalid drilldown layer index:', layerIndex);
|
||||
this.error = "Invalid drilldown configuration";
|
||||
this.givendata = [];
|
||||
this.noDataAvailable = true;
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
console.log('Drilldown config for level', this.currentDrilldownLevel, ':', drilldownConfig);
|
||||
|
||||
// Check if we have valid drilldown configuration
|
||||
if (!drilldownConfig || !drilldownConfig.apiUrl || !drilldownConfig.xAxis || !drilldownConfig.yAxis) {
|
||||
console.warn('Missing drilldown configuration for level:', this.currentDrilldownLevel);
|
||||
this.error = "Missing drilldown configuration";
|
||||
this.givendata = [];
|
||||
this.noDataAvailable = true;
|
||||
return;
|
||||
}
|
||||
|
||||
// Get the parameter value from the drilldown stack
|
||||
let parameterValue = '';
|
||||
if (this.drilldownStack.length > 0) {
|
||||
const lastEntry = this.drilldownStack[this.drilldownStack.length - 1];
|
||||
parameterValue = lastEntry.clickedValue || '';
|
||||
console.log('Parameter value from last click:', parameterValue);
|
||||
}
|
||||
|
||||
// Get the parameter field from drilldown config
|
||||
const parameterField = drilldownConfig.parameter || '';
|
||||
console.log('Parameter field:', parameterField);
|
||||
|
||||
console.log('Fetching drilldown data for level:', this.currentDrilldownLevel, {
|
||||
apiUrl: drilldownConfig.apiUrl,
|
||||
xAxis: drilldownConfig.xAxis,
|
||||
yAxis: drilldownConfig.yAxis,
|
||||
parameterField: parameterField,
|
||||
parameterValue: parameterValue,
|
||||
connection: this.connection
|
||||
});
|
||||
|
||||
// Build the actual API URL with parameter replacement
|
||||
let actualApiUrl = drilldownConfig.apiUrl;
|
||||
console.log('Original API URL:', actualApiUrl);
|
||||
console.log('Parameter value to use:', parameterValue);
|
||||
console.log('Parameter field:', parameterField);
|
||||
|
||||
// Check if the URL contains angle brackets for parameter replacement
|
||||
const hasAngleBrackets = /<[^>]+>/.test(actualApiUrl);
|
||||
|
||||
if (hasAngleBrackets && parameterValue) {
|
||||
// Replace angle brackets placeholder with actual value
|
||||
console.log('Replacing angle brackets with parameter value');
|
||||
const encodedValue = encodeURIComponent(parameterValue);
|
||||
actualApiUrl = actualApiUrl.replace(/<[^>]+>/g, encodedValue);
|
||||
console.log('URL after angle bracket replacement:', actualApiUrl);
|
||||
}
|
||||
|
||||
// Log the URL that will be called
|
||||
let url = `chart/getdashjson/grid?tableName=${actualApiUrl}&xAxis=${drilldownConfig.xAxis}&yAxes=${drilldownConfig.yAxis}${this.connection ? `&sureId=${this.connection}` : ''}`;
|
||||
if (parameterField && parameterValue) {
|
||||
url += `¶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 +627,23 @@ export class GridViewComponent implements OnInit, OnChanges {
|
||||
.replace(/([A-Z])/g, ' $1')
|
||||
.replace(/^./, str => str.toUpperCase());
|
||||
}
|
||||
|
||||
ngOnDestroy() {
|
||||
// Unsubscribe from all subscriptions to prevent memory leaks
|
||||
console.log('GridViewComponent ngOnDestroy called, unsubscribing from', this.subscriptions.length, 'subscriptions');
|
||||
this.subscriptions.forEach(subscription => {
|
||||
if (subscription && !subscription.closed) {
|
||||
subscription.unsubscribe();
|
||||
}
|
||||
});
|
||||
this.subscriptions = [];
|
||||
|
||||
// Clear data to help with garbage collection
|
||||
this.givendata = [];
|
||||
this.dynamicHeaders = [];
|
||||
this.drilldownStack = [];
|
||||
this.originalGridData = [];
|
||||
|
||||
console.log('GridViewComponent destroyed and cleaned up');
|
||||
}
|
||||
}
|
||||
@@ -1,5 +1,7 @@
|
||||
import { Component, OnInit, Input, OnChanges, SimpleChanges } from '@angular/core';
|
||||
import { Component, OnInit, Input, OnChanges, SimpleChanges, OnDestroy } from '@angular/core';
|
||||
import { Dashboard3Service } from 'src/app/services/builder/dashboard3.service';
|
||||
import { FilterService } from '../../common-filter/filter.service';
|
||||
import { Subscription } from 'rxjs';
|
||||
|
||||
@Component({
|
||||
selector: 'app-line-chart',
|
||||
@@ -83,9 +85,23 @@ export class LineChartComponent implements OnInit, OnChanges {
|
||||
// Flag to prevent infinite loops
|
||||
private isFetchingData: boolean = false;
|
||||
|
||||
constructor(private dashboardService: Dashboard3Service) { }
|
||||
// Subscriptions to unsubscribe on destroy
|
||||
private subscriptions: Subscription[] = [];
|
||||
|
||||
constructor(
|
||||
private dashboardService: Dashboard3Service,
|
||||
private filterService: FilterService
|
||||
) { }
|
||||
|
||||
ngOnInit(): void {
|
||||
// Subscribe to filter changes
|
||||
this.subscriptions.push(
|
||||
this.filterService.filterState$.subscribe(filters => {
|
||||
// When filters change, refresh the chart data
|
||||
this.fetchChartData();
|
||||
})
|
||||
);
|
||||
|
||||
// Initialize with default data
|
||||
this.fetchChartData();
|
||||
}
|
||||
@@ -122,6 +138,15 @@ export class LineChartComponent implements OnInit, OnChanges {
|
||||
}
|
||||
}
|
||||
|
||||
ngOnDestroy(): void {
|
||||
this.subscriptions.forEach(sub => sub.unsubscribe());
|
||||
}
|
||||
|
||||
// Public method to refresh data when filters change
|
||||
refreshData(): void {
|
||||
this.fetchChartData();
|
||||
}
|
||||
|
||||
fetchChartData(): void {
|
||||
// Set flag to prevent recursive calls
|
||||
this.isFetchingData = true;
|
||||
@@ -154,7 +179,49 @@ export class LineChartComponent implements OnInit, OnChanges {
|
||||
filterParams = JSON.stringify(filterObj);
|
||||
}
|
||||
}
|
||||
console.log('Base filter parameters:', filterParams);
|
||||
|
||||
// Add common filters to filter parameters
|
||||
const commonFilters = this.filterService.getFilterValues();
|
||||
console.log('Common filters from service:', commonFilters);
|
||||
|
||||
if (Object.keys(commonFilters).length > 0) {
|
||||
// Merge common filters with base filters
|
||||
const mergedFilterObj = {};
|
||||
|
||||
// Add base filters first
|
||||
if (filterParams) {
|
||||
try {
|
||||
const baseFilterObj = JSON.parse(filterParams);
|
||||
Object.assign(mergedFilterObj, baseFilterObj);
|
||||
} catch (e) {
|
||||
console.warn('Failed to parse base filter parameters:', e);
|
||||
}
|
||||
}
|
||||
|
||||
// Add common filters using the field name as the key, not the filter id
|
||||
Object.keys(commonFilters).forEach(filterId => {
|
||||
const filterValue = commonFilters[filterId];
|
||||
// Find the filter definition to get the field name
|
||||
const filterDef = this.filterService.getFilters().find(f => f.id === filterId);
|
||||
if (filterDef && filterDef.field) {
|
||||
const fieldName = filterDef.field;
|
||||
if (filterValue !== undefined && filterValue !== null && filterValue !== '') {
|
||||
mergedFilterObj[fieldName] = filterValue;
|
||||
}
|
||||
} else {
|
||||
// Fallback to using filterId as field name if no field is defined
|
||||
if (filterValue !== undefined && filterValue !== null && filterValue !== '') {
|
||||
mergedFilterObj[filterId] = filterValue;
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
if (Object.keys(mergedFilterObj).length > 0) {
|
||||
filterParams = JSON.stringify(mergedFilterObj);
|
||||
}
|
||||
}
|
||||
|
||||
console.log('Final filter parameters:', filterParams);
|
||||
|
||||
// Log the URL that will be called
|
||||
const url = `chart/getdashjson/line?tableName=${this.table}&xAxis=${this.xAxis}&yAxes=${yAxisString}${this.connection ? `&sureId=${this.connection}` : ''}`;
|
||||
@@ -313,6 +380,35 @@ export class LineChartComponent implements OnInit, OnChanges {
|
||||
}
|
||||
}
|
||||
|
||||
// Add common filters to drilldown filter parameters
|
||||
const commonFilters = this.filterService.getFilterValues();
|
||||
if (Object.keys(commonFilters).length > 0) {
|
||||
// Merge common filters with drilldown filters
|
||||
const mergedFilterObj = {};
|
||||
|
||||
// Add drilldown filters first
|
||||
if (filterParams) {
|
||||
try {
|
||||
const drilldownFilterObj = JSON.parse(filterParams);
|
||||
Object.assign(mergedFilterObj, drilldownFilterObj);
|
||||
} catch (e) {
|
||||
console.warn('Failed to parse drilldown filter parameters:', e);
|
||||
}
|
||||
}
|
||||
|
||||
// Add common filters
|
||||
Object.keys(commonFilters).forEach(key => {
|
||||
const value = commonFilters[key];
|
||||
if (value !== undefined && value !== null && value !== '') {
|
||||
mergedFilterObj[key] = value;
|
||||
}
|
||||
});
|
||||
|
||||
if (Object.keys(mergedFilterObj).length > 0) {
|
||||
filterParams = JSON.stringify(mergedFilterObj);
|
||||
}
|
||||
}
|
||||
|
||||
// Log the URL that will be called
|
||||
const url = `chart/getdashjson/line?tableName=${actualApiUrl}&xAxis=${drilldownConfig.xAxis}&yAxes=${drilldownConfig.yAxis}${this.connection ? `&sureId=${this.connection}` : ''}`;
|
||||
console.log('Drilldown data URL:', url);
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
import { Component, OnInit, Input, OnChanges, SimpleChanges, AfterViewChecked } from '@angular/core';
|
||||
import { Component, OnInit, Input, OnChanges, SimpleChanges, AfterViewChecked, OnDestroy } from '@angular/core';
|
||||
import { Dashboard3Service } from 'src/app/services/builder/dashboard3.service';
|
||||
import { FilterService } from '../../common-filter/filter.service';
|
||||
import { Subscription } from 'rxjs';
|
||||
|
||||
@Component({
|
||||
selector: 'app-pie-chart',
|
||||
@@ -96,7 +98,13 @@ export class PieChartComponent implements OnInit, OnChanges, AfterViewChecked {
|
||||
// Flag to prevent infinite loops
|
||||
private isFetchingData: boolean = false;
|
||||
|
||||
constructor(private dashboardService: Dashboard3Service) { }
|
||||
// Subscriptions to unsubscribe on destroy
|
||||
private subscriptions: Subscription[] = [];
|
||||
|
||||
constructor(
|
||||
private dashboardService: Dashboard3Service,
|
||||
private filterService: FilterService
|
||||
) { }
|
||||
|
||||
/**
|
||||
* Force chart redraw
|
||||
@@ -108,6 +116,14 @@ export class PieChartComponent implements OnInit, OnChanges, AfterViewChecked {
|
||||
}
|
||||
|
||||
ngOnInit(): void {
|
||||
// Subscribe to filter changes
|
||||
this.subscriptions.push(
|
||||
this.filterService.filterState$.subscribe(filters => {
|
||||
// When filters change, refresh the chart data
|
||||
this.fetchChartData();
|
||||
})
|
||||
);
|
||||
|
||||
console.log('PieChartComponent initialized with default data:', { labels: this.pieChartLabels, data: this.pieChartData });
|
||||
// Validate initial data
|
||||
this.validateChartData();
|
||||
@@ -140,6 +156,15 @@ export class PieChartComponent implements OnInit, OnChanges, AfterViewChecked {
|
||||
}
|
||||
}
|
||||
|
||||
ngOnDestroy(): void {
|
||||
this.subscriptions.forEach(sub => sub.unsubscribe());
|
||||
}
|
||||
|
||||
// Public method to refresh data when filters change
|
||||
refreshData(): void {
|
||||
this.fetchChartData();
|
||||
}
|
||||
|
||||
fetchChartData(): void {
|
||||
// Set flag to prevent recursive calls
|
||||
this.isFetchingData = true;
|
||||
@@ -172,7 +197,49 @@ export class PieChartComponent implements OnInit, OnChanges, AfterViewChecked {
|
||||
filterParams = JSON.stringify(filterObj);
|
||||
}
|
||||
}
|
||||
console.log('Base filter parameters:', filterParams);
|
||||
|
||||
// Add common filters to filter parameters
|
||||
const commonFilters = this.filterService.getFilterValues();
|
||||
console.log('Common filters from service:', commonFilters);
|
||||
|
||||
if (Object.keys(commonFilters).length > 0) {
|
||||
// Merge common filters with base filters
|
||||
const mergedFilterObj = {};
|
||||
|
||||
// Add base filters first
|
||||
if (filterParams) {
|
||||
try {
|
||||
const baseFilterObj = JSON.parse(filterParams);
|
||||
Object.assign(mergedFilterObj, baseFilterObj);
|
||||
} catch (e) {
|
||||
console.warn('Failed to parse base filter parameters:', e);
|
||||
}
|
||||
}
|
||||
|
||||
// Add common filters using the field name as the key, not the filter id
|
||||
Object.keys(commonFilters).forEach(filterId => {
|
||||
const filterValue = commonFilters[filterId];
|
||||
// Find the filter definition to get the field name
|
||||
const filterDef = this.filterService.getFilters().find(f => f.id === filterId);
|
||||
if (filterDef && filterDef.field) {
|
||||
const fieldName = filterDef.field;
|
||||
if (filterValue !== undefined && filterValue !== null && filterValue !== '') {
|
||||
mergedFilterObj[fieldName] = filterValue;
|
||||
}
|
||||
} else {
|
||||
// Fallback to using filterId as field name if no field is defined
|
||||
if (filterValue !== undefined && filterValue !== null && filterValue !== '') {
|
||||
mergedFilterObj[filterId] = filterValue;
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
if (Object.keys(mergedFilterObj).length > 0) {
|
||||
filterParams = JSON.stringify(mergedFilterObj);
|
||||
}
|
||||
}
|
||||
|
||||
console.log('Final filter parameters:', filterParams);
|
||||
|
||||
// Log the URL that will be called
|
||||
const url = `chart/getdashjson/pie?tableName=${this.table}&xAxis=${this.xAxis}&yAxes=${yAxisString}${this.connection ? `&sureId=${this.connection}` : ''}`;
|
||||
@@ -362,6 +429,35 @@ export class PieChartComponent implements OnInit, OnChanges, AfterViewChecked {
|
||||
}
|
||||
}
|
||||
|
||||
// Add common filters to drilldown filter parameters
|
||||
const commonFilters = this.filterService.getFilterValues();
|
||||
if (Object.keys(commonFilters).length > 0) {
|
||||
// Merge common filters with drilldown filters
|
||||
const mergedFilterObj = {};
|
||||
|
||||
// Add drilldown filters first
|
||||
if (filterParams) {
|
||||
try {
|
||||
const drilldownFilterObj = JSON.parse(filterParams);
|
||||
Object.assign(mergedFilterObj, drilldownFilterObj);
|
||||
} catch (e) {
|
||||
console.warn('Failed to parse drilldown filter parameters:', e);
|
||||
}
|
||||
}
|
||||
|
||||
// Add common filters
|
||||
Object.keys(commonFilters).forEach(key => {
|
||||
const value = commonFilters[key];
|
||||
if (value !== undefined && value !== null && value !== '') {
|
||||
mergedFilterObj[key] = value;
|
||||
}
|
||||
});
|
||||
|
||||
if (Object.keys(mergedFilterObj).length > 0) {
|
||||
filterParams = JSON.stringify(mergedFilterObj);
|
||||
}
|
||||
}
|
||||
|
||||
// Log the URL that will be called
|
||||
const url = `chart/getdashjson/pie?tableName=${actualApiUrl}&xAxis=${drilldownConfig.xAxis}&yAxes=${drilldownConfig.yAxis}${this.connection ? `&sureId=${this.connection}` : ''}`;
|
||||
console.log('Drilldown data URL:', url);
|
||||
|
||||
@@ -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 { }
|
||||
@@ -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>
|
||||
|
||||
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
//////////////////////////////////////////////
|
||||
|
||||
|
||||
@@ -26,7 +26,14 @@
|
||||
<!-- <span><button class="btn btn-primary" (click)="Export(item.name)">Export</button></span> -->
|
||||
<!-- <span><app-line-runner (buttonClicked)="generatePDFFile()"></app-line-runner></span> -->
|
||||
<!-- <h4 style="margin-top: 10px; margin-left: 10px;">{{ item.charttitle }}</h4> -->
|
||||
<ndc-dynamic class="no-drag" [ndcDynamicComponent]="item.component" (moduleInfo)="display($event)"></ndc-dynamic>
|
||||
|
||||
|
||||
|
||||
<ndc-dynamic class="no-drag"
|
||||
[ndcDynamicComponent]="item.component"
|
||||
[ndcDynamicInputs]="getComponentInputs(item)"
|
||||
(moduleInfo)="display($event)">
|
||||
</ndc-dynamic>
|
||||
</gridster-item>
|
||||
</gridster>
|
||||
</div>
|
||||
|
||||
@@ -17,6 +17,10 @@ import { BubbleRunnerComponent } from './bubble-runner/bubble-runner.component';
|
||||
import { ScatterRunnerComponent } from './scatter-runner/scatter-runner.component';
|
||||
import { PolarRunnerComponent } from './polar-runner/polar-runner.component';
|
||||
import { RadarRunnerComponent } from './radar-runner/radar-runner.component';
|
||||
// Add FilterService import
|
||||
import { FilterService } from '../../dashboardnew/common-filter/filter.service';
|
||||
// Add CompactFilterRunnerComponent import
|
||||
import { CompactFilterRunnerComponent } from './compact-filter-runner/compact-filter-runner.component';
|
||||
|
||||
@Component({
|
||||
selector: 'app-dashrunnerline',
|
||||
@@ -44,10 +48,13 @@ export class DashrunnerlineComponent implements OnInit {
|
||||
{ name: "Radar Chart", componentInstance: RadarRunnerComponent },
|
||||
{ name: "Grid View", componentInstance: GridRunnerComponent },
|
||||
{ name: "To Do Chart", componentInstance: TodoRunnerComponent },
|
||||
{ name: "Compact Filter", componentInstance: CompactFilterRunnerComponent }, // Add Compact Filter Runner
|
||||
];
|
||||
|
||||
constructor(private Dashtestservive:DashrunnerService, private dashboardService: Dashboard3Service,private route: ActivatedRoute,
|
||||
private router : Router,) { }
|
||||
private router : Router,
|
||||
// Add FilterService to constructor
|
||||
private filterService: FilterService) { }
|
||||
|
||||
ngOnInit(): void {
|
||||
|
||||
@@ -288,4 +295,54 @@ dashboard_name = "Dashtest";
|
||||
console.log('Button clicked in SomeComponent');
|
||||
// Add your custom logic here when the button is clicked in SomeComponent
|
||||
}
|
||||
|
||||
// Method to provide inputs for dynamic components based on their type
|
||||
getComponentInputs(item: any): any {
|
||||
const inputs: any = {};
|
||||
|
||||
// Common inputs for all components
|
||||
if (item.table !== undefined) inputs.table = item.table;
|
||||
if (item.xAxis !== undefined) inputs.xAxis = item.xAxis;
|
||||
if (item.yAxis !== undefined) inputs.yAxis = item.yAxis;
|
||||
if (item.connection !== undefined) inputs.connection = item.connection;
|
||||
if (item.charttitle !== undefined) inputs.charttitle = item.charttitle;
|
||||
if (item.chartlegend !== undefined) inputs.chartlegend = item.chartlegend;
|
||||
if (item.showlabel !== undefined) inputs.showlabel = item.showlabel;
|
||||
|
||||
// Compact Filter specific inputs
|
||||
if (item.name === 'Compact Filter') {
|
||||
console.log('=== COMPACT FILTER INPUTS DEBUG ===');
|
||||
console.log('Item data for compact filter:', item);
|
||||
|
||||
if (item.filterKey !== undefined) inputs.filterKey = item.filterKey;
|
||||
if (item.filterType !== undefined) inputs.filterType = item.filterType;
|
||||
if (item.filterLabel !== undefined) inputs.filterLabel = item.filterLabel;
|
||||
if (item.filterOptions !== undefined) inputs.filterOptions = item.filterOptions;
|
||||
if (item.table !== undefined) inputs.apiUrl = item.table; // Use table as API URL for compact filter
|
||||
if (item.connection !== undefined) inputs.connection = item.connection ? parseInt(item.connection, 10) : undefined;
|
||||
|
||||
console.log('Final inputs for compact filter:', inputs);
|
||||
console.log('==============================');
|
||||
}
|
||||
|
||||
// Grid View specific inputs
|
||||
if (item.name === 'Grid View') {
|
||||
if (item.baseFilters !== undefined) inputs.baseFilters = item.baseFilters;
|
||||
}
|
||||
|
||||
// Chart specific inputs
|
||||
if (item.name.includes('Chart') && item.name !== 'Compact Filter') {
|
||||
if (item.baseFilters !== undefined) inputs.baseFilters = item.baseFilters;
|
||||
if (item.drilldownEnabled !== undefined) inputs.drilldownEnabled = item.drilldownEnabled;
|
||||
if (item.drilldownApiUrl !== undefined) inputs.drilldownApiUrl = item.drilldownApiUrl;
|
||||
if (item.drilldownXAxis !== undefined) inputs.drilldownXAxis = item.drilldownXAxis;
|
||||
if (item.drilldownYAxis !== undefined) inputs.drilldownYAxis = item.drilldownYAxis;
|
||||
if (item.drilldownParameter !== undefined) inputs.drilldownParameter = item.drilldownParameter;
|
||||
if (item.drilldownFilters !== undefined) inputs.drilldownFilters = item.drilldownFilters;
|
||||
if (item.drilldownLayers !== undefined) inputs.drilldownLayers = item.drilldownLayers;
|
||||
}
|
||||
|
||||
console.log('Component inputs for', item.name, ':', inputs);
|
||||
return inputs;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -5,6 +5,10 @@ import { ChartDataset, ChartType, } from 'chart.js';
|
||||
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-doughnut-runner',
|
||||
@@ -33,10 +37,16 @@ export class DoughnutRunnerComponent implements OnInit {
|
||||
"chartLabels": ["Project", "Repository", "Wireframe"]
|
||||
}
|
||||
doughnutChartType: ChartType = 'doughnut';
|
||||
ConnectionId: number; // Add ConnectionId property
|
||||
|
||||
// Add subscriptions to unsubscribe on destroy
|
||||
private subscriptions: Subscription[] = [];
|
||||
|
||||
|
||||
constructor(private Dashtestservive:DashrunnerService,private route: ActivatedRoute,private dashboardService: Dashboard3Service,
|
||||
private router : Router,) { }
|
||||
private router : Router,
|
||||
// Add FilterService to constructor
|
||||
private filterService: FilterService) { }
|
||||
ngOnInit(): void {
|
||||
this.doughnutChartData = this.doughnutData.chartData;
|
||||
this.doughnutChartLabels = this.doughnutData.chartLabels;
|
||||
@@ -44,6 +54,14 @@ export class DoughnutRunnerComponent 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);
|
||||
this.workflowLine = data.dashbord1_Line[0].model;
|
||||
@@ -70,22 +88,62 @@ export class DoughnutRunnerComponent implements OnInit {
|
||||
this.YAxis = ChartObject[i].yAxis;
|
||||
this.showlabel = ChartObject[i].showlabel;
|
||||
this.doughnutChartLegend = ChartObject[i].chartlegend;
|
||||
this.ConnectionId = ChartObject[i].connection; // Add connection ID
|
||||
console.log(this.TableName);
|
||||
this.Dashtestservive.getChartData(this.TableName,"Doughnut Chart",this.XAxis,this.YAxis).subscribe((Ldata) => {
|
||||
console.log(Ldata);
|
||||
this.JsonData = Ldata;
|
||||
this.doughnutChartData = this.JsonData.chartData;
|
||||
this.doughnutChartLabels = this.JsonData.chartLabels;
|
||||
|
||||
},(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('DoughnutRunner: Final filter object to send to API:', filterObj);
|
||||
|
||||
// Fetch data from the dashboard service with filters
|
||||
this.Dashtestservive.getChartDataWithFilters(this.TableName, "Doughnut Chart", this.XAxis, this.YAxis, this.ConnectionId, '', '', filterParams).subscribe((Ldata) => {
|
||||
console.log(Ldata);
|
||||
this.JsonData = Ldata;
|
||||
this.doughnutChartData = this.JsonData.chartData;
|
||||
this.doughnutChartLabels = this.JsonData.chartLabels;
|
||||
},(error) => {
|
||||
console.log(error);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
generatePDFFile(){
|
||||
// this.buttonClicked.emit();
|
||||
const content = this.contentContainerRef.nativeElement;
|
||||
@@ -94,6 +152,19 @@ export class DoughnutRunnerComponent implements OnInit {
|
||||
this.Dashtestservive.generatePDF(content, filename);
|
||||
}
|
||||
|
||||
ngOnDestroy() {
|
||||
// Unsubscribe from all subscriptions to prevent memory leaks
|
||||
console.log('DoughnutRunnerComponent ngOnDestroy called, unsubscribing from', this.subscriptions.length, 'subscriptions');
|
||||
this.subscriptions.forEach(subscription => {
|
||||
if (subscription && !subscription.closed) {
|
||||
subscription.unsubscribe();
|
||||
}
|
||||
});
|
||||
this.subscriptions = [];
|
||||
|
||||
console.log('DoughnutRunnerComponent destroyed and cleaned up');
|
||||
}
|
||||
|
||||
|
||||
|
||||
}
|
||||
@@ -39,16 +39,35 @@
|
||||
</div> -->
|
||||
<div><button class="btn btn-primary" (click)="generatePDFFile()">Export</button></div>
|
||||
<div style="max-height: 400px; overflow: auto; padding: 10px;">
|
||||
<table class="table">
|
||||
<!-- Debug information -->
|
||||
<div *ngIf="false" style="background-color: #f0f0f0; padding: 10px; margin-bottom: 10px;">
|
||||
<h4>Debug Information</h4>
|
||||
<p><strong>TableName:</strong> {{ TableName }}</p>
|
||||
<p><strong>XAxis:</strong> {{ XAxis }}</p>
|
||||
<p><strong>YAxis:</strong> {{ YAxis }}</p>
|
||||
<p><strong>Rows:</strong> {{ rows?.length }} items</p>
|
||||
<p><strong>Headers:</strong> {{ getHeaders() | json }}</p>
|
||||
<div *ngIf="error"><strong>Error:</strong> {{ error }}</div>
|
||||
</div>
|
||||
|
||||
<div *ngIf="error" class="error_mess">
|
||||
{{ error }}
|
||||
</div>
|
||||
|
||||
<table class="table" *ngIf="rows && rows.length > 0; else noData">
|
||||
<thead>
|
||||
<tr>
|
||||
<th *ngFor="let co of getHeaders();let i=index">{{co}}</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr *ngFor="let item of rows?.slice()?.reverse()">
|
||||
<tr *ngFor="let item of rows">
|
||||
<td *ngFor="let key of getHeaders()">{{item[key]}}</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
<ng-template #noData>
|
||||
<p *ngIf="!error">No data available</p>
|
||||
</ng-template>
|
||||
</div>
|
||||
@@ -3,6 +3,10 @@ import { DashrunnerService } from '../dashrunner.service';
|
||||
import { DashboardContentModel } from 'src/app/models/builder/dashboard';
|
||||
import { ActivatedRoute, Router } from '@angular/router';
|
||||
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-grid-runner',
|
||||
@@ -26,86 +30,191 @@ export class GridRunnerComponent implements OnInit {
|
||||
public DashtestboardArray: DashboardContentModel[] = [];
|
||||
workflowLine;
|
||||
TableName;
|
||||
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 Dashtestservive:DashrunnerService,
|
||||
private route: ActivatedRoute,
|
||||
private dashboardService: Dashboard3Service,
|
||||
private router : Router,
|
||||
// Add FilterService to constructor
|
||||
private filterService: FilterService
|
||||
) { }
|
||||
|
||||
ngOnInit(): void {
|
||||
|
||||
this.editId = this.route.snapshot.params.id;
|
||||
console.log(this.editId);
|
||||
console.log('GridRunner: Component initialized with editId:', this.editId);
|
||||
// this.getbyId();
|
||||
|
||||
this.dashboardService.getById(this.editId).subscribe((data)=>{
|
||||
console.log(data);
|
||||
// Subscribe to filter changes
|
||||
this.subscriptions.push(
|
||||
this.filterService.filterState$.subscribe(filters => {
|
||||
console.log('GridRunner: Filter state changed:', filters);
|
||||
// When filters change, refresh the grid data
|
||||
this.fetchGridData();
|
||||
})
|
||||
);
|
||||
|
||||
this.dashboardService.getById(this.editId).subscribe((data) => {
|
||||
console.log('GridRunner: Received dashboard data:', data);
|
||||
this.workflowLine = data.dashbord1_Line[0].model;
|
||||
const dash = JSON.parse(this.workflowLine) ;
|
||||
const dash = JSON.parse(this.workflowLine);
|
||||
// this.DashtestboardArray = dash.dashboard;
|
||||
// console.log(this.DashtestboardArray);
|
||||
|
||||
const ChartObject = dash.dashboard.filter(obj => obj.name === "Grid View");
|
||||
console.log(ChartObject);
|
||||
console.log('GridRunner: ChartObject for Grid View:', ChartObject);
|
||||
for (let i = 0; i < ChartObject.length; i++) {
|
||||
const ids = this.Dashtestservive.getgridview();
|
||||
console.log('GridRunner: Current gridview ids:', ids);
|
||||
console.log('GridRunner: Checking chartid:', ChartObject[i].chartid);
|
||||
// console.log(ids);
|
||||
if (ids.includes(ChartObject[i].chartid)) {
|
||||
// If the chartid is already in the ids array, continue to the next iteration
|
||||
console.log('GridRunner: Skipping chartid as it already exists:', ChartObject[i].chartid);
|
||||
continue;
|
||||
}
|
||||
console.log('GridRunner: Adding new chartid:', ChartObject[i].chartid);
|
||||
this.Dashtestservive.setgridview(ChartObject[i].chartid);
|
||||
const id = ids[i];
|
||||
console.log(id);
|
||||
|
||||
if (ChartObject[i].chartid === id) {
|
||||
this.TableName = ChartObject[i].table;
|
||||
this.XAxis = ChartObject[i].xAxis;
|
||||
this.YAxis = ChartObject[i].yAxis;
|
||||
console.log(this.TableName);
|
||||
this.Dashtestservive.getChartData(this.TableName,"Grid View",this.XAxis,this.YAxis).subscribe((Ldata) => {
|
||||
console.log(Ldata);
|
||||
this.rows = Ldata;
|
||||
this.rowdata = this.rows
|
||||
|
||||
},(error) => {
|
||||
console.log(error);
|
||||
});
|
||||
// Add connection ID if available
|
||||
this.ConnectionId = ChartObject[i].connection;
|
||||
console.log('GridRunner: TableName:', this.TableName);
|
||||
console.log('GridRunner: XAxis:', this.XAxis);
|
||||
console.log('GridRunner: YAxis:', this.YAxis);
|
||||
console.log('GridRunner: ConnectionId:', this.ConnectionId);
|
||||
// Fetch data with filters
|
||||
this.fetchGridData();
|
||||
break; // No need to continue the loop once the correct placeholder is found
|
||||
}
|
||||
}
|
||||
}, (error) => {
|
||||
console.log('GridRunner: Error fetching dashboard data:', error);
|
||||
});
|
||||
}
|
||||
|
||||
// Fetch grid data with filter support
|
||||
fetchGridData(): void {
|
||||
console.log('fetching grid data ...')
|
||||
if (this.TableName) {
|
||||
console.log('GridRunner: Fetching data for TableName:', this.TableName, 'XAxis:', this.XAxis, 'YAxis:', this.YAxis);
|
||||
// Convert YAxis to string if it's an array
|
||||
const yAxisString = Array.isArray(this.YAxis) ? this.YAxis.join(',') : this.YAxis;
|
||||
|
||||
// Get filter parameters from common filters
|
||||
const commonFilters = this.filterService.getFilterValues();
|
||||
const filterDefinitions = this.filterService.getFilters();
|
||||
|
||||
//dynamic table
|
||||
// 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);
|
||||
|
||||
getTableData(id){
|
||||
}
|
||||
getHeaders() {
|
||||
let headers: string[] = [];
|
||||
if(this.rows) {
|
||||
this.rows.forEach((value) => {
|
||||
Object.keys(value).forEach((key) => {
|
||||
if(!headers.find((header) => header == key)){
|
||||
headers.push(key)
|
||||
if (filterDef && filterDef.field) {
|
||||
const fieldName = filterDef.field;
|
||||
if (filterValue !== undefined && filterValue !== null && filterValue !== '') {
|
||||
filterObj[fieldName] = filterValue;
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
// Convert to JSON string for API call
|
||||
let filterParams = '';
|
||||
if (Object.keys(filterObj).length > 0) {
|
||||
filterParams = JSON.stringify(filterObj);
|
||||
}
|
||||
|
||||
})
|
||||
console.log('GridRunner: Final filter object to send to API:', filterObj);
|
||||
|
||||
})
|
||||
}
|
||||
return headers;
|
||||
}
|
||||
// Fetch data from the dashboard service with filters
|
||||
this.Dashtestservive.getChartDataWithFilters(this.TableName, "grid", this.XAxis, this.YAxis, this.ConnectionId, '', '', filterParams).subscribe((Ldata) => {
|
||||
console.log('GridRunner: Received data from API:', Ldata);
|
||||
|
||||
generatePDFFile(){
|
||||
// Handle the actual data structure returned by the API
|
||||
if (Ldata && Ldata.chartData) {
|
||||
this.rows = Ldata.chartData;
|
||||
this.rowdata = this.rows;
|
||||
} else if (Ldata && Ldata.data) {
|
||||
// Handle the original expected format as fallback
|
||||
this.rows = Ldata.data;
|
||||
this.rowdata = this.rows;
|
||||
} else if (Array.isArray(Ldata)) {
|
||||
// Handle case where data is directly an array
|
||||
this.rows = Ldata;
|
||||
this.rowdata = this.rows;
|
||||
} else {
|
||||
console.warn('GridRunner: Received data does not have expected structure', Ldata);
|
||||
this.rows = [];
|
||||
this.rowdata = [];
|
||||
}
|
||||
|
||||
// Log the structure of the received data
|
||||
if (this.rows) {
|
||||
console.log('GridRunner: Rows length:', this.rows.length);
|
||||
if (this.rows.length > 0) {
|
||||
console.log('GridRunner: First row structure:', this.rows[0]);
|
||||
}
|
||||
} else {
|
||||
console.log('GridRunner: No data received');
|
||||
}
|
||||
}, (error) => {
|
||||
console.log('GridRunner: Error fetching data:', error);
|
||||
this.error = error;
|
||||
});
|
||||
} else {
|
||||
console.log('GridRunner: Missing TableName or XAxis');
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
//dynamic table
|
||||
|
||||
|
||||
getTableData(id) {
|
||||
}
|
||||
getHeaders() {
|
||||
let headers: string[] = [];
|
||||
if (this.rows) {
|
||||
console.log('GridRunner: Getting headers from rows:', this.rows);
|
||||
this.rows.forEach((value) => {
|
||||
Object.keys(value).forEach((key) => {
|
||||
if (!headers.find((header) => header == key)) {
|
||||
headers.push(key)
|
||||
}
|
||||
})
|
||||
})
|
||||
}
|
||||
console.log('GridRunner: Computed headers:', headers);
|
||||
return headers;
|
||||
}
|
||||
|
||||
generatePDFFile() {
|
||||
this.buttonClicked.emit();
|
||||
const content = this.contentContainerRef.nativeElement;
|
||||
const filename = 'gridview.pdf'; // You can provide any desired filename here
|
||||
|
||||
this.Dashtestservive.generatePDF(content, filename);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
ngOnDestroy() {
|
||||
// Unsubscribe from all subscriptions to prevent memory leaks
|
||||
console.log('GridRunnerComponent ngOnDestroy called, unsubscribing from', this.subscriptions.length, 'subscriptions');
|
||||
this.subscriptions.forEach(subscription => {
|
||||
if (subscription && !subscription.closed) {
|
||||
subscription.unsubscribe();
|
||||
}
|
||||
});
|
||||
this.subscriptions = [];
|
||||
|
||||
console.log('GridRunnerComponent destroyed and cleaned up');
|
||||
}
|
||||
}
|
||||
@@ -7,6 +7,10 @@ import { Dashboard3Service } from 'src/app/services/builder/dashboard3.service';
|
||||
import { DashboardContentModel } from 'src/app/models/builder/dashboard';
|
||||
import { jsPDF } from 'jspdf';
|
||||
import domtoimage from 'dom-to-image';
|
||||
// Add FilterService import
|
||||
import { FilterService } from '../../../dashboardnew/common-filter/filter.service';
|
||||
// Add Subscription import
|
||||
import { Subscription } from 'rxjs';
|
||||
@Component({
|
||||
selector: 'app-line-runner',
|
||||
templateUrl: './line-runner.component.html',
|
||||
@@ -54,8 +58,14 @@ export class LineRunnerComponent implements OnInit {
|
||||
lineChartLegend = false;
|
||||
lineChartPlugins = [];
|
||||
lineChartType = 'line';
|
||||
ConnectionId: number; // Add ConnectionId property
|
||||
|
||||
// Add subscriptions to unsubscribe on destroy
|
||||
private subscriptions: Subscription[] = [];
|
||||
constructor(private Dashtestservive:DashrunnerService,private route: ActivatedRoute,private dashboardService: Dashboard3Service,
|
||||
private router : Router,) { }
|
||||
private router : Router,
|
||||
// Add FilterService to constructor
|
||||
private filterService: FilterService) { }
|
||||
|
||||
ngOnInit(): void {
|
||||
|
||||
@@ -65,6 +75,13 @@ export class LineRunnerComponent 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);
|
||||
@@ -92,16 +109,10 @@ export class LineRunnerComponent implements OnInit {
|
||||
this.YAxis = ChartObject[i].yAxis;
|
||||
this.showlabel = ChartObject[i].showlabel;
|
||||
this.lineChartLegend = ChartObject[i].chartlegend;
|
||||
this.ConnectionId = ChartObject[i].connection; // Add connection ID
|
||||
console.log(this.TableName);
|
||||
this.Dashtestservive.getChartData(this.TableName,"Line Chart",this.XAxis,this.YAxis).subscribe((Ldata) => {
|
||||
console.log(Ldata);
|
||||
this.JsonData = Ldata;
|
||||
this.lineChartData = this.JsonData.chartData;
|
||||
this.lineChartLabels = this.JsonData.chartLabels;
|
||||
|
||||
},(error) => {
|
||||
console.log(error);
|
||||
});
|
||||
// Fetch data with filters
|
||||
this.fetchChartData();
|
||||
break; // No need to continue the loop once the correct placeholder is found
|
||||
}
|
||||
}
|
||||
@@ -128,6 +139,52 @@ export class LineRunnerComponent implements OnInit {
|
||||
// }
|
||||
}
|
||||
|
||||
// Fetch chart data with filter support
|
||||
fetchChartData(): void {
|
||||
if (this.TableName && this.XAxis && this.YAxis) {
|
||||
// Convert YAxis to string if it's an array
|
||||
const yAxisString = Array.isArray(this.YAxis) ? this.YAxis.join(',') : this.YAxis;
|
||||
|
||||
// Get filter parameters from common filters
|
||||
const commonFilters = this.filterService.getFilterValues();
|
||||
const filterDefinitions = this.filterService.getFilters();
|
||||
|
||||
// Build filter object using field names as keys
|
||||
const filterObj = {};
|
||||
Object.keys(commonFilters).forEach(filterId => {
|
||||
const filterValue = commonFilters[filterId];
|
||||
|
||||
// Find the filter definition to get the field name
|
||||
const filterDef = this.filterService.getFilters().find(f => f.id === filterId);
|
||||
|
||||
if (filterDef && filterDef.field) {
|
||||
const fieldName = filterDef.field;
|
||||
if (filterValue !== undefined && filterValue !== null && filterValue !== '') {
|
||||
filterObj[fieldName] = filterValue;
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
// Convert to JSON string for API call
|
||||
let filterParams = '';
|
||||
if (Object.keys(filterObj).length > 0) {
|
||||
filterParams = JSON.stringify(filterObj);
|
||||
}
|
||||
|
||||
console.log('LineRunner: Final filter object to send to API:', filterObj);
|
||||
|
||||
// Fetch data from the dashboard service with filters
|
||||
this.Dashtestservive.getChartDataWithFilters(this.TableName, "Line Chart", this.XAxis, this.YAxis, this.ConnectionId, '', '', filterParams).subscribe((Ldata) => {
|
||||
console.log(Ldata);
|
||||
this.JsonData = Ldata;
|
||||
this.lineChartData = this.JsonData.chartData;
|
||||
this.lineChartLabels = this.JsonData.chartLabels;
|
||||
},(error) => {
|
||||
console.log(error);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
generatePDFFile(){
|
||||
this.buttonClicked.emit();
|
||||
const content = this.contentContainerRef.nativeElement;
|
||||
@@ -166,4 +223,17 @@ export class LineRunnerComponent implements OnInit {
|
||||
// }
|
||||
// }
|
||||
|
||||
ngOnDestroy() {
|
||||
// Unsubscribe from all subscriptions to prevent memory leaks
|
||||
console.log('LineRunnerComponent ngOnDestroy called, unsubscribing from', this.subscriptions.length, 'subscriptions');
|
||||
this.subscriptions.forEach(subscription => {
|
||||
if (subscription && !subscription.closed) {
|
||||
subscription.unsubscribe();
|
||||
}
|
||||
});
|
||||
this.subscriptions = [];
|
||||
|
||||
console.log('LineRunnerComponent destroyed and cleaned up');
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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({
|
||||
@@ -23,9 +27,15 @@ export class PieRunnerComponent implements OnInit {
|
||||
showlabel;
|
||||
JsonData;
|
||||
lineChartNoLabels: any[] = [];
|
||||
ConnectionId: number; // Add ConnectionId property
|
||||
|
||||
// Add subscriptions to unsubscribe on destroy
|
||||
private subscriptions: Subscription[] = [];
|
||||
|
||||
constructor(private Dashtestservive:DashrunnerService,private route: ActivatedRoute,private dashboardService: Dashboard3Service,
|
||||
private router : Router,) { }
|
||||
private router : Router,
|
||||
// Add FilterService to constructor
|
||||
private filterService: FilterService) { }
|
||||
|
||||
public pieChartLabels: string[] = ['SciFi', 'Drama', 'Comedy'];
|
||||
public pieChartData: number[] = [30, 50, 20];
|
||||
@@ -39,6 +49,13 @@ export class PieRunnerComponent 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);
|
||||
@@ -66,22 +83,62 @@ export class PieRunnerComponent 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,"Pie Chart",this.XAxis,this.YAxis).subscribe((Ldata) => {
|
||||
console.log(Ldata);
|
||||
this.JsonData = Ldata;
|
||||
this.pieChartData = this.JsonData.pieChartData;
|
||||
this.pieChartLabels = this.JsonData.pieChartLabels;
|
||||
|
||||
},(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('PieRunner: Final filter object to send to API:', filterObj);
|
||||
|
||||
// Fetch data from the dashboard service with filters
|
||||
this.Dashtestservive.getChartDataWithFilters(this.TableName, "Pie Chart", this.XAxis, this.YAxis, this.ConnectionId, '', '', filterParams).subscribe((Ldata) => {
|
||||
console.log(Ldata);
|
||||
this.JsonData = Ldata;
|
||||
this.pieChartData = this.JsonData.pieChartData;
|
||||
this.pieChartLabels = this.JsonData.pieChartLabels;
|
||||
},(error) => {
|
||||
console.log(error);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
generatePDFFile(){
|
||||
this.buttonClicked.emit();
|
||||
const content = this.contentContainerRef.nativeElement;
|
||||
@@ -89,4 +146,17 @@ export class PieRunnerComponent implements OnInit {
|
||||
|
||||
this.Dashtestservive.generatePDF(content, filename);
|
||||
}
|
||||
|
||||
ngOnDestroy() {
|
||||
// Unsubscribe from all subscriptions to prevent memory leaks
|
||||
console.log('PieRunnerComponent ngOnDestroy called, unsubscribing from', this.subscriptions.length, 'subscriptions');
|
||||
this.subscriptions.forEach(subscription => {
|
||||
if (subscription && !subscription.closed) {
|
||||
subscription.unsubscribe();
|
||||
}
|
||||
});
|
||||
this.subscriptions = [];
|
||||
|
||||
console.log('PieRunnerComponent destroyed and cleaned up');
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4,6 +4,10 @@ import { ActivatedRoute, Router } from '@angular/router';
|
||||
import { Dashboard3Service } from 'src/app/services/builder/dashboard3.service';
|
||||
import { DashboardContentModel } from 'src/app/models/builder/dashboard';
|
||||
// import { Label } from 'ng2-charts';
|
||||
// Add FilterService import
|
||||
import { FilterService } from '../../../dashboardnew/common-filter/filter.service';
|
||||
// Add Subscription import
|
||||
import { Subscription } from 'rxjs';
|
||||
|
||||
@Component({
|
||||
selector: 'app-polar-runner',
|
||||
@@ -23,9 +27,15 @@ export class PolarRunnerComponent implements OnInit {
|
||||
showlabel;
|
||||
JsonData;
|
||||
lineChartNoLabels: any[] = [];
|
||||
ConnectionId: number; // Add ConnectionId property
|
||||
|
||||
// Add subscriptions to unsubscribe on destroy
|
||||
private subscriptions: Subscription[] = [];
|
||||
|
||||
constructor(private Dashtestservive:DashrunnerService,private route: ActivatedRoute,private dashboardService: Dashboard3Service,
|
||||
private router : Router,) { }
|
||||
private router : Router,
|
||||
// Add FilterService to constructor
|
||||
private filterService: FilterService) { }
|
||||
|
||||
public polarAreaChartLabels: string[] = [ 'Download Sales', 'In-Store Sales', 'Mail Sales', 'Telesales', 'Corporate Sales' ];
|
||||
public polarAreaChartData: any = [
|
||||
@@ -41,6 +51,13 @@ export class PolarRunnerComponent 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);
|
||||
@@ -66,22 +83,62 @@ export class PolarRunnerComponent 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,"PolarArea Chart",this.XAxis,this.YAxis).subscribe((Ldata) => {
|
||||
console.log(Ldata);
|
||||
this.JsonData = Ldata;
|
||||
this.polarAreaChartData = this.JsonData.polarAreaChartData;
|
||||
this.polarAreaChartLabels = this.JsonData.polarAreaChartLabels;
|
||||
|
||||
},(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('PolarRunner: Final filter object to send to API:', filterObj);
|
||||
|
||||
// Fetch data from the dashboard service with filters
|
||||
this.Dashtestservive.getChartDataWithFilters(this.TableName, "PolarArea Chart", this.XAxis, this.YAxis, this.ConnectionId, '', '', filterParams).subscribe((Ldata) => {
|
||||
console.log(Ldata);
|
||||
this.JsonData = Ldata;
|
||||
this.polarAreaChartData = this.JsonData.polarAreaChartData;
|
||||
this.polarAreaChartLabels = this.JsonData.polarAreaChartLabels;
|
||||
},(error) => {
|
||||
console.log(error);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
generatePDFFile(){
|
||||
this.buttonClicked.emit();
|
||||
const content = this.contentContainerRef.nativeElement;
|
||||
@@ -90,4 +147,17 @@ export class PolarRunnerComponent implements OnInit {
|
||||
this.Dashtestservive.generatePDF(content, filename);
|
||||
}
|
||||
|
||||
ngOnDestroy() {
|
||||
// Unsubscribe from all subscriptions to prevent memory leaks
|
||||
console.log('PolarRunnerComponent ngOnDestroy called, unsubscribing from', this.subscriptions.length, 'subscriptions');
|
||||
this.subscriptions.forEach(subscription => {
|
||||
if (subscription && !subscription.closed) {
|
||||
subscription.unsubscribe();
|
||||
}
|
||||
});
|
||||
this.subscriptions = [];
|
||||
|
||||
console.log('PolarRunnerComponent destroyed and cleaned up');
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -4,6 +4,10 @@ import { ActivatedRoute, Router } from '@angular/router';
|
||||
import { Dashboard3Service } from 'src/app/services/builder/dashboard3.service';
|
||||
import { DashboardContentModel } from 'src/app/models/builder/dashboard';
|
||||
// import { Label } from 'ng2-charts';
|
||||
// Add FilterService import
|
||||
import { FilterService } from '../../../dashboardnew/common-filter/filter.service';
|
||||
// Add Subscription import
|
||||
import { Subscription } from 'rxjs';
|
||||
|
||||
@Component({
|
||||
selector: 'app-radar-runner',
|
||||
@@ -24,9 +28,15 @@ export class RadarRunnerComponent implements OnInit {
|
||||
JsonData;
|
||||
lineChartNoLabels: any[] = [];
|
||||
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 radarChartLabels: string[] = [
|
||||
"Eating",
|
||||
@@ -50,6 +60,13 @@ export class RadarRunnerComponent 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);
|
||||
@@ -75,22 +92,62 @@ export class RadarRunnerComponent 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,"Radar Chart",this.XAxis,this.YAxis).subscribe((Ldata) => {
|
||||
console.log(Ldata);
|
||||
this.JsonData = Ldata;
|
||||
this.radarChartData = this.JsonData.radarChartData;
|
||||
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('RadarRunner: Final filter object to send to API:', filterObj);
|
||||
|
||||
// Fetch data from the dashboard service with filters
|
||||
this.Dashtestservive.getChartDataWithFilters(this.TableName, "Radar Chart", this.XAxis, this.YAxis, this.ConnectionId, '', '', filterParams).subscribe((Ldata) => {
|
||||
console.log(Ldata);
|
||||
this.JsonData = Ldata;
|
||||
this.radarChartData = this.JsonData.radarChartData;
|
||||
this.radarChartLabels = this.JsonData.radarChartLabels;
|
||||
},(error) => {
|
||||
console.log(error);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
generatePDFFile(){
|
||||
this.buttonClicked.emit();
|
||||
const content = this.contentContainerRef.nativeElement;
|
||||
@@ -99,4 +156,17 @@ export class RadarRunnerComponent implements OnInit {
|
||||
this.Dashtestservive.generatePDF(content, filename);
|
||||
}
|
||||
|
||||
ngOnDestroy() {
|
||||
// Unsubscribe from all subscriptions to prevent memory leaks
|
||||
console.log('RadarRunnerComponent ngOnDestroy called, unsubscribing from', this.subscriptions.length, 'subscriptions');
|
||||
this.subscriptions.forEach(subscription => {
|
||||
if (subscription && !subscription.closed) {
|
||||
subscription.unsubscribe();
|
||||
}
|
||||
});
|
||||
this.subscriptions = [];
|
||||
|
||||
console.log('RadarRunnerComponent destroyed and cleaned up');
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -5,6 +5,10 @@ import { Dashboard3Service } from 'src/app/services/builder/dashboard3.service';
|
||||
import { DashboardContentModel } from 'src/app/models/builder/dashboard';
|
||||
// import { Label } from 'ng2-charts';
|
||||
import { ChartDataset } from 'chart.js';
|
||||
// Add FilterService import
|
||||
import { FilterService } from '../../../dashboardnew/common-filter/filter.service';
|
||||
// Add Subscription import
|
||||
import { Subscription } from 'rxjs';
|
||||
|
||||
@Component({
|
||||
selector: 'app-scatter-runner',
|
||||
@@ -25,9 +29,15 @@ export class ScatterRunnerComponent implements OnInit {
|
||||
JsonData;
|
||||
lineChartNoLabels: any[] = [];
|
||||
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 scatterChartLabels: string[] = [ 'Eating', 'Drinking', 'Sleeping', 'Designing', 'Coding', 'Cycling', 'Running' ];
|
||||
|
||||
@@ -69,6 +79,13 @@ export class ScatterRunnerComponent implements OnInit {
|
||||
this.editId = this.route.snapshot.params.id;
|
||||
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);
|
||||
@@ -94,22 +111,62 @@ export class ScatterRunnerComponent 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,"Scatter Chart",this.XAxis,this.YAxis).subscribe((Ldata) => {
|
||||
console.log(Ldata);
|
||||
this.JsonData = Ldata;
|
||||
this.scatterChartData = this.JsonData.scatterChartData;
|
||||
this.scatterChartLabels = this.JsonData.scatterChartLabels;
|
||||
|
||||
},(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('ScatterRunner: Final filter object to send to API:', filterObj);
|
||||
|
||||
// Fetch data from the dashboard service with filters
|
||||
this.Dashtestservive.getChartDataWithFilters(this.TableName, "Scatter Chart", this.XAxis, this.YAxis, this.ConnectionId, '', '', filterParams).subscribe((Ldata) => {
|
||||
console.log(Ldata);
|
||||
this.JsonData = Ldata;
|
||||
this.scatterChartData = this.JsonData.scatterChartData;
|
||||
this.scatterChartLabels = this.JsonData.scatterChartLabels;
|
||||
},(error) => {
|
||||
console.log(error);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
generatePDFFile(){
|
||||
this.buttonClicked.emit();
|
||||
const content = this.contentContainerRef.nativeElement;
|
||||
@@ -118,4 +175,17 @@ export class ScatterRunnerComponent implements OnInit {
|
||||
this.Dashtestservive.generatePDF(content, filename);
|
||||
}
|
||||
|
||||
ngOnDestroy() {
|
||||
// Unsubscribe from all subscriptions to prevent memory leaks
|
||||
console.log('ScatterRunnerComponent ngOnDestroy called, unsubscribing from', this.subscriptions.length, 'subscriptions');
|
||||
this.subscriptions.forEach(subscription => {
|
||||
if (subscription && !subscription.closed) {
|
||||
subscription.unsubscribe();
|
||||
}
|
||||
});
|
||||
this.subscriptions = [];
|
||||
|
||||
console.log('ScatterRunnerComponent destroyed and cleaned up');
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -3,6 +3,10 @@ import { DashrunnerService } from '../dashrunner.service';
|
||||
import { DashboardContentModel } from 'src/app/models/builder/dashboard';
|
||||
import { ActivatedRoute, Router } from '@angular/router';
|
||||
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-todo-runner',
|
||||
@@ -12,8 +16,9 @@ import { Dashboard3Service } from 'src/app/services/builder/dashboard3.service';
|
||||
export class TodoRunnerComponent implements OnInit {
|
||||
@ViewChild('contentContainer') contentContainerRef!: ElementRef;
|
||||
@Output() buttonClicked = new EventEmitter<void>();
|
||||
constructor( private Dashtestservive:DashrunnerService,private route: ActivatedRoute,private dashboardService: Dashboard3Service,
|
||||
private router : Router) { }
|
||||
|
||||
// Add subscriptions to unsubscribe on destroy
|
||||
private subscriptions: Subscription[] = [];
|
||||
|
||||
loading = false;
|
||||
givendata;
|
||||
@@ -25,6 +30,7 @@ export class TodoRunnerComponent implements OnInit {
|
||||
public DashtestboardArray: DashboardContentModel[] = [];
|
||||
workflowLine;
|
||||
TableName;
|
||||
ConnectionId: number; // Add ConnectionId property
|
||||
|
||||
list;
|
||||
data: any;
|
||||
@@ -34,11 +40,25 @@ export class TodoRunnerComponent implements OnInit {
|
||||
listName: "title123",
|
||||
List:['todo 1','todo 2'],
|
||||
}
|
||||
|
||||
constructor( private Dashtestservive:DashrunnerService,private route: ActivatedRoute,private dashboardService: Dashboard3Service,
|
||||
private router : Router,
|
||||
// Add FilterService to constructor
|
||||
private filterService: FilterService) { }
|
||||
|
||||
ngOnInit(): void {
|
||||
this.editId = this.route.snapshot.params.id;
|
||||
console.log(this.editId);
|
||||
// this.getbyId();
|
||||
|
||||
// Subscribe to filter changes
|
||||
this.subscriptions.push(
|
||||
this.filterService.filterState$.subscribe(filters => {
|
||||
// When filters change, refresh the todo data
|
||||
this.fetchTodoData();
|
||||
})
|
||||
);
|
||||
|
||||
this.dashboardService.getById(this.editId).subscribe((data)=>{
|
||||
console.log(data);
|
||||
this.workflowLine = data.dashbord1_Line[0].model;
|
||||
@@ -63,15 +83,10 @@ export class TodoRunnerComponent implements OnInit {
|
||||
this.TableName = ChartObject[i].table;
|
||||
this.XAxis = ChartObject[i].xAxis;
|
||||
this.YAxis = ChartObject[i].yAxis;
|
||||
this.ConnectionId = ChartObject[i].connection; // Add connection ID
|
||||
console.log(this.TableName);
|
||||
this.Dashtestservive.getChartData(this.TableName,"Todo List",this.XAxis,this.YAxis).subscribe((Ldata) => {
|
||||
console.log(Ldata);
|
||||
this.todoList.listName = Ldata.listName;
|
||||
this.todoList.List = Ldata.List;
|
||||
|
||||
},(error) => {
|
||||
console.log(error);
|
||||
});
|
||||
// Fetch data with filters
|
||||
this.fetchTodoData();
|
||||
break; // No need to continue the loop once the correct placeholder is found
|
||||
}
|
||||
}
|
||||
@@ -100,4 +115,58 @@ generatePDFFile(){
|
||||
|
||||
this.Dashtestservive.generatePDF(content, filename);
|
||||
}
|
||||
|
||||
// Fetch todo data with filter support
|
||||
fetchTodoData(): void {
|
||||
if (this.TableName && this.XAxis && this.YAxis) {
|
||||
// Get filter parameters from common filters
|
||||
const commonFilters = this.filterService.getFilterValues();
|
||||
|
||||
// Build filter object using field names as keys
|
||||
const filterObj = {};
|
||||
Object.keys(commonFilters).forEach(filterId => {
|
||||
const filterValue = commonFilters[filterId];
|
||||
|
||||
// Find the filter definition to get the field name
|
||||
const filterDef = this.filterService.getFilters().find(f => f.id === filterId);
|
||||
|
||||
if (filterDef && filterDef.field) {
|
||||
const fieldName = filterDef.field;
|
||||
if (filterValue !== undefined && filterValue !== null && filterValue !== '') {
|
||||
filterObj[fieldName] = filterValue;
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
// Convert to JSON string for API call
|
||||
let filterParams = '';
|
||||
if (Object.keys(filterObj).length > 0) {
|
||||
filterParams = JSON.stringify(filterObj);
|
||||
}
|
||||
|
||||
console.log('TodoRunner: Final filter object to send to API:', filterObj);
|
||||
|
||||
// Fetch data from the dashboard service with filters
|
||||
this.Dashtestservive.getChartDataWithFilters(this.TableName, "Todo List", this.XAxis, this.YAxis, this.ConnectionId, '', '', filterParams).subscribe((Ldata) => {
|
||||
console.log(Ldata);
|
||||
this.todoList.listName = Ldata.listName;
|
||||
this.todoList.List = Ldata.List;
|
||||
},(error) => {
|
||||
console.log(error);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
ngOnDestroy() {
|
||||
// Unsubscribe from all subscriptions to prevent memory leaks
|
||||
console.log('TodoRunnerComponent ngOnDestroy called, unsubscribing from', this.subscriptions.length, 'subscriptions');
|
||||
this.subscriptions.forEach(subscription => {
|
||||
if (subscription && !subscription.closed) {
|
||||
subscription.unsubscribe();
|
||||
}
|
||||
});
|
||||
this.subscriptions = [];
|
||||
|
||||
console.log('TodoRunnerComponent destroyed and cleaned up');
|
||||
}
|
||||
}
|
||||
@@ -1,7 +1,7 @@
|
||||
import { Injectable } from '@angular/core';
|
||||
import { Router } from '@angular/router';
|
||||
|
||||
import { Observable, BehaviorSubject } from 'rxjs';
|
||||
import { Observable, BehaviorSubject, Subject } from 'rxjs';
|
||||
import { UserInfoService, LoginInfoInStorage} from '../user-info.service';
|
||||
import { ApiRequestService } from './api-request.service';
|
||||
import { HttpClient } from '@angular/common/http';
|
||||
@@ -43,11 +43,14 @@ export class LoginService {
|
||||
|
||||
*/
|
||||
|
||||
let loginDataSubject:BehaviorSubject<any> = new BehaviorSubject<any>([]); // Will use this BehaviorSubject to emit data that we want after ajax login attempt
|
||||
// let loginDataSubject:BehaviorSubject<any> = new BehaviorSubject<any>([]); // Will use this BehaviorSubject to emit data that we want after ajax login attempt
|
||||
let loginDataSubject: Subject<any> = new Subject<any>();
|
||||
|
||||
let loginInfoReturn:LoginInfoInStorage; // Object that we want to send back to Login Page
|
||||
|
||||
this.apiRequest.loginAuthentication('token/session', bodyData)
|
||||
.subscribe(jsonResp => {
|
||||
.subscribe({
|
||||
next: (jsonResp) => {
|
||||
console.log('login response in service : ', jsonResp);
|
||||
if (jsonResp.operationMessage=='Login Failed') {
|
||||
this.toastr.warning('Not Login Getting Error check your Username and password');
|
||||
@@ -78,7 +81,7 @@ export class LoginService {
|
||||
this.userInfoService.storeUserInfo(JSON.stringify(loginInfoReturn.user));
|
||||
}
|
||||
else {
|
||||
//Create a faliure object that we want to send back to login page
|
||||
//Create a failure object that we want to send back to login page
|
||||
loginInfoReturn = {
|
||||
"success":false,
|
||||
"message":jsonResp.operationMessage,
|
||||
@@ -86,8 +89,9 @@ export class LoginService {
|
||||
};
|
||||
}
|
||||
loginDataSubject.next(loginInfoReturn);
|
||||
loginDataSubject.complete(); // Complete the subject
|
||||
},
|
||||
err => {
|
||||
error: (err) => {
|
||||
console.log('login error ', err);
|
||||
loginInfoReturn = {
|
||||
"success": false,
|
||||
@@ -97,6 +101,9 @@ export class LoginService {
|
||||
if (err) {
|
||||
this.toastr.error('Getting Server Error');
|
||||
}
|
||||
loginDataSubject.next(loginInfoReturn); // Send the error response
|
||||
loginDataSubject.complete(); // Complete the subject
|
||||
}
|
||||
});
|
||||
|
||||
return loginDataSubject;
|
||||
|
||||
@@ -281,7 +281,7 @@ export class Dashboard3Service {
|
||||
return this.apiRequest.get(`Dashboard/Dashboard`);
|
||||
}
|
||||
|
||||
public getChartData(tableName: string, jobType: string, xAxis?: any, yAxes?: any, sureId?: number, parameter?: string, parameterValue?: string): Observable<any> {
|
||||
public getChartData(tableName: string, jobType: string, xAxis?: any, yAxes?: any, sureId?: number, parameter?: string, parameterValue?: string, filters?: string): Observable<any> {
|
||||
let url = `${baseUrl}/chart/getdashjson/${jobType}?tableName=${tableName}&xAxis=${xAxis}&yAxes=${yAxes}`;
|
||||
if (sureId) {
|
||||
url += `&sureId=${sureId}`;
|
||||
@@ -293,7 +293,26 @@ export class Dashboard3Service {
|
||||
url += `¶meterValue=${encodeURIComponent(parameterValue)}`;
|
||||
}
|
||||
|
||||
console.log('=== DASHBOARD SERVICE DEBUG INFO ===');
|
||||
console.log('Base URL:', url);
|
||||
console.log('Filters parameter:', filters);
|
||||
|
||||
// Parse filters JSON and add as a single "filters" parameter
|
||||
if (filters) {
|
||||
try {
|
||||
const filterObj = JSON.parse(filters);
|
||||
console.log('Parsed filter object:', filterObj);
|
||||
|
||||
// Add all filters as a single "filters" parameter with JSON object
|
||||
url += `&filters=${encodeURIComponent(JSON.stringify(filterObj))}`;
|
||||
console.log('Added filters parameter:', JSON.stringify(filterObj));
|
||||
} catch (e) {
|
||||
console.warn('Failed to parse filter parameters:', e);
|
||||
}
|
||||
}
|
||||
|
||||
console.log('Final constructed URL:', url);
|
||||
console.log('=== END DASHBOARD SERVICE DEBUG ===');
|
||||
return this._http.get(url);
|
||||
}
|
||||
|
||||
|
||||
@@ -372,5 +372,9 @@
|
||||
"select_Field2": "select_Field2",
|
||||
"Password_Field": "Password_Field",
|
||||
"age": "age",
|
||||
"Button_Field": "Button_Field"
|
||||
"Button_Field": "Button_Field",
|
||||
"API_REGISTERY": "API Registry",
|
||||
"API_REGISTERY_DESCRIPTION": "API Registry Description",
|
||||
"TOKEN_REGISTERY": "Token Registry",
|
||||
"TOKEN_REGISTERY_DESCRIPTION": "Token Registry Description"
|
||||
}
|
||||
@@ -105,6 +105,8 @@
|
||||
"REPORT_DESCRIPTION": "रिपोर्ट विवरण",
|
||||
"API_REGISTERY": "एपीआई रजिस्ट्री",
|
||||
"API_REGISTERY_DESCRIPTION": "एपीआई रजिस्ट्री विवरण",
|
||||
"TOKEN_REGISTERY": "टोकन रजिस्ट्री",
|
||||
"TOKEN_REGISTERY_DESCRIPTION": "टोकन रजिस्ट्री विवरण",
|
||||
"ACTIVE": "सक्रिय",
|
||||
"FOLDER_NAME": "फ़ोल्डर नाम",
|
||||
"ACTION": "क्रिया",
|
||||
|
||||
Reference in New Issue
Block a user