Commit c81ea227 by Ooh-Ao

kpi

parent 1fc00b6e
...@@ -86,6 +86,80 @@ export class WidgetConfigComponent implements OnInit { ...@@ -86,6 +86,80 @@ export class WidgetConfigComponent implements OnInit {
if (!this.currentConfig.yFields) this.currentConfig.yFields = []; if (!this.currentConfig.yFields) this.currentConfig.yFields = [];
if (!this.currentConfig.series) this.currentConfig.series = []; 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 { resetConfig(): void {
......
...@@ -21,7 +21,7 @@ ...@@ -21,7 +21,7 @@
<!-- Content --> <!-- Content -->
<div *ngIf="!isLoading && !hasError" class="grid grid-cols-1 sm:grid-cols-3 gap-4 h-full"> <div *ngIf="!isLoading && !hasError" class="grid grid-cols-1 sm:grid-cols-3 gap-4 h-full">
<!-- Present --> <!-- Present -->
<div class="flex flex-col items-center justify-center p-4 bg-green-50 rounded-lg"> <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> <i class="bi bi-person-check-fill text-3xl text-green-500"></i>
......
<!-- simple-kpi-widget.component.html --> <!-- 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 --> <!-- 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="widget-header" [style.background]="backgroundColor">
<div class="flex items-center justify-between"> <div class="header-content">
<div class="flex items-center gap-3"> <div class="header-left">
<i *ngIf="icon" [style.color]="iconColor" [class]="'bi ' + icon + ' text-3xl'"></i> <i *ngIf="icon" [style.color]="iconColor" [class]="'bi ' + icon + ' header-icon'"></i>
<h4 class="text-lg font-semibold truncate">{{ title }}</h4> <h4 class="widget-title" [style.color]="textColor">{{ title }}</h4>
</div> </div>
<div *ngIf="configObj?.trendValue" class="text-sm font-medium"> <div *ngIf="showTrend && trendValue" class="trend-indicator" [ngStyle]="getTrendStyles()">
{{ configObj.trendValue }} {{ trendValue }}
</div> </div>
</div> </div>
</div> </div>
<!-- Body --> <!-- Body -->
<div class="flex-1 flex justify-center items-center p-3"> <div class="widget-body">
<!-- Loading State --> <!-- Loading State -->
<div *ngIf="isLoading" class="text-center"> <div *ngIf="isLoading" class="loading-state">
<div class="animate-spin rounded-full h-12 w-12 border-t-4 border-b-4 border-blue-500 mx-auto"></div> <div class="loading-spinner"></div>
<p class="text-gray-500 mt-2 text-base">Loading Data...</p> <p class="loading-text" [style.color]="textColor">Loading Data...</p>
</div> </div>
<!-- Error State --> <!-- Error State -->
<div *ngIf="hasError" class="text-center text-red-500"> <div *ngIf="hasError" class="error-state">
<i class="bi bi-x-octagon-fill text-4xl"></i> <i class="bi bi-x-octagon-fill error-icon"></i>
<p class="mt-2 text-lg font-semibold">Error Loading</p> <p class="error-title">Error Loading</p>
<p class="text-sm text-red-400">{{ errorMessage }}</p> <p class="error-message">{{ errorMessage }}</p>
</div> </div>
<!-- Content --> <!-- Content -->
<div *ngIf="!isLoading && !hasError" class="text-center"> <div *ngIf="!isLoading && !hasError && hasRequiredRole()" class="widget-content">
<p class="block font-sans text-5xl font-bold leading-snug tracking-normal text-blue-gray-900 antialiased"> <div class="kpi-value" [style.color]="accentColor">
{{ value }} {{ value }}
</p> </div>
<p *ngIf="unit" class="block font-sans text-xl font-normal leading-relaxed text-blue-gray-600 antialiased"> <div *ngIf="unit" class="kpi-unit" [style.color]="textColor">
{{ unit }} {{ unit }}
</p> </div>
<!-- Data Source Info (Debug Mode) -->
<div *ngIf="dataSource !== 'static'" class="data-source-info" [style.color]="textColor">
<small>{{ getDataSourceInfo() }}</small>
</div>
</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>
</div> </div>
/* 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;
}
}
}
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment