naptar-es-feladatkezelo/lib/dialogs.dart

1284 lines
41 KiB
Dart

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<PersonDialog> createState() => _PersonDialogState();
}
class _PersonDialogState extends State<PersonDialog> {
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<int>(
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<String> todos;
}
class EventAddDialog extends StatefulWidget {
const EventAddDialog({
super.key,
required this.people,
this.event,
this.todoSuggestions = const [],
});
final List<Person> people;
final CalendarEvent? event;
final List<String> todoSuggestions;
@override
State<EventAddDialog> createState() => _EventAddDialogState();
}
class _EventAddDialogState extends State<EventAddDialog> {
late final TextEditingController title;
late final TextEditingController start;
late final TextEditingController date;
late final TextEditingController note;
final todo = TextEditingController();
late bool hasTodos;
late List<String> 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 <String>[]];
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<String?>(
initialValue: personId,
decoration: const InputDecoration(
labelText: 'Kire vonatkozik?',
),
items: [
const DropdownMenuItem<String?>(
value: null,
child: Text('Mindenki'),
),
...widget.people.map(
(person) => DropdownMenuItem<String?>(
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<String>(
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<QuickAddDialog> createState() => _QuickAddDialogState();
}
class _QuickAddDialogState extends State<QuickAddDialog> {
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<StockAddDialog> createState() => _StockAddDialogState();
}
class _StockAddDialogState extends State<StockAddDialog> {
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<JarvisProduct>(
textEditingController: name,
focusNode: productFocus,
displayStringForOption: (product) => product.displayName,
optionsBuilder: (value) async {
final query = value.text.trim();
if (query.length < 2) {
return const Iterable<JarvisProduct>.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<String>(
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<String, String> get _headers => {
'X-Otthon-Token': widget.jarvisSettings.apiToken.trim(),
};
Future<List<JarvisProduct>> _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<String, dynamic>) return const [];
final rawProducts = payload['products'];
if (rawProducts is! List) return const [];
return rawProducts
.whereType<Map<String, dynamic>>()
.map(JarvisProduct.fromJson)
.where((product) => product.name.isNotEmpty)
.toList();
} catch (_) {
return const [];
}
}
Future<void> _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<String, dynamic>) {
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<void> _scanBarcode() async {
final result = await showDialog<String>(
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<BarcodeScanDialog> createState() => _BarcodeScanDialogState();
}
class _BarcodeScanDialogState extends State<BarcodeScanDialog> {
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<JarvisSettings> Function(JarvisSettings settings, String deviceName);
typedef JarvisStatusCallback =
Future<JarvisSettings> 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<JarvisSettingsDialog> createState() => _JarvisSettingsDialogState();
}
class _JarvisSettingsDialogState extends State<JarvisSettingsDialog> {
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<void> _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<void> _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);
}
}
}