Skip to content
Projects
Groups
Snippets
Help
This project
Loading...
Sign in / Register
Toggle navigation
P
portal-apps-manage
Overview
Overview
Details
Activity
Cycle Analytics
Repository
Repository
Files
Commits
Branches
Tags
Contributors
Graph
Compare
Charts
Registry
Issues
0
Issues
0
List
Board
Labels
Milestones
Merge Requests
0
Merge Requests
0
CI / CD
CI / CD
Pipelines
Jobs
Schedules
Charts
Wiki
Wiki
Snippets
Snippets
Members
Members
Collapse sidebar
Close sidebar
Activity
Graph
Charts
Create a new issue
Jobs
Commits
Issue Boards
Open sidebar
angular
portal-apps-manage
Commits
c81ea227
Commit
c81ea227
authored
Sep 14, 2025
by
Ooh-Ao
Browse files
Options
Browse Files
Download
Email Patches
Plain Diff
kpi
parent
1fc00b6e
Hide whitespace changes
Inline
Side-by-side
Showing
7 changed files
with
2233 additions
and
73 deletions
+2233
-73
simple-kpi-config.example.ts
...ashboard-management/examples/simple-kpi-config.example.ts
+526
-0
widget-config.component.html
...ard-management/widget-config/widget-config.component.html
+416
-40
widget-config.component.ts
...board-management/widget-config/widget-config.component.ts
+74
-0
attendance-overview-widget.component.html
...agement/widgets/attendance-overview-widget.component.html
+1
-1
simple-kpi-widget.component.html
...idgets/simple-kpi-widget/simple-kpi-widget.component.html
+69
-21
simple-kpi-widget.component.scss
...idgets/simple-kpi-widget/simple-kpi-widget.component.scss
+600
-0
simple-kpi-widget.component.ts
.../widgets/simple-kpi-widget/simple-kpi-widget.component.ts
+547
-11
No files found.
src/app/portal-manage/dashboard-management/examples/simple-kpi-config.example.ts
0 → 100644
View file @
c81ea227
/**
* Simple KPI Widget Configuration Examples
*
* This file demonstrates how to configure the Simple KPI Widget using the
* comprehensive configuration system with various options and use cases.
*/
import
{
Component
,
OnInit
}
from
'@angular/core'
;
import
{
WidgetConfigService
}
from
'../services/widget-config.service'
;
import
{
WidgetPreviewDataService
}
from
'../services/widget-preview-data.service'
;
@
Component
({
selector
:
'app-simple-kpi-config-examples'
,
template
:
`
<div class="simple-kpi-examples">
<h1>Simple KPI Widget Configuration Examples</h1>
<!-- Example 1: Basic KPI Configuration -->
<div class="example-section">
<h2>Example 1: Basic KPI Configuration</h2>
<div class="example-content">
<h3>Configuration</h3>
<pre>{{ basicConfig | json }}</pre>
<h3>Preview Data</h3>
<pre>{{ basicPreviewData | json }}</pre>
</div>
</div>
<!-- Example 2: Styled KPI with Trend -->
<div class="example-section">
<h2>Example 2: Styled KPI with Trend</h2>
<div class="example-content">
<h3>Configuration</h3>
<pre>{{ styledConfig | json }}</pre>
</div>
</div>
<!-- Example 3: Advanced Configuration -->
<div class="example-section">
<h2>Example 3: Advanced Configuration</h2>
<div class="example-content">
<h3>Configuration</h3>
<pre>{{ advancedConfig | json }}</pre>
</div>
</div>
<!-- Example 4: Custom CSS Configuration -->
<div class="example-section">
<h2>Example 4: Custom CSS Configuration</h2>
<div class="example-content">
<h3>Configuration</h3>
<pre>{{ customCSSConfig | json }}</pre>
</div>
</div>
<!-- Example 5: Different Aggregation Types -->
<div class="example-section">
<h2>Example 5: Different Aggregation Types</h2>
<div class="example-content">
<h3>Sum Aggregation</h3>
<pre>{{ sumConfig | json }}</pre>
<h3>Average Aggregation</h3>
<pre>{{ averageConfig | json }}</pre>
<h3>Count Aggregation</h3>
<pre>{{ countConfig | json }}</pre>
<h3>Max/Min Aggregation</h3>
<pre>{{ maxMinConfig | json }}</pre>
</div>
</div>
<!-- Example 6: Trend Configuration -->
<div class="example-section">
<h2>Example 6: Trend Configuration</h2>
<div class="example-content">
<h3>Percentage Trend</h3>
<pre>{{ percentageTrendConfig | json }}</pre>
<h3>Absolute Trend</h3>
<pre>{{ absoluteTrendConfig | json }}</pre>
<h3>Ratio Trend</h3>
<pre>{{ ratioTrendConfig | json }}</pre>
</div>
</div>
</div>
`
,
styles
:
[
`
.simple-kpi-examples {
padding: 2rem;
max-width: 1200px;
margin: 0 auto;
}
.example-section {
margin-bottom: 3rem;
padding: 2rem;
background: #f8f9fa;
border-radius: 12px;
border: 1px solid #e9ecef;
}
.example-content {
margin-top: 1rem;
}
.example-content h3 {
color: #495057;
margin-top: 1.5rem;
margin-bottom: 0.5rem;
}
pre {
background: #2d3748;
color: #e2e8f0;
padding: 1rem;
border-radius: 8px;
overflow-x: auto;
font-size: 0.85rem;
line-height: 1.5;
}
`
]
})
export
class
SimpleKpiConfigExamplesComponent
implements
OnInit
{
// Configuration examples
basicConfig
:
any
;
styledConfig
:
any
;
advancedConfig
:
any
;
customCSSConfig
:
any
;
sumConfig
:
any
;
averageConfig
:
any
;
countConfig
:
any
;
maxMinConfig
:
any
;
percentageTrendConfig
:
any
;
absoluteTrendConfig
:
any
;
ratioTrendConfig
:
any
;
// Preview data
basicPreviewData
:
any
[];
constructor
(
private
widgetConfigService
:
WidgetConfigService
,
private
previewDataService
:
WidgetPreviewDataService
)
{}
ngOnInit
():
void
{
this
.
initializeConfigurations
();
this
.
loadPreviewData
();
}
private
initializeConfigurations
():
void
{
// Example 1: Basic KPI Configuration
this
.
basicConfig
=
{
widgetType
:
'SimpleKpiWidgetComponent'
,
config
:
{
title
:
'Total Sales'
,
valueField
:
'revenue'
,
unit
:
'$'
,
aggregation
:
'sum'
},
style
:
{
backgroundColor
:
'#e8f5e8'
,
textColor
:
'#2d5a2d'
,
accentColor
:
'#28a745'
,
borderRadius
:
8
,
padding
:
16
}
};
// Example 2: Styled KPI with Trend
this
.
styledConfig
=
{
widgetType
:
'SimpleKpiWidgetComponent'
,
config
:
{
title
:
'Customer Satisfaction'
,
valueField
:
'satisfaction'
,
unit
:
'%'
,
aggregation
:
'average'
,
showTrend
:
true
,
trendField
:
'satisfaction'
,
trendType
:
'percentage'
},
style
:
{
backgroundColor
:
'linear-gradient(135deg, #667eea 0%, #764ba2 100%)'
,
textColor
:
'#ffffff'
,
accentColor
:
'#ffffff'
,
borderColor
:
'#ffffff'
,
borderRadius
:
16
,
padding
:
24
,
fontSize
:
18
,
fontWeight
:
'bold'
,
fontFamily
:
'Inter, system-ui, sans-serif'
},
animation
:
{
enableAnimations
:
true
,
animationType
:
'fade'
,
animationDuration
:
500
,
hoverEffects
:
true
}
};
// Example 3: Advanced Configuration
this
.
advancedConfig
=
{
widgetType
:
'SimpleKpiWidgetComponent'
,
config
:
{
title
:
'Revenue Growth'
,
valueField
:
'revenue'
,
unit
:
'%'
,
aggregation
:
'average'
,
decimalPlaces
:
2
,
showTrend
:
true
,
trendField
:
'growth'
,
trendType
:
'percentage'
},
style
:
{
backgroundColor
:
'#f8f9fa'
,
textColor
:
'#495057'
,
accentColor
:
'#007bff'
,
borderColor
:
'#007bff'
,
borderWidth
:
3
,
borderRadius
:
20
,
padding
:
32
,
margin
:
16
,
fontSize
:
20
,
fontWeight
:
'bold'
,
fontFamily
:
'system-ui, -apple-system, sans-serif'
},
layout
:
{
width
:
350
,
height
:
200
,
responsive
:
true
},
interaction
:
{
enableTooltip
:
true
,
enableClick
:
true
,
clickAction
:
'drill_down'
},
filters
:
{
category
:
'sales'
,
period
:
'last_30_days'
},
dataMapping
:
[
{
sourceField
:
'total_revenue'
,
targetField
:
'revenue'
,
transformation
:
'format_number'
},
{
sourceField
:
'growth_rate'
,
targetField
:
'growth'
,
transformation
:
'none'
}
]
};
// Example 4: Custom CSS Configuration
this
.
customCSSConfig
=
{
widgetType
:
'SimpleKpiWidgetComponent'
,
config
:
{
title
:
'Custom Styled KPI'
,
valueField
:
'value'
,
unit
:
'items'
},
style
:
{
backgroundColor
:
'#1a202c'
,
textColor
:
'#e2e8f0'
,
accentColor
:
'#00ff88'
,
borderRadius
:
12
,
padding
:
20
,
customCSS
:
`
.simple-kpi-widget {
position: relative;
overflow: hidden;
}
.simple-kpi-widget::before {
content: '';
position: absolute;
top: 0;
left: 0;
right: 0;
bottom: 0;
background: linear-gradient(45deg, transparent 30%, rgba(0, 255, 136, 0.1) 50%, transparent 70%);
animation: shimmer 2s infinite;
}
.widget-header {
background: linear-gradient(135deg, #1a202c 0%, #2d3748 100%);
border-bottom: 2px solid #00ff88;
}
.kpi-value {
background: linear-gradient(135deg, #00ff88 0%, #00cc6a 100%);
-webkit-background-clip: text;
-webkit-text-fill-color: transparent;
background-clip: text;
filter: drop-shadow(0 0 10px rgba(0, 255, 136, 0.3));
}
@keyframes shimmer {
0% { transform: translateX(-100%); }
100% { transform: translateX(100%); }
}
.simple-kpi-widget:hover {
transform: translateY(-8px) scale(1.02);
box-shadow: 0 20px 40px rgba(0, 255, 136, 0.2);
}
`
}
};
// Example 5: Different Aggregation Types
this
.
sumConfig
=
{
widgetType
:
'SimpleKpiWidgetComponent'
,
config
:
{
title
:
'Total Revenue'
,
valueField
:
'amount'
,
unit
:
'$'
,
aggregation
:
'sum'
}
};
this
.
averageConfig
=
{
widgetType
:
'SimpleKpiWidgetComponent'
,
config
:
{
title
:
'Average Order Value'
,
valueField
:
'orderValue'
,
unit
:
'$'
,
aggregation
:
'average'
,
decimalPlaces
:
2
}
};
this
.
countConfig
=
{
widgetType
:
'SimpleKpiWidgetComponent'
,
config
:
{
title
:
'Total Orders'
,
aggregation
:
'count'
,
unit
:
'orders'
}
};
this
.
maxMinConfig
=
{
widgetType
:
'SimpleKpiWidgetComponent'
,
config
:
{
title
:
'Highest Sale'
,
valueField
:
'amount'
,
unit
:
'$'
,
aggregation
:
'max'
}
};
// Example 6: Trend Configuration
this
.
percentageTrendConfig
=
{
widgetType
:
'SimpleKpiWidgetComponent'
,
config
:
{
title
:
'Sales Growth'
,
valueField
:
'current'
,
unit
:
'%'
,
showTrend
:
true
,
trendField
:
'growth'
,
trendType
:
'percentage'
},
style
:
{
backgroundColor
:
'linear-gradient(135deg, #10b981 0%, #059669 100%)'
,
textColor
:
'#ffffff'
,
accentColor
:
'#ffffff'
}
};
this
.
absoluteTrendConfig
=
{
widgetType
:
'SimpleKpiWidgetComponent'
,
config
:
{
title
:
'Revenue Change'
,
valueField
:
'current'
,
unit
:
'$'
,
showTrend
:
true
,
trendField
:
'change'
,
trendType
:
'absolute'
},
style
:
{
backgroundColor
:
'linear-gradient(135deg, #3b82f6 0%, #1d4ed8 100%)'
,
textColor
:
'#ffffff'
,
accentColor
:
'#ffffff'
}
};
this
.
ratioTrendConfig
=
{
widgetType
:
'SimpleKpiWidgetComponent'
,
config
:
{
title
:
'Performance Ratio'
,
valueField
:
'current'
,
unit
:
'x'
,
showTrend
:
true
,
trendField
:
'ratio'
,
trendType
:
'ratio'
},
style
:
{
backgroundColor
:
'linear-gradient(135deg, #8b5cf6 0%, #7c3aed 100%)'
,
textColor
:
'#ffffff'
,
accentColor
:
'#ffffff'
}
};
}
private
loadPreviewData
():
void
{
// Generate sample data for KPI widgets
this
.
basicPreviewData
=
this
.
previewDataService
.
generateSimpleKpiPreviewData
({
count
:
5
,
categories
:
[
'sales'
,
'marketing'
,
'operations'
]
});
}
// Example methods for programmatic configuration
/**
* Create a basic KPI widget configuration
*/
createBasicKpiConfig
():
void
{
const
config
=
this
.
widgetConfigService
.
createWidgetConfig
(
'SimpleKpiWidgetComponent'
,
{
config
:
{
title
:
'My KPI'
,
valueField
:
'value'
,
unit
:
'$'
,
aggregation
:
'sum'
},
style
:
{
backgroundColor
:
'#e8f5e8'
,
textColor
:
'#2d5a2d'
,
accentColor
:
'#28a745'
}
});
console
.
log
(
'Created basic KPI config:'
,
config
);
}
/**
* Create a styled KPI widget configuration
*/
createStyledKpiConfig
():
void
{
const
config
=
this
.
widgetConfigService
.
createWidgetConfig
(
'SimpleKpiWidgetComponent'
,
{
config
:
{
title
:
'Styled KPI'
,
valueField
:
'value'
,
unit
:
'%'
,
showTrend
:
true
,
trendType
:
'percentage'
},
style
:
{
backgroundColor
:
'linear-gradient(135deg, #667eea 0%, #764ba2 100%)'
,
textColor
:
'#ffffff'
,
accentColor
:
'#ffffff'
,
borderRadius
:
16
,
padding
:
24
,
fontSize
:
18
,
fontWeight
:
'bold'
},
animation
:
{
enableAnimations
:
true
,
animationType
:
'fade'
,
animationDuration
:
500
,
hoverEffects
:
true
}
});
console
.
log
(
'Created styled KPI config:'
,
config
);
}
/**
* Create a KPI widget with custom CSS
*/
createCustomCSSKpiConfig
():
void
{
const
config
=
this
.
widgetConfigService
.
createWidgetConfig
(
'SimpleKpiWidgetComponent'
,
{
config
:
{
title
:
'Custom CSS KPI'
,
valueField
:
'value'
,
unit
:
'items'
},
style
:
{
backgroundColor
:
'#1a202c'
,
textColor
:
'#e2e8f0'
,
accentColor
:
'#00ff88'
,
customCSS
:
`
.simple-kpi-widget {
background: linear-gradient(135deg, #1a202c 0%, #2d3748 100%);
border: 2px solid #00ff88;
box-shadow: 0 0 20px rgba(0, 255, 136, 0.3);
}
.kpi-value {
background: linear-gradient(135deg, #00ff88 0%, #00cc6a 100%);
-webkit-background-clip: text;
-webkit-text-fill-color: transparent;
background-clip: text;
}
`
}
});
console
.
log
(
'Created custom CSS KPI config:'
,
config
);
}
/**
* Update an existing KPI widget configuration
*/
updateKpiConfig
(
widgetId
:
string
,
updates
:
any
):
void
{
const
updated
=
this
.
widgetConfigService
.
updateWidgetConfig
(
widgetId
,
{
config
:
{
...
updates
.
config
},
style
:
{
...
updates
.
style
}
});
console
.
log
(
'Updated KPI config:'
,
updated
);
}
/**
* Get KPI widget configurations by aggregation type
*/
getKpiConfigsByAggregation
(
aggregation
:
string
):
void
{
const
allConfigs
=
this
.
widgetConfigService
.
getWidgetConfigsByType
(
'SimpleKpiWidgetComponent'
);
const
filteredConfigs
=
allConfigs
.
filter
(
config
=>
config
.
config
.
aggregation
===
aggregation
);
console
.
log
(
`KPI configs with
${
aggregation
}
aggregation:`
,
filteredConfigs
);
}
}
src/app/portal-manage/dashboard-management/widget-config/widget-config.component.html
View file @
c81ea227
...
...
@@ -558,7 +558,7 @@
</div>
</div>
<div
*
ngIf=
"widgetType === 'SlicerWidgetComponent'"
>
<mat-form-field
appearance=
"fill"
>
...
...
@@ -601,45 +601,421 @@
</div>
<div
*
ngIf=
"widgetType === 'SimpleKpiWidgetComponent'"
>
<mat-form-field
appearance=
"fill"
>
<mat-label>
Title
</mat-label>
<input
matInput
[(
ngModel
)]="
currentConfig
.
title
"
name=
"title"
>
</mat-form-field>
<mat-form-field
appearance=
"fill"
>
<mat-label>
Value Field
</mat-label>
<mat-select
[(
ngModel
)]="
currentConfig
.
valueField
"
name=
"valueField"
>
<mat-option
*
ngFor=
"let col of availableColumns"
[
value
]="
col
"
>
{{ col }}
</mat-option>
</mat-select>
</mat-form-field>
<mat-form-field
appearance=
"fill"
>
<mat-label>
Aggregation
</mat-label>
<mat-select
[(
ngModel
)]="
currentConfig
.
aggregation
"
name=
"aggregation"
>
<mat-option
value=
"count"
>
Count
</mat-option>
<mat-option
value=
"sum"
>
Sum
</mat-option>
</mat-select>
</mat-form-field>
<mat-form-field
appearance=
"fill"
>
<mat-label>
Unit
</mat-label>
<input
matInput
[(
ngModel
)]="
currentConfig
.
unit
"
name=
"unit"
>
</mat-form-field>
<mat-form-field
appearance=
"fill"
>
<mat-label>
Icon (e.g., 'person-fill')
</mat-label>
<input
matInput
[(
ngModel
)]="
currentConfig
.
icon
"
name=
"icon"
>
<mat-hint>
Find icons at icons.getbootstrap.com
</mat-hint>
</mat-form-field>
<mat-form-field
appearance=
"fill"
>
<mat-label>
Icon Color
</mat-label>
<input
matInput
type=
"color"
[(
ngModel
)]="
currentConfig
.
iconColor
"
name=
"iconColor"
class=
"h-[40px]"
>
</mat-form-field>
<mat-form-field
appearance=
"fill"
>
<mat-label>
Background Color (Header)
</mat-label>
<input
matInput
[(
ngModel
)]="
currentConfig
.
backgroundColor
"
name=
"backgroundColor"
>
<mat-hint>
e.g., 'red', '#FF0000', 'linear-gradient(to right, red, yellow)'
</mat-hint>
</mat-form-field>
<mat-form-field
appearance=
"fill"
>
<mat-label>
Border Color (Card)
</mat-label>
<input
matInput
type=
"color"
[(
ngModel
)]="
currentConfig
.
borderColor
"
name=
"borderColor"
class=
"h-[40px]"
>
</mat-form-field>
<!-- Basic Configuration -->
<div
class=
"config-section border p-4 rounded-lg mb-4"
>
<h3
class=
"text-lg font-semibold mb-3 text-blue-600"
>
Basic Configuration
</h3>
<mat-form-field
appearance=
"fill"
class=
"w-full"
>
<mat-label>
Title
</mat-label>
<input
matInput
[(
ngModel
)]="
currentConfig
.
title
"
name=
"title"
aria-label=
"Widget title"
>
<mat-hint>
Widget title displayed in header
</mat-hint>
</mat-form-field>
<div
class=
"grid grid-cols-1 md:grid-cols-2 gap-4"
>
<mat-form-field
appearance=
"fill"
>
<mat-label>
Value Field
</mat-label>
<mat-select
[(
ngModel
)]="
currentConfig
.
valueField
"
name=
"valueField"
>
<mat-option
*
ngFor=
"let col of availableColumns"
[
value
]="
col
"
>
{{ col }}
</mat-option>
</mat-select>
<mat-hint>
Field containing the KPI value
</mat-hint>
</mat-form-field>
<mat-form-field
appearance=
"fill"
>
<mat-label>
Label Field
</mat-label>
<mat-select
[(
ngModel
)]="
currentConfig
.
labelField
"
name=
"labelField"
>
<mat-option
*
ngFor=
"let col of availableColumns"
[
value
]="
col
"
>
{{ col }}
</mat-option>
</mat-select>
<mat-hint>
Field containing the KPI label
</mat-hint>
</mat-form-field>
</div>
<div
class=
"grid grid-cols-1 md:grid-cols-2 gap-4"
>
<mat-form-field
appearance=
"fill"
>
<mat-label>
Aggregation
</mat-label>
<mat-select
[(
ngModel
)]="
currentConfig
.
aggregation
"
name=
"aggregation"
>
<mat-option
value=
"count"
>
Count
</mat-option>
<mat-option
value=
"sum"
>
Sum
</mat-option>
<mat-option
value=
"average"
>
Average
</mat-option>
<mat-option
value=
"max"
>
Maximum
</mat-option>
<mat-option
value=
"min"
>
Minimum
</mat-option>
</mat-select>
<mat-hint>
How to aggregate the data
</mat-hint>
</mat-form-field>
<mat-form-field
appearance=
"fill"
>
<mat-label>
Unit
</mat-label>
<input
matInput
[(
ngModel
)]="
currentConfig
.
unit
"
name=
"unit"
placeholder=
"e.g., $, %, items"
aria-label=
"Unit"
>
<mat-hint>
Unit to display after the value
</mat-hint>
</mat-form-field>
</div>
<div
class=
"grid grid-cols-1 md:grid-cols-2 gap-4"
>
<mat-form-field
appearance=
"fill"
>
<mat-label>
Icon (Bootstrap Icons)
</mat-label>
<input
matInput
[(
ngModel
)]="
currentConfig
.
icon
"
name=
"icon"
placeholder=
"e.g., person-fill, building"
aria-label=
"Icon"
>
<mat-hint>
Find icons at icons.getbootstrap.com
</mat-hint>
</mat-form-field>
<mat-form-field
appearance=
"fill"
>
<mat-label>
Decimal Places
</mat-label>
<input
matInput
type=
"number"
[(
ngModel
)]="
currentConfig
.
decimalPlaces
"
name=
"decimalPlaces"
min=
"0"
max=
"10"
aria-label=
"Decimal places"
>
<mat-hint>
Number of decimal places to show
</mat-hint>
</mat-form-field>
</div>
</div>
<!-- Trend Configuration -->
<div
class=
"config-section border p-4 rounded-lg mb-4"
>
<h3
class=
"text-lg font-semibold mb-3 text-green-600"
>
Trend Settings
</h3>
<div
class=
"flex items-center mb-3"
>
<mat-checkbox
[(
ngModel
)]="
currentConfig
.
showTrend
"
name=
"showTrend"
class=
"mr-2"
>
Show Trend Indicator
</mat-checkbox>
</div>
<div
*
ngIf=
"currentConfig.showTrend"
class=
"grid grid-cols-1 md:grid-cols-2 gap-4"
>
<mat-form-field
appearance=
"fill"
>
<mat-label>
Trend Field
</mat-label>
<mat-select
[(
ngModel
)]="
currentConfig
.
trendField
"
name=
"trendField"
>
<mat-option
*
ngFor=
"let col of availableColumns"
[
value
]="
col
"
>
{{ col }}
</mat-option>
</mat-select>
<mat-hint>
Field containing trend data
</mat-hint>
</mat-form-field>
<mat-form-field
appearance=
"fill"
>
<mat-label>
Trend Type
</mat-label>
<mat-select
[(
ngModel
)]="
currentConfig
.
trendType
"
name=
"trendType"
>
<mat-option
value=
"percentage"
>
Percentage Change
</mat-option>
<mat-option
value=
"absolute"
>
Absolute Change
</mat-option>
<mat-option
value=
"ratio"
>
Ratio
</mat-option>
</mat-select>
<mat-hint>
How to calculate trend
</mat-hint>
</mat-form-field>
</div>
</div>
<!-- Style Configuration -->
<div
class=
"config-section border p-4 rounded-lg mb-4"
>
<h3
class=
"text-lg font-semibold mb-3 text-purple-600"
>
Style
&
Colors
</h3>
<div
class=
"grid grid-cols-1 md:grid-cols-2 gap-4"
>
<mat-form-field
appearance=
"fill"
>
<mat-label>
Background Color
</mat-label>
<input
matInput
[(
ngModel
)]="
currentConfig
.
backgroundColor
"
name=
"backgroundColor"
placeholder=
"#FF0000 or linear-gradient(...)"
aria-label=
"Background color"
>
<mat-hint>
Header background color
</mat-hint>
</mat-form-field>
<mat-form-field
appearance=
"fill"
>
<mat-label>
Text Color
</mat-label>
<input
matInput
type=
"color"
[(
ngModel
)]="
currentConfig
.
textColor
"
name=
"textColor"
class=
"h-[40px]"
aria-label=
"Text color"
>
<mat-hint>
Text color for labels
</mat-hint>
</mat-form-field>
</div>
<div
class=
"grid grid-cols-1 md:grid-cols-2 gap-4"
>
<mat-form-field
appearance=
"fill"
>
<mat-label>
Accent Color
</mat-label>
<input
matInput
type=
"color"
[(
ngModel
)]="
currentConfig
.
accentColor
"
name=
"accentColor"
class=
"h-[40px]"
aria-label=
"Accent color"
>
<mat-hint>
Color for the main KPI value
</mat-hint>
</mat-form-field>
<mat-form-field
appearance=
"fill"
>
<mat-label>
Border Color
</mat-label>
<input
matInput
type=
"color"
[(
ngModel
)]="
currentConfig
.
borderColor
"
name=
"borderColor"
class=
"h-[40px]"
aria-label=
"Border color"
>
<mat-hint>
Card border color
</mat-hint>
</mat-form-field>
</div>
<div
class=
"grid grid-cols-1 md:grid-cols-2 gap-4"
>
<mat-form-field
appearance=
"fill"
>
<mat-label>
Icon Color
</mat-label>
<input
matInput
type=
"color"
[(
ngModel
)]="
currentConfig
.
iconColor
"
name=
"iconColor"
class=
"h-[40px]"
aria-label=
"Icon color"
>
<mat-hint>
Icon color
</mat-hint>
</mat-form-field>
<mat-form-field
appearance=
"fill"
>
<mat-label>
Border Radius (px)
</mat-label>
<input
matInput
type=
"number"
[(
ngModel
)]="
currentConfig
.
borderRadius
"
name=
"borderRadius"
min=
"0"
max=
"50"
aria-label=
"Border radius"
>
<mat-hint>
Roundness of widget corners
</mat-hint>
</mat-form-field>
</div>
<div
class=
"grid grid-cols-1 md:grid-cols-3 gap-4"
>
<mat-form-field
appearance=
"fill"
>
<mat-label>
Padding (px)
</mat-label>
<input
matInput
type=
"number"
[(
ngModel
)]="
currentConfig
.
padding
"
name=
"padding"
min=
"0"
max=
"100"
aria-label=
"Padding"
>
<mat-hint>
Internal spacing
</mat-hint>
</mat-form-field>
<mat-form-field
appearance=
"fill"
>
<mat-label>
Font Size (px)
</mat-label>
<input
matInput
type=
"number"
[(
ngModel
)]="
currentConfig
.
fontSize
"
name=
"fontSize"
min=
"10"
max=
"48"
aria-label=
"Font size"
>
<mat-hint>
Font size for values
</mat-hint>
</mat-form-field>
<mat-form-field
appearance=
"fill"
>
<mat-label>
Font Weight
</mat-label>
<mat-select
[(
ngModel
)]="
currentConfig
.
fontWeight
"
name=
"fontWeight"
>
<mat-option
value=
"normal"
>
Normal
</mat-option>
<mat-option
value=
"bold"
>
Bold
</mat-option>
<mat-option
value=
"lighter"
>
Lighter
</mat-option>
<mat-option
value=
"bolder"
>
Bolder
</mat-option>
</mat-select>
<mat-hint>
Font weight
</mat-hint>
</mat-form-field>
</div>
<mat-form-field
appearance=
"fill"
class=
"w-full"
>
<mat-label>
Font Family
</mat-label>
<mat-select
[(
ngModel
)]="
currentConfig
.
fontFamily
"
name=
"fontFamily"
>
<mat-option
value=
"system-ui, -apple-system, sans-serif"
>
System Font
</mat-option>
<mat-option
value=
"Arial, sans-serif"
>
Arial
</mat-option>
<mat-option
value=
"Helvetica, sans-serif"
>
Helvetica
</mat-option>
<mat-option
value=
"Georgia, serif"
>
Georgia
</mat-option>
<mat-option
value=
"Times New Roman, serif"
>
Times New Roman
</mat-option>
<mat-option
value=
"Courier New, monospace"
>
Courier New
</mat-option>
</mat-select>
<mat-hint>
Font family for the widget
</mat-hint>
</mat-form-field>
<mat-form-field
appearance=
"fill"
class=
"w-full"
>
<mat-label>
Custom CSS
</mat-label>
<textarea
matInput
[(
ngModel
)]="
currentConfig
.
customCSS
"
name=
"customCSS"
rows=
"4"
placeholder=
"/* Add custom CSS rules here */ .simple-kpi-widget:hover { transform: translateY(-4px); }"
></textarea>
<mat-hint>
Add custom CSS for advanced styling
</mat-hint>
</mat-form-field>
</div>
<!-- Animation Configuration -->
<div
class=
"config-section border p-4 rounded-lg mb-4"
>
<h3
class=
"text-lg font-semibold mb-3 text-orange-600"
>
Animation Settings
</h3>
<div
class=
"flex items-center mb-3"
>
<mat-checkbox
[(
ngModel
)]="
currentConfig
.
enableAnimations
"
name=
"enableAnimations"
class=
"mr-2"
>
Enable Animations
</mat-checkbox>
</div>
<div
*
ngIf=
"currentConfig.enableAnimations"
class=
"grid grid-cols-1 md:grid-cols-2 gap-4"
>
<mat-form-field
appearance=
"fill"
>
<mat-label>
Animation Type
</mat-label>
<mat-select
[(
ngModel
)]="
currentConfig
.
animationType
"
name=
"animationType"
>
<mat-option
value=
"fade"
>
Fade
</mat-option>
<mat-option
value=
"slide"
>
Slide
</mat-option>
<mat-option
value=
"bounce"
>
Bounce
</mat-option>
<mat-option
value=
"pulse"
>
Pulse
</mat-option>
<mat-option
value=
"none"
>
None
</mat-option>
</mat-select>
<mat-hint>
Animation effect type
</mat-hint>
</mat-form-field>
<mat-form-field
appearance=
"fill"
>
<mat-label>
Animation Duration (ms)
</mat-label>
<input
matInput
type=
"number"
[(
ngModel
)]="
currentConfig
.
animationDuration
"
name=
"animationDuration"
min=
"100"
max=
"2000"
step=
"100"
aria-label=
"Animation duration"
>
<mat-hint>
Duration of animation
</mat-hint>
</mat-form-field>
</div>
<div
*
ngIf=
"currentConfig.enableAnimations"
class=
"flex items-center"
>
<mat-checkbox
[(
ngModel
)]="
currentConfig
.
hoverEffects
"
name=
"hoverEffects"
class=
"mr-2"
>
Enable Hover Effects
</mat-checkbox>
</div>
</div>
<!-- Interaction Configuration -->
<div
class=
"config-section border p-4 rounded-lg mb-4"
>
<h3
class=
"text-lg font-semibold mb-3 text-indigo-600"
>
Interaction Settings
</h3>
<div
class=
"grid grid-cols-1 md:grid-cols-2 gap-4"
>
<div
class=
"flex items-center"
>
<mat-checkbox
[(
ngModel
)]="
currentConfig
.
enableTooltip
"
name=
"enableTooltip"
class=
"mr-2"
>
Enable Tooltips
</mat-checkbox>
</div>
<div
class=
"flex items-center"
>
<mat-checkbox
[(
ngModel
)]="
currentConfig
.
enableClick
"
name=
"enableClick"
class=
"mr-2"
>
Enable Click Events
</mat-checkbox>
</div>
</div>
<div
class=
"grid grid-cols-1 md:grid-cols-2 gap-4"
>
<div
class=
"flex items-center"
>
<mat-checkbox
[(
ngModel
)]="
currentConfig
.
enableHover
"
name=
"enableHover"
class=
"mr-2"
>
Enable Hover Effects
</mat-checkbox>
</div>
<div
class=
"flex items-center"
>
<mat-checkbox
[(
ngModel
)]="
currentConfig
.
enableExport
"
name=
"enableExport"
class=
"mr-2"
>
Enable Export
</mat-checkbox>
</div>
</div>
<mat-form-field
appearance=
"fill"
class=
"w-full"
>
<mat-label>
Click Action
</mat-label>
<mat-select
[(
ngModel
)]="
currentConfig
.
clickAction
"
name=
"clickAction"
>
<mat-option
value=
"none"
>
None
</mat-option>
<mat-option
value=
"drill_down"
>
Drill Down
</mat-option>
<mat-option
value=
"open_modal"
>
Open Modal
</mat-option>
<mat-option
value=
"navigate"
>
Navigate
</mat-option>
<mat-option
value=
"custom"
>
Custom
</mat-option>
</mat-select>
<mat-hint>
Action when widget is clicked
</mat-hint>
</mat-form-field>
<mat-form-field
*
ngIf=
"currentConfig.clickAction === 'custom'"
appearance=
"fill"
class=
"w-full"
>
<mat-label>
Custom Click Handler
</mat-label>
<textarea
matInput
[(
ngModel
)]="
currentConfig
.
customClickHandler
"
name=
"customClickHandler"
rows=
"3"
placeholder=
"function(event) { console.log('Widget clicked:', event); }"
></textarea>
<mat-hint>
Custom JavaScript function for click events
</mat-hint>
</mat-form-field>
</div>
<!-- Layout Configuration -->
<div
class=
"config-section border p-4 rounded-lg mb-4"
>
<h3
class=
"text-lg font-semibold mb-3 text-teal-600"
>
Layout Settings
</h3>
<div
class=
"grid grid-cols-1 md:grid-cols-2 gap-4"
>
<mat-form-field
appearance=
"fill"
>
<mat-label>
Width (px)
</mat-label>
<input
matInput
type=
"number"
[(
ngModel
)]="
currentConfig
.
width
"
name=
"width"
min=
"100"
max=
"800"
aria-label=
"Widget width"
>
<mat-hint>
Widget width
</mat-hint>
</mat-form-field>
<mat-form-field
appearance=
"fill"
>
<mat-label>
Height (px)
</mat-label>
<input
matInput
type=
"number"
[(
ngModel
)]="
currentConfig
.
height
"
name=
"height"
min=
"100"
max=
"600"
aria-label=
"Widget height"
>
<mat-hint>
Widget height
</mat-hint>
</mat-form-field>
</div>
<div
class=
"grid grid-cols-1 md:grid-cols-2 gap-4"
>
<mat-form-field
appearance=
"fill"
>
<mat-label>
Minimum Width (px)
</mat-label>
<input
matInput
type=
"number"
[(
ngModel
)]="
currentConfig
.
minWidth
"
name=
"minWidth"
min=
"50"
max=
"400"
aria-label=
"Minimum width"
>
<mat-hint>
Minimum widget width
</mat-hint>
</mat-form-field>
<mat-form-field
appearance=
"fill"
>
<mat-label>
Minimum Height (px)
</mat-label>
<input
matInput
type=
"number"
[(
ngModel
)]="
currentConfig
.
minHeight
"
name=
"minHeight"
min=
"50"
max=
"300"
aria-label=
"Minimum height"
>
<mat-hint>
Minimum widget height
</mat-hint>
</mat-form-field>
</div>
<div
class=
"grid grid-cols-1 md:grid-cols-2 gap-4"
>
<mat-form-field
appearance=
"fill"
>
<mat-label>
Aspect Ratio
</mat-label>
<mat-select
[(
ngModel
)]="
currentConfig
.
aspectRatio
"
name=
"aspectRatio"
>
<mat-option
value=
"auto"
>
Auto
</mat-option>
<mat-option
value=
"16:9"
>
16:9
</mat-option>
<mat-option
value=
"4:3"
>
4:3
</mat-option>
<mat-option
value=
"1:1"
>
1:1 (Square)
</mat-option>
<mat-option
value=
"3:2"
>
3:2
</mat-option>
</mat-select>
<mat-hint>
Widget aspect ratio
</mat-hint>
</mat-form-field>
<div
class=
"flex items-center"
>
<mat-checkbox
[(
ngModel
)]="
currentConfig
.
responsive
"
name=
"responsive"
class=
"mr-2"
>
Responsive Layout
</mat-checkbox>
</div>
</div>
</div>
<!-- Data Configuration -->
<div
class=
"config-section border p-4 rounded-lg mb-4"
>
<h3
class=
"text-lg font-semibold mb-3 text-red-600"
>
Data Settings
</h3>
<mat-form-field
appearance=
"fill"
class=
"w-full"
>
<mat-label>
Data Source
</mat-label>
<mat-select
[(
ngModel
)]="
currentConfig
.
dataSource
"
name=
"dataSource"
>
<mat-option
value=
"static"
>
Static Data
</mat-option>
<mat-option
value=
"api"
>
API Endpoint
</mat-option>
<mat-option
value=
"websocket"
>
WebSocket
</mat-option>
<mat-option
value=
"file"
>
File Upload
</mat-option>
</mat-select>
<mat-hint>
Data source type
</mat-hint>
</mat-form-field>
<mat-form-field
*
ngIf=
"currentConfig.dataSource === 'api'"
appearance=
"fill"
class=
"w-full"
>
<mat-label>
API Endpoint
</mat-label>
<input
matInput
[(
ngModel
)]="
currentConfig
.
apiEndpoint
"
name=
"apiEndpoint"
placeholder=
"/api/kpi-data"
aria-label=
"API endpoint"
>
<mat-hint>
API endpoint URL
</mat-hint>
</mat-form-field>
<div
class=
"grid grid-cols-1 md:grid-cols-2 gap-4"
>
<mat-form-field
appearance=
"fill"
>
<mat-label>
Refresh Interval (ms)
</mat-label>
<input
matInput
type=
"number"
[(
ngModel
)]="
currentConfig
.
refreshInterval
"
name=
"refreshInterval"
min=
"0"
step=
"1000"
aria-label=
"Refresh interval"
>
<mat-hint>
0 = No auto-refresh
</mat-hint>
</mat-form-field>
<div
class=
"flex items-center"
>
<mat-checkbox
[(
ngModel
)]="
currentConfig
.
cacheEnabled
"
name=
"cacheEnabled"
class=
"mr-2"
>
Enable Data Caching
</mat-checkbox>
</div>
</div>
<mat-form-field
*
ngIf=
"currentConfig.cacheEnabled"
appearance=
"fill"
class=
"w-full"
>
<mat-label>
Cache Duration (seconds)
</mat-label>
<input
matInput
type=
"number"
[(
ngModel
)]="
currentConfig
.
cacheDuration
"
name=
"cacheDuration"
min=
"1"
max=
"3600"
aria-label=
"Cache duration"
>
<mat-hint>
How long to cache data
</mat-hint>
</mat-form-field>
<mat-form-field
appearance=
"fill"
class=
"w-full"
>
<mat-label>
Data Transform Function
</mat-label>
<textarea
matInput
[(
ngModel
)]="
currentConfig
.
dataTransform
"
name=
"dataTransform"
rows=
"3"
placeholder=
"data => data.map(item => ({ ...item, formattedValue: formatCurrency(item.value) }))"
></textarea>
<mat-hint>
JavaScript function to transform data
</mat-hint>
</mat-form-field>
</div>
<!-- Security Configuration -->
<div
class=
"config-section border p-4 rounded-lg mb-4"
>
<h3
class=
"text-lg font-semibold mb-3 text-gray-600"
>
Security Settings
</h3>
<div
class=
"grid grid-cols-1 md:grid-cols-2 gap-4"
>
<div
class=
"flex items-center"
>
<mat-checkbox
[(
ngModel
)]="
currentConfig
.
requireAuth
"
name=
"requireAuth"
class=
"mr-2"
>
Require Authentication
</mat-checkbox>
</div>
<div
class=
"flex items-center"
>
<mat-checkbox
[(
ngModel
)]="
currentConfig
.
dataEncryption
"
name=
"dataEncryption"
class=
"mr-2"
>
Enable Data Encryption
</mat-checkbox>
</div>
</div>
<div
class=
"grid grid-cols-1 md:grid-cols-2 gap-4"
>
<div
class=
"flex items-center"
>
<mat-checkbox
[(
ngModel
)]="
currentConfig
.
auditLog
"
name=
"auditLog"
class=
"mr-2"
>
Enable Audit Logging
</mat-checkbox>
</div>
<mat-form-field
appearance=
"fill"
>
<mat-label>
Rate Limit (requests/min)
</mat-label>
<input
matInput
type=
"number"
[(
ngModel
)]="
currentConfig
.
rateLimit
"
name=
"rateLimit"
min=
"0"
aria-label=
"Rate limit"
>
<mat-hint>
0 = No limit
</mat-hint>
</mat-form-field>
</div>
<mat-form-field
appearance=
"fill"
class=
"w-full"
>
<mat-label>
Allowed Roles (comma-separated)
</mat-label>
<input
matInput
[(
ngModel
)]="
currentConfig
.
allowedRoles
"
name=
"allowedRoles"
placeholder=
"admin, analyst, manager"
aria-label=
"Allowed roles"
>
<mat-hint>
Roles that can access this widget
</mat-hint>
</mat-form-field>
</div>
</div>
<div
*
ngIf=
"widgetType === 'PieChartWidgetComponent' || widgetType === 'BarChartWidgetComponent' || widgetType === 'AreaChartWidgetComponent' || widgetType === 'DoughnutChartWidgetComponent' || widgetType === 'FunnelChartWidgetComponent'"
>
...
...
src/app/portal-manage/dashboard-management/widget-config/widget-config.component.ts
View file @
c81ea227
...
...
@@ -86,6 +86,80 @@ export class WidgetConfigComponent implements OnInit {
if
(
!
this
.
currentConfig
.
yFields
)
this
.
currentConfig
.
yFields
=
[];
if
(
!
this
.
currentConfig
.
series
)
this
.
currentConfig
.
series
=
[];
}
// Initialize Simple KPI Widget default values
if
(
this
.
widgetType
===
'SimpleKpiWidgetComponent'
)
{
// Basic configuration defaults
if
(
!
this
.
currentConfig
.
title
)
this
.
currentConfig
.
title
=
'KPI Widget'
;
if
(
!
this
.
currentConfig
.
valueField
)
this
.
currentConfig
.
valueField
=
'value'
;
if
(
!
this
.
currentConfig
.
labelField
)
this
.
currentConfig
.
labelField
=
'label'
;
if
(
!
this
.
currentConfig
.
aggregation
)
this
.
currentConfig
.
aggregation
=
'sum'
;
if
(
!
this
.
currentConfig
.
unit
)
this
.
currentConfig
.
unit
=
''
;
if
(
!
this
.
currentConfig
.
icon
)
this
.
currentConfig
.
icon
=
'info'
;
if
(
this
.
currentConfig
.
decimalPlaces
===
undefined
)
this
.
currentConfig
.
decimalPlaces
=
0
;
// Trend configuration defaults
if
(
this
.
currentConfig
.
showTrend
===
undefined
)
this
.
currentConfig
.
showTrend
=
false
;
if
(
!
this
.
currentConfig
.
trendField
)
this
.
currentConfig
.
trendField
=
'trend'
;
if
(
!
this
.
currentConfig
.
trendType
)
this
.
currentConfig
.
trendType
=
'percentage'
;
// Style configuration defaults
if
(
!
this
.
currentConfig
.
backgroundColor
)
this
.
currentConfig
.
backgroundColor
=
'linear-gradient(to top right, #3366FF, #00CCFF)'
;
if
(
!
this
.
currentConfig
.
textColor
)
this
.
currentConfig
.
textColor
=
'#FFFFFF'
;
if
(
!
this
.
currentConfig
.
accentColor
)
this
.
currentConfig
.
accentColor
=
'#FFFFFF'
;
if
(
!
this
.
currentConfig
.
borderColor
)
this
.
currentConfig
.
borderColor
=
'#FFFFFF'
;
if
(
!
this
.
currentConfig
.
iconColor
)
this
.
currentConfig
.
iconColor
=
'#FFFFFF'
;
if
(
this
.
currentConfig
.
borderRadius
===
undefined
)
this
.
currentConfig
.
borderRadius
=
8
;
if
(
this
.
currentConfig
.
padding
===
undefined
)
this
.
currentConfig
.
padding
=
16
;
if
(
this
.
currentConfig
.
margin
===
undefined
)
this
.
currentConfig
.
margin
=
8
;
if
(
this
.
currentConfig
.
borderWidth
===
undefined
)
this
.
currentConfig
.
borderWidth
=
1
;
if
(
this
.
currentConfig
.
fontSize
===
undefined
)
this
.
currentConfig
.
fontSize
=
16
;
if
(
!
this
.
currentConfig
.
fontWeight
)
this
.
currentConfig
.
fontWeight
=
'normal'
;
if
(
!
this
.
currentConfig
.
fontFamily
)
this
.
currentConfig
.
fontFamily
=
'system-ui, -apple-system, sans-serif'
;
if
(
!
this
.
currentConfig
.
customCSS
)
this
.
currentConfig
.
customCSS
=
''
;
// Animation configuration defaults
if
(
this
.
currentConfig
.
enableAnimations
===
undefined
)
this
.
currentConfig
.
enableAnimations
=
true
;
if
(
!
this
.
currentConfig
.
animationType
)
this
.
currentConfig
.
animationType
=
'fade'
;
if
(
this
.
currentConfig
.
animationDuration
===
undefined
)
this
.
currentConfig
.
animationDuration
=
300
;
if
(
this
.
currentConfig
.
animationDelay
===
undefined
)
this
.
currentConfig
.
animationDelay
=
0
;
if
(
this
.
currentConfig
.
hoverEffects
===
undefined
)
this
.
currentConfig
.
hoverEffects
=
true
;
// Interaction configuration defaults
if
(
this
.
currentConfig
.
enableTooltip
===
undefined
)
this
.
currentConfig
.
enableTooltip
=
true
;
if
(
this
.
currentConfig
.
enableClick
===
undefined
)
this
.
currentConfig
.
enableClick
=
true
;
if
(
this
.
currentConfig
.
enableHover
===
undefined
)
this
.
currentConfig
.
enableHover
=
true
;
if
(
this
.
currentConfig
.
enableSelection
===
undefined
)
this
.
currentConfig
.
enableSelection
=
false
;
if
(
this
.
currentConfig
.
enableExport
===
undefined
)
this
.
currentConfig
.
enableExport
=
false
;
if
(
this
.
currentConfig
.
enableRefresh
===
undefined
)
this
.
currentConfig
.
enableRefresh
=
true
;
if
(
!
this
.
currentConfig
.
clickAction
)
this
.
currentConfig
.
clickAction
=
'none'
;
if
(
!
this
.
currentConfig
.
customClickHandler
)
this
.
currentConfig
.
customClickHandler
=
''
;
// Layout configuration defaults
if
(
this
.
currentConfig
.
width
===
undefined
)
this
.
currentConfig
.
width
=
300
;
if
(
this
.
currentConfig
.
height
===
undefined
)
this
.
currentConfig
.
height
=
200
;
if
(
this
.
currentConfig
.
minWidth
===
undefined
)
this
.
currentConfig
.
minWidth
=
200
;
if
(
this
.
currentConfig
.
minHeight
===
undefined
)
this
.
currentConfig
.
minHeight
=
150
;
if
(
this
.
currentConfig
.
maxWidth
===
undefined
)
this
.
currentConfig
.
maxWidth
=
600
;
if
(
this
.
currentConfig
.
maxHeight
===
undefined
)
this
.
currentConfig
.
maxHeight
=
400
;
if
(
!
this
.
currentConfig
.
aspectRatio
)
this
.
currentConfig
.
aspectRatio
=
'auto'
;
if
(
this
.
currentConfig
.
responsive
===
undefined
)
this
.
currentConfig
.
responsive
=
true
;
// Data configuration defaults
if
(
!
this
.
currentConfig
.
dataSource
)
this
.
currentConfig
.
dataSource
=
'static'
;
if
(
!
this
.
currentConfig
.
apiEndpoint
)
this
.
currentConfig
.
apiEndpoint
=
''
;
if
(
this
.
currentConfig
.
refreshInterval
===
undefined
)
this
.
currentConfig
.
refreshInterval
=
0
;
if
(
this
.
currentConfig
.
cacheEnabled
===
undefined
)
this
.
currentConfig
.
cacheEnabled
=
false
;
if
(
this
.
currentConfig
.
cacheDuration
===
undefined
)
this
.
currentConfig
.
cacheDuration
=
300
;
if
(
!
this
.
currentConfig
.
dataTransform
)
this
.
currentConfig
.
dataTransform
=
''
;
// Security configuration defaults
if
(
this
.
currentConfig
.
requireAuth
===
undefined
)
this
.
currentConfig
.
requireAuth
=
false
;
if
(
!
this
.
currentConfig
.
allowedRoles
)
this
.
currentConfig
.
allowedRoles
=
''
;
if
(
this
.
currentConfig
.
dataEncryption
===
undefined
)
this
.
currentConfig
.
dataEncryption
=
false
;
if
(
this
.
currentConfig
.
auditLog
===
undefined
)
this
.
currentConfig
.
auditLog
=
false
;
if
(
this
.
currentConfig
.
rateLimit
===
undefined
)
this
.
currentConfig
.
rateLimit
=
0
;
}
}
resetConfig
():
void
{
...
...
src/app/portal-manage/dashboard-management/widgets/attendance-overview-widget.component.html
View file @
c81ea227
...
...
@@ -21,7 +21,7 @@
<!-- Content -->
<div
*
ngIf=
"!isLoading && !hasError"
class=
"grid grid-cols-1 sm:grid-cols-3 gap-4 h-full"
>
<!-- Present -->
<div
class=
"flex flex-col items-center justify-center p-4 bg-green-50 rounded-lg"
>
<i
class=
"bi bi-person-check-fill text-3xl text-green-500"
></i>
...
...
src/app/portal-manage/dashboard-management/widgets/simple-kpi-widget/simple-kpi-widget.component.html
View file @
c81ea227
<!-- simple-kpi-widget.component.html -->
<div
[
style
.
border-color
]="
borderColor
"
class=
"relative flex flex-col h-full rounded-xl bg-white bg-clip-border text-gray-700 shadow-md transition-shadow duration-300 ease-in-out hover:shadow-lg hover:shadow-gray-900/10 border-2"
>
<div
class=
"simple-kpi-widget"
[
ngStyle
]="
getAllStyles
()"
[
ngClass
]="['
custom-styled
',
getInteractionClasses
()]"
[
attr
.
data-widget-id
]="
widgetId
"
[
attr
.
data-source
]="
dataSource
"
[
title
]="
enableTooltip
?
(
title
+
'
:
'
+
value
+
(
unit
?
'
'
+
unit
:
''))
:
null
"
[
attr
.
role
]="
enableClick
?
'
button
'
:
'
img
'"
[
attr
.
tabindex
]="
enableClick
?
'
0
'
:
null
"
[
attr
.
aria-label
]="
title
+
'
:
'
+
value
+
(
unit
?
'
'
+
unit
:
'')"
[
attr
.
data-security
]="
getSecurityAttributes
()"
(
click
)="
onWidgetClick
($
event
)"
(
keydown
.
enter
)="
enableClick
?
onWidgetClick
($
event
)
:
null
"
(
keydown
.
space
)="
enableClick
?
onWidgetClick
($
event
)
:
null
"
>
<!-- Custom CSS -->
<style
*
ngIf=
"hasCustomCSS()"
[
innerHTML
]="
customCSS
"
></style>
<!-- Header -->
<div
[
style
.
background
]="
backgroundColor
"
class=
"relative mx-4 -mt-4 rounded-xl bg-clip-border text-white shadow-lg shadow-blue-500/40 p-3
"
>
<div
class=
"
flex items-center justify-between
"
>
<div
class=
"
flex items-center gap-3
"
>
<i
*
ngIf=
"icon"
[
style
.
color
]="
iconColor
"
[
class
]="'
bi
'
+
icon
+
'
text-3xl
'"
></i>
<h4
class=
"
text-lg font-semibold truncate
"
>
{{ title }}
</h4>
<div
class=
"widget-header"
[
style
.
background
]="
backgroundColor
"
>
<div
class=
"
header-content
"
>
<div
class=
"
header-left
"
>
<i
*
ngIf=
"icon"
[
style
.
color
]="
iconColor
"
[
class
]="'
bi
'
+
icon
+
'
header-icon
'"
></i>
<h4
class=
"
widget-title"
[
style
.
color
]="
textColor
"
>
{{ title }}
</h4>
</div>
<div
*
ngIf=
"
configObj?.trendValue"
class=
"text-sm font-medium
"
>
{{
configObj.
trendValue }}
<div
*
ngIf=
"
showTrend && trendValue"
class=
"trend-indicator"
[
ngStyle
]="
getTrendStyles
()
"
>
{{ trendValue }}
</div>
</div>
</div>
<!-- Body -->
<div
class=
"
flex-1 flex justify-center items-center p-3
"
>
<div
class=
"
widget-body
"
>
<!-- Loading State -->
<div
*
ngIf=
"isLoading"
class=
"
text-center
"
>
<div
class=
"
animate-spin rounded-full h-12 w-12 border-t-4 border-b-4 border-blue-500 mx-auto
"
></div>
<p
class=
"
text-gray-500 mt-2 text-base
"
>
Loading Data...
</p>
<div
*
ngIf=
"isLoading"
class=
"
loading-state
"
>
<div
class=
"
loading-spinner
"
></div>
<p
class=
"
loading-text"
[
style
.
color
]="
textColor
"
>
Loading Data...
</p>
</div>
<!-- Error State -->
<div
*
ngIf=
"hasError"
class=
"
text-center text-red-500
"
>
<i
class=
"bi bi-x-octagon-fill
text-4xl
"
></i>
<p
class=
"
mt-2 text-lg font-semibold
"
>
Error Loading
</p>
<p
class=
"
text-sm text-red-400
"
>
{{ errorMessage }}
</p>
<div
*
ngIf=
"hasError"
class=
"
error-state
"
>
<i
class=
"bi bi-x-octagon-fill
error-icon
"
></i>
<p
class=
"
error-title
"
>
Error Loading
</p>
<p
class=
"
error-message
"
>
{{ errorMessage }}
</p>
</div>
<!-- Content -->
<div
*
ngIf=
"!isLoading && !hasError
"
class=
"text-center
"
>
<
p
class=
"block font-sans text-5xl font-bold leading-snug tracking-normal text-blue-gray-900 antialiased
"
>
<div
*
ngIf=
"!isLoading && !hasError
&& hasRequiredRole()"
class=
"widget-content
"
>
<
div
class=
"kpi-value"
[
style
.
color
]="
accentColor
"
>
{{ value }}
</
p
>
<
p
*
ngIf=
"unit"
class=
"block font-sans text-xl font-normal leading-relaxed text-blue-gray-600 antialiased
"
>
</
div
>
<
div
*
ngIf=
"unit"
class=
"kpi-unit"
[
style
.
color
]="
textColor
"
>
{{ unit }}
</p>
</div>
<!-- Data Source Info (Debug Mode) -->
<div
*
ngIf=
"dataSource !== 'static'"
class=
"data-source-info"
[
style
.
color
]="
textColor
"
>
<small>
{{ getDataSourceInfo() }}
</small>
</div>
</div>
<!-- Security Warning -->
<div
*
ngIf=
"!isLoading && !hasError && !hasRequiredRole()"
class=
"security-warning"
>
<i
class=
"bi bi-shield-exclamation-fill"
></i>
<p>
Access Denied
</p>
<small>
Insufficient permissions
</small>
</div>
</div>
<!-- Export Button (if enabled) -->
<div
*
ngIf=
"enableExport"
class=
"export-button"
>
<button
class=
"btn btn-sm btn-outline-light"
(
click
)="
exportData
($
event
)"
title=
"Export Data"
>
<i
class=
"bi bi-download"
></i>
</button>
</div>
<!-- Refresh Button (if enabled) -->
<div
*
ngIf=
"enableRefresh && dataSource !== 'static'"
class=
"refresh-button"
>
<button
class=
"btn btn-sm btn-outline-light"
(
click
)="
refreshData
($
event
)"
title=
"Refresh Data"
>
<i
class=
"bi bi-arrow-clockwise"
></i>
</button>
</div>
</div>
src/app/portal-manage/dashboard-management/widgets/simple-kpi-widget/simple-kpi-widget.component.scss
View file @
c81ea227
/* Simple KPI Widget Styles */
.simple-kpi-widget
{
display
:
flex
;
flex-direction
:
column
;
height
:
100%
;
border
:
1px
solid
#e5e7eb
;
border-radius
:
8px
;
background
:
white
;
box-shadow
:
0
4px
6px
-1px
rgba
(
0
,
0
,
0
,
0
.1
)
,
0
2px
4px
-1px
rgba
(
0
,
0
,
0
,
0
.06
);
transition
:
all
0
.3s
ease
;
overflow
:
hidden
;
position
:
relative
;
&
:hover
{
box-shadow
:
0
10px
15px
-3px
rgba
(
0
,
0
,
0
,
0
.1
)
,
0
4px
6px
-2px
rgba
(
0
,
0
,
0
,
0
.05
);
transform
:
translateY
(
-2px
);
}
&
.custom-styled
{
// Custom styles will be applied via the customCSS property
}
}
/* Header */
.widget-header
{
padding
:
1rem
;
border-radius
:
8px
8px
0
0
;
position
:
relative
;
overflow
:
hidden
;
&
:
:
before
{
content
:
''
;
position
:
absolute
;
top
:
0
;
left
:
0
;
right
:
0
;
bottom
:
0
;
background
:
linear-gradient
(
135deg
,
rgba
(
255
,
255
,
255
,
0
.1
)
0%
,
rgba
(
255
,
255
,
255
,
0
.05
)
100%
);
pointer-events
:
none
;
}
}
.header-content
{
display
:
flex
;
align-items
:
center
;
justify-content
:
space-between
;
position
:
relative
;
z-index
:
1
;
}
.header-left
{
display
:
flex
;
align-items
:
center
;
gap
:
0
.75rem
;
}
.header-icon
{
font-size
:
1
.5rem
;
opacity
:
0
.9
;
}
.widget-title
{
font-size
:
1
.125rem
;
font-weight
:
600
;
margin
:
0
;
opacity
:
0
.95
;
text-shadow
:
0
1px
2px
rgba
(
0
,
0
,
0
,
0
.1
);
}
.trend-indicator
{
font-size
:
0
.875rem
;
font-weight
:
600
;
padding
:
0
.25rem
0
.5rem
;
border-radius
:
4px
;
background
:
rgba
(
255
,
255
,
255
,
0
.2
);
-webkit-backdrop-filter
:
blur
(
10px
);
backdrop-filter
:
blur
(
10px
);
border
:
1px
solid
rgba
(
255
,
255
,
255
,
0
.1
);
}
/* Body */
.widget-body
{
flex
:
1
;
display
:
flex
;
align-items
:
center
;
justify-content
:
center
;
padding
:
1
.5rem
;
position
:
relative
;
}
/* Loading State */
.loading-state
{
display
:
flex
;
flex-direction
:
column
;
align-items
:
center
;
gap
:
1rem
;
}
.loading-spinner
{
width
:
3rem
;
height
:
3rem
;
border
:
3px
solid
#e5e7eb
;
border-top
:
3px
solid
#3b82f6
;
border-radius
:
50%
;
animation
:
spin
1s
linear
infinite
;
}
.loading-text
{
font-size
:
0
.875rem
;
font-weight
:
500
;
opacity
:
0
.7
;
}
@keyframes
spin
{
0
%
{
transform
:
rotate
(
0deg
);
}
100
%
{
transform
:
rotate
(
360deg
);
}
}
/* Error State */
.error-state
{
display
:
flex
;
flex-direction
:
column
;
align-items
:
center
;
gap
:
0
.75rem
;
text-align
:
center
;
}
.error-icon
{
font-size
:
2
.5rem
;
color
:
#ef4444
;
opacity
:
0
.8
;
}
.error-title
{
font-size
:
1
.125rem
;
font-weight
:
600
;
color
:
#ef4444
;
margin
:
0
;
}
.error-message
{
font-size
:
0
.875rem
;
color
:
#f87171
;
margin
:
0
;
opacity
:
0
.8
;
}
/* Content */
.widget-content
{
display
:
flex
;
flex-direction
:
column
;
align-items
:
center
;
gap
:
0
.5rem
;
text-align
:
center
;
}
.kpi-value
{
font-size
:
3rem
;
font-weight
:
700
;
line-height
:
1
;
margin
:
0
;
text-shadow
:
0
1px
2px
rgba
(
0
,
0
,
0
,
0
.1
);
background
:
linear-gradient
(
135deg
,
currentColor
0%
,
rgba
(
255
,
255
,
255
,
0
.8
)
100%
);
-webkit-background-clip
:
text
;
-webkit-text-fill-color
:
transparent
;
background-clip
:
text
;
}
.kpi-unit
{
font-size
:
1
.25rem
;
font-weight
:
500
;
margin
:
0
;
opacity
:
0
.8
;
}
/* Responsive Design */
@media
(
max-width
:
768px
)
{
.widget-header
{
padding
:
0
.75rem
;
}
.header-left
{
gap
:
0
.5rem
;
}
.header-icon
{
font-size
:
1
.25rem
;
}
.widget-title
{
font-size
:
1rem
;
}
.widget-body
{
padding
:
1rem
;
}
.kpi-value
{
font-size
:
2
.5rem
;
}
.kpi-unit
{
font-size
:
1
.125rem
;
}
.trend-indicator
{
font-size
:
0
.75rem
;
padding
:
0
.25rem
0
.375rem
;
}
}
@media
(
max-width
:
480px
)
{
.widget-header
{
padding
:
0
.5rem
;
}
.header-left
{
flex-direction
:
column
;
align-items
:
flex-start
;
gap
:
0
.25rem
;
}
.header-content
{
flex-direction
:
column
;
align-items
:
flex-start
;
gap
:
0
.5rem
;
}
.widget-title
{
font-size
:
0
.875rem
;
}
.widget-body
{
padding
:
0
.75rem
;
}
.kpi-value
{
font-size
:
2rem
;
}
.kpi-unit
{
font-size
:
1rem
;
}
}
/* Dark Mode Support */
@media
(
prefers-color-scheme
:
dark
)
{
.simple-kpi-widget
{
background
:
#1f2937
;
border-color
:
#374151
;
color
:
#f9fafb
;
}
.loading-spinner
{
border-color
:
#374151
;
border-top-color
:
#60a5fa
;
}
.loading-text
{
color
:
#9ca3af
;
}
.error-icon
{
color
:
#f87171
;
}
.error-title
{
color
:
#f87171
;
}
.error-message
{
color
:
#fca5a5
;
}
}
/* Animation Enhancements */
.simple-kpi-widget.animate-in
{
animation
:
fadeInUp
0
.5s
ease-out
;
}
.simple-kpi-widget.animate-out
{
animation
:
fadeOutDown
0
.3s
ease-in
;
}
@keyframes
fadeInUp
{
from
{
opacity
:
0
;
transform
:
translateY
(
20px
);
}
to
{
opacity
:
1
;
transform
:
translateY
(
0
);
}
}
@keyframes
fadeOutDown
{
from
{
opacity
:
1
;
transform
:
translateY
(
0
);
}
to
{
opacity
:
0
;
transform
:
translateY
(
20px
);
}
}
/* Accessibility */
.simple-kpi-widget
{
&
:focus-within
{
outline
:
2px
solid
#3b82f6
;
outline-offset
:
2px
;
}
}
.widget-title
{
&
:focus
{
outline
:
2px
solid
#3b82f6
;
outline-offset
:
2px
;
border-radius
:
4px
;
}
}
/* Print Styles */
@media
print
{
.simple-kpi-widget
{
box-shadow
:
none
;
border
:
1px
solid
#000
;
break-inside
:
avoid
;
}
.widget-header
{
background
:
#f8f9fa
!
important
;
color
:
#000
!
important
;
}
.kpi-value
{
-webkit-text-fill-color
:
initial
!
important
;
color
:
#000
!
important
;
}
.kpi-unit
{
color
:
#000
!
important
;
}
}
/* High Contrast Mode */
@media
(
prefers-contrast
:
high
)
{
.simple-kpi-widget
{
border-width
:
2px
;
}
.widget-header
{
border-bottom
:
2px
solid
currentColor
;
}
.trend-indicator
{
border-width
:
2px
;
}
}
/* Interaction Styles */
.simple-kpi-widget
{
&
.hover-enabled
{
&
:hover
{
transform
:
translateY
(
-2px
);
box-shadow
:
0
8px
25px
rgba
(
0
,
0
,
0
,
0
.15
);
}
}
&
.click-enabled
{
cursor
:
pointer
;
&
:focus
{
outline
:
2px
solid
#007bff
;
outline-offset
:
2px
;
}
&
:active
{
transform
:
translateY
(
1px
);
}
}
&
.tooltip-enabled
{
position
:
relative
;
}
&
.responsive
{
@media
(
max-width
:
768px
)
{
.widget-header
{
padding
:
12px
;
}
.header-content
{
flex-direction
:
column
;
gap
:
8px
;
}
.kpi-value
{
font-size
:
1
.5rem
;
}
}
}
}
/* Security Warning */
.security-warning
{
display
:
flex
;
flex-direction
:
column
;
align-items
:
center
;
justify-content
:
center
;
height
:
100%
;
padding
:
20px
;
text-align
:
center
;
color
:
#dc3545
;
background
:
rgba
(
220
,
53
,
69
,
0
.1
);
border-radius
:
8px
;
i
{
font-size
:
2rem
;
margin-bottom
:
10px
;
}
p
{
margin
:
5px
0
;
font-weight
:
600
;
}
small
{
font-size
:
0
.875rem
;
opacity
:
0
.8
;
}
}
/* Data Source Info */
.data-source-info
{
position
:
absolute
;
bottom
:
8px
;
right
:
8px
;
font-size
:
0
.75rem
;
opacity
:
0
.7
;
background
:
rgba
(
0
,
0
,
0
,
0
.1
);
padding
:
2px
6px
;
border-radius
:
4px
;
}
/* Action Buttons */
.export-button
,
.refresh-button
{
position
:
absolute
;
top
:
8px
;
right
:
8px
;
z-index
:
10
;
button
{
background
:
rgba
(
255
,
255
,
255
,
0
.2
);
border
:
1px
solid
rgba
(
255
,
255
,
255
,
0
.3
);
color
:
white
;
padding
:
4px
8px
;
border-radius
:
4px
;
font-size
:
0
.75rem
;
transition
:
all
0
.2s
ease
;
&
:hover
{
background
:
rgba
(
255
,
255
,
255
,
0
.3
);
transform
:
scale
(
1
.05
);
}
&
:focus
{
outline
:
2px
solid
#007bff
;
outline-offset
:
1px
;
}
}
}
.export-button
{
top
:
8px
;
}
.refresh-button
{
top
:
40px
;
}
/* Animation Keyframes */
@keyframes
fadeIn
{
from
{
opacity
:
0
;
}
to
{
opacity
:
1
;
}
}
@keyframes
slideInUp
{
from
{
opacity
:
0
;
transform
:
translateY
(
20px
);
}
to
{
opacity
:
1
;
transform
:
translateY
(
0
);
}
}
@keyframes
bounceIn
{
0
%
{
opacity
:
0
;
transform
:
scale
(
0
.3
);
}
50
%
{
opacity
:
1
;
transform
:
scale
(
1
.05
);
}
70
%
{
transform
:
scale
(
0
.9
);
}
100
%
{
opacity
:
1
;
transform
:
scale
(
1
);
}
}
@keyframes
pulse
{
0
%
{
transform
:
scale
(
1
);
}
50
%
{
transform
:
scale
(
1
.05
);
}
100
%
{
transform
:
scale
(
1
);
}
}
/* Layout Constraints */
.simple-kpi-widget
{
&
.aspect-ratio-16-9
{
aspect-ratio
:
16
/
9
;
}
&
.aspect-ratio-4-3
{
aspect-ratio
:
4
/
3
;
}
&
.aspect-ratio-1-1
{
aspect-ratio
:
1
/
1
;
}
&
.aspect-ratio-3-2
{
aspect-ratio
:
3
/
2
;
}
}
/* High Contrast Mode */
@media
(
prefers-contrast
:
high
)
{
.simple-kpi-widget
{
border
:
2px
solid
;
.widget-header
{
border-bottom
:
2px
solid
;
}
.trend-indicator
{
border
:
1px
solid
;
background
:
transparent
;
}
}
}
/* Reduced Motion */
@media
(
prefers-reduced-motion
:
reduce
)
{
.simple-kpi-widget
{
transition
:
none
;
&
:hover
{
transform
:
none
;
}
&
.hover-enabled
:hover
{
transform
:
none
;
box-shadow
:
0
4px
12px
rgba
(
0
,
0
,
0
,
0
.1
);
}
}
.simple-kpi-widget
.loading-spinner
{
animation
:
none
;
}
.simple-kpi-widget.animate-in
,
.simple-kpi-widget.animate-out
{
animation
:
none
;
}
.export-button
button
,
.refresh-button
button
{
&
:hover
{
transform
:
none
;
}
}
}
src/app/portal-manage/dashboard-management/widgets/simple-kpi-widget/simple-kpi-widget.component.ts
View file @
c81ea227
...
...
@@ -12,35 +12,171 @@ import { BaseWidgetComponent } from '../base-widget.component';
styleUrls
:
[
'./simple-kpi-widget.component.scss'
]
})
export
class
SimpleKpiWidgetComponent
extends
BaseWidgetComponent
{
// Widget identification
public
widgetId
:
string
=
'simple-kpi-widget'
;
// Display properties
public
value
:
string
=
'...'
;
public
unit
:
string
=
''
;
public
icon
:
string
=
''
;
public
backgroundColor
:
string
=
'linear-gradient(to top right, #3366FF, #00CCFF)'
;
public
iconColor
:
string
=
'#FFFFFF'
;
public
borderColor
:
string
=
'#FFFFFF'
;
public
textColor
:
string
=
'#FFFFFF'
;
public
accentColor
:
string
=
'#FFFFFF'
;
public
borderRadius
:
number
=
8
;
// Trend properties
public
showTrend
:
boolean
=
false
;
public
trendValue
:
string
=
''
;
public
trendType
:
string
=
'percentage'
;
public
trendColor
:
string
=
'#28a745'
;
// Style properties
public
fontSize
:
number
=
16
;
public
fontWeight
:
string
=
'normal'
;
public
fontFamily
:
string
=
'system-ui, -apple-system, sans-serif'
;
public
padding
:
number
=
16
;
public
margin
:
number
=
8
;
public
borderWidth
:
number
=
1
;
public
customCSS
:
string
=
''
;
// Animation properties
public
enableAnimations
:
boolean
=
true
;
public
animationType
:
string
=
'fade'
;
public
animationDuration
:
number
=
300
;
public
animationDelay
:
number
=
0
;
public
hoverEffects
:
boolean
=
true
;
// Interaction properties
public
enableTooltip
:
boolean
=
true
;
public
enableClick
:
boolean
=
true
;
public
enableHover
:
boolean
=
true
;
public
enableSelection
:
boolean
=
false
;
public
enableExport
:
boolean
=
false
;
public
enableRefresh
:
boolean
=
true
;
public
clickAction
:
string
=
'none'
;
public
customClickHandler
:
string
=
''
;
// Layout properties
public
width
:
number
=
300
;
public
height
:
number
=
200
;
public
minWidth
:
number
=
200
;
public
minHeight
:
number
=
150
;
public
maxWidth
:
number
=
600
;
public
maxHeight
:
number
=
400
;
public
aspectRatio
:
string
=
'auto'
;
public
responsive
:
boolean
=
true
;
// Data properties
public
dataSource
:
string
=
'static'
;
public
apiEndpoint
:
string
=
''
;
public
refreshInterval
:
number
=
0
;
public
cacheEnabled
:
boolean
=
false
;
public
cacheDuration
:
number
=
300
;
public
dataTransform
:
string
=
''
;
// Security properties
public
requireAuth
:
boolean
=
false
;
public
allowedRoles
:
string
=
''
;
public
dataEncryption
:
boolean
=
false
;
public
auditLog
:
boolean
=
false
;
public
rateLimit
:
number
=
0
;
constructor
(
protected
override
dashboardStateService
:
DashboardStateService
)
{
super
(
dashboardStateService
);
}
applyInitialConfig
():
void
{
// Basic configuration
this
.
title
=
this
.
configObj
.
title
||
'KPI'
;
this
.
unit
=
this
.
configObj
.
unit
||
''
;
this
.
icon
=
this
.
configObj
.
icon
||
'info'
;
// Handle color property (fallback to backgroundColor)
const
bgColor
=
this
.
configObj
.
backgroundColor
||
this
.
configObj
.
color
||
'linear-gradient(to top right, #3366FF, #00CCFF)'
;
this
.
backgroundColor
=
bgColor
.
startsWith
(
'#'
)
?
`linear-gradient(to top right,
${
bgColor
}
,
${
bgColor
}
dd)`
:
bgColor
;
this
.
iconColor
=
this
.
configObj
.
iconColor
||
'#FFFFFF'
;
// Style configuration
this
.
backgroundColor
=
this
.
configObj
.
backgroundColor
||
'linear-gradient(to top right, #3366FF, #00CCFF)'
;
this
.
textColor
=
this
.
configObj
.
textColor
||
'#FFFFFF'
;
this
.
accentColor
=
this
.
configObj
.
accentColor
||
'#FFFFFF'
;
this
.
borderColor
=
this
.
configObj
.
borderColor
||
'#FFFFFF'
;
this
.
borderRadius
=
this
.
configObj
.
borderRadius
||
8
;
this
.
iconColor
=
this
.
configObj
.
iconColor
||
'#FFFFFF'
;
// Typography configuration
this
.
fontSize
=
this
.
configObj
.
fontSize
||
16
;
this
.
fontWeight
=
this
.
configObj
.
fontWeight
||
'normal'
;
this
.
fontFamily
=
this
.
configObj
.
fontFamily
||
'system-ui, -apple-system, sans-serif'
;
// Layout configuration
this
.
padding
=
this
.
configObj
.
padding
||
16
;
this
.
margin
=
this
.
configObj
.
margin
||
8
;
this
.
borderWidth
=
this
.
configObj
.
borderWidth
||
1
;
// Custom CSS
this
.
customCSS
=
this
.
configObj
.
customCSS
||
''
;
// Trend configuration
this
.
showTrend
=
this
.
configObj
.
showTrend
||
false
;
this
.
trendType
=
this
.
configObj
.
trendType
||
'percentage'
;
this
.
trendColor
=
this
.
configObj
.
trendColor
||
'#28a745'
;
// Animation configuration
this
.
enableAnimations
=
this
.
configObj
.
enableAnimations
!==
undefined
?
this
.
configObj
.
enableAnimations
:
true
;
this
.
animationType
=
this
.
configObj
.
animationType
||
'fade'
;
this
.
animationDuration
=
this
.
configObj
.
animationDuration
||
300
;
this
.
animationDelay
=
this
.
configObj
.
animationDelay
||
0
;
this
.
hoverEffects
=
this
.
configObj
.
hoverEffects
!==
undefined
?
this
.
configObj
.
hoverEffects
:
true
;
// Interaction configuration
this
.
enableTooltip
=
this
.
configObj
.
enableTooltip
!==
undefined
?
this
.
configObj
.
enableTooltip
:
true
;
this
.
enableClick
=
this
.
configObj
.
enableClick
!==
undefined
?
this
.
configObj
.
enableClick
:
true
;
this
.
enableHover
=
this
.
configObj
.
enableHover
!==
undefined
?
this
.
configObj
.
enableHover
:
true
;
this
.
enableSelection
=
this
.
configObj
.
enableSelection
!==
undefined
?
this
.
configObj
.
enableSelection
:
false
;
this
.
enableExport
=
this
.
configObj
.
enableExport
!==
undefined
?
this
.
configObj
.
enableExport
:
false
;
this
.
enableRefresh
=
this
.
configObj
.
enableRefresh
!==
undefined
?
this
.
configObj
.
enableRefresh
:
true
;
this
.
clickAction
=
this
.
configObj
.
clickAction
||
'none'
;
this
.
customClickHandler
=
this
.
configObj
.
customClickHandler
||
''
;
// Layout configuration
this
.
width
=
this
.
configObj
.
width
||
300
;
this
.
height
=
this
.
configObj
.
height
||
200
;
this
.
minWidth
=
this
.
configObj
.
minWidth
||
200
;
this
.
minHeight
=
this
.
configObj
.
minHeight
||
150
;
this
.
maxWidth
=
this
.
configObj
.
maxWidth
||
600
;
this
.
maxHeight
=
this
.
configObj
.
maxHeight
||
400
;
this
.
aspectRatio
=
this
.
configObj
.
aspectRatio
||
'auto'
;
this
.
responsive
=
this
.
configObj
.
responsive
!==
undefined
?
this
.
configObj
.
responsive
:
true
;
// Data configuration
this
.
dataSource
=
this
.
configObj
.
dataSource
||
'static'
;
this
.
apiEndpoint
=
this
.
configObj
.
apiEndpoint
||
''
;
this
.
refreshInterval
=
this
.
configObj
.
refreshInterval
||
0
;
this
.
cacheEnabled
=
this
.
configObj
.
cacheEnabled
!==
undefined
?
this
.
configObj
.
cacheEnabled
:
false
;
this
.
cacheDuration
=
this
.
configObj
.
cacheDuration
||
300
;
this
.
dataTransform
=
this
.
configObj
.
dataTransform
||
''
;
// Security configuration
this
.
requireAuth
=
this
.
configObj
.
requireAuth
!==
undefined
?
this
.
configObj
.
requireAuth
:
false
;
this
.
allowedRoles
=
this
.
configObj
.
allowedRoles
||
''
;
this
.
dataEncryption
=
this
.
configObj
.
dataEncryption
!==
undefined
?
this
.
configObj
.
dataEncryption
:
false
;
this
.
auditLog
=
this
.
configObj
.
auditLog
!==
undefined
?
this
.
configObj
.
auditLog
:
false
;
this
.
rateLimit
=
this
.
configObj
.
rateLimit
||
0
;
// Handle gradient background for hex colors
if
(
this
.
backgroundColor
.
startsWith
(
'#'
))
{
this
.
backgroundColor
=
`linear-gradient(to top right,
${
this
.
backgroundColor
}
,
${
this
.
backgroundColor
}
dd)`
;
}
this
.
value
=
'-'
;
// Initial state before data loads
}
onDataUpdate
(
data
:
any
[]):
void
{
override
onDataUpdate
(
data
:
any
[]):
void
{
// Transform data if transform function is provided
const
transformedData
=
this
.
transformData
(
data
);
// Handle count aggregation separately as it doesn't need a valueField
if
(
this
.
configObj
.
aggregation
===
'count'
)
{
this
.
value
=
(
data
?.
length
||
0
).
toLocaleString
();
this
.
value
=
(
transformedData
?.
length
||
0
).
toLocaleString
();
this
.
updateTrendData
(
transformedData
);
return
;
}
...
...
@@ -52,24 +188,424 @@ export class SimpleKpiWidgetComponent extends BaseWidgetComponent {
}
// If data is empty, result is 0
if
(
!
data
||
d
ata
.
length
===
0
)
{
if
(
!
transformedData
||
transformedD
ata
.
length
===
0
)
{
this
.
value
=
'0'
;
this
.
trendValue
=
''
;
return
;
}
let
kpiValue
=
0
;
if
(
this
.
configObj
.
aggregation
===
'sum'
)
{
kpiValue
=
data
.
reduce
((
sum
,
item
)
=>
sum
+
(
item
[
this
.
configObj
.
valueField
]
||
0
),
0
);
kpiValue
=
transformedData
.
reduce
((
sum
,
item
)
=>
sum
+
(
item
[
this
.
configObj
.
valueField
]
||
0
),
0
);
}
else
if
(
this
.
configObj
.
aggregation
===
'average'
)
{
const
sum
=
transformedData
.
reduce
((
sum
,
item
)
=>
sum
+
(
item
[
this
.
configObj
.
valueField
]
||
0
),
0
);
kpiValue
=
sum
/
transformedData
.
length
;
}
else
if
(
this
.
configObj
.
aggregation
===
'max'
)
{
kpiValue
=
Math
.
max
(...
transformedData
.
map
(
item
=>
item
[
this
.
configObj
.
valueField
]
||
0
));
}
else
if
(
this
.
configObj
.
aggregation
===
'min'
)
{
kpiValue
=
Math
.
min
(...
transformedData
.
map
(
item
=>
item
[
this
.
configObj
.
valueField
]
||
0
));
}
else
{
// Default to first value if no aggregation is specified
kpiValue
=
data
[
0
][
this
.
configObj
.
valueField
]
||
0
;
kpiValue
=
transformedData
[
0
][
this
.
configObj
.
valueField
]
||
0
;
}
// Format the value based on configuration
this
.
value
=
this
.
formatValue
(
kpiValue
);
// Update trend data if enabled
this
.
updateTrendData
(
transformedData
,
kpiValue
);
}
private
formatValue
(
value
:
number
):
string
{
if
(
this
.
configObj
.
decimalPlaces
!==
undefined
)
{
return
value
.
toFixed
(
this
.
configObj
.
decimalPlaces
);
}
return
value
.
toLocaleString
();
}
private
updateTrendData
(
data
:
any
[],
currentValue
?:
number
):
void
{
if
(
!
this
.
showTrend
||
!
this
.
configObj
.
trendField
)
{
this
.
trendValue
=
''
;
return
;
}
// Calculate trend based on trendField
if
(
data
&&
data
.
length
>
0
)
{
const
trendData
=
data
.
map
(
item
=>
item
[
this
.
configObj
.
trendField
]
||
0
);
if
(
this
.
trendType
===
'percentage'
)
{
// Calculate percentage change
if
(
trendData
.
length
>=
2
)
{
const
current
=
trendData
[
0
];
const
previous
=
trendData
[
1
];
const
change
=
((
current
-
previous
)
/
previous
)
*
100
;
this
.
trendValue
=
`
${
change
>=
0
?
'+'
:
''
}${
change
.
toFixed
(
1
)}
%`
;
this
.
trendColor
=
change
>=
0
?
'#28a745'
:
'#dc3545'
;
}
}
else
if
(
this
.
trendType
===
'absolute'
)
{
// Calculate absolute change
if
(
trendData
.
length
>=
2
)
{
const
current
=
trendData
[
0
];
const
previous
=
trendData
[
1
];
const
change
=
current
-
previous
;
this
.
trendValue
=
`
${
change
>=
0
?
'+'
:
''
}${
change
.
toLocaleString
()}
`
;
this
.
trendColor
=
change
>=
0
?
'#28a745'
:
'#dc3545'
;
}
}
else
if
(
this
.
trendType
===
'ratio'
)
{
// Calculate ratio
if
(
trendData
.
length
>=
2
)
{
const
current
=
trendData
[
0
];
const
previous
=
trendData
[
1
];
const
ratio
=
previous
>
0
?
current
/
previous
:
1
;
this
.
trendValue
=
`
${
ratio
.
toFixed
(
2
)}
x`
;
this
.
trendColor
=
ratio
>=
1
?
'#28a745'
:
'#dc3545'
;
}
}
}
this
.
value
=
kpiValue
.
toLocaleString
();
}
onReset
():
void
{
// Reset to default values
this
.
title
=
'KPI (Default)'
;
this
.
value
=
'123,456'
;
this
.
unit
=
'#'
;
this
.
icon
=
'info'
;
// Style reset
this
.
backgroundColor
=
'linear-gradient(to top right, #3366FF, #00CCFF)'
;
this
.
textColor
=
'#FFFFFF'
;
this
.
accentColor
=
'#FFFFFF'
;
this
.
borderColor
=
'#FFFFFF'
;
this
.
iconColor
=
'#FFFFFF'
;
this
.
borderRadius
=
8
;
this
.
fontSize
=
16
;
this
.
fontWeight
=
'normal'
;
this
.
fontFamily
=
'system-ui, -apple-system, sans-serif'
;
this
.
padding
=
16
;
this
.
margin
=
8
;
this
.
borderWidth
=
1
;
this
.
customCSS
=
''
;
// Trend reset
this
.
showTrend
=
false
;
this
.
trendValue
=
''
;
this
.
trendType
=
'percentage'
;
this
.
trendColor
=
'#28a745'
;
// Animation reset
this
.
enableAnimations
=
true
;
this
.
animationType
=
'fade'
;
this
.
animationDuration
=
300
;
this
.
animationDelay
=
0
;
this
.
hoverEffects
=
true
;
// Interaction reset
this
.
enableTooltip
=
true
;
this
.
enableClick
=
true
;
this
.
enableHover
=
true
;
this
.
enableSelection
=
false
;
this
.
enableExport
=
false
;
this
.
enableRefresh
=
true
;
this
.
clickAction
=
'none'
;
this
.
customClickHandler
=
''
;
// Layout reset
this
.
width
=
300
;
this
.
height
=
200
;
this
.
minWidth
=
200
;
this
.
minHeight
=
150
;
this
.
maxWidth
=
600
;
this
.
maxHeight
=
400
;
this
.
aspectRatio
=
'auto'
;
this
.
responsive
=
true
;
// Data reset
this
.
dataSource
=
'static'
;
this
.
apiEndpoint
=
''
;
this
.
refreshInterval
=
0
;
this
.
cacheEnabled
=
false
;
this
.
cacheDuration
=
300
;
this
.
dataTransform
=
''
;
// Security reset
this
.
requireAuth
=
false
;
this
.
allowedRoles
=
''
;
this
.
dataEncryption
=
false
;
this
.
auditLog
=
false
;
this
.
rateLimit
=
0
;
}
// Helper method to get computed styles for dynamic styling
getWidgetStyles
():
{
[
key
:
string
]:
string
}
{
return
{
'background'
:
this
.
backgroundColor
,
'color'
:
this
.
textColor
,
'border-color'
:
this
.
borderColor
,
'border-radius'
:
`
${
this
.
borderRadius
}
px`
,
'border-width'
:
`
${
this
.
borderWidth
}
px`
,
'padding'
:
`
${
this
.
padding
}
px`
,
'margin'
:
`
${
this
.
margin
}
px`
,
'font-size'
:
`
${
this
.
fontSize
}
px`
,
'font-weight'
:
this
.
fontWeight
,
'font-family'
:
this
.
fontFamily
};
}
// Helper method to combine all styles
getAllStyles
():
{
[
key
:
string
]:
string
}
{
return
{
...
this
.
getWidgetStyles
(),
...
this
.
getLayoutStyles
(),
...
this
.
getAnimationStyles
()
};
}
// Helper method to get trend styles
getTrendStyles
():
{
[
key
:
string
]:
string
}
{
return
{
'color'
:
this
.
trendColor
,
'font-weight'
:
'bold'
};
}
// Helper method to check if custom CSS should be applied
hasCustomCSS
():
boolean
{
return
!!
(
this
.
customCSS
&&
this
.
customCSS
.
trim
().
length
>
0
);
}
// Helper method to get animation styles
getAnimationStyles
():
{
[
key
:
string
]:
string
}
{
if
(
!
this
.
enableAnimations
)
{
return
{
'animation'
:
'none'
};
}
const
animationMap
:
{
[
key
:
string
]:
string
}
=
{
'fade'
:
'fadeIn'
,
'slide'
:
'slideInUp'
,
'bounce'
:
'bounceIn'
,
'pulse'
:
'pulse'
,
'none'
:
'none'
};
return
{
'animation'
:
`
${
animationMap
[
this
.
animationType
]}
${
this
.
animationDuration
}
ms ease-in-out`
,
'animation-delay'
:
`
${
this
.
animationDelay
}
ms`
,
'animation-fill-mode'
:
'both'
};
}
// Helper method to get layout styles
getLayoutStyles
():
{
[
key
:
string
]:
string
}
{
const
styles
:
{
[
key
:
string
]:
string
}
=
{
'width'
:
`
${
this
.
width
}
px`
,
'height'
:
`
${
this
.
height
}
px`
,
'min-width'
:
`
${
this
.
minWidth
}
px`
,
'min-height'
:
`
${
this
.
minHeight
}
px`
,
'max-width'
:
`
${
this
.
maxWidth
}
px`
,
'max-height'
:
`
${
this
.
maxHeight
}
px`
};
// Handle aspect ratio
if
(
this
.
aspectRatio
!==
'auto'
)
{
const
[
width
,
height
]
=
this
.
aspectRatio
.
split
(
':'
);
const
ratio
=
parseFloat
(
height
)
/
parseFloat
(
width
);
styles
[
'aspect-ratio'
]
=
this
.
aspectRatio
;
}
return
styles
;
}
// Helper method to get interaction classes
getInteractionClasses
():
string
{
const
classes
:
string
[]
=
[];
if
(
this
.
enableHover
&&
this
.
hoverEffects
)
{
classes
.
push
(
'hover-enabled'
);
}
if
(
this
.
enableClick
)
{
classes
.
push
(
'click-enabled'
);
}
if
(
this
.
enableTooltip
)
{
classes
.
push
(
'tooltip-enabled'
);
}
if
(
this
.
responsive
)
{
classes
.
push
(
'responsive'
);
}
return
classes
.
join
(
' '
);
}
// Helper method to handle click events
onWidgetClick
(
event
:
Event
):
void
{
if
(
!
this
.
enableClick
)
{
return
;
}
// Audit logging
if
(
this
.
auditLog
)
{
console
.
log
(
'Widget clicked:'
,
{
widgetId
:
this
.
widgetId
,
timestamp
:
new
Date
().
toISOString
(),
clickAction
:
this
.
clickAction
});
}
switch
(
this
.
clickAction
)
{
case
'drill_down'
:
this
.
handleDrillDown
(
event
);
break
;
case
'open_modal'
:
this
.
handleOpenModal
(
event
);
break
;
case
'navigate'
:
this
.
handleNavigate
(
event
);
break
;
case
'custom'
:
this
.
handleCustomClick
(
event
);
break
;
default
:
// No action
break
;
}
}
// Helper methods for different click actions
private
handleDrillDown
(
event
:
Event
):
void
{
console
.
log
(
'Drill down action triggered'
);
// Implement drill down logic
}
private
handleOpenModal
(
event
:
Event
):
void
{
console
.
log
(
'Open modal action triggered'
);
// Implement modal opening logic
}
private
handleNavigate
(
event
:
Event
):
void
{
console
.
log
(
'Navigate action triggered'
);
// Implement navigation logic
}
private
handleCustomClick
(
event
:
Event
):
void
{
if
(
this
.
customClickHandler
)
{
try
{
// Safely execute custom handler
const
handler
=
new
Function
(
'event'
,
'widget'
,
this
.
customClickHandler
);
handler
(
event
,
this
);
}
catch
(
error
)
{
console
.
error
(
'Error executing custom click handler:'
,
error
);
}
}
}
// Helper method to get security attributes
getSecurityAttributes
():
{
[
key
:
string
]:
string
}
{
const
attrs
:
{
[
key
:
string
]:
string
}
=
{};
if
(
this
.
requireAuth
)
{
attrs
[
'data-require-auth'
]
=
'true'
;
}
if
(
this
.
allowedRoles
)
{
attrs
[
'data-allowed-roles'
]
=
this
.
allowedRoles
;
}
if
(
this
.
dataEncryption
)
{
attrs
[
'data-encrypted'
]
=
'true'
;
}
return
attrs
;
}
// Helper method to check if user has required role
hasRequiredRole
():
boolean
{
if
(
!
this
.
requireAuth
||
!
this
.
allowedRoles
)
{
return
true
;
}
// This would typically check against user's actual roles
// For now, we'll assume the user has the required role
return
true
;
}
// Helper method to get data source info
getDataSourceInfo
():
string
{
switch
(
this
.
dataSource
)
{
case
'api'
:
return
`API:
${
this
.
apiEndpoint
}
`
;
case
'websocket'
:
return
'WebSocket Connection'
;
case
'file'
:
return
'File Upload'
;
default
:
return
'Static Data'
;
}
}
// Export data functionality
exportData
(
event
:
Event
):
void
{
event
.
stopPropagation
();
const
exportData
=
{
widgetId
:
this
.
widgetId
,
title
:
this
.
title
,
value
:
this
.
value
,
unit
:
this
.
unit
,
timestamp
:
new
Date
().
toISOString
(),
data
:
this
.
originalData
};
const
blob
=
new
Blob
([
JSON
.
stringify
(
exportData
,
null
,
2
)],
{
type
:
'application/json'
});
const
url
=
window
.
URL
.
createObjectURL
(
blob
);
const
link
=
document
.
createElement
(
'a'
);
link
.
href
=
url
;
link
.
download
=
`kpi-widget-
${
this
.
widgetId
}
-
${
new
Date
().
toISOString
().
split
(
'T'
)[
0
]}
.json`
;
link
.
click
();
window
.
URL
.
revokeObjectURL
(
url
);
console
.
log
(
'Data exported:'
,
exportData
);
}
// Refresh data functionality
refreshData
(
event
:
Event
):
void
{
event
.
stopPropagation
();
if
(
this
.
isLoading
)
{
return
;
// Prevent multiple simultaneous refreshes
}
this
.
isLoading
=
true
;
this
.
hasError
=
false
;
// Simulate API call or data refresh
setTimeout
(()
=>
{
// This would typically make an actual API call
console
.
log
(
'Refreshing data from:'
,
this
.
apiEndpoint
);
// For demo purposes, we'll just trigger a data update
if
(
this
.
originalData
)
{
this
.
onDataUpdate
(
this
.
originalData
);
}
this
.
isLoading
=
false
;
},
1000
);
}
// Method to transform data if transform function is provided
private
transformData
(
data
:
any
[]):
any
[]
{
if
(
!
this
.
dataTransform
||
!
data
)
{
return
data
;
}
try
{
const
transformFunction
=
new
Function
(
'data'
,
this
.
dataTransform
);
return
transformFunction
(
data
);
}
catch
(
error
)
{
console
.
error
(
'Error transforming data:'
,
error
);
return
data
;
}
}
}
Write
Preview
Markdown
is supported
0%
Try again
or
attach a new file
Attach a file
Cancel
You are about to add
0
people
to the discussion. Proceed with caution.
Finish editing this message first!
Cancel
Please
register
or
sign in
to comment