Commit 1fc00b6e by Ooh-Ao

config

parent f83d7aee
import { WidgetConfigSchema } from '../models/widget-config-schema.model';
export const WIDGET_CONFIG_SCHEMAS: { [key: string]: WidgetConfigSchema } = {
// Simple KPI Widget
'SimpleKpiWidgetComponent': {
widgetType: 'SimpleKpiWidgetComponent',
displayName: 'Simple KPI',
description: 'Display key performance indicators with values and trends',
icon: 'bi-graph-up',
category: 'Metrics',
groups: [
{
id: 'basic',
label: 'Basic Settings',
fields: [
{
id: 'title',
label: 'Title',
type: 'text',
defaultValue: 'KPI Widget',
validation: { required: true },
helpText: 'The title displayed at the top of the widget'
},
{
id: 'valueField',
label: 'Value Field',
type: 'text',
defaultValue: 'value',
validation: { required: true },
helpText: 'Field name containing the KPI value'
},
{
id: 'labelField',
label: 'Label Field',
type: 'text',
defaultValue: 'label',
helpText: 'Field name containing the KPI label'
},
{
id: 'unit',
label: 'Unit',
type: 'text',
defaultValue: '',
helpText: 'Unit to display after the value (e.g., %, $, items)'
}
]
},
{
id: 'style',
label: 'Style & Colors',
fields: [
{
id: 'backgroundColor',
label: 'Background Color',
type: 'color',
defaultValue: '#f8f9fa',
helpText: 'Background color of the widget'
},
{
id: 'textColor',
label: 'Text Color',
type: 'color',
defaultValue: '#333333',
helpText: 'Text color for labels and values'
},
{
id: 'accentColor',
label: 'Accent Color',
type: 'color',
defaultValue: '#007bff',
helpText: 'Color for the main KPI value'
},
{
id: 'borderRadius',
label: 'Border Radius',
type: 'range',
defaultValue: 8,
validation: { min: 0, max: 50 },
helpText: 'Roundness of widget corners'
}
]
},
{
id: 'trend',
label: 'Trend Settings',
fields: [
{
id: 'showTrend',
label: 'Show Trend',
type: 'boolean',
defaultValue: true,
helpText: 'Display trend indicator'
},
{
id: 'trendField',
label: 'Trend Field',
type: 'text',
defaultValue: 'trend',
helpText: 'Field name containing trend data'
},
{
id: 'trendType',
label: 'Trend Type',
type: 'select',
defaultValue: 'percentage',
options: [
{ value: 'percentage', label: 'Percentage' },
{ value: 'absolute', label: 'Absolute Value' },
{ value: 'ratio', label: 'Ratio' }
],
helpText: 'How to calculate and display trend'
}
]
}
],
dataFields: [
{
id: 'valueField',
label: 'Value Field',
type: 'text',
defaultValue: 'value',
validation: { required: true }
},
{
id: 'labelField',
label: 'Label Field',
type: 'text',
defaultValue: 'label'
},
{
id: 'trendField',
label: 'Trend Field',
type: 'text',
defaultValue: 'trend'
}
],
filterFields: [
{
id: 'category',
label: 'Category',
type: 'select',
defaultValue: '',
options: [
{ value: '', label: 'All Categories' },
{ value: 'sales', label: 'Sales' },
{ value: 'marketing', label: 'Marketing' },
{ value: 'operations', label: 'Operations' }
]
}
],
previewData: [
{ value: 1250, label: 'Total Sales', trend: 12.5, category: 'sales' },
{ value: 89, label: 'Customer Satisfaction', trend: -2.1, category: 'operations' },
{ value: 342, label: 'New Users', trend: 8.7, category: 'marketing' }
],
defaultConfig: {
title: 'KPI Dashboard',
valueField: 'value',
labelField: 'label',
unit: '',
backgroundColor: '#f8f9fa',
textColor: '#333333',
accentColor: '#007bff',
borderRadius: 8,
showTrend: true,
trendField: 'trend',
trendType: 'percentage'
}
},
// Calendar Widget
'CalendarWidgetComponent': {
widgetType: 'CalendarWidgetComponent',
displayName: 'Calendar',
description: 'Interactive calendar with events and date selection',
icon: 'bi-calendar3',
category: 'Scheduling',
groups: [
{
id: 'basic',
label: 'Basic Settings',
fields: [
{
id: 'title',
label: 'Title',
type: 'text',
defaultValue: 'Calendar',
validation: { required: true }
},
{
id: 'enableMultiSelection',
label: 'Multi Selection',
type: 'boolean',
defaultValue: false,
helpText: 'Allow selecting multiple dates'
},
{
id: 'showWeekNumber',
label: 'Show Week Numbers',
type: 'boolean',
defaultValue: false,
helpText: 'Display week numbers on the calendar'
},
{
id: 'start',
label: 'Start View',
type: 'select',
defaultValue: 'Month',
options: [
{ value: 'Year', label: 'Year' },
{ value: 'Month', label: 'Month' },
{ value: 'Decade', label: 'Decade' }
]
}
]
},
{
id: 'events',
label: 'Event Settings',
fields: [
{
id: 'dateField',
label: 'Date Field',
type: 'text',
defaultValue: 'date',
validation: { required: true },
helpText: 'Field containing event dates'
},
{
id: 'titleField',
label: 'Title Field',
type: 'text',
defaultValue: 'title',
helpText: 'Field containing event titles'
},
{
id: 'descriptionField',
label: 'Description Field',
type: 'text',
defaultValue: 'description',
helpText: 'Field containing event descriptions'
},
{
id: 'typeField',
label: 'Type Field',
type: 'text',
defaultValue: 'type',
helpText: 'Field containing event types'
}
]
},
{
id: 'style',
label: 'Style & Colors',
fields: [
{
id: 'primaryColor',
label: 'Primary Color',
type: 'color',
defaultValue: '#007bff',
helpText: 'Primary color for calendar elements'
},
{
id: 'eventColor',
label: 'Event Color',
type: 'color',
defaultValue: '#28a745',
helpText: 'Color for event indicators'
},
{
id: 'backgroundColor',
label: 'Background Color',
type: 'color',
defaultValue: '#ffffff',
helpText: 'Calendar background color'
}
]
}
],
dataFields: [
{
id: 'dateField',
label: 'Date Field',
type: 'text',
defaultValue: 'date',
validation: { required: true }
},
{
id: 'titleField',
label: 'Title Field',
type: 'text',
defaultValue: 'title'
},
{
id: 'descriptionField',
label: 'Description Field',
type: 'text',
defaultValue: 'description'
},
{
id: 'typeField',
label: 'Type Field',
type: 'text',
defaultValue: 'type'
}
],
filterFields: [
{
id: 'eventType',
label: 'Event Type',
type: 'multiselect',
defaultValue: [],
options: [
{ value: 'meeting', label: 'Meeting' },
{ value: 'deadline', label: 'Deadline' },
{ value: 'holiday', label: 'Holiday' },
{ value: 'event', label: 'Event' }
]
}
],
previewData: [
{
date: '2024-01-15',
title: 'Team Meeting',
description: 'Weekly team sync',
type: 'meeting'
},
{
date: '2024-01-20',
title: 'Project Deadline',
description: 'Submit final report',
type: 'deadline'
},
{
date: '2024-01-25',
title: 'Company Event',
description: 'Annual company party',
type: 'event'
}
],
defaultConfig: {
title: 'Calendar',
enableMultiSelection: false,
showWeekNumber: false,
start: 'Month',
dateField: 'date',
titleField: 'title',
descriptionField: 'description',
typeField: 'type',
primaryColor: '#007bff',
eventColor: '#28a745',
backgroundColor: '#ffffff'
}
},
// Notification Widget
'NotificationWidgetComponent': {
widgetType: 'NotificationWidgetComponent',
displayName: 'Notifications',
description: 'Real-time notification display with toast and message support',
icon: 'bi-bell',
category: 'Communication',
groups: [
{
id: 'basic',
label: 'Basic Settings',
fields: [
{
id: 'title',
label: 'Title',
type: 'text',
defaultValue: 'Notifications',
validation: { required: true }
},
{
id: 'maxNotifications',
label: 'Max Notifications',
type: 'number',
defaultValue: 10,
validation: { min: 1, max: 100 },
helpText: 'Maximum number of notifications to display'
},
{
id: 'autoHide',
label: 'Auto Hide',
type: 'boolean',
defaultValue: true,
helpText: 'Automatically hide notifications after timeout'
},
{
id: 'timeout',
label: 'Timeout (seconds)',
type: 'number',
defaultValue: 5,
validation: { min: 1, max: 60 },
helpText: 'Time before notification auto-hides'
}
]
},
{
id: 'display',
label: 'Display Settings',
fields: [
{
id: 'showIcon',
label: 'Show Icons',
type: 'boolean',
defaultValue: true,
helpText: 'Display notification icons'
},
{
id: 'showTimestamp',
label: 'Show Timestamps',
type: 'boolean',
defaultValue: true,
helpText: 'Display notification timestamps'
},
{
id: 'showActions',
label: 'Show Actions',
type: 'boolean',
defaultValue: true,
helpText: 'Display action buttons (mark as read, delete)'
},
{
id: 'groupByType',
label: 'Group by Type',
type: 'boolean',
defaultValue: false,
helpText: 'Group notifications by type'
}
]
},
{
id: 'style',
label: 'Style & Colors',
fields: [
{
id: 'backgroundColor',
label: 'Background Color',
type: 'color',
defaultValue: '#ffffff',
helpText: 'Notification background color'
},
{
id: 'borderColor',
label: 'Border Color',
type: 'color',
defaultValue: '#dee2e6',
helpText: 'Notification border color'
},
{
id: 'textColor',
label: 'Text Color',
type: 'color',
defaultValue: '#333333',
helpText: 'Notification text color'
},
{
id: 'accentColor',
label: 'Accent Color',
type: 'color',
defaultValue: '#007bff',
helpText: 'Accent color for actions and highlights'
}
]
}
],
dataFields: [
{
id: 'idField',
label: 'ID Field',
type: 'text',
defaultValue: 'id',
validation: { required: true }
},
{
id: 'titleField',
label: 'Title Field',
type: 'text',
defaultValue: 'title',
validation: { required: true }
},
{
id: 'messageField',
label: 'Message Field',
type: 'text',
defaultValue: 'message',
validation: { required: true }
},
{
id: 'typeField',
label: 'Type Field',
type: 'text',
defaultValue: 'type'
},
{
id: 'timestampField',
label: 'Timestamp Field',
type: 'text',
defaultValue: 'timestamp'
},
{
id: 'isReadField',
label: 'Is Read Field',
type: 'text',
defaultValue: 'isRead'
},
{
id: 'priorityField',
label: 'Priority Field',
type: 'text',
defaultValue: 'priority'
}
],
filterFields: [
{
id: 'type',
label: 'Type',
type: 'multiselect',
defaultValue: [],
options: [
{ value: 'info', label: 'Info' },
{ value: 'success', label: 'Success' },
{ value: 'warning', label: 'Warning' },
{ value: 'error', label: 'Error' }
]
},
{
id: 'priority',
label: 'Priority',
type: 'multiselect',
defaultValue: [],
options: [
{ value: 'low', label: 'Low' },
{ value: 'medium', label: 'Medium' },
{ value: 'high', label: 'High' },
{ value: 'urgent', label: 'Urgent' }
]
},
{
id: 'isRead',
label: 'Read Status',
type: 'select',
defaultValue: 'all',
options: [
{ value: 'all', label: 'All' },
{ value: 'read', label: 'Read' },
{ value: 'unread', label: 'Unread' }
]
}
],
previewData: [
{
id: 1,
title: 'New Message',
message: 'You have received a new message from John Doe',
type: 'info',
timestamp: '2024-01-15T10:30:00Z',
isRead: false,
priority: 'medium'
},
{
id: 2,
title: 'System Update',
message: 'System maintenance scheduled for tonight',
type: 'warning',
timestamp: '2024-01-15T09:15:00Z',
isRead: true,
priority: 'high'
},
{
id: 3,
title: 'Task Completed',
message: 'Your report has been successfully submitted',
type: 'success',
timestamp: '2024-01-15T08:45:00Z',
isRead: false,
priority: 'low'
}
],
defaultConfig: {
title: 'Notifications',
maxNotifications: 10,
autoHide: true,
timeout: 5,
showIcon: true,
showTimestamp: true,
showActions: true,
groupByType: false,
backgroundColor: '#ffffff',
borderColor: '#dee2e6',
textColor: '#333333',
accentColor: '#007bff',
idField: 'id',
titleField: 'title',
messageField: 'message',
typeField: 'type',
timestampField: 'timestamp',
isReadField: 'isRead',
priorityField: 'priority'
}
},
// Weather Widget
'WeatherWidgetComponent': {
widgetType: 'WeatherWidgetComponent',
displayName: 'Weather',
description: 'Current weather and forecast display',
icon: 'bi-cloud-sun',
category: 'Information',
groups: [
{
id: 'basic',
label: 'Basic Settings',
fields: [
{
id: 'title',
label: 'Title',
type: 'text',
defaultValue: 'Weather',
validation: { required: true }
},
{
id: 'location',
label: 'Location',
type: 'text',
defaultValue: 'Bangkok, Thailand',
validation: { required: true },
helpText: 'Default location for weather data'
},
{
id: 'showForecast',
label: 'Show Forecast',
type: 'boolean',
defaultValue: true,
helpText: 'Display weather forecast'
},
{
id: 'forecastDays',
label: 'Forecast Days',
type: 'range',
defaultValue: 5,
validation: { min: 1, max: 7 },
helpText: 'Number of forecast days to display'
}
]
},
{
id: 'display',
label: 'Display Options',
fields: [
{
id: 'showHumidity',
label: 'Show Humidity',
type: 'boolean',
defaultValue: true,
helpText: 'Display humidity information'
},
{
id: 'showWindSpeed',
label: 'Show Wind Speed',
type: 'boolean',
defaultValue: true,
helpText: 'Display wind speed information'
},
{
id: 'showPressure',
label: 'Show Pressure',
type: 'boolean',
defaultValue: true,
helpText: 'Display atmospheric pressure'
},
{
id: 'showFeelsLike',
label: 'Show Feels Like',
type: 'boolean',
defaultValue: true,
helpText: 'Display "feels like" temperature'
}
]
},
{
id: 'style',
label: 'Style & Colors',
fields: [
{
id: 'backgroundColor',
label: 'Background Color',
type: 'color',
defaultValue: '#e3f2fd',
helpText: 'Widget background color'
},
{
id: 'textColor',
label: 'Text Color',
type: 'color',
defaultValue: '#1976d2',
helpText: 'Text color for weather information'
},
{
id: 'accentColor',
label: 'Accent Color',
type: 'color',
defaultValue: '#ff9800',
helpText: 'Accent color for temperature and highlights'
},
{
id: 'borderRadius',
label: 'Border Radius',
type: 'range',
defaultValue: 12,
validation: { min: 0, max: 50 }
}
]
}
],
dataFields: [
{
id: 'temperatureField',
label: 'Temperature Field',
type: 'text',
defaultValue: 'temperature',
validation: { required: true }
},
{
id: 'humidityField',
label: 'Humidity Field',
type: 'text',
defaultValue: 'humidity'
},
{
id: 'windSpeedField',
label: 'Wind Speed Field',
type: 'text',
defaultValue: 'windSpeed'
},
{
id: 'pressureField',
label: 'Pressure Field',
type: 'text',
defaultValue: 'pressure'
},
{
id: 'descriptionField',
label: 'Description Field',
type: 'text',
defaultValue: 'description'
},
{
id: 'iconField',
label: 'Icon Field',
type: 'text',
defaultValue: 'icon'
},
{
id: 'feelsLikeField',
label: 'Feels Like Field',
type: 'text',
defaultValue: 'feelsLike'
}
],
filterFields: [
{
id: 'location',
label: 'Location',
type: 'select',
defaultValue: '',
options: [
{ value: '', label: 'All Locations' },
{ value: 'bangkok', label: 'Bangkok' },
{ value: 'chiang_mai', label: 'Chiang Mai' },
{ value: 'phuket', label: 'Phuket' }
]
}
],
previewData: [
{
location: 'Bangkok',
temperature: 32,
humidity: 65,
windSpeed: 12,
pressure: 1013,
description: 'Partly Cloudy',
icon: 'partly-cloudy',
feelsLike: 35,
date: '2024-01-15'
},
{
location: 'Bangkok',
temperature: 28,
humidity: 70,
windSpeed: 8,
pressure: 1015,
description: 'Light Rain',
icon: 'rain',
feelsLike: 30,
date: '2024-01-16'
}
],
defaultConfig: {
title: 'Weather',
location: 'Bangkok, Thailand',
showForecast: true,
forecastDays: 5,
showHumidity: true,
showWindSpeed: true,
showPressure: true,
showFeelsLike: true,
backgroundColor: '#e3f2fd',
textColor: '#1976d2',
accentColor: '#ff9800',
borderRadius: 12,
temperatureField: 'temperature',
humidityField: 'humidity',
windSpeedField: 'windSpeed',
pressureField: 'pressure',
descriptionField: 'description',
iconField: 'icon',
feelsLikeField: 'feelsLike'
}
},
// Clock Widget
'ClockWidgetComponent': {
widgetType: 'ClockWidgetComponent',
displayName: 'Clock',
description: 'Real-time clock display with multiple formats and timezone support',
icon: 'bi-clock',
category: 'Utility',
groups: [
{
id: 'basic',
label: 'Basic Settings',
fields: [
{
id: 'title',
label: 'Title',
type: 'text',
defaultValue: 'Clock',
validation: { required: true }
},
{
id: 'clockType',
label: 'Clock Type',
type: 'select',
defaultValue: 'digital',
options: [
{ value: 'digital', label: 'Digital' },
{ value: 'analog', label: 'Analog' },
{ value: 'gauge', label: 'Gauge' }
],
helpText: 'Type of clock display'
},
{
id: 'timezone',
label: 'Timezone',
type: 'text',
defaultValue: 'Asia/Bangkok',
helpText: 'Timezone for clock display (e.g., Asia/Bangkok, UTC)'
},
{
id: 'format24Hour',
label: '24-Hour Format',
type: 'boolean',
defaultValue: true,
helpText: 'Use 24-hour time format'
}
]
},
{
id: 'display',
label: 'Display Options',
fields: [
{
id: 'showSeconds',
label: 'Show Seconds',
type: 'boolean',
defaultValue: true,
helpText: 'Display seconds in time'
},
{
id: 'showDate',
label: 'Show Date',
type: 'boolean',
defaultValue: true,
helpText: 'Display current date'
},
{
id: 'showTimezone',
label: 'Show Timezone',
type: 'boolean',
defaultValue: false,
helpText: 'Display timezone information'
},
{
id: 'showAmPm',
label: 'Show AM/PM',
type: 'boolean',
defaultValue: false,
helpText: 'Display AM/PM indicator (12-hour format only)'
}
]
},
{
id: 'style',
label: 'Style & Colors',
fields: [
{
id: 'backgroundColor',
label: 'Background Color',
type: 'color',
defaultValue: '#ffffff',
helpText: 'Clock background color'
},
{
id: 'textColor',
label: 'Text Color',
type: 'color',
defaultValue: '#333333',
helpText: 'Clock text color'
},
{
id: 'accentColor',
label: 'Accent Color',
type: 'color',
defaultValue: '#007bff',
helpText: 'Accent color for highlights'
},
{
id: 'borderRadius',
label: 'Border Radius',
type: 'range',
defaultValue: 8,
validation: { min: 0, max: 50 }
}
]
}
],
dataFields: [
{
id: 'timezoneField',
label: 'Timezone Field',
type: 'text',
defaultValue: 'timezone'
},
{
id: 'clockTypeField',
label: 'Clock Type Field',
type: 'text',
defaultValue: 'clockType'
},
{
id: 'format24HourField',
label: '24-Hour Format Field',
type: 'text',
defaultValue: 'format24Hour'
}
],
filterFields: [
{
id: 'timezone',
label: 'Timezone',
type: 'select',
defaultValue: '',
options: [
{ value: '', label: 'All Timezones' },
{ value: 'Asia/Bangkok', label: 'Bangkok' },
{ value: 'UTC', label: 'UTC' },
{ value: 'America/New_York', label: 'New York' },
{ value: 'Europe/London', label: 'London' }
]
}
],
previewData: [
{
timezone: 'Asia/Bangkok',
clockType: 'digital',
format24Hour: true,
currentTime: '14:30:25',
currentDate: '2024-01-15'
},
{
timezone: 'UTC',
clockType: 'analog',
format24Hour: false,
currentTime: '07:30:25 AM',
currentDate: '2024-01-15'
}
],
defaultConfig: {
title: 'Clock',
clockType: 'digital',
timezone: 'Asia/Bangkok',
format24Hour: true,
showSeconds: true,
showDate: true,
showTimezone: false,
showAmPm: false,
backgroundColor: '#ffffff',
textColor: '#333333',
accentColor: '#007bff',
borderRadius: 8,
timezoneField: 'timezone',
clockTypeField: 'clockType',
format24HourField: 'format24Hour'
}
}
};
// Helper function to get config schema for a widget type
export function getWidgetConfigSchema(widgetType: string): WidgetConfigSchema | null {
return WIDGET_CONFIG_SCHEMAS[widgetType] || null;
}
// Helper function to get all available widget types
export function getAvailableWidgetTypes(): string[] {
return Object.keys(WIDGET_CONFIG_SCHEMAS);
}
// Helper function to get widgets by category
export function getWidgetsByCategory(category: string): WidgetConfigSchema[] {
return Object.values(WIDGET_CONFIG_SCHEMAS).filter(widget => widget.category === category);
}
// Helper function to get all categories
export function getWidgetCategories(): string[] {
const categories = new Set<string>();
Object.values(WIDGET_CONFIG_SCHEMAS).forEach(widget => {
categories.add(widget.category);
});
return Array.from(categories).sort();
}
# Widget Configuration System
## Overview
The Widget Configuration System is a comprehensive solution for creating, managing, and customizing widgets in the dashboard management application. It provides a flexible and extensible framework for configuring widget properties, styling, data mapping, filters, animations, and security settings.
## Features
### 🎨 **Rich Configuration Options**
- **Basic Settings**: Title, data fields, units, and core functionality
- **Style & Colors**: Background, text, border colors, fonts, and custom CSS
- **Data & Filters**: Field mapping, filtering options, and data transformations
- **Layout**: Dimensions, responsive breakpoints, and aspect ratios
- **Animation**: Animation types, durations, delays, and hover effects
- **Interaction**: Click actions, tooltips, hover effects, and custom handlers
- **Security**: Authentication, role-based access, encryption, and audit logging
### 📊 **Comprehensive Widget Support**
- **Simple KPI Widget**: Key performance indicators with trends
- **Calendar Widget**: Interactive calendar with events and date selection
- **Notification Widget**: Real-time notifications with toast and message support
- **Weather Widget**: Current weather and forecast display
- **Clock Widget**: Multiple timezone clocks with various formats
### 🔧 **Developer-Friendly**
- **Type-Safe**: Full TypeScript support with comprehensive interfaces
- **Extensible**: Easy to add new widget types and configuration options
- **Validation**: Built-in validation for required fields and data types
- **Preview**: Real-time preview with sample data generation
- **Import/Export**: JSON-based configuration import/export
## Architecture
### Core Components
```
src/app/portal-manage/dashboard-management/
├── models/
│ └── widget-config-schema.model.ts # TypeScript interfaces
├── configs/
│ └── widget-configs.ts # Widget configuration schemas
├── services/
│ ├── widget-config.service.ts # Configuration management
│ └── widget-preview-data.service.ts # Preview data generation
├── widgets/
│ └── widget-config-editor/
│ ├── widget-config-editor.component.ts # Configuration UI
│ ├── widget-config-editor.component.html
│ └── widget-config-editor.component.scss
└── examples/
└── widget-config-usage.example.ts # Usage examples
```
### Data Flow
```mermaid
graph TD
A[Widget Schema] --> B[Config Editor]
B --> C[Form Validation]
C --> D[Configuration Service]
D --> E[Widget Instance]
E --> F[Dashboard Rendering]
G[Preview Data Service] --> H[Sample Data]
H --> I[Preview Component]
I --> B
```
## Usage
### 1. Basic Widget Configuration
```typescript
import { WidgetConfigService } from './services/widget-config.service';
// Create a new widget configuration
const widgetConfig = widgetConfigService.createWidgetConfig('SimpleKpiWidgetComponent', {
config: {
title: 'Sales Performance',
valueField: 'revenue',
unit: '$',
backgroundColor: '#e8f5e8'
},
style: {
borderRadius: 12,
padding: 16
}
});
```
### 2. Advanced Configuration with All Options
```typescript
const advancedConfig = {
widgetType: 'SimpleKpiWidgetComponent',
config: {
title: 'Advanced KPI Dashboard',
valueField: 'value',
labelField: 'label',
unit: '%',
showTrend: true,
trendField: 'growth'
},
style: {
backgroundColor: '#f8f9fa',
textColor: '#495057',
borderColor: '#667eea',
borderWidth: 3,
borderRadius: 16,
padding: 24,
fontSize: 18,
fontFamily: 'Inter, system-ui, sans-serif',
fontWeight: 'bold',
customCSS: `
.kpi-widget:hover {
transform: translateY(-4px);
box-shadow: 0 12px 48px rgba(102, 126, 234, 0.25);
}
`
},
animation: {
enableAnimations: true,
animationType: 'slide',
animationDuration: 600,
hoverEffects: true
},
layout: {
width: 400,
height: 250,
responsive: true,
breakpoints: {
mobile: { width: 280, height: 180 },
tablet: { width: 350, height: 220 },
desktop: { width: 400, height: 250 }
}
},
interaction: {
enableTooltip: true,
enableClick: true,
clickAction: 'drill_down',
customClickHandler: `
function(event) {
const widgetData = event.target.closest('.kpi-widget').dataset;
window.open('/analytics/detail/' + widgetData.kpiId, '_blank');
}
`
},
dataConfig: {
dataSource: 'api',
apiEndpoint: '/api/kpi-data',
refreshInterval: 60000,
cacheEnabled: true
},
security: {
requireAuth: true,
allowedRoles: ['admin', 'analyst'],
dataEncryption: true,
auditLog: true
},
filters: {
category: ['sales', 'marketing'],
period: 'last_30_days'
},
dataMapping: [
{ sourceField: 'kpi_value', targetField: 'value', transformation: 'format_number' },
{ sourceField: 'kpi_name', targetField: 'label', transformation: 'capitalize' }
]
};
```
### 3. Using the Configuration Editor Component
```html
<app-widget-config-editor
[widgetId]="currentWidgetId"
[widgetType]="selectedWidgetType"
[initialConfig]="existingConfig"
[mode]="'edit'"
(configSaved)="onConfigSaved($event)"
(configCancelled)="onConfigCancelled()"
(previewRequested)="onPreviewRequested($event)">
</app-widget-config-editor>
```
### 4. Generating Preview Data
```typescript
import { WidgetPreviewDataService } from './services/widget-preview-data.service';
// Generate sample data for different widget types
const kpiData = previewDataService.generateSimpleKpiPreviewData({
count: 5,
categories: ['sales', 'marketing', 'operations']
});
const calendarData = previewDataService.generateCalendarPreviewData({
count: 10,
dateRange: {
start: new Date(),
end: new Date(Date.now() + 30 * 24 * 60 * 60 * 1000)
}
});
const notificationData = previewDataService.generateNotificationPreviewData({
count: 8
});
```
## Widget Types
### Simple KPI Widget
**Purpose**: Display key performance indicators with values and trends
**Configuration Options**:
- Basic: Title, value field, label field, unit
- Style: Colors, borders, typography
- Trend: Show/hide trends, trend calculation type
**Sample Configuration**:
```typescript
{
title: 'Sales Performance',
valueField: 'revenue',
labelField: 'metric',
unit: '$',
showTrend: true,
trendField: 'growth',
trendType: 'percentage'
}
```
### Calendar Widget
**Purpose**: Interactive calendar with events and date selection
**Configuration Options**:
- Basic: Multi-selection, week numbers, start view
- Events: Date field, title field, description field, type field
- Style: Primary color, event color, background color
**Sample Configuration**:
```typescript
{
title: 'Company Calendar',
enableMultiSelection: false,
showWeekNumber: true,
start: 'Month',
dateField: 'eventDate',
titleField: 'eventTitle',
typeField: 'eventType'
}
```
### Notification Widget
**Purpose**: Real-time notification display with management features
**Configuration Options**:
- Basic: Max notifications, auto-hide, timeout
- Display: Icons, timestamps, actions, grouping
- Style: Background, border, text colors
**Sample Configuration**:
```typescript
{
title: 'System Alerts',
maxNotifications: 15,
autoHide: true,
timeout: 8,
showIcon: true,
showTimestamp: true,
groupByType: true
}
```
### Weather Widget
**Purpose**: Current weather and forecast display
**Configuration Options**:
- Basic: Location, forecast days, display options
- Display: Humidity, wind speed, pressure, feels like
- Style: Weather-themed colors and styling
**Sample Configuration**:
```typescript
{
title: 'Weather Forecast',
location: 'Bangkok, Thailand',
showForecast: true,
forecastDays: 7,
showHumidity: true,
showWindSpeed: true
}
```
### Clock Widget
**Purpose**: Real-time clock display with multiple formats
**Configuration Options**:
- Basic: Clock type, timezone, format
- Display: Seconds, date, timezone info
- Style: Clock-specific styling
**Sample Configuration**:
```typescript
{
title: 'World Clock',
clockType: 'analog',
timezone: 'Asia/Bangkok',
format24Hour: true,
showSeconds: true,
showDate: true
}
```
## Configuration Schema
### WidgetConfigField
```typescript
interface WidgetConfigField {
id: string;
label: string;
type: 'text' | 'number' | 'boolean' | 'select' | 'multiselect' | 'color' | 'file' | 'range' | 'textarea';
defaultValue: any;
options?: { value: any; label: string }[];
validation?: {
required?: boolean;
min?: number;
max?: number;
pattern?: string;
custom?: string;
};
helpText?: string;
placeholder?: string;
category?: string;
}
```
### WidgetConfigSchema
```typescript
interface WidgetConfigSchema {
widgetType: string;
displayName: string;
description: string;
icon: string;
category: string;
groups: WidgetConfigGroup[];
dataFields: WidgetConfigField[];
filterFields: WidgetConfigField[];
previewData: any[];
defaultConfig: any;
}
```
### CompleteWidgetConfig
```typescript
interface CompleteWidgetConfig {
basic: any;
style: WidgetStyleConfig;
animation: WidgetAnimationConfig;
layout: WidgetLayoutConfig;
interaction: WidgetInteractionConfig;
data: WidgetDataConfig;
security: WidgetSecurityConfig;
filters: WidgetFilterConfig[];
dataMapping: DataMappingConfig[];
}
```
## Services
### WidgetConfigService
**Purpose**: Manage widget configurations (CRUD operations)
**Key Methods**:
- `createWidgetConfig(widgetType, customConfig)` - Create new configuration
- `updateWidgetConfig(widgetId, updates)` - Update existing configuration
- `deleteWidgetConfig(widgetId)` - Delete configuration
- `cloneWidgetConfig(widgetId, newWidgetId)` - Clone configuration
- `exportWidgetConfig(widgetId)` - Export as JSON
- `importWidgetConfig(configJson)` - Import from JSON
- `validateWidgetConfig(config)` - Validate configuration
- `getWidgetConfigsByType(widgetType)` - Get configs by type
- `getWidgetConfigsByCategory(category)` - Get configs by category
### WidgetPreviewDataService
**Purpose**: Generate realistic sample data for widget previews
**Key Methods**:
- `generateSimpleKpiPreviewData(options)` - Generate KPI data
- `generateCalendarPreviewData(options)` - Generate calendar events
- `generateNotificationPreviewData(options)` - Generate notifications
- `generateWeatherPreviewData(options)` - Generate weather data
- `generateClockPreviewData(options)` - Generate clock data
## Best Practices
### 1. Configuration Design
- **Use descriptive field labels**: Make configuration options clear and self-explanatory
- **Provide helpful text**: Include help text for complex configuration options
- **Set sensible defaults**: Provide reasonable default values for all fields
- **Group related options**: Organize configuration fields into logical groups
- **Validate inputs**: Use validation rules to ensure data integrity
### 2. Widget Development
- **Extend BaseWidgetComponent**: All widgets should extend the base component
- **Implement required methods**: `applyInitialConfig()`, `onDataUpdate()`, `onReset()`
- **Handle configuration changes**: Respond to configuration updates appropriately
- **Provide error handling**: Gracefully handle configuration errors
- **Support data mapping**: Use the data mapping system for field transformations
### 3. Performance Considerations
- **Lazy load configurations**: Load widget configurations on demand
- **Cache frequently used data**: Cache configuration data to improve performance
- **Optimize preview data**: Generate preview data efficiently
- **Minimize re-renders**: Use OnPush change detection where appropriate
- **Debounce updates**: Debounce configuration changes to prevent excessive updates
### 4. Security
- **Validate all inputs**: Validate configuration data before processing
- **Sanitize custom CSS**: Sanitize custom CSS to prevent XSS attacks
- **Implement role-based access**: Use security configurations for access control
- **Audit configuration changes**: Log configuration changes for security auditing
- **Encrypt sensitive data**: Encrypt sensitive configuration data when needed
## Extending the System
### Adding New Widget Types
1. **Create Widget Schema**:
```typescript
const newWidgetSchema: WidgetConfigSchema = {
widgetType: 'NewWidgetComponent',
displayName: 'New Widget',
description: 'Description of the new widget',
icon: 'bi-new-icon',
category: 'Custom',
groups: [
// Configuration groups
],
dataFields: [
// Data field definitions
],
filterFields: [
// Filter field definitions
],
previewData: [
// Sample data
],
defaultConfig: {
// Default configuration
}
};
```
2. **Register in WidgetConfigs**:
```typescript
// Add to WIDGET_CONFIG_SCHEMAS in widget-configs.ts
WIDGET_CONFIG_SCHEMAS['NewWidgetComponent'] = newWidgetSchema;
```
3. **Implement Widget Component**:
```typescript
@Component({
selector: 'app-new-widget',
template: `<!-- Widget template -->`,
styleUrls: ['./new-widget.component.scss']
})
export class NewWidgetComponent extends BaseWidgetComponent {
applyInitialConfig(): void {
// Initialize widget with configuration
}
onDataUpdate(data: any[]): void {
// Update widget with new data
}
onReset(): void {
// Reset widget to default state
}
}
```
4. **Add Preview Data Generation**:
```typescript
// Add to WidgetPreviewDataService
generateNewWidgetPreviewData(options: PreviewDataOptions = {}): any[] {
// Generate sample data for new widget
}
```
### Adding New Configuration Options
1. **Update Interfaces**:
```typescript
// Add new fields to WidgetConfigField or create new interfaces
interface NewConfigOption {
// New configuration option definition
}
```
2. **Update Schemas**:
```typescript
// Add new fields to existing widget schemas
const updatedSchema = {
// ... existing schema
groups: [
// ... existing groups
{
id: 'newOptions',
label: 'New Options',
fields: [
// New configuration fields
]
}
]
};
```
3. **Update Components**:
```typescript
// Update widget components to handle new configuration options
export class ExistingWidgetComponent extends BaseWidgetComponent {
applyInitialConfig(): void {
// Handle new configuration options
if (this.configObj.newOption) {
// Apply new option
}
}
}
```
## Troubleshooting
### Common Issues
1. **Configuration Not Applied**:
- Check if `applyInitialConfig()` is called in widget component
- Verify configuration parsing in `BaseWidgetComponent`
- Ensure configuration schema is properly defined
2. **Preview Data Not Showing**:
- Verify preview data service is generating data correctly
- Check if widget component is handling data updates properly
- Ensure data field mappings are correct
3. **Validation Errors**:
- Check required field validation in schema
- Verify data types match expected types
- Ensure validation rules are properly defined
4. **Styling Issues**:
- Check if custom CSS is properly sanitized
- Verify color values are valid hex codes
- Ensure CSS selectors don't conflict with existing styles
### Debug Tips
1. **Enable Console Logging**:
```typescript
// Add to widget component for debugging
console.log('Widget Config:', this.configObj);
console.log('Widget Data:', this.filteredData);
```
2. **Use Browser DevTools**:
- Inspect DOM elements for applied styles
- Check network requests for data loading
- Use Angular DevTools for component state inspection
3. **Test with Sample Data**:
- Use preview data service to generate test data
- Verify widget behavior with different data sets
- Test edge cases and error conditions
## Conclusion
The Widget Configuration System provides a powerful and flexible framework for creating and managing dashboard widgets. With its comprehensive configuration options, type-safe interfaces, and extensible architecture, it enables developers to build sophisticated dashboard solutions while maintaining code quality and user experience.
For more information, refer to the example implementations and API documentation provided in the codebase.
/**
* Widget Configuration System Usage Examples
*
* This file demonstrates how to use the comprehensive widget configuration system
* for creating, managing, and customizing widgets with rich configuration options.
*/
import { Component, OnInit } from '@angular/core';
import { WidgetConfigService, WidgetConfigInstance } from '../services/widget-config.service';
import { WidgetPreviewDataService } from '../services/widget-preview-data.service';
import { getWidgetConfigSchema, WIDGET_CONFIG_SCHEMAS } from '../configs/widget-configs';
@Component({
selector: 'app-widget-config-examples',
template: `
<div class="widget-config-examples">
<h1>Widget Configuration System Examples</h1>
<!-- Example 1: Creating a Simple KPI Widget -->
<div class="example-section">
<h2>Example 1: Simple KPI Widget</h2>
<div class="example-content">
<h3>Basic Configuration</h3>
<pre>{{ simpleKpiExample | json }}</pre>
<h3>Preview Data</h3>
<pre>{{ simpleKpiPreviewData | json }}</pre>
</div>
</div>
<!-- Example 2: Calendar Widget with Events -->
<div class="example-section">
<h2>Example 2: Calendar Widget</h2>
<div class="example-content">
<h3>Configuration with Events</h3>
<pre>{{ calendarExample | json }}</pre>
<h3>Event Data</h3>
<pre>{{ calendarPreviewData | json }}</pre>
</div>
</div>
<!-- Example 3: Notification Widget -->
<div class="example-section">
<h2>Example 3: Notification Widget</h2>
<div class="example-content">
<h3>Real-time Notifications</h3>
<pre>{{ notificationExample | json }}</pre>
<h3>Notification Data</h3>
<pre>{{ notificationPreviewData | json }}</pre>
</div>
</div>
<!-- Example 4: Weather Widget -->
<div class="example-section">
<h2>Example 4: Weather Widget</h2>
<div class="example-content">
<h3>Weather Configuration</h3>
<pre>{{ weatherExample | json }}</pre>
<h3>Weather Data</h3>
<pre>{{ weatherPreviewData | json }}</pre>
</div>
</div>
<!-- Example 5: Clock Widget -->
<div class="example-section">
<h2>Example 5: Clock Widget</h2>
<div class="example-content">
<h3>Multi-timezone Clock</h3>
<pre>{{ clockExample | json }}</pre>
<h3>Clock Data</h3>
<pre>{{ clockPreviewData | json }}</pre>
</div>
</div>
<!-- Example 6: Advanced Configuration -->
<div class="example-section">
<h2>Example 6: Advanced Configuration</h2>
<div class="example-content">
<h3>Complete Widget Configuration</h3>
<pre>{{ advancedExample | json }}</pre>
</div>
</div>
<!-- Example 7: Configuration Management -->
<div class="example-section">
<h2>Example 7: Configuration Management</h2>
<div class="example-content">
<h3>Available Widget Types</h3>
<ul>
<li *ngFor="let widgetType of availableWidgetTypes">{{ widgetType }}</li>
</ul>
<h3>Widget Categories</h3>
<ul>
<li *ngFor="let category of widgetCategories">{{ category }}</li>
</ul>
</div>
</div>
</div>
`,
styles: [`
.widget-config-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;
}
ul {
list-style-type: disc;
padding-left: 2rem;
}
li {
margin-bottom: 0.25rem;
color: #495057;
}
`]
})
export class WidgetConfigExamplesComponent implements OnInit {
// Example configurations
simpleKpiExample: any;
calendarExample: any;
notificationExample: any;
weatherExample: any;
clockExample: any;
advancedExample: any;
// Preview data
simpleKpiPreviewData: any[];
calendarPreviewData: any[];
notificationPreviewData: any[];
weatherPreviewData: any[];
clockPreviewData: any[];
// Available options
availableWidgetTypes: string[];
widgetCategories: string[];
constructor(
private widgetConfigService: WidgetConfigService,
private previewDataService: WidgetPreviewDataService
) {}
ngOnInit(): void {
this.initializeExamples();
this.loadPreviewData();
this.loadAvailableOptions();
}
private initializeExamples(): void {
// Example 1: Simple KPI Widget
this.simpleKpiExample = {
widgetType: 'SimpleKpiWidgetComponent',
config: {
title: 'Sales Performance',
valueField: 'sales',
labelField: 'metric',
unit: '$',
backgroundColor: '#e8f5e8',
textColor: '#2d5a2d',
accentColor: '#28a745',
borderRadius: 12,
showTrend: true,
trendField: 'growth',
trendType: 'percentage'
},
style: {
backgroundColor: '#e8f5e8',
textColor: '#2d5a2d',
borderColor: '#28a745',
borderWidth: 2,
borderRadius: 12,
padding: 20,
fontSize: 16,
fontWeight: 'bold'
},
animation: {
enableAnimations: true,
animationType: 'fade',
animationDuration: 500,
hoverEffects: true
},
layout: {
width: 300,
height: 200,
responsive: true
},
interaction: {
enableTooltip: true,
enableClick: true,
clickAction: 'drill_down'
}
};
// Example 2: Calendar Widget
this.calendarExample = {
widgetType: 'CalendarWidgetComponent',
config: {
title: 'Company Calendar',
enableMultiSelection: false,
showWeekNumber: true,
start: 'Month',
dateField: 'date',
titleField: 'title',
descriptionField: 'description',
typeField: 'type',
primaryColor: '#007bff',
eventColor: '#28a745',
backgroundColor: '#ffffff'
},
filters: {
eventType: ['meeting', 'deadline'],
priority: ['high', 'medium']
},
dataMapping: [
{ sourceField: 'eventDate', targetField: 'date', transformation: 'format_date' },
{ sourceField: 'eventTitle', targetField: 'title', transformation: 'capitalize' },
{ sourceField: 'eventDesc', targetField: 'description', transformation: 'none' }
]
};
// Example 3: Notification Widget
this.notificationExample = {
widgetType: 'NotificationWidgetComponent',
config: {
title: 'System Alerts',
maxNotifications: 15,
autoHide: true,
timeout: 8,
showIcon: true,
showTimestamp: true,
showActions: true,
groupByType: true,
backgroundColor: '#fff3cd',
borderColor: '#ffeaa7',
textColor: '#856404',
accentColor: '#ffc107'
},
filters: {
type: ['warning', 'error'],
priority: ['high', 'urgent'],
isRead: 'unread'
},
interaction: {
enableClick: true,
clickAction: 'open_modal',
customClickHandler: 'function(event) { console.log("Notification clicked:", event); }'
}
};
// Example 4: Weather Widget
this.weatherExample = {
widgetType: 'WeatherWidgetComponent',
config: {
title: 'Weather Forecast',
location: 'Bangkok, Thailand',
showForecast: true,
forecastDays: 7,
showHumidity: true,
showWindSpeed: true,
showPressure: true,
showFeelsLike: true,
backgroundColor: '#e3f2fd',
textColor: '#1976d2',
accentColor: '#ff9800',
borderRadius: 16
},
dataConfig: {
dataSource: 'api',
apiEndpoint: 'https://api.weather.com/v1/forecast',
refreshInterval: 300000, // 5 minutes
cacheEnabled: true,
cacheDuration: 600
},
security: {
requireAuth: false,
rateLimit: 100
}
};
// Example 5: Clock Widget
this.clockExample = {
widgetType: 'ClockWidgetComponent',
config: {
title: 'World Clock',
clockType: 'analog',
timezone: 'Asia/Bangkok',
format24Hour: true,
showSeconds: true,
showDate: true,
showTimezone: true,
backgroundColor: '#1a1a1a',
textColor: '#00ff00',
accentColor: '#ff6b6b',
borderRadius: 20
},
animation: {
enableAnimations: true,
animationType: 'pulse',
animationDuration: 1000,
hoverEffects: false
},
layout: {
width: 250,
height: 250,
aspectRatio: '1:1'
}
};
// Example 6: Advanced Configuration
this.advancedExample = {
widgetType: 'SimpleKpiWidgetComponent',
config: {
title: 'Advanced KPI Dashboard',
valueField: 'value',
labelField: 'label',
unit: '%',
backgroundColor: '#f8f9fa',
textColor: '#495057',
accentColor: '#667eea',
borderRadius: 16,
showTrend: true,
trendField: 'trend',
trendType: 'percentage'
},
style: {
backgroundColor: '#f8f9fa',
textColor: '#495057',
borderColor: '#667eea',
borderWidth: 3,
borderRadius: 16,
padding: 24,
margin: 12,
fontSize: 18,
fontFamily: 'Inter, system-ui, sans-serif',
fontWeight: 'bold',
customCSS: `
.kpi-widget {
box-shadow: 0 8px 32px rgba(102, 126, 234, 0.15);
transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
}
.kpi-widget:hover {
transform: translateY(-4px);
box-shadow: 0 12px 48px rgba(102, 126, 234, 0.25);
}
.kpi-value {
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
-webkit-background-clip: text;
-webkit-text-fill-color: transparent;
background-clip: text;
}
`,
theme: 'light'
},
animation: {
enableAnimations: true,
animationType: 'slide',
animationDuration: 600,
animationDelay: 200,
hoverEffects: true
},
layout: {
width: 400,
height: 250,
minWidth: 300,
minHeight: 200,
maxWidth: 600,
maxHeight: 400,
aspectRatio: 'auto',
responsive: true,
breakpoints: {
mobile: { width: 280, height: 180 },
tablet: { width: 350, height: 220 },
desktop: { width: 400, height: 250 }
}
},
interaction: {
enableTooltip: true,
enableClick: true,
enableHover: true,
enableSelection: false,
enableExport: true,
enableRefresh: true,
clickAction: 'drill_down',
customClickHandler: `
function(event) {
const widgetData = event.target.closest('.kpi-widget').dataset;
window.open('/analytics/detail/' + widgetData.kpiId, '_blank');
}
`
},
dataConfig: {
dataSource: 'api',
apiEndpoint: '/api/kpi-data',
refreshInterval: 60000, // 1 minute
cacheEnabled: true,
cacheDuration: 300,
dataTransform: 'data => data.map(item => ({ ...item, formattedValue: formatCurrency(item.value) }))',
pagination: {
enabled: false,
pageSize: 10,
pageSizeOptions: [10, 25, 50, 100]
}
},
security: {
requireAuth: true,
allowedRoles: ['admin', 'analyst', 'manager'],
dataEncryption: true,
auditLog: true,
rateLimit: 60
},
filters: {
category: ['sales', 'marketing'],
period: 'last_30_days',
region: 'all'
},
dataMapping: [
{ sourceField: 'kpi_value', targetField: 'value', transformation: 'format_number' },
{ sourceField: 'kpi_name', targetField: 'label', transformation: 'capitalize' },
{ sourceField: 'growth_rate', targetField: 'trend', transformation: 'none' },
{ sourceField: 'kpi_category', targetField: 'category', transformation: 'uppercase' }
]
};
}
private loadPreviewData(): void {
// Generate preview data for each widget type
this.simpleKpiPreviewData = this.previewDataService.generateSimpleKpiPreviewData({
count: 5,
categories: ['sales', 'marketing', 'operations']
});
this.calendarPreviewData = this.previewDataService.generateCalendarPreviewData({
count: 8
});
this.notificationPreviewData = this.previewDataService.generateNotificationPreviewData({
count: 6
});
this.weatherPreviewData = this.previewDataService.generateWeatherPreviewData({
count: 7,
locations: ['Bangkok', 'Chiang Mai', 'Phuket']
});
this.clockPreviewData = this.previewDataService.generateClockPreviewData({
count: 4
});
}
private loadAvailableOptions(): void {
this.availableWidgetTypes = Object.keys(WIDGET_CONFIG_SCHEMAS);
this.widgetCategories = Array.from(new Set(
Object.values(WIDGET_CONFIG_SCHEMAS).map(schema => schema.category)
)).sort();
}
// Example methods for programmatic configuration management
/**
* Create a new widget configuration programmatically
*/
createWidgetConfig(): void {
const newConfig = this.widgetConfigService.createWidgetConfig('SimpleKpiWidgetComponent', {
config: {
title: 'My Custom KPI',
valueField: 'revenue',
unit: '$',
backgroundColor: '#e8f5e8'
},
style: {
borderRadius: 16,
padding: 24
}
});
console.log('Created widget config:', newConfig);
}
/**
* Update an existing widget configuration
*/
updateWidgetConfig(widgetId: string): void {
const updated = this.widgetConfigService.updateWidgetConfig(widgetId, {
config: {
title: 'Updated KPI Title',
backgroundColor: '#f0f8ff'
},
style: {
borderRadius: 20,
borderWidth: 3
}
});
console.log('Updated widget config:', updated);
}
/**
* Clone a widget configuration
*/
cloneWidgetConfig(widgetId: string): void {
const cloned = this.widgetConfigService.cloneWidgetConfig(widgetId, 'cloned-widget-id');
console.log('Cloned widget config:', cloned);
}
/**
* Export widget configuration
*/
exportWidgetConfig(widgetId: string): void {
const configJson = this.widgetConfigService.exportWidgetConfig(widgetId);
if (configJson) {
// Create download link
const blob = new Blob([configJson], { type: 'application/json' });
const url = window.URL.createObjectURL(blob);
const link = document.createElement('a');
link.href = url;
link.download = `widget-config-${widgetId}.json`;
link.click();
window.URL.revokeObjectURL(url);
}
}
/**
* Import widget configuration
*/
importWidgetConfig(configFile: File): void {
const reader = new FileReader();
reader.onload = (e) => {
const configJson = e.target?.result as string;
const imported = this.widgetConfigService.importWidgetConfig(configJson);
if (imported) {
console.log('Imported widget config:', imported);
} else {
console.error('Failed to import widget configuration');
}
};
reader.readAsText(configFile);
}
/**
* Get widgets by category
*/
getWidgetsByCategory(category: string): void {
const widgets = this.widgetConfigService.getWidgetConfigsByCategory(category);
console.log(`Widgets in category "${category}":`, widgets);
}
/**
* Validate widget configuration
*/
validateConfig(widgetId: string): void {
const config = this.widgetConfigService.getWidgetConfig(widgetId);
if (config) {
const validation = this.widgetConfigService.validateWidgetConfig(config);
console.log('Validation result:', validation);
}
}
}
export interface WidgetConfigField {
id: string;
label: string;
type: 'text' | 'number' | 'boolean' | 'select' | 'multiselect' | 'color' | 'file' | 'range' | 'textarea';
defaultValue: any;
options?: { value: any; label: string }[];
validation?: {
required?: boolean;
min?: number;
max?: number;
pattern?: string;
custom?: string;
};
helpText?: string;
placeholder?: string;
category?: string;
}
export interface WidgetConfigGroup {
id: string;
label: string;
description?: string;
fields: WidgetConfigField[];
collapsed?: boolean;
}
export interface WidgetConfigSchema {
widgetType: string;
displayName: string;
description: string;
icon: string;
category: string;
groups: WidgetConfigGroup[];
dataFields: WidgetConfigField[];
filterFields: WidgetConfigField[];
previewData: any[];
defaultConfig: any;
}
export interface WidgetFilterConfig {
field: string;
type: 'text' | 'number' | 'date' | 'select' | 'multiselect' | 'range';
operator?: 'equals' | 'contains' | 'greater_than' | 'less_than' | 'between' | 'in' | 'not_in';
options?: { value: any; label: string }[];
defaultValue?: any;
}
export interface DataMappingConfig {
sourceField: string;
targetField: string;
transformation?: 'none' | 'uppercase' | 'lowercase' | 'capitalize' | 'format_date' | 'format_number' | 'custom';
customFunction?: string;
}
export interface WidgetStyleConfig {
backgroundColor?: string;
textColor?: string;
borderColor?: string;
borderWidth?: number;
borderRadius?: number;
padding?: number;
margin?: number;
fontSize?: number;
fontFamily?: string;
fontWeight?: 'normal' | 'bold' | 'lighter' | 'bolder';
customCSS?: string;
theme?: 'light' | 'dark' | 'auto';
}
export interface WidgetAnimationConfig {
enableAnimations?: boolean;
animationType?: 'fade' | 'slide' | 'bounce' | 'pulse' | 'none';
animationDuration?: number;
animationDelay?: number;
hoverEffects?: boolean;
}
export interface WidgetLayoutConfig {
width?: number;
height?: number;
minWidth?: number;
minHeight?: number;
maxWidth?: number;
maxHeight?: number;
aspectRatio?: string;
responsive?: boolean;
breakpoints?: {
mobile?: any;
tablet?: any;
desktop?: any;
};
}
export interface WidgetInteractionConfig {
enableTooltip?: boolean;
enableClick?: boolean;
enableHover?: boolean;
enableSelection?: boolean;
enableExport?: boolean;
enableRefresh?: boolean;
clickAction?: 'none' | 'drill_down' | 'open_modal' | 'navigate' | 'custom';
customClickHandler?: string;
}
export interface WidgetDataConfig {
dataSource?: 'static' | 'api' | 'websocket' | 'file';
apiEndpoint?: string;
refreshInterval?: number;
cacheEnabled?: boolean;
cacheDuration?: number;
dataTransform?: string;
pagination?: {
enabled: boolean;
pageSize: number;
pageSizeOptions: number[];
};
}
export interface WidgetSecurityConfig {
requireAuth?: boolean;
allowedRoles?: string[];
dataEncryption?: boolean;
auditLog?: boolean;
rateLimit?: number;
}
export interface CompleteWidgetConfig {
basic: any;
style: WidgetStyleConfig;
animation: WidgetAnimationConfig;
layout: WidgetLayoutConfig;
interaction: WidgetInteractionConfig;
data: WidgetDataConfig;
security: WidgetSecurityConfig;
filters: WidgetFilterConfig[];
dataMapping: DataMappingConfig[];
}
import { Injectable } from '@angular/core';
import { BehaviorSubject, Observable } from 'rxjs';
import {
WidgetConfigSchema,
WidgetFilterConfig,
DataMappingConfig,
WidgetStyleConfig,
WidgetAnimationConfig,
WidgetLayoutConfig,
WidgetInteractionConfig,
WidgetDataConfig,
WidgetSecurityConfig,
CompleteWidgetConfig
} from '../models/widget-config-schema.model';
import { WIDGET_CONFIG_SCHEMAS, getWidgetConfigSchema } from '../configs/widget-configs';
export interface WidgetConfigInstance {
widgetId: string;
widgetType: string;
config: any;
data: any[];
filters: { [key: string]: any };
dataMapping: DataMappingConfig[];
style: WidgetStyleConfig;
animation: WidgetAnimationConfig;
layout: WidgetLayoutConfig;
interaction: WidgetInteractionConfig;
dataConfig: WidgetDataConfig;
security: WidgetSecurityConfig;
lastModified: Date;
createdBy: string;
version: string;
}
@Injectable({
providedIn: 'root'
})
export class WidgetConfigService {
private widgetConfigs = new BehaviorSubject<Map<string, WidgetConfigInstance>>(new Map());
private currentEditingWidget = new BehaviorSubject<WidgetConfigInstance | null>(null);
constructor() {
this.initializeDefaultConfigs();
}
/**
* Initialize default configurations for all available widgets
*/
private initializeDefaultConfigs(): void {
const configs = new Map<string, WidgetConfigInstance>();
Object.keys(WIDGET_CONFIG_SCHEMAS).forEach(widgetType => {
const schema = WIDGET_CONFIG_SCHEMAS[widgetType];
const defaultConfig = this.createDefaultWidgetConfig(widgetType, schema);
configs.set(defaultConfig.widgetId, defaultConfig);
});
this.widgetConfigs.next(configs);
}
/**
* Create default widget configuration from schema
*/
private createDefaultWidgetConfig(widgetType: string, schema: WidgetConfigSchema): WidgetConfigInstance {
return {
widgetId: `default-${widgetType.toLowerCase().replace('component', '')}`,
widgetType,
config: { ...schema.defaultConfig },
data: [...schema.previewData],
filters: {},
dataMapping: this.createDefaultDataMapping(schema),
style: this.createDefaultStyleConfig(),
animation: this.createDefaultAnimationConfig(),
layout: this.createDefaultLayoutConfig(),
interaction: this.createDefaultInteractionConfig(),
dataConfig: this.createDefaultDataConfig(),
security: this.createDefaultSecurityConfig(),
lastModified: new Date(),
createdBy: 'system',
version: '1.0.0'
};
}
/**
* Create default data mapping configuration
*/
private createDefaultDataMapping(schema: WidgetConfigSchema): DataMappingConfig[] {
return schema.dataFields.map(field => ({
sourceField: field.id,
targetField: field.id,
transformation: 'none'
}));
}
/**
* Create default style configuration
*/
private createDefaultStyleConfig(): WidgetStyleConfig {
return {
backgroundColor: '#ffffff',
textColor: '#333333',
borderColor: '#dee2e6',
borderWidth: 1,
borderRadius: 8,
padding: 16,
margin: 8,
fontSize: 14,
fontFamily: 'system-ui, -apple-system, sans-serif',
fontWeight: 'normal',
customCSS: '',
theme: 'light'
};
}
/**
* Create default animation configuration
*/
private createDefaultAnimationConfig(): WidgetAnimationConfig {
return {
enableAnimations: true,
animationType: 'fade',
animationDuration: 300,
animationDelay: 0,
hoverEffects: true
};
}
/**
* Create default layout configuration
*/
private createDefaultLayoutConfig(): WidgetLayoutConfig {
return {
width: 300,
height: 200,
minWidth: 200,
minHeight: 150,
maxWidth: 600,
maxHeight: 400,
aspectRatio: 'auto',
responsive: true,
breakpoints: {
mobile: { width: 280, height: 180 },
tablet: { width: 320, height: 220 },
desktop: { width: 400, height: 280 }
}
};
}
/**
* Create default interaction configuration
*/
private createDefaultInteractionConfig(): WidgetInteractionConfig {
return {
enableTooltip: true,
enableClick: true,
enableHover: true,
enableSelection: false,
enableExport: false,
enableRefresh: true,
clickAction: 'none',
customClickHandler: ''
};
}
/**
* Create default data configuration
*/
private createDefaultDataConfig(): WidgetDataConfig {
return {
dataSource: 'static',
apiEndpoint: '',
refreshInterval: 0,
cacheEnabled: false,
cacheDuration: 300,
dataTransform: '',
pagination: {
enabled: false,
pageSize: 10,
pageSizeOptions: [10, 25, 50, 100]
}
};
}
/**
* Create default security configuration
*/
private createDefaultSecurityConfig(): WidgetSecurityConfig {
return {
requireAuth: false,
allowedRoles: [],
dataEncryption: false,
auditLog: false,
rateLimit: 0
};
}
/**
* Get all widget configurations
*/
getWidgetConfigs(): Observable<Map<string, WidgetConfigInstance>> {
return this.widgetConfigs.asObservable();
}
/**
* Get configuration for a specific widget
*/
getWidgetConfig(widgetId: string): WidgetConfigInstance | null {
const configs = this.widgetConfigs.value;
return configs.get(widgetId) || null;
}
/**
* Create a new widget configuration
*/
createWidgetConfig(
widgetType: string,
customConfig: Partial<WidgetConfigInstance> = {}
): WidgetConfigInstance {
const schema = getWidgetConfigSchema(widgetType);
if (!schema) {
throw new Error(`Widget type ${widgetType} not found`);
}
const widgetId = customConfig.widgetId || this.generateWidgetId(widgetType);
const baseConfig = this.createDefaultWidgetConfig(widgetType, schema);
const newConfig: WidgetConfigInstance = {
...baseConfig,
...customConfig,
widgetId,
lastModified: new Date()
};
const configs = this.widgetConfigs.value;
configs.set(widgetId, newConfig);
this.widgetConfigs.next(configs);
return newConfig;
}
/**
* Update widget configuration
*/
updateWidgetConfig(widgetId: string, updates: Partial<WidgetConfigInstance>): boolean {
const configs = this.widgetConfigs.value;
const existingConfig = configs.get(widgetId);
if (!existingConfig) {
return false;
}
const updatedConfig: WidgetConfigInstance = {
...existingConfig,
...updates,
lastModified: new Date()
};
configs.set(widgetId, updatedConfig);
this.widgetConfigs.next(configs);
return true;
}
/**
* Delete widget configuration
*/
deleteWidgetConfig(widgetId: string): boolean {
const configs = this.widgetConfigs.value;
const deleted = configs.delete(widgetId);
if (deleted) {
this.widgetConfigs.next(configs);
}
return deleted;
}
/**
* Clone widget configuration
*/
cloneWidgetConfig(widgetId: string, newWidgetId?: string): WidgetConfigInstance | null {
const configs = this.widgetConfigs.value;
const originalConfig = configs.get(widgetId);
if (!originalConfig) {
return null;
}
const clonedConfig: WidgetConfigInstance = {
...originalConfig,
widgetId: newWidgetId || this.generateWidgetId(originalConfig.widgetType),
lastModified: new Date(),
createdBy: 'user' // Update this based on current user
};
configs.set(clonedConfig.widgetId, clonedConfig);
this.widgetConfigs.next(configs);
return clonedConfig;
}
/**
* Get widget configuration schema
*/
getWidgetSchema(widgetType: string): WidgetConfigSchema | null {
return getWidgetConfigSchema(widgetType);
}
/**
* Get all available widget schemas
*/
getAllWidgetSchemas(): WidgetConfigSchema[] {
return Object.values(WIDGET_CONFIG_SCHEMAS);
}
/**
* Validate widget configuration
*/
validateWidgetConfig(config: WidgetConfigInstance): { isValid: boolean; errors: string[] } {
const errors: string[] = [];
if (!config.widgetId) {
errors.push('Widget ID is required');
}
if (!config.widgetType) {
errors.push('Widget type is required');
}
const schema = getWidgetConfigSchema(config.widgetType);
if (!schema) {
errors.push(`Invalid widget type: ${config.widgetType}`);
}
// Validate required fields based on schema
if (schema) {
schema.groups.forEach(group => {
group.fields.forEach(field => {
if (field.validation?.required && !config.config[field.id]) {
errors.push(`${field.label} is required`);
}
});
});
}
return {
isValid: errors.length === 0,
errors
};
}
/**
* Generate unique widget ID
*/
private generateWidgetId(widgetType: string): string {
const timestamp = Date.now();
const random = Math.random().toString(36).substr(2, 5);
const typePrefix = widgetType.toLowerCase().replace('component', '');
return `${typePrefix}-${timestamp}-${random}`;
}
/**
* Get current editing widget
*/
getCurrentEditingWidget(): Observable<WidgetConfigInstance | null> {
return this.currentEditingWidget.asObservable();
}
/**
* Set current editing widget
*/
setCurrentEditingWidget(widget: WidgetConfigInstance | null): void {
this.currentEditingWidget.next(widget);
}
/**
* Reset widget configuration to default
*/
resetWidgetConfig(widgetId: string): boolean {
const configs = this.widgetConfigs.value;
const existingConfig = configs.get(widgetId);
if (!existingConfig) {
return false;
}
const schema = getWidgetConfigSchema(existingConfig.widgetType);
if (!schema) {
return false;
}
const resetConfig = this.createDefaultWidgetConfig(existingConfig.widgetType, schema);
resetConfig.widgetId = widgetId; // Keep original ID
configs.set(widgetId, resetConfig);
this.widgetConfigs.next(configs);
return true;
}
/**
* Export widget configuration
*/
exportWidgetConfig(widgetId: string): string | null {
const config = this.getWidgetConfig(widgetId);
if (!config) {
return null;
}
return JSON.stringify(config, null, 2);
}
/**
* Import widget configuration
*/
importWidgetConfig(configJson: string): WidgetConfigInstance | null {
try {
const config: WidgetConfigInstance = JSON.parse(configJson);
// Validate the imported configuration
const validation = this.validateWidgetConfig(config);
if (!validation.isValid) {
console.error('Invalid configuration:', validation.errors);
return null;
}
// Generate new ID to avoid conflicts
config.widgetId = this.generateWidgetId(config.widgetType);
config.lastModified = new Date();
const configs = this.widgetConfigs.value;
configs.set(config.widgetId, config);
this.widgetConfigs.next(configs);
return config;
} catch (error) {
console.error('Failed to import configuration:', error);
return null;
}
}
/**
* Get widget configurations by type
*/
getWidgetConfigsByType(widgetType: string): WidgetConfigInstance[] {
const configs = this.widgetConfigs.value;
return Array.from(configs.values()).filter(config => config.widgetType === widgetType);
}
/**
* Get widget configurations by category
*/
getWidgetConfigsByCategory(category: string): WidgetConfigInstance[] {
const configs = this.widgetConfigs.value;
return Array.from(configs.values()).filter(config => {
const schema = getWidgetConfigSchema(config.widgetType);
return schema?.category === category;
});
}
}
import { Injectable } from '@angular/core';
export interface PreviewDataOptions {
count?: number;
dateRange?: { start: Date; end: Date };
categories?: string[];
locations?: string[];
customFields?: { [key: string]: any };
}
@Injectable({
providedIn: 'root'
})
export class WidgetPreviewDataService {
constructor() { }
/**
* Generate comprehensive preview data for Simple KPI Widget
*/
generateSimpleKpiPreviewData(options: PreviewDataOptions = {}): any[] {
const count = options.count || 5;
const categories = options.categories || ['sales', 'marketing', 'operations', 'finance', 'hr'];
const kpis = [
{ name: 'Total Sales', unit: '$', category: 'sales' },
{ name: 'Customer Satisfaction', unit: '%', category: 'operations' },
{ name: 'New Users', unit: 'users', category: 'marketing' },
{ name: 'Revenue Growth', unit: '%', category: 'finance' },
{ name: 'Employee Retention', unit: '%', category: 'hr' },
{ name: 'Website Traffic', unit: 'visits', category: 'marketing' },
{ name: 'Conversion Rate', unit: '%', category: 'sales' },
{ name: 'Average Order Value', unit: '$', category: 'sales' }
];
return Array.from({ length: count }, (_, index) => {
const kpi = kpis[index % kpis.length];
const baseValue = this.getRandomNumber(100, 10000);
const trend = this.getRandomNumber(-20, 20);
return {
id: `kpi-${index + 1}`,
label: kpi.name,
value: baseValue,
unit: kpi.unit,
trend: trend,
trendValue: Math.abs(trend),
category: kpi.category,
target: Math.round(baseValue * 1.1),
previousValue: Math.round(baseValue * (1 - trend / 100)),
color: this.getCategoryColor(kpi.category),
description: `Current ${kpi.name.toLowerCase()} performance`,
lastUpdated: this.getRandomDate(),
status: trend > 0 ? 'positive' : trend < 0 ? 'negative' : 'neutral'
};
});
}
/**
* Generate comprehensive preview data for Calendar Widget
*/
generateCalendarPreviewData(options: PreviewDataOptions = {}): any[] {
const count = options.count || 10;
const dateRange = options.dateRange || {
start: new Date(),
end: new Date(Date.now() + 30 * 24 * 60 * 60 * 1000) // 30 days from now
};
const eventTypes = ['meeting', 'deadline', 'holiday', 'event', 'training', 'review'];
const eventTitles = [
'Team Meeting', 'Project Deadline', 'Company Holiday', 'Product Launch',
'Training Session', 'Performance Review', 'Client Presentation', 'Budget Review',
'All Hands Meeting', 'Sprint Planning', 'Code Review', 'Design Review'
];
return Array.from({ length: count }, (_, index) => {
const eventDate = this.getRandomDateInRange(dateRange.start, dateRange.end);
const eventType = eventTypes[index % eventTypes.length];
const title = eventTitles[index % eventTitles.length];
return {
id: `event-${index + 1}`,
date: this.formatDate(eventDate),
title: title,
description: `${title} - Detailed description and agenda`,
type: eventType,
startTime: this.getRandomTime(),
endTime: this.getRandomTime(),
location: this.getRandomLocation(),
attendees: this.getRandomNumber(2, 15),
priority: this.getRandomPriority(),
status: this.getRandomStatus(),
color: this.getEventTypeColor(eventType),
isRecurring: Math.random() > 0.7,
reminder: this.getRandomNumber(0, 60) // minutes before
};
});
}
/**
* Generate comprehensive preview data for Notification Widget
*/
generateNotificationPreviewData(options: PreviewDataOptions = {}): any[] {
const count = options.count || 8;
const types = ['info', 'success', 'warning', 'error'];
const priorities = ['low', 'medium', 'high', 'urgent'];
const notificationTemplates = [
{ type: 'info', title: 'New Message', message: 'You have received a new message from {sender}' },
{ type: 'success', title: 'Task Completed', message: '{task} has been successfully completed' },
{ type: 'warning', title: 'System Alert', message: 'System maintenance scheduled for {time}' },
{ type: 'error', title: 'Error Occurred', message: 'An error occurred while processing {operation}' },
{ type: 'info', title: 'Update Available', message: 'New version {version} is available for download' },
{ type: 'success', title: 'Backup Complete', message: 'Data backup completed successfully' },
{ type: 'warning', title: 'Storage Warning', message: 'Storage space is running low ({percentage}% used)' },
{ type: 'info', title: 'Meeting Reminder', message: 'Meeting "{meeting}" starts in {minutes} minutes' }
];
return Array.from({ length: count }, (_, index) => {
const template = notificationTemplates[index % notificationTemplates.length];
const type = template.type;
const priority = priorities[Math.floor(Math.random() * priorities.length)];
const timestamp = this.getRandomPastDate();
const isRead = Math.random() > 0.3;
return {
id: `notification-${index + 1}`,
title: template.title,
message: this.interpolateMessage(template.message),
type: type,
priority: priority,
timestamp: timestamp.toISOString(),
isRead: isRead,
readAt: isRead ? this.getRandomDateAfter(timestamp).toISOString() : null,
source: this.getRandomSource(),
actionUrl: Math.random() > 0.5 ? this.getRandomActionUrl() : null,
expiresAt: this.getRandomDateAfter(timestamp).toISOString(),
category: this.getNotificationCategory(type),
metadata: {
userId: `user-${this.getRandomNumber(1, 100)}`,
sessionId: this.generateSessionId(),
deviceType: this.getRandomDeviceType()
}
};
});
}
/**
* Generate comprehensive preview data for Weather Widget
*/
generateWeatherPreviewData(options: PreviewDataOptions = {}): any[] {
const locations = options.locations || ['Bangkok', 'Chiang Mai', 'Phuket', 'Pattaya'];
const count = options.count || 7; // 7 days forecast
const weatherConditions = [
{ condition: 'sunny', description: 'Clear Sky', icon: 'sun', tempRange: [25, 35] },
{ condition: 'partly-cloudy', description: 'Partly Cloudy', icon: 'cloud-sun', tempRange: [23, 32] },
{ condition: 'cloudy', description: 'Overcast', icon: 'cloud', tempRange: [20, 28] },
{ condition: 'rainy', description: 'Light Rain', icon: 'cloud-rain', tempRange: [18, 26] },
{ condition: 'stormy', description: 'Thunderstorm', icon: 'cloud-lightning', tempRange: [16, 24] }
];
return Array.from({ length: count }, (_, index) => {
const location = locations[index % locations.length];
const weather = weatherConditions[Math.floor(Math.random() * weatherConditions.length)];
const date = new Date();
date.setDate(date.getDate() + index);
const temperature = this.getRandomNumber(weather.tempRange[0], weather.tempRange[1]);
const humidity = this.getRandomNumber(40, 90);
const windSpeed = this.getRandomNumber(5, 25);
const pressure = this.getRandomNumber(1000, 1020);
return {
id: `weather-${index + 1}`,
location: location,
date: this.formatDate(date),
temperature: temperature,
feelsLike: temperature + this.getRandomNumber(-3, 5),
humidity: humidity,
windSpeed: windSpeed,
windDirection: this.getRandomWindDirection(),
pressure: pressure,
description: weather.description,
condition: weather.condition,
icon: weather.icon,
uvIndex: this.getRandomNumber(0, 11),
visibility: this.getRandomNumber(5, 15),
sunrise: this.getRandomTime('06:00', '07:00'),
sunset: this.getRandomTime('18:00', '19:00'),
precipitation: this.getRandomNumber(0, 50),
hourlyForecast: this.generateHourlyForecast(temperature, weather)
};
});
}
/**
* Generate comprehensive preview data for Clock Widget
*/
generateClockPreviewData(options: PreviewDataOptions = {}): any[] {
const timezones = [
'Asia/Bangkok', 'UTC', 'America/New_York', 'Europe/London',
'Asia/Tokyo', 'Australia/Sydney', 'America/Los_Angeles'
];
const count = options.count || timezones.length;
return Array.from({ length: count }, (_, index) => {
const timezone = timezones[index % timezones.length];
const now = new Date();
return {
id: `clock-${index + 1}`,
timezone: timezone,
currentTime: now.toLocaleTimeString('en-US', {
timeZone: timezone,
hour12: false,
hour: '2-digit',
minute: '2-digit',
second: '2-digit'
}),
currentDate: now.toLocaleDateString('en-US', {
timeZone: timezone,
year: 'numeric',
month: 'long',
day: 'numeric'
}),
dayOfWeek: now.toLocaleDateString('en-US', {
timeZone: timezone,
weekday: 'long'
}),
clockType: this.getRandomClockType(),
format24Hour: Math.random() > 0.5,
showSeconds: Math.random() > 0.3,
showDate: Math.random() > 0.2,
offset: this.getTimezoneOffset(timezone),
isDST: this.isDST(now, timezone),
nextDSTChange: this.getNextDSTChange(timezone)
};
});
}
// Helper methods
private getRandomNumber(min: number, max: number): number {
return Math.floor(Math.random() * (max - min + 1)) + min;
}
private getRandomDate(): Date {
const now = new Date();
const past = new Date(now.getTime() - 30 * 24 * 60 * 60 * 1000);
return new Date(past.getTime() + Math.random() * (now.getTime() - past.getTime()));
}
private getRandomPastDate(): Date {
const now = new Date();
const past = new Date(now.getTime() - 7 * 24 * 60 * 60 * 1000);
return new Date(past.getTime() + Math.random() * (now.getTime() - past.getTime()));
}
private getRandomDateInRange(start: Date, end: Date): Date {
return new Date(start.getTime() + Math.random() * (end.getTime() - start.getTime()));
}
private getRandomDateAfter(date: Date): Date {
const future = new Date(date.getTime() + 24 * 60 * 60 * 1000);
return new Date(future.getTime() + Math.random() * (7 * 24 * 60 * 60 * 1000));
}
private formatDate(date: Date): string {
return date.toISOString().split('T')[0];
}
private getRandomTime(start?: string, end?: string): string {
const startHour = start ? parseInt(start.split(':')[0]) : 8;
const endHour = end ? parseInt(end.split(':')[0]) : 18;
const hour = this.getRandomNumber(startHour, endHour);
const minute = this.getRandomNumber(0, 59);
return `${hour.toString().padStart(2, '0')}:${minute.toString().padStart(2, '0')}`;
}
private getRandomLocation(): string {
const locations = ['Conference Room A', 'Main Office', 'Remote', 'Client Site', 'Training Center'];
return locations[Math.floor(Math.random() * locations.length)];
}
private getRandomPriority(): string {
const priorities = ['low', 'medium', 'high', 'urgent'];
return priorities[Math.floor(Math.random() * priorities.length)];
}
private getRandomStatus(): string {
const statuses = ['pending', 'in-progress', 'completed', 'cancelled'];
return statuses[Math.floor(Math.random() * statuses.length)];
}
private getRandomSource(): string {
const sources = ['System', 'User', 'API', 'Email', 'Mobile App', 'Web Portal'];
return sources[Math.floor(Math.random() * sources.length)];
}
private getRandomActionUrl(): string {
const actions = ['/dashboard', '/profile', '/settings', '/reports', '/tasks'];
return actions[Math.floor(Math.random() * actions.length)];
}
private getRandomDeviceType(): string {
const devices = ['desktop', 'mobile', 'tablet'];
return devices[Math.floor(Math.random() * devices.length)];
}
private generateSessionId(): string {
return Math.random().toString(36).substr(2, 9);
}
private getNotificationCategory(type: string): string {
const categories = {
'info': 'general',
'success': 'system',
'warning': 'alert',
'error': 'error'
};
return categories[type] || 'general';
}
private getRandomWindDirection(): string {
const directions = ['N', 'NE', 'E', 'SE', 'S', 'SW', 'W', 'NW'];
return directions[Math.floor(Math.random() * directions.length)];
}
private getRandomClockType(): string {
const types = ['digital', 'analog', 'gauge'];
return types[Math.floor(Math.random() * types.length)];
}
private getTimezoneOffset(timezone: string): number {
// Simplified timezone offset calculation
const offsets: { [key: string]: number } = {
'UTC': 0,
'Asia/Bangkok': 7,
'America/New_York': -5,
'Europe/London': 0,
'Asia/Tokyo': 9,
'Australia/Sydney': 10,
'America/Los_Angeles': -8
};
return offsets[timezone] || 0;
}
private isDST(date: Date, timezone: string): boolean {
// Simplified DST check
const month = date.getMonth();
return month >= 3 && month <= 10; // Rough approximation
}
private getNextDSTChange(timezone: string): Date {
const now = new Date();
return new Date(now.getFullYear() + 1, 3, 1); // Simplified
}
private generateHourlyForecast(baseTemp: number, weather: any): any[] {
return Array.from({ length: 24 }, (_, hour) => ({
hour: hour,
time: `${hour.toString().padStart(2, '0')}:00`,
temperature: baseTemp + this.getRandomNumber(-5, 5),
condition: weather.condition,
precipitation: Math.random() > 0.7 ? this.getRandomNumber(0, 10) : 0
}));
}
private interpolateMessage(message: string): string {
const replacements: { [key: string]: string } = {
'{sender}': 'John Doe',
'{task}': 'Project Alpha',
'{time}': 'tonight at 10 PM',
'{operation}': 'user authentication',
'{version}': '2.1.0',
'{percentage}': '85%',
'{meeting}': 'Weekly Standup',
'{minutes}': '15'
};
let result = message;
Object.entries(replacements).forEach(([key, value]) => {
result = result.replace(key, value);
});
return result;
}
private getCategoryColor(category: string): string {
const colors: { [key: string]: string } = {
'sales': '#28a745',
'marketing': '#17a2b8',
'operations': '#ffc107',
'finance': '#dc3545',
'hr': '#6f42c1'
};
return colors[category] || '#6c757d';
}
private getEventTypeColor(eventType: string): string {
const colors: { [key: string]: string } = {
'meeting': '#007bff',
'deadline': '#dc3545',
'holiday': '#28a745',
'event': '#ffc107',
'training': '#17a2b8',
'review': '#6f42c1'
};
return colors[eventType] || '#6c757d';
}
}
<div class="widget-config-editor">
<!-- Header -->
<div class="config-header">
<div class="header-content">
<div class="header-left">
<div class="widget-icon">
<i [class]="currentSchema?.icon || 'bi-gear'"></i>
</div>
<div class="header-info">
<h2 class="widget-title">{{ currentSchema?.displayName || 'Widget Configuration' }}</h2>
<p class="widget-description">{{ currentSchema?.description || '' }}</p>
</div>
</div>
<div class="header-actions">
<button
type="button"
class="btn btn-outline-secondary me-2"
(click)="onPreview()"
[disabled]="isLoading">
<i class="bi bi-eye"></i>
Preview
</button>
<button
type="button"
class="btn btn-secondary me-2"
(click)="onCancel()"
[disabled]="isLoading">
Cancel
</button>
<button
type="button"
class="btn btn-primary"
(click)="onSave()"
[disabled]="isLoading || configForm.invalid">
<i class="bi bi-check-lg" *ngIf="!isLoading"></i>
<span class="spinner-border spinner-border-sm" *ngIf="isLoading"></span>
{{ isLoading ? 'Saving...' : 'Save Configuration' }}
</button>
</div>
</div>
</div>
<!-- Error Messages -->
<div class="error-messages" *ngIf="errors.length > 0">
<div class="alert alert-danger" role="alert">
<i class="bi bi-exclamation-triangle-fill"></i>
<ul class="mb-0">
<li *ngFor="let error of errors">{{ error }}</li>
</ul>
</div>
</div>
<!-- Tab Navigation -->
<div class="config-tabs">
<nav class="nav nav-tabs">
<button
*ngFor="let tab of tabs"
class="nav-link"
[class.active]="activeTab === tab.id"
(click)="onTabChange(tab.id)"
[disabled]="isLoading">
<i [class]="tab.icon"></i>
{{ tab.label }}
</button>
</nav>
</div>
<!-- Tab Content -->
<div class="config-content">
<form [formGroup]="configForm" (ngSubmit)="onSave()">
<!-- Basic Settings Tab -->
<div *ngIf="activeTab === 'basic'" class="tab-pane active">
<div class="config-section">
<h3 class="section-title">Basic Configuration</h3>
<div class="config-groups" *ngIf="currentSchema">
<div *ngFor="let group of currentSchema.groups" class="config-group">
<div class="group-header">
<h4 class="group-title">{{ group.label }}</h4>
<p class="group-description" *ngIf="group.description">{{ group.description }}</p>
</div>
<div class="group-fields">
<div *ngFor="let field of group.fields" class="config-field">
<label [for]="field.id" class="field-label">
{{ field.label }}
<span *ngIf="field.validation?.required" class="required">*</span>
</label>
<!-- Text Input -->
<input
*ngIf="field.type === 'text'"
type="text"
[id]="field.id"
class="form-control"
[class.is-invalid]="basicFormGroup.get(field.id)?.invalid && basicFormGroup.get(field.id)?.touched"
[formControlName]="field.id"
[placeholder]="field.placeholder || ''">
<!-- Number Input -->
<input
*ngIf="field.type === 'number'"
type="number"
[id]="field.id"
class="form-control"
[class.is-invalid]="basicFormGroup.get(field.id)?.invalid && basicFormGroup.get(field.id)?.touched"
[formControlName]="field.id"
[min]="field.validation?.min"
[max]="field.validation?.max">
<!-- Boolean Input -->
<div *ngIf="field.type === 'boolean'" class="form-check form-switch">
<input
type="checkbox"
[id]="field.id"
class="form-check-input"
[formControlName]="field.id">
<label [for]="field.id" class="form-check-label">
Enable {{ field.label }}
</label>
</div>
<!-- Select Input -->
<select
*ngIf="field.type === 'select'"
[id]="field.id"
class="form-select"
[class.is-invalid]="basicFormGroup.get(field.id)?.invalid && basicFormGroup.get(field.id)?.touched"
[formControlName]="field.id">
<option value="">Select {{ field.label }}</option>
<option *ngFor="let option of field.options" [value]="option.value">
{{ option.label }}
</option>
</select>
<!-- Multi-Select Input -->
<div *ngIf="field.type === 'multiselect'" class="multi-select-container">
<div class="form-check" *ngFor="let option of field.options">
<input
type="checkbox"
[id]="field.id + '_' + option.value"
class="form-check-input"
[value]="option.value">
<label [for]="field.id + '_' + option.value" class="form-check-label">
{{ option.label }}
</label>
</div>
</div>
<!-- Color Input -->
<div *ngIf="field.type === 'color'" class="color-input-container">
<input
type="color"
[id]="field.id"
class="form-control form-control-color"
[formControlName]="field.id">
<input
type="text"
class="form-control color-text"
[formControlName]="field.id"
placeholder="#000000">
</div>
<!-- Range Input -->
<div *ngIf="field.type === 'range'" class="range-container">
<input
type="range"
[id]="field.id"
class="form-range"
[min]="field.validation?.min || 0"
[max]="field.validation?.max || 100"
[formControlName]="field.id">
<div class="range-value">
Value: {{ basicFormGroup.get(field.id)?.value || field.defaultValue }}
</div>
</div>
<!-- Textarea Input -->
<textarea
*ngIf="field.type === 'textarea'"
[id]="field.id"
class="form-control"
[class.is-invalid]="basicFormGroup.get(field.id)?.invalid && basicFormGroup.get(field.id)?.touched"
[formControlName]="field.id"
[placeholder]="field.placeholder || ''"
rows="3"></textarea>
<!-- Help Text -->
<div *ngIf="field.helpText" class="form-text">
{{ field.helpText }}
</div>
<!-- Validation Errors -->
<div *ngIf="basicFormGroup.get(field.id)?.invalid && basicFormGroup.get(field.id)?.touched" class="invalid-feedback">
<div *ngIf="basicFormGroup.get(field.id)?.errors?.['required']">
{{ field.label }} is required
</div>
<div *ngIf="basicFormGroup.get(field.id)?.errors?.['min']">
Minimum value is {{ field.validation?.min }}
</div>
<div *ngIf="basicFormGroup.get(field.id)?.errors?.['max']">
Maximum value is {{ field.validation?.max }}
</div>
<div *ngIf="basicFormGroup.get(field.id)?.errors?.['pattern']">
Invalid format for {{ field.label }}
</div>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
<!-- Style & Colors Tab -->
<div *ngIf="activeTab === 'style'" class="tab-pane active">
<div class="config-section">
<h3 class="section-title">Style & Colors</h3>
<div formGroupName="style" class="style-config">
<div class="row">
<div class="col-md-6">
<div class="config-field">
<label for="backgroundColor" class="field-label">Background Color</label>
<div class="color-input-container">
<input
type="color"
id="backgroundColor"
class="form-control form-control-color"
formControlName="backgroundColor">
<input
type="text"
class="form-control color-text"
formControlName="backgroundColor">
</div>
</div>
<div class="config-field">
<label for="textColor" class="field-label">Text Color</label>
<div class="color-input-container">
<input
type="color"
id="textColor"
class="form-control form-control-color"
formControlName="textColor">
<input
type="text"
class="form-control color-text"
formControlName="textColor">
</div>
</div>
<div class="config-field">
<label for="borderColor" class="field-label">Border Color</label>
<div class="color-input-container">
<input
type="color"
id="borderColor"
class="form-control form-control-color"
formControlName="borderColor">
<input
type="text"
class="form-control color-text"
formControlName="borderColor">
</div>
</div>
</div>
<div class="col-md-6">
<div class="config-field">
<label for="borderRadius" class="field-label">Border Radius</label>
<div class="range-container">
<input
type="range"
id="borderRadius"
class="form-range"
min="0"
max="50"
formControlName="borderRadius">
<div class="range-value">
Value: {{ styleFormGroup.get('borderRadius')?.value }}px
</div>
</div>
</div>
<div class="config-field">
<label for="fontSize" class="field-label">Font Size</label>
<div class="range-container">
<input
type="range"
id="fontSize"
class="form-range"
min="10"
max="24"
formControlName="fontSize">
<div class="range-value">
Value: {{ styleFormGroup.get('fontSize')?.value }}px
</div>
</div>
</div>
<div class="config-field">
<label for="fontFamily" class="field-label">Font Family</label>
<select id="fontFamily" class="form-select" formControlName="fontFamily">
<option value="system-ui, -apple-system, sans-serif">System Font</option>
<option value="Arial, sans-serif">Arial</option>
<option value="Helvetica, sans-serif">Helvetica</option>
<option value="Georgia, serif">Georgia</option>
<option value="Times New Roman, serif">Times New Roman</option>
<option value="Courier New, monospace">Courier New</option>
</select>
</div>
</div>
</div>
<div class="config-field">
<label for="customCSS" class="field-label">Custom CSS</label>
<textarea
id="customCSS"
class="form-control"
formControlName="customCSS"
placeholder="Enter custom CSS rules..."
rows="6"></textarea>
<div class="form-text">
Add custom CSS rules for advanced styling
</div>
</div>
</div>
</div>
</div>
<!-- Data & Filters Tab -->
<div *ngIf="activeTab === 'data'" class="tab-pane active">
<div class="config-section">
<h3 class="section-title">Data & Filters</h3>
<div formGroupName="data" class="data-config">
<!-- Data Fields -->
<div class="config-subsection">
<h4 class="subsection-title">Data Field Mapping</h4>
<div class="data-fields" formArrayName="dataFields">
<div *ngFor="let field of dataFieldsFormArray.controls; let i = index"
[formGroupName]="i"
class="data-field-item">
<div class="row">
<div class="col-md-3">
<label class="field-label">Field ID</label>
<input type="text" class="form-control" formControlName="id" readonly>
</div>
<div class="col-md-3">
<label class="field-label">Label</label>
<input type="text" class="form-control" formControlName="label" readonly>
</div>
<div class="col-md-3">
<label class="field-label">Type</label>
<input type="text" class="form-control" formControlName="type" readonly>
</div>
<div class="col-md-3">
<label class="field-label">Mapping</label>
<input type="text" class="form-control" formControlName="mapping">
</div>
</div>
</div>
</div>
</div>
<!-- Filters -->
<div class="config-subsection">
<h4 class="subsection-title">Filters</h4>
<div class="filters" formArrayName="filters">
<div *ngFor="let filter of filtersFormArray.controls; let i = index"
[formGroupName]="i"
class="filter-item">
<div class="row">
<div class="col-md-2">
<label class="field-label">Field</label>
<input type="text" class="form-control" formControlName="field" readonly>
</div>
<div class="col-md-2">
<label class="field-label">Type</label>
<input type="text" class="form-control" formControlName="type" readonly>
</div>
<div class="col-md-2">
<label class="field-label">Operator</label>
<input type="text" class="form-control" formControlName="operator" readonly>
</div>
<div class="col-md-3">
<label class="field-label">Default Value</label>
<input type="text" class="form-control" formControlName="defaultValue">
</div>
<div class="col-md-3">
<label class="field-label">Options</label>
<div formArrayName="options">
<div *ngFor="let option of filter.get('options')?.controls; let j = index"
[formGroupName]="j"
class="option-item">
<input type="text" class="form-control" formControlName="value" readonly>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
<!-- Layout Tab -->
<div *ngIf="activeTab === 'layout'" class="tab-pane active">
<div class="config-section">
<h3 class="section-title">Layout Configuration</h3>
<div formGroupName="layout" class="layout-config">
<div class="row">
<div class="col-md-6">
<div class="config-field">
<label for="width" class="field-label">Width</label>
<input type="number" id="width" class="form-control" formControlName="width" min="100">
</div>
<div class="config-field">
<label for="height" class="field-label">Height</label>
<input type="number" id="height" class="form-control" formControlName="height" min="100">
</div>
<div class="config-field">
<label for="minWidth" class="field-label">Minimum Width</label>
<input type="number" id="minWidth" class="form-control" formControlName="minWidth" min="50">
</div>
<div class="config-field">
<label for="minHeight" class="field-label">Minimum Height</label>
<input type="number" id="minHeight" class="form-control" formControlName="minHeight" min="50">
</div>
</div>
<div class="col-md-6">
<div class="config-field">
<label for="maxWidth" class="field-label">Maximum Width</label>
<input type="number" id="maxWidth" class="form-control" formControlName="maxWidth" min="200">
</div>
<div class="config-field">
<label for="maxHeight" class="field-label">Maximum Height</label>
<input type="number" id="maxHeight" class="form-control" formControlName="maxHeight" min="200">
</div>
<div class="config-field">
<label for="aspectRatio" class="field-label">Aspect Ratio</label>
<select id="aspectRatio" class="form-select" formControlName="aspectRatio">
<option value="auto">Auto</option>
<option value="16:9">16:9</option>
<option value="4:3">4:3</option>
<option value="1:1">1:1</option>
<option value="3:2">3:2</option>
</select>
</div>
<div class="config-field">
<div class="form-check form-switch">
<input type="checkbox" id="responsive" class="form-check-input" formControlName="responsive">
<label for="responsive" class="form-check-label">Responsive Layout</label>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
<!-- Animation Tab -->
<div *ngIf="activeTab === 'animation'" class="tab-pane active">
<div class="config-section">
<h3 class="section-title">Animation Settings</h3>
<div formGroupName="animation" class="animation-config">
<div class="row">
<div class="col-md-6">
<div class="config-field">
<div class="form-check form-switch">
<input type="checkbox" id="enableAnimations" class="form-check-input" formControlName="enableAnimations">
<label for="enableAnimations" class="form-check-label">Enable Animations</label>
</div>
</div>
<div class="config-field">
<label for="animationType" class="field-label">Animation Type</label>
<select id="animationType" class="form-select" formControlName="animationType">
<option value="fade">Fade</option>
<option value="slide">Slide</option>
<option value="bounce">Bounce</option>
<option value="pulse">Pulse</option>
<option value="none">None</option>
</select>
</div>
<div class="config-field">
<label for="animationDuration" class="field-label">Duration (ms)</label>
<div class="range-container">
<input
type="range"
id="animationDuration"
class="form-range"
min="100"
max="2000"
step="100"
formControlName="animationDuration">
<div class="range-value">
Value: {{ animationFormGroup.get('animationDuration')?.value }}ms
</div>
</div>
</div>
</div>
<div class="col-md-6">
<div class="config-field">
<label for="animationDelay" class="field-label">Delay (ms)</label>
<div class="range-container">
<input
type="range"
id="animationDelay"
class="form-range"
min="0"
max="1000"
step="50"
formControlName="animationDelay">
<div class="range-value">
Value: {{ animationFormGroup.get('animationDelay')?.value }}ms
</div>
</div>
</div>
<div class="config-field">
<div class="form-check form-switch">
<input type="checkbox" id="hoverEffects" class="form-check-input" formControlName="hoverEffects">
<label for="hoverEffects" class="form-check-label">Enable Hover Effects</label>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
<!-- Interaction Tab -->
<div *ngIf="activeTab === 'interaction'" class="tab-pane active">
<div class="config-section">
<h3 class="section-title">Interaction Settings</h3>
<div formGroupName="interaction" class="interaction-config">
<div class="row">
<div class="col-md-6">
<div class="config-field">
<div class="form-check form-switch">
<input type="checkbox" id="enableTooltip" class="form-check-input" formControlName="enableTooltip">
<label for="enableTooltip" class="form-check-label">Enable Tooltips</label>
</div>
</div>
<div class="config-field">
<div class="form-check form-switch">
<input type="checkbox" id="enableClick" class="form-check-input" formControlName="enableClick">
<label for="enableClick" class="form-check-label">Enable Click Events</label>
</div>
</div>
<div class="config-field">
<div class="form-check form-switch">
<input type="checkbox" id="enableHover" class="form-check-input" formControlName="enableHover">
<label for="enableHover" class="form-check-label">Enable Hover Events</label>
</div>
</div>
<div class="config-field">
<div class="form-check form-switch">
<input type="checkbox" id="enableSelection" class="form-check-input" formControlName="enableSelection">
<label for="enableSelection" class="form-check-label">Enable Selection</label>
</div>
</div>
</div>
<div class="col-md-6">
<div class="config-field">
<div class="form-check form-switch">
<input type="checkbox" id="enableExport" class="form-check-input" formControlName="enableExport">
<label for="enableExport" class="form-check-label">Enable Export</label>
</div>
</div>
<div class="config-field">
<div class="form-check form-switch">
<input type="checkbox" id="enableRefresh" class="form-check-input" formControlName="enableRefresh">
<label for="enableRefresh" class="form-check-label">Enable Refresh</label>
</div>
</div>
<div class="config-field">
<label for="clickAction" class="field-label">Click Action</label>
<select id="clickAction" class="form-select" formControlName="clickAction">
<option value="none">None</option>
<option value="drill_down">Drill Down</option>
<option value="open_modal">Open Modal</option>
<option value="navigate">Navigate</option>
<option value="custom">Custom</option>
</select>
</div>
<div class="config-field" *ngIf="interactionFormGroup.get('clickAction')?.value === 'custom'">
<label for="customClickHandler" class="field-label">Custom Handler</label>
<textarea
id="customClickHandler"
class="form-control"
formControlName="customClickHandler"
placeholder="Enter custom JavaScript function..."
rows="3"></textarea>
</div>
</div>
</div>
</div>
</div>
</div>
<!-- Security Tab -->
<div *ngIf="activeTab === 'security'" class="tab-pane active">
<div class="config-section">
<h3 class="section-title">Security Settings</h3>
<div formGroupName="security" class="security-config">
<div class="row">
<div class="col-md-6">
<div class="config-field">
<div class="form-check form-switch">
<input type="checkbox" id="requireAuth" class="form-check-input" formControlName="requireAuth">
<label for="requireAuth" class="form-check-label">Require Authentication</label>
</div>
</div>
<div class="config-field">
<div class="form-check form-switch">
<input type="checkbox" id="dataEncryption" class="form-check-input" formControlName="dataEncryption">
<label for="dataEncryption" class="form-check-label">Enable Data Encryption</label>
</div>
</div>
<div class="config-field">
<div class="form-check form-switch">
<input type="checkbox" id="auditLog" class="form-check-input" formControlName="auditLog">
<label for="auditLog" class="form-check-label">Enable Audit Logging</label>
</div>
</div>
</div>
<div class="col-md-6">
<div class="config-field">
<label for="rateLimit" class="field-label">Rate Limit (requests/min)</label>
<input type="number" id="rateLimit" class="form-control" formControlName="rateLimit" min="0">
<div class="form-text">0 = No limit</div>
</div>
<div class="config-field">
<label class="field-label">Allowed Roles</label>
<div formArrayName="allowedRoles" class="roles-list">
<div *ngFor="let role of allowedRolesFormArray.controls; let i = index"
[formGroupName]="i"
class="role-item">
<input type="text" class="form-control" formControlName="role" placeholder="Enter role name">
</div>
</div>
<button type="button" class="btn btn-sm btn-outline-secondary"
(click)="addRole()">
<i class="bi bi-plus"></i> Add Role
</button>
</div>
</div>
</div>
</div>
</div>
</div>
<!-- Preview Tab -->
<div *ngIf="activeTab === 'preview'" class="tab-pane active">
<div class="config-section">
<h3 class="section-title">Preview</h3>
<div class="preview-container">
<div class="preview-header">
<h4>Preview Data</h4>
<p class="text-muted">This is sample data for preview. Your actual data will be loaded when the widget is deployed.</p>
</div>
<div class="preview-data">
<div class="data-table-container">
<table class="table table-striped">
<thead>
<tr>
<th *ngFor="let key of getPreviewDataKeys()">{{ key }}</th>
</tr>
</thead>
<tbody>
<tr *ngFor="let item of previewData; let i = index">
<td *ngFor="let key of getPreviewDataKeys()">
<span *ngIf="isDateField(key)">{{ formatDate(item[key]) }}</span>
<span *ngIf="!isDateField(key)">{{ item[key] }}</span>
</td>
</tr>
</tbody>
</table>
</div>
</div>
<div class="preview-actions">
<button type="button" class="btn btn-outline-primary" (click)="updatePreviewData()">
<i class="bi bi-arrow-clockwise"></i>
Refresh Preview Data
</button>
</div>
</div>
</div>
</div>
</form>
</div>
</div>
/* Widget Config Editor Styles */
.widget-config-editor {
background: #ffffff;
border-radius: 12px;
box-shadow: 0 4px 20px rgba(0, 0, 0, 0.1);
overflow: hidden;
height: 100%;
display: flex;
flex-direction: column;
}
/* Header */
.config-header {
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
color: white;
padding: 1.5rem 2rem;
border-bottom: 1px solid rgba(255, 255, 255, 0.1);
}
.header-content {
display: flex;
align-items: center;
justify-content: space-between;
gap: 2rem;
}
.header-left {
display: flex;
align-items: center;
gap: 1rem;
}
.widget-icon {
width: 3rem;
height: 3rem;
background: rgba(255, 255, 255, 0.2);
border-radius: 12px;
display: flex;
align-items: center;
justify-content: center;
font-size: 1.5rem;
}
.header-info {
h2 {
margin: 0;
font-size: 1.5rem;
font-weight: 600;
}
p {
margin: 0.25rem 0 0 0;
opacity: 0.9;
font-size: 0.9rem;
}
}
.header-actions {
display: flex;
align-items: center;
gap: 0.75rem;
.btn {
border-radius: 8px;
font-weight: 500;
padding: 0.5rem 1rem;
&.btn-primary {
background: rgba(255, 255, 255, 0.2);
border-color: rgba(255, 255, 255, 0.3);
&:hover {
background: rgba(255, 255, 255, 0.3);
border-color: rgba(255, 255, 255, 0.4);
}
}
&.btn-secondary {
background: transparent;
border-color: rgba(255, 255, 255, 0.3);
color: white;
&:hover {
background: rgba(255, 255, 255, 0.1);
border-color: rgba(255, 255, 255, 0.4);
}
}
&.btn-outline-secondary {
background: transparent;
border-color: rgba(255, 255, 255, 0.3);
color: white;
&:hover {
background: rgba(255, 255, 255, 0.1);
border-color: rgba(255, 255, 255, 0.4);
}
}
}
}
/* Error Messages */
.error-messages {
padding: 1rem 2rem;
.alert {
border-radius: 8px;
border: none;
i {
margin-right: 0.5rem;
}
ul {
margin-top: 0.5rem;
padding-left: 1.5rem;
}
}
}
/* Tab Navigation */
.config-tabs {
background: #f8f9fa;
border-bottom: 1px solid #dee2e6;
.nav-tabs {
border-bottom: none;
padding: 0 2rem;
.nav-link {
border: none;
background: transparent;
color: #6c757d;
padding: 1rem 1.5rem;
border-radius: 0;
font-weight: 500;
transition: all 0.3s ease;
&:hover {
color: #495057;
background: rgba(0, 0, 0, 0.05);
}
&.active {
color: #667eea;
background: white;
border-bottom: 3px solid #667eea;
}
i {
margin-right: 0.5rem;
font-size: 1rem;
}
}
}
}
/* Tab Content */
.config-content {
flex: 1;
overflow-y: auto;
padding: 2rem;
.tab-pane {
display: block;
}
}
/* Configuration Sections */
.config-section {
margin-bottom: 2rem;
.section-title {
font-size: 1.25rem;
font-weight: 600;
color: #495057;
margin-bottom: 1.5rem;
padding-bottom: 0.5rem;
border-bottom: 2px solid #e9ecef;
}
.subsection-title {
font-size: 1.1rem;
font-weight: 600;
color: #6c757d;
margin-bottom: 1rem;
margin-top: 2rem;
}
}
/* Configuration Groups */
.config-groups {
display: flex;
flex-direction: column;
gap: 2rem;
}
.config-group {
background: #f8f9fa;
border-radius: 12px;
padding: 1.5rem;
border: 1px solid #e9ecef;
.group-header {
margin-bottom: 1.5rem;
.group-title {
font-size: 1.1rem;
font-weight: 600;
color: #495057;
margin: 0 0 0.5rem 0;
}
.group-description {
color: #6c757d;
font-size: 0.9rem;
margin: 0;
}
}
}
/* Configuration Fields */
.config-field {
margin-bottom: 1.5rem;
.field-label {
display: block;
font-weight: 600;
color: #495057;
margin-bottom: 0.5rem;
.required {
color: #dc3545;
margin-left: 0.25rem;
}
}
.form-control,
.form-select {
border-radius: 8px;
border: 1px solid #ced4da;
padding: 0.75rem;
font-size: 0.9rem;
transition: all 0.3s ease;
&:focus {
border-color: #667eea;
box-shadow: 0 0 0 0.2rem rgba(102, 126, 234, 0.25);
}
&.is-invalid {
border-color: #dc3545;
&:focus {
border-color: #dc3545;
box-shadow: 0 0 0 0.2rem rgba(220, 53, 69, 0.25);
}
}
}
.form-check {
.form-check-input {
&:checked {
background-color: #667eea;
border-color: #667eea;
}
}
.form-check-label {
color: #495057;
font-weight: 500;
}
}
.form-text {
color: #6c757d;
font-size: 0.85rem;
margin-top: 0.25rem;
}
.invalid-feedback {
color: #dc3545;
font-size: 0.85rem;
margin-top: 0.25rem;
}
}
/* Color Input Container */
.color-input-container {
display: flex;
gap: 0.75rem;
align-items: center;
.form-control-color {
width: 3rem;
height: 2.5rem;
border-radius: 6px;
border: 1px solid #ced4da;
cursor: pointer;
&:focus {
border-color: #667eea;
box-shadow: 0 0 0 0.2rem rgba(102, 126, 234, 0.25);
}
}
.color-text {
flex: 1;
font-family: 'Courier New', monospace;
font-size: 0.85rem;
}
}
/* Range Container */
.range-container {
.form-range {
margin-bottom: 0.5rem;
&::-webkit-slider-thumb {
background: #667eea;
}
&::-moz-range-thumb {
background: #667eea;
border: none;
}
}
.range-value {
font-size: 0.85rem;
color: #6c757d;
text-align: center;
font-weight: 500;
}
}
/* Multi-Select Container */
.multi-select-container {
background: #f8f9fa;
border-radius: 8px;
padding: 1rem;
border: 1px solid #e9ecef;
.form-check {
margin-bottom: 0.5rem;
&:last-child {
margin-bottom: 0;
}
}
}
/* Data Configuration */
.data-config {
.data-fields,
.filters {
.data-field-item,
.filter-item {
background: #f8f9fa;
border-radius: 8px;
padding: 1rem;
margin-bottom: 1rem;
border: 1px solid #e9ecef;
&:last-child {
margin-bottom: 0;
}
}
}
.option-item {
background: white;
border-radius: 6px;
padding: 0.5rem;
margin-bottom: 0.5rem;
border: 1px solid #dee2e6;
&:last-child {
margin-bottom: 0;
}
}
}
/* Style Configuration */
.style-config {
.row {
margin-bottom: 1rem;
}
}
/* Layout Configuration */
.layout-config {
.row {
margin-bottom: 1rem;
}
}
/* Animation Configuration */
.animation-config {
.row {
margin-bottom: 1rem;
}
}
/* Interaction Configuration */
.interaction-config {
.row {
margin-bottom: 1rem;
}
}
/* Security Configuration */
.security-config {
.roles-list {
background: #f8f9fa;
border-radius: 8px;
padding: 1rem;
margin-bottom: 1rem;
border: 1px solid #e9ecef;
.role-item {
margin-bottom: 0.5rem;
&:last-child {
margin-bottom: 0;
}
}
}
}
/* Preview Configuration */
.preview-container {
.preview-header {
margin-bottom: 1.5rem;
h4 {
color: #495057;
margin-bottom: 0.5rem;
}
p {
color: #6c757d;
margin: 0;
}
}
.preview-data {
background: #f8f9fa;
border-radius: 12px;
padding: 1.5rem;
margin-bottom: 1.5rem;
border: 1px solid #e9ecef;
.data-table-container {
overflow-x: auto;
.table {
margin-bottom: 0;
background: white;
border-radius: 8px;
overflow: hidden;
th {
background: #667eea;
color: white;
border: none;
font-weight: 600;
padding: 0.75rem;
}
td {
border-color: #e9ecef;
padding: 0.75rem;
vertical-align: middle;
}
tbody tr:hover {
background-color: rgba(102, 126, 234, 0.05);
}
}
}
}
.preview-actions {
text-align: center;
.btn {
border-radius: 8px;
font-weight: 500;
}
}
}
/* Responsive Design */
@media (max-width: 768px) {
.config-header {
padding: 1rem;
.header-content {
flex-direction: column;
align-items: stretch;
gap: 1rem;
}
.header-left {
justify-content: center;
text-align: center;
}
.header-actions {
justify-content: center;
flex-wrap: wrap;
}
}
.config-content {
padding: 1rem;
}
.config-tabs .nav-tabs {
padding: 0 1rem;
overflow-x: auto;
white-space: nowrap;
.nav-link {
padding: 0.75rem 1rem;
font-size: 0.9rem;
}
}
.config-group {
padding: 1rem;
}
.color-input-container {
flex-direction: column;
align-items: stretch;
.form-control-color {
width: 100%;
}
}
}
@media (max-width: 480px) {
.widget-config-editor {
border-radius: 0;
}
.config-header {
padding: 0.75rem;
.header-info h2 {
font-size: 1.25rem;
}
}
.config-content {
padding: 0.75rem;
}
.config-group {
padding: 0.75rem;
}
}
/* Loading States */
.spinner-border-sm {
width: 1rem;
height: 1rem;
}
/* Animation */
@keyframes fadeIn {
from {
opacity: 0;
transform: translateY(10px);
}
to {
opacity: 1;
transform: translateY(0);
}
}
.tab-pane {
animation: fadeIn 0.3s ease-out;
}
/* Dark Mode Support */
@media (prefers-color-scheme: dark) {
.widget-config-editor {
background: #1a202c;
color: #e2e8f0;
}
.config-tabs {
background: #2d3748;
border-bottom-color: #4a5568;
}
.config-group {
background: #2d3748;
border-color: #4a5568;
}
.config-field {
.field-label {
color: #e2e8f0;
}
.form-control,
.form-select {
background: #2d3748;
border-color: #4a5568;
color: #e2e8f0;
&:focus {
background: #2d3748;
border-color: #667eea;
}
}
.form-text {
color: #a0aec0;
}
}
.preview-container .preview-data {
background: #2d3748;
border-color: #4a5568;
.table {
background: #2d3748;
color: #e2e8f0;
th {
background: #667eea;
}
td {
border-color: #4a5568;
}
}
}
}
import { Component, Input, Output, EventEmitter, OnInit, OnDestroy } from '@angular/core';
import { FormBuilder, FormGroup, FormArray, Validators, FormControl } from '@angular/forms';
import { Subscription } from 'rxjs';
import {
WidgetConfigSchema,
WidgetConfigField,
WidgetConfigGroup,
WidgetFilterConfig,
DataMappingConfig,
WidgetStyleConfig,
WidgetAnimationConfig,
WidgetLayoutConfig,
WidgetInteractionConfig,
WidgetDataConfig,
WidgetSecurityConfig
} from '../../models/widget-config-schema.model';
import { WidgetConfigService, WidgetConfigInstance } from '../../services/widget-config.service';
import { WidgetPreviewDataService } from '../../services/widget-preview-data.service';
@Component({
selector: 'app-widget-config-editor',
templateUrl: './widget-config-editor.component.html',
styleUrls: ['./widget-config-editor.component.scss']
})
export class WidgetConfigEditorComponent implements OnInit, OnDestroy {
@Input() widgetId: string | null = null;
@Input() widgetType: string = '';
@Input() initialConfig: any = {};
@Input() mode: 'create' | 'edit' | 'preview' = 'edit';
@Output() configSaved = new EventEmitter<WidgetConfigInstance>();
@Output() configCancelled = new EventEmitter<void>();
@Output() previewRequested = new EventEmitter<any>();
configForm: FormGroup;
currentSchema: WidgetConfigSchema | null = null;
currentConfig: WidgetConfigInstance | null = null;
previewData: any[] = [];
activeTab = 'basic';
isLoading = false;
errors: string[] = [];
private subscription = new Subscription();
tabs = [
{ id: 'basic', label: 'Basic Settings', icon: 'bi-gear' },
{ id: 'style', label: 'Style & Colors', icon: 'bi-palette' },
{ id: 'data', label: 'Data & Filters', icon: 'bi-database' },
{ id: 'layout', label: 'Layout', icon: 'bi-layout-text-window' },
{ id: 'animation', label: 'Animation', icon: 'bi-magic' },
{ id: 'interaction', label: 'Interaction', icon: 'bi-cursor-click' },
{ id: 'security', label: 'Security', icon: 'bi-shield-lock' },
{ id: 'preview', label: 'Preview', icon: 'bi-eye' }
];
constructor(
private fb: FormBuilder,
private widgetConfigService: WidgetConfigService,
private previewDataService: WidgetPreviewDataService
) {
this.configForm = this.createForm();
}
ngOnInit(): void {
this.loadWidgetSchema();
this.loadPreviewData();
if (this.widgetId) {
this.loadExistingConfig();
} else {
this.initializeNewConfig();
}
}
ngOnDestroy(): void {
this.subscription.unsubscribe();
}
private createForm(): FormGroup {
return this.fb.group({
basic: this.fb.group({}),
style: this.fb.group({
backgroundColor: ['#ffffff'],
textColor: ['#333333'],
borderColor: ['#dee2e6'],
borderWidth: [1],
borderRadius: [8],
padding: [16],
margin: [8],
fontSize: [14],
fontFamily: ['system-ui, -apple-system, sans-serif'],
fontWeight: ['normal'],
customCSS: [''],
theme: ['light']
}),
data: this.fb.group({
dataFields: this.fb.array([]),
filters: this.fb.array([]),
dataMapping: this.fb.array([])
}),
layout: this.fb.group({
width: [300],
height: [200],
minWidth: [200],
minHeight: [150],
maxWidth: [600],
maxHeight: [400],
aspectRatio: ['auto'],
responsive: [true]
}),
animation: this.fb.group({
enableAnimations: [true],
animationType: ['fade'],
animationDuration: [300],
animationDelay: [0],
hoverEffects: [true]
}),
interaction: this.fb.group({
enableTooltip: [true],
enableClick: [true],
enableHover: [true],
enableSelection: [false],
enableExport: [false],
enableRefresh: [true],
clickAction: ['none'],
customClickHandler: ['']
}),
security: this.fb.group({
requireAuth: [false],
allowedRoles: this.fb.array([]),
dataEncryption: [false],
auditLog: [false],
rateLimit: [0]
})
});
}
private loadWidgetSchema(): void {
this.currentSchema = this.widgetConfigService.getWidgetSchema(this.widgetType);
if (this.currentSchema) {
this.buildBasicFormGroup();
this.buildDataFieldsFormArray();
this.buildFiltersFormArray();
this.buildDataMappingFormArray();
}
}
private buildBasicFormGroup(): void {
if (!this.currentSchema) return;
const basicGroup = this.fb.group({});
this.currentSchema.groups.forEach(group => {
group.fields.forEach(field => {
const validators = [];
if (field.validation?.required) {
validators.push(Validators.required);
}
if (field.validation?.min !== undefined) {
validators.push(Validators.min(field.validation.min));
}
if (field.validation?.max !== undefined) {
validators.push(Validators.max(field.validation.max));
}
if (field.validation?.pattern) {
validators.push(Validators.pattern(field.validation.pattern));
}
basicGroup.addControl(
field.id,
this.fb.control(field.defaultValue, validators)
);
});
});
this.configForm.setControl('basic', basicGroup);
}
private buildDataFieldsFormArray(): void {
if (!this.currentSchema) return;
const dataFieldsArray = this.fb.array([]);
this.currentSchema.dataFields.forEach(field => {
const fieldGroup = this.fb.group({
id: [field.id],
label: [field.label],
type: [field.type],
defaultValue: [field.defaultValue],
required: [field.validation?.required || false],
mapping: [field.id]
});
dataFieldsArray.push(fieldGroup);
});
const dataGroup = this.configForm.get('data') as FormGroup;
dataGroup.setControl('dataFields', dataFieldsArray);
}
private buildFiltersFormArray(): void {
if (!this.currentSchema) return;
const filtersArray = this.fb.array([]);
this.currentSchema.filterFields.forEach(filter => {
const filterGroup = this.fb.group({
field: [filter.field],
type: [filter.type],
operator: [filter.operator || 'equals'],
options: this.fb.array([]),
defaultValue: [filter.defaultValue || '']
});
if (filter.options) {
const optionsArray = this.fb.array([]);
filter.options.forEach(option => {
optionsArray.push(this.fb.group({
value: [option.value],
label: [option.label]
}));
});
filterGroup.setControl('options', optionsArray);
}
filtersArray.push(filterGroup);
});
const dataGroup = this.configForm.get('data') as FormGroup;
dataGroup.setControl('filters', filtersArray);
}
private buildDataMappingFormArray(): void {
if (!this.currentSchema) return;
const mappingArray = this.fb.array([]);
this.currentSchema.dataFields.forEach(field => {
const mappingGroup = this.fb.group({
sourceField: [field.id],
targetField: [field.id],
transformation: ['none'],
customFunction: ['']
});
mappingArray.push(mappingGroup);
});
const dataGroup = this.configForm.get('data') as FormGroup;
dataGroup.setControl('dataMapping', mappingArray);
}
private loadExistingConfig(): void {
if (!this.widgetId) return;
this.currentConfig = this.widgetConfigService.getWidgetConfig(this.widgetId);
if (this.currentConfig) {
this.populateForm(this.currentConfig);
}
}
private initializeNewConfig(): void {
if (this.currentSchema) {
// Initialize with default values from schema
const basicGroup = this.configForm.get('basic') as FormGroup;
if (basicGroup) {
Object.keys(basicGroup.controls).forEach(key => {
const field = this.findFieldInSchema(key);
if (field) {
basicGroup.get(key)?.setValue(field.defaultValue);
}
});
}
}
}
private populateForm(config: WidgetConfigInstance): void {
// Populate basic config
const basicGroup = this.configForm.get('basic') as FormGroup;
if (basicGroup && config.config) {
Object.keys(config.config).forEach(key => {
if (basicGroup.get(key)) {
basicGroup.get(key)?.setValue(config.config[key]);
}
});
}
// Populate style config
const styleGroup = this.configForm.get('style') as FormGroup;
if (styleGroup && config.style) {
Object.keys(config.style).forEach(key => {
if (styleGroup.get(key)) {
styleGroup.get(key)?.setValue(config.style[key]);
}
});
}
// Populate other config sections similarly...
// (Implementation for layout, animation, interaction, security)
}
private loadPreviewData(): void {
if (!this.widgetType) return;
try {
switch (this.widgetType) {
case 'SimpleKpiWidgetComponent':
this.previewData = this.previewDataService.generateSimpleKpiPreviewData({ count: 5 });
break;
case 'CalendarWidgetComponent':
this.previewData = this.previewDataService.generateCalendarPreviewData({ count: 8 });
break;
case 'NotificationWidgetComponent':
this.previewData = this.previewDataService.generateNotificationPreviewData({ count: 6 });
break;
case 'WeatherWidgetComponent':
this.previewData = this.previewDataService.generateWeatherPreviewData({ count: 7 });
break;
case 'ClockWidgetComponent':
this.previewData = this.previewDataService.generateClockPreviewData({ count: 4 });
break;
default:
this.previewData = [];
}
} catch (error) {
console.error('Error generating preview data:', error);
this.previewData = [];
}
}
private findFieldInSchema(fieldId: string): WidgetConfigField | null {
if (!this.currentSchema) return null;
for (const group of this.currentSchema.groups) {
const field = group.fields.find(f => f.id === fieldId);
if (field) return field;
}
return null;
}
onTabChange(tabId: string): void {
this.activeTab = tabId;
if (tabId === 'preview') {
this.updatePreviewData();
}
}
private updatePreviewData(): void {
// Regenerate preview data with current form values
const basicValues = this.configForm.get('basic')?.value || {};
try {
switch (this.widgetType) {
case 'SimpleKpiWidgetComponent':
this.previewData = this.previewDataService.generateSimpleKpiPreviewData({
count: 5,
categories: basicValues.categories || ['sales', 'marketing', 'operations']
});
break;
case 'CalendarWidgetComponent':
this.previewData = this.previewDataService.generateCalendarPreviewData({
count: 8
});
break;
case 'NotificationWidgetComponent':
this.previewData = this.previewDataService.generateNotificationPreviewData({
count: 6
});
break;
case 'WeatherWidgetComponent':
this.previewData = this.previewDataService.generateWeatherPreviewData({
count: 7,
locations: basicValues.locations ? [basicValues.locations] : ['Bangkok']
});
break;
case 'ClockWidgetComponent':
this.previewData = this.previewDataService.generateClockPreviewData({
count: 4
});
break;
}
} catch (error) {
console.error('Error updating preview data:', error);
}
}
onSave(): void {
if (this.configForm.invalid) {
this.markFormGroupTouched();
return;
}
this.isLoading = true;
this.errors = [];
try {
const formValue = this.configForm.value;
const configData = this.buildConfigFromForm(formValue);
let savedConfig: WidgetConfigInstance;
if (this.widgetId && this.currentConfig) {
// Update existing config
savedConfig = {
...this.currentConfig,
...configData,
lastModified: new Date()
};
this.widgetConfigService.updateWidgetConfig(this.widgetId, savedConfig);
} else {
// Create new config
savedConfig = this.widgetConfigService.createWidgetConfig(
this.widgetType,
configData
);
}
this.configSaved.emit(savedConfig);
} catch (error) {
this.errors.push('Failed to save configuration');
console.error('Save error:', error);
} finally {
this.isLoading = false;
}
}
private buildConfigFromForm(formValue: any): Partial<WidgetConfigInstance> {
return {
widgetType: this.widgetType,
config: formValue.basic || {},
data: this.previewData,
filters: this.buildFiltersFromForm(formValue.data?.filters),
dataMapping: this.buildDataMappingFromForm(formValue.data?.dataMapping),
style: formValue.style || {},
animation: formValue.animation || {},
layout: formValue.layout || {},
interaction: formValue.interaction || {},
dataConfig: this.buildDataConfigFromForm(formValue.data),
security: formValue.security || {}
};
}
private buildFiltersFromForm(filtersFormArray: any[]): { [key: string]: any } {
const filters: { [key: string]: any } = {};
if (filtersFormArray) {
filtersFormArray.forEach(filter => {
if (filter.field && filter.defaultValue) {
filters[filter.field] = filter.defaultValue;
}
});
}
return filters;
}
private buildDataMappingFromForm(mappingFormArray: any[]): DataMappingConfig[] {
const mappings: DataMappingConfig[] = [];
if (mappingFormArray) {
mappingFormArray.forEach(mapping => {
mappings.push({
sourceField: mapping.sourceField,
targetField: mapping.targetField,
transformation: mapping.transformation,
customFunction: mapping.customFunction
});
});
}
return mappings;
}
private buildDataConfigFromForm(dataForm: any): WidgetDataConfig {
return {
dataSource: 'static',
apiEndpoint: '',
refreshInterval: 0,
cacheEnabled: false,
cacheDuration: 300,
dataTransform: '',
pagination: {
enabled: false,
pageSize: 10,
pageSizeOptions: [10, 25, 50, 100]
}
};
}
onCancel(): void {
this.configCancelled.emit();
}
onPreview(): void {
const formValue = this.configForm.value;
const configData = this.buildConfigFromForm(formValue);
this.previewRequested.emit({
widgetType: this.widgetType,
config: configData,
data: this.previewData
});
}
private markFormGroupTouched(): void {
Object.keys(this.configForm.controls).forEach(key => {
const control = this.configForm.get(key);
if (control instanceof FormGroup) {
this.markFormGroupTouchedRecursive(control);
} else {
control?.markAsTouched();
}
});
}
private markFormGroupTouchedRecursive(formGroup: FormGroup): void {
Object.keys(formGroup.controls).forEach(key => {
const control = formGroup.get(key);
if (control instanceof FormGroup) {
this.markFormGroupTouchedRecursive(control);
} else if (control instanceof FormArray) {
control.controls.forEach(arrayControl => {
if (arrayControl instanceof FormGroup) {
this.markFormGroupTouchedRecursive(arrayControl);
} else {
arrayControl.markAsTouched();
}
});
} else {
control?.markAsTouched();
}
});
}
// Getter methods for template access
get basicFormGroup(): FormGroup {
return this.configForm.get('basic') as FormGroup;
}
get styleFormGroup(): FormGroup {
return this.configForm.get('style') as FormGroup;
}
get dataFormGroup(): FormGroup {
return this.configForm.get('data') as FormGroup;
}
get layoutFormGroup(): FormGroup {
return this.configForm.get('layout') as FormGroup;
}
get animationFormGroup(): FormGroup {
return this.configForm.get('animation') as FormGroup;
}
get interactionFormGroup(): FormGroup {
return this.configForm.get('interaction') as FormGroup;
}
get securityFormGroup(): FormGroup {
return this.configForm.get('security') as FormGroup;
}
get dataFieldsFormArray(): FormArray {
return this.dataFormGroup.get('dataFields') as FormArray;
}
get filtersFormArray(): FormArray {
return this.dataFormGroup.get('filters') as FormArray;
}
get dataMappingFormArray(): FormArray {
return this.dataFormGroup.get('dataMapping') as FormArray;
}
get allowedRolesFormArray(): FormArray {
return this.securityFormGroup.get('allowedRoles') as FormArray;
}
// Helper methods for template
getPreviewDataKeys(): string[] {
if (this.previewData.length === 0) return [];
return Object.keys(this.previewData[0]);
}
isDateField(key: string): boolean {
return key.toLowerCase().includes('date') ||
key.toLowerCase().includes('time') ||
key.toLowerCase().includes('timestamp');
}
formatDate(value: any): string {
if (!value) return '';
try {
const date = new Date(value);
return date.toLocaleDateString();
} catch {
return value.toString();
}
}
addRole(): void {
const rolesArray = this.allowedRolesFormArray;
rolesArray.push(this.fb.group({
role: ['', Validators.required]
}));
}
removeRole(index: number): void {
this.allowedRolesFormArray.removeAt(index);
}
updatePreviewData(): void {
this.loadPreviewData();
}
}
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