part of 'main.dart'; class PersonDraft { PersonDraft({ required this.name, required this.color, required this.avatar, required this.nextEventLimit, }); final String name; final Color color; final String avatar; final int nextEventLimit; } class PersonDialog extends StatefulWidget { const PersonDialog({ super.key, required this.title, this.initialName = '', required this.initialColor, required this.initialAvatar, this.initialNextEventLimit = 1, }); final String title; final String initialName; final Color initialColor; final String initialAvatar; final int initialNextEventLimit; @override State createState() => _PersonDialogState(); } class _PersonDialogState extends State { late final TextEditingController name; late Color selectedColor; late String selectedAvatar; late int selectedNextEventLimit; @override void initState() { super.initState(); name = TextEditingController(text: widget.initialName); selectedColor = widget.initialColor; selectedAvatar = widget.initialAvatar; selectedNextEventLimit = widget.initialNextEventLimit; } @override void dispose() { name.dispose(); super.dispose(); } @override Widget build(BuildContext context) { return AlertDialog( title: Text(widget.title), content: SizedBox( width: 440, child: SingleChildScrollView( child: Column( mainAxisSize: MainAxisSize.min, crossAxisAlignment: CrossAxisAlignment.start, children: [ TextField( controller: name, autofocus: true, decoration: const InputDecoration(labelText: 'N\u00e9v'), ), const SizedBox(height: 18), const Text( 'Sz\u00edn', style: TextStyle(color: ink, fontWeight: FontWeight.w800), ), const SizedBox(height: 8), Wrap( spacing: 8, runSpacing: 8, children: personColors.map((color) { final selected = color.toARGB32() == selectedColor.toARGB32(); return InkWell( borderRadius: BorderRadius.circular(999), onTap: () => setState(() => selectedColor = color), child: CircleAvatar( backgroundColor: color, child: selected ? const Icon(Icons.check, color: Colors.white) : null, ), ); }).toList(), ), const SizedBox(height: 18), const Text( 'Avatar / ovis jel', style: TextStyle(color: ink, fontWeight: FontWeight.w800), ), const SizedBox(height: 8), Wrap( spacing: 8, runSpacing: 8, children: avatarPresets.map((preset) { final selected = preset.symbol == selectedAvatar; return ChoiceChip( selected: selected, label: Text('${preset.symbol} ${preset.label}'), onSelected: (_) => setState(() => selectedAvatar = preset.symbol), ); }).toList(), ), const SizedBox(height: 18), const Text( 'K\u00f6vetkez\u0151 esem\u00e9nyek sz\u00e1ma a csal\u00e1di widgeten', style: TextStyle(color: ink, fontWeight: FontWeight.w800), ), const SizedBox(height: 8), SegmentedButton( segments: const [ ButtonSegment(value: 1, label: Text('1')), ButtonSegment(value: 2, label: Text('2')), ButtonSegment(value: 3, label: Text('3')), ButtonSegment(value: 5, label: Text('5')), ], selected: {selectedNextEventLimit}, onSelectionChanged: (values) => setState(() => selectedNextEventLimit = values.first), ), ], ), ), ), actions: [ TextButton( onPressed: () => Navigator.pop(context), child: const Text('M\u00e9gse'), ), FilledButton( onPressed: () { final trimmed = name.text.trim(); if (trimmed.isEmpty) return; Navigator.pop( context, PersonDraft( name: trimmed, color: selectedColor, avatar: selectedAvatar, nextEventLimit: selectedNextEventLimit, ), ); }, child: const Text('Ment\u00e9s'), ), ], ); } } class EventInput { EventInput({ required this.title, required this.start, required this.date, required this.personId, required this.note, required this.hasTodos, required this.todos, }); final String title; final String start; final DateTime date; final String? personId; final String note; final bool hasTodos; final List todos; } class EventAddDialog extends StatefulWidget { const EventAddDialog({ super.key, required this.people, this.event, this.todoSuggestions = const [], }); final List people; final CalendarEvent? event; final List todoSuggestions; @override State createState() => _EventAddDialogState(); } class _EventAddDialogState extends State { late final TextEditingController title; late final TextEditingController start; late final TextEditingController date; late final TextEditingController note; final todo = TextEditingController(); late bool hasTodos; late List todos; String? personId; @override void initState() { super.initState(); final event = widget.event; title = TextEditingController(text: event?.title ?? ''); start = TextEditingController(text: event?.start ?? ''); date = TextEditingController( text: event == null ? formatDate(DateTime.now()) : formatDate(event.date), ); note = TextEditingController(text: event?.note ?? ''); hasTodos = event?.hasTodos ?? false; todos = [...event?.todos ?? const []]; personId = event?.personId == everyonePersonId ? null : event?.personId; } @override void dispose() { title.dispose(); start.dispose(); date.dispose(); note.dispose(); todo.dispose(); super.dispose(); } void _addTodo(String value) { final cleaned = value.trim(); if (cleaned.isEmpty || todos.contains(cleaned)) return; setState(() { todos.add(cleaned); todo.clear(); }); } @override Widget build(BuildContext context) { return AlertDialog( title: Text( widget.event == null ? '\u00daj esem\u00e9ny' : 'Esem\u00e9ny szerkeszt\u00e9se', ), content: SizedBox( width: 460, child: SingleChildScrollView( child: Column( mainAxisSize: MainAxisSize.min, children: [ TextField( controller: title, autofocus: true, decoration: const InputDecoration( labelText: 'Mi legyen az esem\u00e9ny?', ), ), const SizedBox(height: 12), Row( children: [ Expanded( child: TextField( controller: start, decoration: const InputDecoration( labelText: 'Id\u0151pont (pl. 14:30)', ), ), ), const SizedBox(width: 12), Expanded( child: TextField( controller: date, decoration: const InputDecoration( labelText: 'D\u00e1tum (pl. 2026.08.01)', ), ), ), ], ), const SizedBox(height: 12), DropdownButtonFormField( initialValue: personId, decoration: const InputDecoration( labelText: 'Kire vonatkozik?', ), items: [ const DropdownMenuItem( value: null, child: Text('Mindenki'), ), ...widget.people.map( (person) => DropdownMenuItem( value: person.id, child: Text('${person.avatar} ${person.name}'), ), ), ], onChanged: (value) => setState(() => personId = value), ), const SizedBox(height: 12), TextField( controller: note, minLines: 2, maxLines: 4, decoration: const InputDecoration(labelText: 'Megjegyz\u00e9s'), ), const SizedBox(height: 8), CheckboxListTile( contentPadding: EdgeInsets.zero, value: hasTodos, onChanged: (value) => setState(() => hasTodos = value ?? false), title: const Text('Teend\u0151 van'), controlAffinity: ListTileControlAffinity.leading, ), if (hasTodos) ...[ Row( children: [ Expanded( child: RawAutocomplete( textEditingController: todo, focusNode: FocusNode(), optionsBuilder: (value) { final query = value.text.trim().toLowerCase(); if (query.isEmpty) return widget.todoSuggestions; return widget.todoSuggestions.where( (item) => item.toLowerCase().contains(query), ); }, fieldViewBuilder: (context, controller, focusNode, onFieldSubmitted) { return TextField( controller: controller, focusNode: focusNode, decoration: const InputDecoration( labelText: 'Teend\u0151 hozz\u00e1ad\u00e1sa', ), onSubmitted: _addTodo, ); }, optionsViewBuilder: (context, onSelected, options) { return Material( elevation: 4, child: ListView( padding: EdgeInsets.zero, shrinkWrap: true, children: options .map( (option) => ListTile( title: Text(option), onTap: () => onSelected(option), ), ) .toList(), ), ); }, onSelected: _addTodo, ), ), const SizedBox(width: 8), IconButton.filledTonal( onPressed: () => _addTodo(todo.text), icon: const Icon(Icons.add), ), ], ), const SizedBox(height: 8), ...todos.map( (item) => ListTile( dense: true, contentPadding: EdgeInsets.zero, leading: const Icon( Icons.check_circle_outline, color: sage, ), title: Text(item), trailing: IconButton( icon: const Icon(Icons.close), onPressed: () => setState(() => todos.remove(item)), ), ), ), ], ], ), ), ), actions: [ TextButton( onPressed: () => Navigator.pop(context), child: const Text('M\u00e9gse'), ), FilledButton( onPressed: () { final eventDate = parseDate(date.text) ?? DateTime.now(); Navigator.pop( context, EventInput( title: title.text, start: start.text, date: eventDate, personId: personId, note: note.text, hasTodos: hasTodos, todos: hasTodos ? todos : const [], ), ); }, child: Text( widget.event == null ? 'Hozz\u00e1ad\u00e1s' : 'Ment\u00e9s', ), ), ], ); } } class EmptyState extends StatelessWidget { const EmptyState({ super.key, required this.icon, required this.title, required this.subtitle, }); final IconData icon; final String title; final String subtitle; @override Widget build(BuildContext context) { return Center( child: Column( mainAxisSize: MainAxisSize.min, children: [ Icon(icon, size: 48, color: sage), const SizedBox(height: 12), Text( title, style: const TextStyle( color: ink, fontSize: 20, fontWeight: FontWeight.w800, ), ), const SizedBox(height: 5), Text(subtitle, style: const TextStyle(color: Colors.black45)), ], ), ); } } class QuickAddDialog extends StatefulWidget { const QuickAddDialog({ super.key, required this.title, required this.firstLabel, required this.secondLabel, }); final String title; final String firstLabel; final String secondLabel; @override State createState() => _QuickAddDialogState(); } class _QuickAddDialogState extends State { final first = TextEditingController(); final second = TextEditingController(); @override void dispose() { first.dispose(); second.dispose(); super.dispose(); } @override Widget build(BuildContext context) { return AlertDialog( title: Text(widget.title), content: SizedBox( width: 380, child: Column( mainAxisSize: MainAxisSize.min, children: [ TextField( controller: first, autofocus: true, decoration: InputDecoration(labelText: widget.firstLabel), ), const SizedBox(height: 12), TextField( controller: second, decoration: InputDecoration(labelText: widget.secondLabel), ), ], ), ), actions: [ TextButton( onPressed: () => Navigator.pop(context), child: const Text('M\u00e9gse'), ), FilledButton( onPressed: () => Navigator.pop(context, [first.text, second.text]), child: const Text('Hozz\u00e1ad\u00e1s'), ), ], ); } } class StockInput { StockInput({ required this.name, required this.barcode, required this.amount, required this.unit, required this.location, required this.expiry, }); final String name; final String barcode; final int amount; final String unit; final String location; final String expiry; } class StockAddDialog extends StatefulWidget { const StockAddDialog({ super.key, required this.jarvisSettings, this.initialItem, }); final JarvisSettings jarvisSettings; final StockItem? initialItem; @override State createState() => _StockAddDialogState(); } class _StockAddDialogState extends State { final name = TextEditingController(); final barcode = TextEditingController(); final amount = TextEditingController(text: '1'); final unit = TextEditingController(text: 'db'); final expiry = TextEditingController(); final productFocus = FocusNode(); String location = 'Kamra'; bool loadingProducts = false; String? apiMessage; bool get editing => widget.initialItem != null; @override void initState() { super.initState(); final item = widget.initialItem; if (item == null) return; name.text = item.name; amount.text = item.amount.toString(); unit.text = item.unit; location = item.location; expiry.text = item.expiry == 'nincs megadva' ? '' : item.expiry; } @override void dispose() { name.dispose(); barcode.dispose(); amount.dispose(); unit.dispose(); expiry.dispose(); productFocus.dispose(); super.dispose(); } @override Widget build(BuildContext context) { return AlertDialog( title: Text(editing ? 'K\u00e9szlet szerkeszt\u00e9se' : 'K\u00e9szlet felv\u00e9tele'), content: SizedBox( width: 420, child: Column( mainAxisSize: MainAxisSize.min, children: [ Row( children: [ Expanded( child: TextField( controller: barcode, decoration: const InputDecoration( labelText: 'Vonalk\u00f3d', hintText: 'kamera vagy k\u00e9zi bevitel', ), onSubmitted: (_) => _lookupBarcode(), ), ), const SizedBox(width: 10), IconButton.filledTonal( tooltip: 'Vonalk\u00f3d beolvas\u00e1sa', onPressed: _scanBarcode, icon: const Icon(Icons.qr_code_scanner), ), IconButton.filledTonal( tooltip: 'Keres\u00e9s JARVIS API-ban', onPressed: loadingProducts ? null : _lookupBarcode, icon: loadingProducts ? const SizedBox( width: 18, height: 18, child: CircularProgressIndicator(strokeWidth: 2), ) : const Icon(Icons.manage_search), ), ], ), const SizedBox(height: 12), RawAutocomplete( textEditingController: name, focusNode: productFocus, displayStringForOption: (product) => product.displayName, optionsBuilder: (value) async { final query = value.text.trim(); if (query.length < 2) { return const Iterable.empty(); } return _searchProducts(query); }, onSelected: _applyProduct, fieldViewBuilder: (context, controller, focusNode, onFieldSubmitted) { return TextField( controller: controller, focusNode: focusNode, autofocus: true, decoration: const InputDecoration( labelText: '\u00c9lelmiszer neve', hintText: 'G\u00e9pelj, \u00e9s a JARVIS felk\u00edn\u00e1lja a mentett term\u00e9keket', ), ); }, optionsViewBuilder: (context, onSelected, options) => Align( alignment: Alignment.topLeft, child: Material( elevation: 8, borderRadius: BorderRadius.circular(16), child: ConstrainedBox( constraints: const BoxConstraints( maxHeight: 260, maxWidth: 420, ), child: ListView.builder( padding: EdgeInsets.zero, itemCount: options.length, itemBuilder: (context, index) { final product = options.elementAt(index); return ListTile( leading: const Icon(Icons.inventory_2_outlined), title: Text(product.name), subtitle: Text( [ if (product.brand != null) product.brand!, if (product.quantity != null) product.quantity!, product.barcode, ].join(' \u00b7 '), ), onTap: () => onSelected(product), ); }, ), ), ), ), ), if (apiMessage != null) ...[ const SizedBox(height: 8), Text( apiMessage!, style: TextStyle( color: apiMessage!.startsWith('Hiba') ? Colors.red.shade700 : sage, fontWeight: FontWeight.w700, ), ), ], const SizedBox(height: 12), Row( children: [ Expanded( child: TextField( controller: amount, keyboardType: TextInputType.number, decoration: const InputDecoration( labelText: 'Mennyis\u00e9g', ), ), ), const SizedBox(width: 12), Expanded( child: TextField( controller: unit, decoration: const InputDecoration(labelText: 'Egys\u00e9g'), ), ), ], ), const SizedBox(height: 12), DropdownButtonFormField( initialValue: location, decoration: const InputDecoration(labelText: 'Hely'), items: const [ DropdownMenuItem( value: 'H\u0171t\u0151', child: Text('H\u0171t\u0151szekr\u00e9ny'), ), DropdownMenuItem( value: 'Kamra', child: Text('Sz\u00e1raz\u00e1ru'), ), ], onChanged: (value) { if (value == null) return; setState(() => location = value); }, ), const SizedBox(height: 12), TextField( controller: expiry, decoration: const InputDecoration( labelText: 'Lej\u00e1rat', hintText: 'pl. 2026.08.31 vagy nincs megadva', ), ), ], ), ), actions: [ TextButton( onPressed: () => Navigator.pop(context), child: const Text('M\u00e9gse'), ), FilledButton( onPressed: () { final trimmedName = name.text.trim(); if (trimmedName.isEmpty) return; Navigator.pop( context, StockInput( name: trimmedName, barcode: barcode.text.trim(), amount: int.tryParse(amount.text.trim()) ?? 1, unit: unit.text.trim().isEmpty ? 'db' : unit.text.trim(), location: location, expiry: expiry.text.trim().isEmpty ? 'nincs megadva' : expiry.text.trim(), ), ); }, child: Text(editing ? 'Ment\u00e9s' : 'Hozz\u00e1ad\u00e1s'), ), ], ); } bool get _jarvisConfigured => widget.jarvisSettings.apiUrl.trim().isNotEmpty && widget.jarvisSettings.apiToken.trim().isNotEmpty; Map get _headers => { 'X-Otthon-Token': widget.jarvisSettings.apiToken.trim(), }; Future> _searchProducts(String query) async { if (!_jarvisConfigured) return const []; try { final baseUri = Uri.parse( normalizeJarvisUrl(widget.jarvisSettings.apiUrl), ); final uri = baseUri.resolve( '/api/products?query=${Uri.encodeQueryComponent(query)}&take=20', ); final response = await http .get(uri, headers: _headers) .timeout(const Duration(seconds: 8)); if (response.statusCode != 200) return const []; final payload = jsonDecode(response.body); if (payload is! Map) return const []; final rawProducts = payload['products']; if (rawProducts is! List) return const []; return rawProducts .whereType>() .map(JarvisProduct.fromJson) .where((product) => product.name.isNotEmpty) .toList(); } catch (_) { return const []; } } Future _lookupBarcode() async { final normalized = barcode.text.replaceAll(RegExp(r'\D'), ''); if (normalized.isEmpty) { setState(() => apiMessage = 'Adj meg vagy olvass be vonalk\u00f3dot.'); return; } if (!_jarvisConfigured) { setState(() => apiMessage = 'A JARVIS API nincs be\u00e1ll\u00edtva.'); return; } setState(() { loadingProducts = true; apiMessage = 'JARVIS keres\u00e9s: $normalized'; }); try { final baseUri = Uri.parse( normalizeJarvisUrl(widget.jarvisSettings.apiUrl), ); final response = await http .get(baseUri.resolve('/api/products/$normalized'), headers: _headers) .timeout(const Duration(seconds: 12)); if (response.statusCode == 404) { setState( () => apiMessage = 'Nem tal\u00e1ltam ilyen term\u00e9ket ($normalized). K\u00e9zzel felviheted.', ); return; } if (response.statusCode != 200) { setState(() => apiMessage = 'Hiba: API ${response.statusCode}'); return; } final payload = jsonDecode(response.body); if (payload is! Map) { setState(() => apiMessage = 'Hiba: hib\u00e1s API v\u00e1lasz.'); return; } final product = JarvisProduct.fromJson(payload); if (product.name.trim().isEmpty) { setState( () => apiMessage = 'A JARVIS v\u00e1laszban nem volt term\u00e9kn\u00e9v.', ); return; } _applyProduct(product); setState( () => apiMessage = product.fromCache ? 'Tal\u00e1lat cache-b\u0151l: ${product.name}' : 'Tal\u00e1lat Open Food Facts alapj\u00e1n: ${product.name}', ); } catch (error) { setState(() => apiMessage = 'Hiba: $error'); } finally { if (mounted) setState(() => loadingProducts = false); } } void _applyProduct(JarvisProduct product) { setState(() { barcode.text = product.barcode; name.text = product.name; if (product.quantity != null && product.quantity!.trim().isNotEmpty) { unit.text = product.quantity!; } }); } Future _scanBarcode() async { final result = await showDialog( context: context, builder: (_) => const BarcodeScanDialog(), ); if (result == null || result.trim().isEmpty) return; setState(() => apiMessage = 'Vonalk\u00f3d beolvasva: ${result.trim()}'); barcode.text = result.trim(); await _lookupBarcode(); } } class BarcodeScanDialog extends StatefulWidget { const BarcodeScanDialog({super.key}); @override State createState() => _BarcodeScanDialogState(); } class _BarcodeScanDialogState extends State { final controller = MobileScannerController( facing: CameraFacing.front, formats: const [ BarcodeFormat.ean13, BarcodeFormat.ean8, BarcodeFormat.upcA, BarcodeFormat.upcE, BarcodeFormat.code128, ], ); bool handled = false; @override void dispose() { controller.dispose(); super.dispose(); } @override Widget build(BuildContext context) { return AlertDialog( title: const Text('Vonalk\u00f3d beolvas\u00e1sa'), content: SizedBox( width: 520, height: 360, child: ClipRRect( borderRadius: BorderRadius.circular(18), child: Stack( fit: StackFit.expand, children: [ MobileScanner( controller: controller, onDetect: (capture) { if (handled) return; for (final code in capture.barcodes) { final value = code.rawValue; if (value == null || value.trim().isEmpty) continue; handled = true; Navigator.pop(context, value); break; } }, ), const Align( alignment: Alignment.bottomCenter, child: Padding( padding: EdgeInsets.all(12), child: DecoratedBox( decoration: BoxDecoration( color: Colors.black54, borderRadius: BorderRadius.all(Radius.circular(16)), ), child: Padding( padding: EdgeInsets.symmetric( horizontal: 12, vertical: 8, ), child: Text( 'Tartsd a vonalk\u00f3dot az el\u0151lapi kamera el\u00e9.', style: TextStyle(color: Colors.white), ), ), ), ), ), ], ), ), ), actions: [ TextButton( onPressed: () => Navigator.pop(context), child: const Text('M\u00e9gse'), ), ], ); } } typedef JarvisRegisterCallback = Future Function(JarvisSettings settings, String deviceName); typedef JarvisStatusCallback = Future Function(JarvisSettings settings); class JarvisSettingsDialog extends StatefulWidget { const JarvisSettingsDialog({ super.key, required this.settings, required this.onRegister, required this.onRefreshStatus, }); final JarvisSettings settings; final JarvisRegisterCallback onRegister; final JarvisStatusCallback onRefreshStatus; @override State createState() => _JarvisSettingsDialogState(); } class _JarvisSettingsDialogState extends State { late final TextEditingController apiUrl; late final TextEditingController apiToken; late final TextEditingController workspaceToken; late final TextEditingController deviceName; late JarvisSettings currentSettings; bool registering = false; bool refreshing = false; String? message; @override void initState() { super.initState(); currentSettings = widget.settings; apiUrl = TextEditingController(text: currentSettings.apiUrl); apiToken = TextEditingController(text: currentSettings.apiToken); workspaceToken = TextEditingController( text: currentSettings.workspaceToken ?? '', ); deviceName = TextEditingController( text: currentSettings.deviceName ?? 'Family tablet', ); } @override void dispose() { apiUrl.dispose(); apiToken.dispose(); workspaceToken.dispose(); deviceName.dispose(); super.dispose(); } JarvisSettings get draft => currentSettings.copyWith( apiUrl: normalizeJarvisUrl(apiUrl.text), apiToken: apiToken.text.trim(), workspaceToken: workspaceToken.text.trim().isEmpty ? null : workspaceToken.text.trim(), ); @override Widget build(BuildContext context) { final status = currentSettings.registrationStatus; final registered = currentSettings.deviceId != null || (currentSettings.deviceToken?.isNotEmpty ?? false); final approved = status == 'approved'; final pending = status == 'pending'; final deviceToken = currentSettings.deviceToken; final shortDeviceToken = deviceToken == null || deviceToken.length <= 10 ? deviceToken : '${deviceToken.substring(0, 6)}...${deviceToken.substring(deviceToken.length - 4)}'; return AlertDialog( title: const Text('JARVIS Backend be\u00e1ll\u00edt\u00e1sok'), content: SizedBox( width: 500, child: SingleChildScrollView( child: Column( mainAxisSize: MainAxisSize.min, crossAxisAlignment: CrossAxisAlignment.start, children: [ TextField( controller: apiUrl, decoration: const InputDecoration( labelText: 'API URL', hintText: defaultJarvisApiUrl, ), ), const SizedBox(height: 12), TextField( controller: apiToken, obscureText: true, decoration: const InputDecoration( labelText: 'Szerver token', hintText: 'JARVIS API token', ), ), const SizedBox(height: 12), TextField( controller: workspaceToken, obscureText: true, decoration: const InputDecoration( labelText: 'K\u00f6z\u00f6s munkat\u00e9r token', hintText: 'Ugyanez ker\u00fclj\u00f6n minden csal\u00e1di eszk\u00f6zre', helperText: 'Ezzel l\u00e1tja t\u00f6bb app ugyanazt a csal\u00e1dot, k\u00e9szletet \u00e9s bev\u00e1s\u00e1rl\u00f3list\u00e1t.', ), ), const SizedBox(height: 12), TextField( controller: deviceName, decoration: const InputDecoration( labelText: 'Eszk\u00f6z neve', hintText: 'pl. Konyhai tablet', ), ), const SizedBox(height: 16), Container( width: double.infinity, padding: const EdgeInsets.all(12), decoration: BoxDecoration( color: approved ? mint : pending ? const Color(0xFFFFF4DD) : const Color(0xFFF1F3EF), borderRadius: BorderRadius.circular(16), ), child: Row( crossAxisAlignment: CrossAxisAlignment.start, children: [ Icon( approved ? Icons.verified_outlined : pending ? Icons.hourglass_top : Icons.info_outline, color: approved ? sage : pending ? orange : Colors.black45, ), const SizedBox(width: 10), Expanded( child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Text( approved ? 'Az eszk\u00f6z j\u00f3v\u00e1hagyva \u00e9s haszn\u00e1lhat\u00f3.' : pending ? 'A regisztr\u00e1ci\u00f3 bek\u00fcldve, admin j\u00f3v\u00e1hagy\u00e1sra v\u00e1r.' : registered ? 'Az eszk\u00f6z regisztr\u00e1lva van, de jelenleg nem akt\u00edv.' : 'M\u00e9g nincs bek\u00fcld\u00f6tt regisztr\u00e1ci\u00f3.', style: const TextStyle( color: ink, fontWeight: FontWeight.w800, ), ), if (registered) ...[ const SizedBox(height: 6), Text( 'Eszk\u00f6z: ${currentSettings.deviceName ?? deviceName.text}', style: const TextStyle( color: Colors.black54, fontWeight: FontWeight.w700, ), ), if (currentSettings.deviceId != null) Text( 'Azonos\u00edt\u00f3: #${currentSettings.deviceId}', style: const TextStyle(color: Colors.black54), ), if (shortDeviceToken != null) Text( 'Eszk\u00f6z token: $shortDeviceToken', style: const TextStyle(color: Colors.black54), ), ], ], ), ), ], ), ), if (message != null) ...[ const SizedBox(height: 10), Text( message!, style: TextStyle( color: message!.startsWith('Hiba') ? Colors.red.shade700 : sage, fontWeight: FontWeight.w700, ), ), ], ], ), ), ), actions: [ TextButton( onPressed: registering ? null : () => Navigator.pop(context), child: const Text('M\u00e9gse'), ), if (registered) TextButton.icon( onPressed: registering || refreshing ? null : _clearRegistration, icon: const Icon(Icons.link_off), label: const Text('Regisztr\u00e1ci\u00f3 t\u00f6rl\u00e9se'), ), OutlinedButton.icon( onPressed: registering || refreshing ? null : _refreshStatus, icon: refreshing ? const SizedBox( width: 16, height: 16, child: CircularProgressIndicator(strokeWidth: 2), ) : const Icon(Icons.refresh), label: Text(refreshing ? 'Friss\u00edt\u00e9s...' : 'St\u00e1tusz'), ), OutlinedButton.icon( onPressed: registering || refreshing ? null : _register, icon: registering ? const SizedBox( width: 16, height: 16, child: CircularProgressIndicator(strokeWidth: 2), ) : const Icon(Icons.app_registration), label: Text( registering ? 'K\u00fcld\u00e9s...' : 'Regisztr\u00e1ci\u00f3', ), ), FilledButton( onPressed: registering ? null : () => Navigator.pop(context, draft), child: const Text('Ment\u00e9s'), ), ], ); } void _clearRegistration() { setState(() { currentSettings = draft.copyWith(clearDevice: true); deviceName.text = 'Family tablet'; message = 'A tablet helyi regisztr\u00e1ci\u00f3ja t\u00f6r\u00f6lve. A szerveren nincs m\u00f3dos\u00edt\u00e1s.'; }); } Future _register() async { setState(() { registering = true; message = null; }); try { final registered = await widget.onRegister(draft, deviceName.text); setState(() { currentSettings = registered; apiUrl.text = registered.apiUrl; apiToken.text = registered.apiToken; message = 'Regisztr\u00e1ci\u00f3 bek\u00fcldve. A JARVIS admin fel\u00fcleten kell j\u00f3v\u00e1hagyni.'; }); } catch (error) { setState(() => message = 'Hiba: $error'); } finally { if (mounted) setState(() => registering = false); } } Future _refreshStatus() async { setState(() { refreshing = true; message = null; }); try { final refreshed = await widget.onRefreshStatus(draft); setState(() { currentSettings = refreshed; message = refreshed.registrationStatus == 'approved' ? 'Az eszk\u00f6z j\u00f3v\u00e1 van hagyva.' : 'Az eszk\u00f6z m\u00e9g j\u00f3v\u00e1hagy\u00e1sra v\u00e1r.'; }); } catch (error) { setState(() => message = 'Hiba: $error'); } finally { if (mounted) setState(() => refreshing = false); } } }