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
Expand all
Hide whitespace changes
Inline
Side-by-side
Showing
7 changed files
with
744 additions
and
22 deletions
+744
-22
simple-kpi-config.example.ts
...ashboard-management/examples/simple-kpi-config.example.ts
+0
-0
widget-config.component.html
...ard-management/widget-config/widget-config.component.html
+0
-0
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
+0
-0
No files found.
src/app/portal-manage/dashboard-management/examples/simple-kpi-config.example.ts
0 → 100644
View file @
c81ea227
This diff is collapsed.
Click to expand it.
src/app/portal-manage/dashboard-management/widget-config/widget-config.component.html
View file @
c81ea227
This diff is collapsed.
Click to expand it.
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
This diff is collapsed.
Click to expand it.
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