import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; import '../../data/models/action_template.dart'; import '../../data/models/action_output.dart'; /// Dialog for picking an action with parameters /// Matches the native GeViSet app's action picker UI: /// - Left pane: Category dropdown + Action list /// - Right pane: Parameters + Caption + Delay + Description class ActionPickerDialog extends StatefulWidget { final Map> categories; final Map templates; final ActionOutput? existingAction; final List gcoreServers; final List gscServers; const ActionPickerDialog({ Key? key, required this.categories, required this.templates, this.existingAction, this.gcoreServers = const [], this.gscServers = const [], }) : super(key: key); @override State createState() => _ActionPickerDialogState(); } class _ActionPickerDialogState extends State { String? _selectedCategory; String? _selectedActionName; ActionTemplate? _selectedTemplate; String? _categoryPrefix; // 'gcore', 'gsc', or null for base final Map _paramControllers = {}; final Map _paramEnabled = {}; late TextEditingController _captionController; late TextEditingController _delayController; // Categories that should have G-Core and GSC variants // Based on native GeViSet app screenshots static const List _serverCategories = [ 'Camera Control', 'Video', 'Device', 'Digital Contacts', 'Backup', 'Remote Export', 'Cash Management', 'Viewer', 'Viewer Notification', 'Point of Sale', 'Ski Data', 'License Plate System', 'Logistics', // Supply chain security 'Lenel Access Control', 'Imex', 'System', ]; @override void initState() { super.initState(); _captionController = TextEditingController(); _delayController = TextEditingController(text: '0'); // Initialize from existing action if provided if (widget.existingAction != null) { _initializeFromExisting(); } else { // Default to first category if (widget.categories.isNotEmpty) { _selectedCategory = widget.categories.keys.first; } } } void _initializeFromExisting() { final existing = widget.existingAction!; _captionController.text = existing.action; // Find the template that matches this action final template = widget.templates[existing.action]; if (template != null) { _selectedCategory = template.category; _selectedActionName = template.actionName; _selectedTemplate = template; // Initialize parameter controllers with existing values for (final param in template.parameters) { final value = existing.parameters[param]?.toString() ?? ''; _paramControllers[param] = TextEditingController(text: value); _paramEnabled[param] = existing.parameters.containsKey(param); } } } @override void dispose() { _captionController.dispose(); _delayController.dispose(); for (final controller in _paramControllers.values) { controller.dispose(); } super.dispose(); } void _selectAction(String actionName) { setState(() { _selectedActionName = actionName; _selectedTemplate = widget.templates[actionName]; print('[DEBUG] _selectAction: action=$actionName, categoryPrefix=$_categoryPrefix'); print('[DEBUG] _selectAction: gscServers count=${widget.gscServers.length}'); print('[DEBUG] _selectAction: gcoreServers count=${widget.gcoreServers.length}'); // Clear previous parameter controllers for (final controller in _paramControllers.values) { controller.dispose(); } _paramControllers.clear(); _paramEnabled.clear(); // Initialize new parameter controllers if (_selectedTemplate != null) { print('[DEBUG] _selectAction: template parameters=${_selectedTemplate!.parameters}'); for (final param in _selectedTemplate!.parameters) { _paramControllers[param] = TextEditingController(); _paramEnabled[param] = false; // Start with parameters disabled } // Auto-enable and set server parameter based on category prefix if (_categoryPrefix == 'gcore') { // Add G-Core server parameter const serverParam = 'GCoreServer'; print('[DEBUG] Adding G-Core server parameter'); if (!_paramControllers.containsKey(serverParam)) { _paramControllers[serverParam] = TextEditingController(); print('[DEBUG] Created new controller for $serverParam'); } _paramEnabled[serverParam] = true; // Auto-select first enabled server if available final enabledServers = widget.gcoreServers.where((s) => s.enabled).toList(); if (enabledServers.isNotEmpty) { _paramControllers[serverParam]?.text = enabledServers.first.alias; print('[DEBUG] Set $serverParam to ${enabledServers.first.alias}'); } } else if (_categoryPrefix == 'gsc') { // Add GSC server parameter const serverParam = 'GscServer'; print('[DEBUG] Adding GSC server parameter'); if (!_paramControllers.containsKey(serverParam)) { _paramControllers[serverParam] = TextEditingController(); print('[DEBUG] Created new controller for $serverParam'); } _paramEnabled[serverParam] = true; // Auto-select first enabled server if available final enabledServers = widget.gscServers.where((s) => s.enabled).toList(); print('[DEBUG] GSC enabled servers: ${enabledServers.map((s) => s.alias).toList()}'); if (enabledServers.isNotEmpty) { _paramControllers[serverParam]?.text = enabledServers.first.alias; print('[DEBUG] Set $serverParam to ${enabledServers.first.alias}'); } else { print('[DEBUG] WARNING: No enabled GSC servers found!'); } } print('[DEBUG] Final _paramEnabled keys: ${_paramEnabled.keys.toList()}'); print('[DEBUG] Final _paramControllers keys: ${_paramControllers.keys.toList()}'); // Auto-fill caption if empty if (_captionController.text.isEmpty) { _captionController.text = actionName; } } }); } void _setDefaults() { setState(() { // Reset all parameters to disabled for (final key in _paramEnabled.keys) { _paramEnabled[key] = false; _paramControllers[key]?.text = ''; } _delayController.text = '0'; if (_selectedActionName != null) { _captionController.text = _selectedActionName!; } }); } void _onOk() { // Validate required caption if (_captionController.text.trim().isEmpty) { ScaffoldMessenger.of(context).showSnackBar( const SnackBar( content: Text('Caption is required'), backgroundColor: Colors.red, ), ); return; } if (_selectedActionName == null) { ScaffoldMessenger.of(context).showSnackBar( const SnackBar( content: Text('Please select an action'), backgroundColor: Colors.red, ), ); return; } // Validate server parameter for GSC/G-Core actions if (_categoryPrefix == 'gsc') { final gscServerValue = _paramControllers['GscServer']?.text.trim() ?? ''; if (gscServerValue.isEmpty) { ScaffoldMessenger.of(context).showSnackBar( const SnackBar( content: Text('GSC server is required. Please configure a GeViScope server first.'), backgroundColor: Colors.red, duration: Duration(seconds: 4), ), ); return; } } else if (_categoryPrefix == 'gcore') { final gcoreServerValue = _paramControllers['GCoreServer']?.text.trim() ?? ''; if (gcoreServerValue.isEmpty) { ScaffoldMessenger.of(context).showSnackBar( const SnackBar( content: Text('G-Core server is required. Please configure a G-Core server first.'), backgroundColor: Colors.red, duration: Duration(seconds: 4), ), ); return; } } print('[DEBUG] _onOk: Building parameters...'); print('[DEBUG] _onOk: _categoryPrefix=$_categoryPrefix'); print('[DEBUG] _onOk: _paramEnabled=${_paramEnabled}'); // Build parameters map from enabled parameters final parameters = {}; for (final entry in _paramEnabled.entries) { print('[DEBUG] _onOk: Checking param ${entry.key}, enabled=${entry.value}'); if (entry.value) { final value = _paramControllers[entry.key]?.text.trim() ?? ''; print('[DEBUG] _onOk: Param ${entry.key} value="$value"'); if (value.isNotEmpty) { parameters[entry.key] = value; print('[DEBUG] _onOk: Added param ${entry.key}=$value'); } } } // Add caption to parameters final caption = _captionController.text.trim(); if (caption.isNotEmpty) { parameters['Caption'] = caption; } // Add delay to parameters if non-zero final delay = _delayController.text.trim(); if (delay.isNotEmpty && delay != '0') { parameters['Delay'] = delay; } print('[DEBUG] _onOk: Final parameters=$parameters'); // Create ActionOutput with the actual action name (NOT the caption!) final result = ActionOutput( action: _selectedActionName!, parameters: parameters, ); print('[DEBUG] _onOk: Created ActionOutput: action=$_selectedActionName, parameters=$parameters'); Navigator.of(context).pop(result); } /// Generate enhanced categories including G-Core and GSC variants Map> _getEnhancedCategories() { final enhanced = >{}; // Add base categories enhanced.addAll(widget.categories); // Add G-Core variants for applicable categories // ALWAYS show these categories - even if no servers are configured // User can see the category but won't be able to select a server if none exist for (final category in _serverCategories) { if (widget.categories.containsKey(category)) { enhanced['G-Core: $category'] = widget.categories[category]!; } } // Add GSC variants for applicable categories // ALWAYS show these categories - even if no servers are configured // User can see the category but won't be able to select a server if none exist for (final category in _serverCategories) { if (widget.categories.containsKey(category)) { enhanced['GSC: $category'] = widget.categories[category]!; } } return enhanced; } /// Extract category prefix and base name from display category void _parseCategoryName(String displayCategory) { if (displayCategory.startsWith('G-Core: ')) { _categoryPrefix = 'gcore'; _selectedCategory = displayCategory.substring(8); // Remove "G-Core: " print('[DEBUG] Parsed category: prefix=gcore, base=$_selectedCategory'); } else if (displayCategory.startsWith('GSC: ')) { _categoryPrefix = 'gsc'; _selectedCategory = displayCategory.substring(5); // Remove "GSC: " print('[DEBUG] Parsed category: prefix=gsc, base=$_selectedCategory'); } else { _categoryPrefix = null; _selectedCategory = displayCategory; print('[DEBUG] Parsed category: prefix=null, base=$_selectedCategory'); } } @override Widget build(BuildContext context) { final enhancedCategories = _getEnhancedCategories(); return Dialog( child: Container( width: 800, height: 600, child: Column( children: [ // Title bar Container( padding: const EdgeInsets.all(16), decoration: BoxDecoration( color: Theme.of(context).primaryColor, borderRadius: const BorderRadius.only( topLeft: Radius.circular(4), topRight: Radius.circular(4), ), ), child: Row( children: [ const Icon(Icons.settings, color: Colors.white), const SizedBox(width: 8), const Text( 'Action settings...', style: TextStyle( color: Colors.white, fontSize: 16, fontWeight: FontWeight.bold, ), ), const Spacer(), IconButton( icon: const Icon(Icons.close, color: Colors.white), onPressed: () => Navigator.of(context).pop(), ), ], ), ), // Main content: Two-pane layout Expanded( child: Row( crossAxisAlignment: CrossAxisAlignment.start, children: [ // LEFT PANE: Category + Action List _buildLeftPane(), const VerticalDivider(width: 1), // RIGHT PANE: Parameters _buildRightPane(), ], ), ), // Bottom buttons Container( padding: const EdgeInsets.all(16), decoration: BoxDecoration( border: Border( top: BorderSide(color: Colors.grey[300]!), ), ), child: Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ ElevatedButton( onPressed: _setDefaults, child: const Text('Default'), ), Row( children: [ ElevatedButton( onPressed: _onOk, style: ElevatedButton.styleFrom( backgroundColor: Theme.of(context).primaryColor, foregroundColor: Colors.white, ), child: const Text('Ok'), ), const SizedBox(width: 8), OutlinedButton( onPressed: () => Navigator.of(context).pop(), child: const Text('Cancel'), ), ], ), ], ), ), ], ), ), ); } Widget _buildLeftPane() { final enhancedCategories = _getEnhancedCategories(); // Find current display category (with prefix) String? displayCategory; if (_selectedCategory != null && _categoryPrefix != null) { if (_categoryPrefix == 'gcore') { displayCategory = 'G-Core: $_selectedCategory'; } else if (_categoryPrefix == 'gsc') { displayCategory = 'GSC: $_selectedCategory'; } } else { displayCategory = _selectedCategory; } return Container( width: 280, padding: const EdgeInsets.all(16), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ // Category dropdown const Text( 'Category:', style: TextStyle(fontWeight: FontWeight.bold), ), const SizedBox(height: 8), DropdownButtonFormField( value: displayCategory, decoration: const InputDecoration( border: OutlineInputBorder(), contentPadding: EdgeInsets.symmetric(horizontal: 12, vertical: 8), ), items: enhancedCategories.keys.map((category) { return DropdownMenuItem( value: category, child: Text(category), ); }).toList(), onChanged: (value) { if (value != null) { setState(() { _parseCategoryName(value); _selectedActionName = null; _selectedTemplate = null; }); } }, ), const SizedBox(height: 16), // Action list const Text( 'Action:', style: TextStyle(fontWeight: FontWeight.bold), ), const SizedBox(height: 8), Expanded( child: Container( decoration: BoxDecoration( border: Border.all(color: Colors.grey[300]!), borderRadius: BorderRadius.circular(4), ), child: _selectedCategory == null ? const Center( child: Text('Select a category'), ) : ListView.builder( itemCount: widget.categories[_selectedCategory]?.length ?? 0, itemBuilder: (context, index) { final actionName = widget.categories[_selectedCategory]![index]; final isSelected = actionName == _selectedActionName; return ListTile( title: Text( actionName, style: TextStyle( fontSize: 13, color: isSelected ? Colors.white : Colors.black, ), ), selected: isSelected, selectedTileColor: Theme.of(context).primaryColor, onTap: () => _selectAction(actionName), dense: true, ); }, ), ), ), ], ), ); } Widget _buildRightPane() { return Expanded( child: Container( padding: const EdgeInsets.all(16), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ // Parameters section const Text( 'Parameters:', style: TextStyle(fontWeight: FontWeight.bold), ), const SizedBox(height: 8), // Dynamic parameter fields Expanded( child: _selectedTemplate == null ? const Center( child: Text('Select an action to view parameters'), ) : SingleChildScrollView( child: _buildParameterFields(), ), ), const Divider(), // Caption and Delay fields Row( children: [ Expanded( flex: 3, child: TextField( controller: _captionController, decoration: const InputDecoration( labelText: 'Caption (required)', border: OutlineInputBorder(), ), ), ), const SizedBox(width: 16), if (_selectedTemplate?.supportsDelay ?? false) ...[ const Text('Delay execution:'), const SizedBox(width: 8), SizedBox( width: 100, child: TextField( controller: _delayController, decoration: const InputDecoration( border: OutlineInputBorder(), suffixText: 'ms', ), keyboardType: TextInputType.number, inputFormatters: [ FilteringTextInputFormatter.digitsOnly, ], ), ), ], ], ), const SizedBox(height: 8), // Description box Container( width: double.infinity, padding: const EdgeInsets.all(12), decoration: BoxDecoration( color: Colors.grey[100], border: Border.all(color: Colors.grey[300]!), borderRadius: BorderRadius.circular(4), ), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ const Text( 'Description:', style: TextStyle( fontWeight: FontWeight.bold, fontSize: 12, ), ), const SizedBox(height: 4), Text( _selectedTemplate?.description ?? '', style: const TextStyle(fontSize: 12), ), ], ), ), ], ), ), ); } Widget _buildParameterFields() { if (_selectedTemplate == null) { return const Padding( padding: EdgeInsets.all(16.0), child: Text('Select an action to view parameters'), ); } // Collect all parameters including dynamically added ones final allParams = {}; allParams.addAll(_selectedTemplate!.parameters); allParams.addAll(_paramControllers.keys); if (allParams.isEmpty) { return const Padding( padding: EdgeInsets.all(16.0), child: Text('No parameters required'), ); } return Column( children: allParams.map((param) { return Padding( padding: const EdgeInsets.only(bottom: 12), child: Row( children: [ Checkbox( value: _paramEnabled[param] ?? false, onChanged: (value) { setState(() { _paramEnabled[param] = value ?? false; }); }, ), Expanded( child: _buildParameterInput(param), ), ], ), ); }).toList(), ); } /// Build appropriate input widget for parameter based on its type Widget _buildParameterInput(String param) { // Check if this is a server selection parameter if (param == 'GCoreServer' || param == 'G-Core alias' || param.toLowerCase().contains('gcore')) { return _buildServerDropdown(param, widget.gcoreServers, 'G-Core Server'); } else if (param == 'GscServer' || param == 'GeViScope alias' || param.toLowerCase().contains('geviscope')) { return _buildServerDropdown(param, widget.gscServers, 'GeViScope Server'); } // Default: text field return TextField( controller: _paramControllers[param], enabled: _paramEnabled[param] ?? false, decoration: InputDecoration( labelText: param, border: const OutlineInputBorder(), contentPadding: const EdgeInsets.symmetric( horizontal: 12, vertical: 8, ), ), ); } /// Build dropdown for server selection Widget _buildServerDropdown(String param, List servers, String label) { final enabled = _paramEnabled[param] ?? false; final currentValue = _paramControllers[param]?.text; return DropdownButtonFormField( value: servers.any((s) => s.alias == currentValue) ? currentValue : null, decoration: InputDecoration( labelText: label, border: const OutlineInputBorder(), contentPadding: const EdgeInsets.symmetric( horizontal: 12, vertical: 8, ), ), items: servers.map((server) { return DropdownMenuItem( value: server.alias, child: Text( '${server.alias} (ID: ${server.id})${server.enabled ? '' : ' [DISABLED]'}', style: TextStyle( color: server.enabled ? Colors.black : Colors.grey, ), ), ); }).toList(), onChanged: enabled ? (value) { setState(() { _paramControllers[param]?.text = value ?? ''; }); } : null, ); } }