MobilApp/lib/widgets/project/project_picker_view.dart

609 lines
20 KiB
Dart

import 'package:flutter/material.dart';
import 'package:get/get.dart';
import 'package:intl/intl.dart';
import '../../models/project.dart';
import '../../services/project_service.dart';
// ─── Segéd: hex string → Flutter Color ───────────────────────────────────────
Color _hexColor(String hex) {
final h = hex.replaceFirst('#', '');
return Color(int.parse(h.length == 6 ? 'FF$h' : h, radix: 16));
}
// ─── ProjectPickerView ────────────────────────────────────────────────────────
class ProjectPickerView extends StatelessWidget {
const ProjectPickerView({super.key});
@override
Widget build(BuildContext context) {
final svc = ProjectService.to;
return Scaffold(
appBar: AppBar(
title: const Text('Projekt választó'),
actions: [
IconButton(
icon: const Icon(Icons.add),
tooltip: 'Új projekt',
onPressed: () => ProjectCreateDialog.show(),
),
],
),
body: Obx(() {
if (svc.projects.isEmpty) {
return _EmptyState(onCreateTap: ProjectCreateDialog.show);
}
return RefreshIndicator(
onRefresh: svc.reloadProjects,
child: ListView.separated(
padding: const EdgeInsets.symmetric(vertical: 8),
itemCount: svc.projects.length,
separatorBuilder: (_, __) => const Divider(height: 1),
itemBuilder: (_, i) => _ProjectTile(project: svc.projects[i]),
),
);
}),
);
}
}
// ─── Projekt sor ──────────────────────────────────────────────────────────────
class _ProjectTile extends StatefulWidget {
final Project project;
const _ProjectTile({required this.project});
@override
State<_ProjectTile> createState() => _ProjectTileState();
}
class _ProjectTileState extends State<_ProjectTile> {
Map<String, int> _stats = {'points': 0, 'tracks': 0, 'notes': 0};
@override
void initState() {
super.initState();
_loadStats();
}
Future<void> _loadStats() async {
if (widget.project.id == null) return;
final s = await ProjectService.to.getStats(widget.project.id!);
if (mounted) setState(() => _stats = s);
}
@override
Widget build(BuildContext context) {
final svc = ProjectService.to;
final project = widget.project;
final color = _hexColor(project.color);
return Obx(() {
final isActive = svc.activeProject.value?.id == project.id;
return ListTile(
selected: isActive,
selectedTileColor: color.withOpacity(0.08),
leading: Stack(children: [
CircleAvatar(
radius: 22,
backgroundColor: color.withOpacity(0.15),
child: Text(
project.name.substring(0, 1).toUpperCase(),
style: TextStyle(
color: color,
fontWeight: FontWeight.w700,
fontSize: 16,
),
),
),
if (isActive)
Positioned(
right: 0,
bottom: 0,
child: Container(
width: 12,
height: 12,
decoration: BoxDecoration(
color: Colors.green,
shape: BoxShape.circle,
border: Border.all(
color: Theme.of(context).colorScheme.surface,
width: 1.5,
),
),
),
),
]),
title: Row(children: [
Expanded(
child: Text(
project.name,
style: TextStyle(
fontWeight: FontWeight.w600,
color: isActive ? color : null,
),
),
),
_SyncBadge(isLocalOnly: project.isLocalOnly),
]),
subtitle: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
if (project.client.isNotEmpty) ...[
Text(project.client, style: const TextStyle(fontSize: 12)),
const SizedBox(height: 2),
],
Row(children: [
_StatBadge(
Icons.location_on_outlined, _stats['points'] ?? 0, 'pont'),
const SizedBox(width: 8),
_StatBadge(Icons.route, _stats['tracks'] ?? 0, 'track'),
const SizedBox(width: 8),
_StatBadge(Icons.sticky_note_2_outlined, _stats['notes'] ?? 0,
'jegyzet'),
]),
const SizedBox(height: 2),
Text(
'Létrehozva: ${DateFormat('yyyy.MM.dd').format(project.createdAt)}'
' · ${project.crs.name.toUpperCase()}',
style: const TextStyle(fontSize: 11, color: Colors.grey),
),
],
),
isThreeLine: true,
trailing: PopupMenuButton<_Action>(
icon: const Icon(Icons.more_vert, size: 20),
onSelected: (a) => _onAction(a),
itemBuilder: (_) => [
const PopupMenuItem(
value: _Action.edit,
child: ListTile(
leading: Icon(Icons.edit_outlined),
title: Text('Szerkesztés'),
dense: true,
),
),
const PopupMenuDivider(),
const PopupMenuItem(
value: _Action.archive,
child: ListTile(
leading: Icon(Icons.archive_outlined, color: Colors.orange),
title:
Text('Archiválás', style: TextStyle(color: Colors.orange)),
dense: true,
),
),
],
),
onTap: () async {
await svc.setActiveProject(project);
Get.back();
},
);
});
}
void _onAction(_Action action) {
switch (action) {
case _Action.edit:
ProjectCreateDialog.show(existing: widget.project);
case _Action.archive:
_confirmArchive();
}
}
void _confirmArchive() {
Get.dialog(AlertDialog(
title: const Text('Projekt archiválása'),
content: Text(
'"${widget.project.name}" archiválásra kerül.\n'
'Az adatok megmaradnak, de a projekt nem lesz aktívan elérhető.',
),
actions: [
TextButton(onPressed: Get.back, child: const Text('Mégse')),
FilledButton(
style: FilledButton.styleFrom(backgroundColor: Colors.orange),
onPressed: () {
Get.back();
ProjectService.to.archiveProject(widget.project.id!);
},
child: const Text('Archiválás'),
),
],
));
}
}
enum _Action { edit, archive }
// ─── Üres állapot ─────────────────────────────────────────────────────────────
class _EmptyState extends StatelessWidget {
final VoidCallback onCreateTap;
const _EmptyState({required this.onCreateTap});
@override
Widget build(BuildContext context) {
return Center(
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
Icon(Icons.folder_outlined, size: 64, color: Colors.grey.shade400),
const SizedBox(height: 16),
Text('Nincs még projekt',
style: Theme.of(context)
.textTheme
.titleMedium
?.copyWith(color: Colors.grey)),
const SizedBox(height: 8),
Text('Hozz létre egy projektet a kezdéshez.',
style: TextStyle(color: Colors.grey.shade500)),
const SizedBox(height: 24),
FilledButton.icon(
icon: const Icon(Icons.add),
label: const Text('Új projekt létrehozása'),
onPressed: onCreateTap,
),
],
),
);
}
}
// ─── Segéd widgetek ───────────────────────────────────────────────────────────
class _SyncBadge extends StatelessWidget {
final bool isLocalOnly;
const _SyncBadge({required this.isLocalOnly});
@override
Widget build(BuildContext context) {
final color = isLocalOnly ? Colors.grey : Colors.blue;
return Container(
padding: const EdgeInsets.symmetric(horizontal: 6, vertical: 2),
decoration: BoxDecoration(
color: color.withOpacity(0.1),
borderRadius: BorderRadius.circular(4),
border: Border.all(color: color.withOpacity(0.3)),
),
child: Row(mainAxisSize: MainAxisSize.min, children: [
Icon(
isLocalOnly ? Icons.phone_android : Icons.cloud_outlined,
size: 10,
color: color,
),
const SizedBox(width: 3),
Text(isLocalOnly ? 'Lokális' : 'Felhős',
style: TextStyle(fontSize: 10, color: color)),
]),
);
}
}
class _StatBadge extends StatelessWidget {
final IconData icon;
final int count;
final String label;
const _StatBadge(this.icon, this.count, this.label);
@override
Widget build(BuildContext context) => Row(
mainAxisSize: MainAxisSize.min,
children: [
Icon(icon, size: 11, color: Colors.grey),
const SizedBox(width: 2),
Text('$count $label',
style: const TextStyle(fontSize: 11, color: Colors.grey)),
],
);
}
// ─── ProjectCreateDialog ──────────────────────────────────────────────────────
class ProjectCreateDialog extends StatefulWidget {
final Project? existing;
const ProjectCreateDialog({super.key, this.existing});
static void show({Project? existing}) => Get.dialog(
ProjectCreateDialog(existing: existing),
barrierDismissible: false,
);
@override
State<ProjectCreateDialog> createState() => _ProjectCreateDialogState();
}
class _ProjectCreateDialogState extends State<ProjectCreateDialog> {
late final TextEditingController _nameCtrl;
late final TextEditingController _clientCtrl;
late final TextEditingController _descCtrl;
late ProjectCrs _crs;
late String _colorHex;
late bool _isLocalOnly;
bool _isSubmitting = false;
bool get _isEditing => widget.existing != null;
static const _colorOptions = [
'#185FA5',
'#2E7D32',
'#C62828',
'#E65100',
'#6A1B9A',
'#00695C',
'#283593',
'#4E342E',
];
@override
void initState() {
super.initState();
final p = widget.existing;
_nameCtrl = TextEditingController(text: p?.name ?? '');
_clientCtrl = TextEditingController(text: p?.client ?? '');
_descCtrl = TextEditingController(text: p?.description ?? '');
_crs = p?.crs ?? ProjectCrs.eov;
_colorHex = p?.color ?? _colorOptions.first;
_isLocalOnly = p?.isLocalOnly ?? true;
}
@override
void dispose() {
_nameCtrl.dispose();
_clientCtrl.dispose();
_descCtrl.dispose();
super.dispose();
}
@override
Widget build(BuildContext context) {
return Dialog(
insetPadding: const EdgeInsets.symmetric(horizontal: 20, vertical: 24),
child: ConstrainedBox(
constraints: const BoxConstraints(maxWidth: 480),
child: SingleChildScrollView(
padding: const EdgeInsets.all(24),
child: Column(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
// Fejléc
Row(children: [
Text(
_isEditing ? 'Projekt szerkesztése' : 'Új projekt',
style: Theme.of(context).textTheme.titleLarge,
),
const Spacer(),
IconButton(icon: const Icon(Icons.close), onPressed: Get.back),
]),
const SizedBox(height: 20),
// Név
TextField(
controller: _nameCtrl,
autofocus: true,
decoration: const InputDecoration(
labelText: 'Projekt neve *',
hintText: 'pl. Dunántúli 2D vonal',
border: OutlineInputBorder(),
prefixIcon: Icon(Icons.folder_outlined),
),
textCapitalization: TextCapitalization.sentences,
),
const SizedBox(height: 12),
// // Megrendelő
// TextField(
// controller: _clientCtrl,
// decoration: const InputDecoration(
// labelText: 'Megrendelő',
// hintText: 'pl. MOL Nyrt.',
// border: OutlineInputBorder(),
// prefixIcon: Icon(Icons.business_outlined),
// ),
// ),
// const SizedBox(height: 12),
// Leírás
TextField(
controller: _descCtrl,
maxLines: 2,
decoration: const InputDecoration(
labelText: 'Megjegyzés',
border: OutlineInputBorder(),
prefixIcon: Icon(Icons.notes),
),
),
const SizedBox(height: 16),
// Koordináta-rendszer
const _Label('Koordináta-rendszer'),
const SizedBox(height: 6),
SegmentedButton<ProjectCrs>(
segments: const [
ButtonSegment(
value: ProjectCrs.eov,
label: Text('EOV (HD72)'),
icon: Icon(Icons.straighten, size: 14),
),
ButtonSegment(
value: ProjectCrs.wgs84,
label: Text('WGS84'),
icon: Icon(Icons.public, size: 14),
),
],
selected: {_crs},
onSelectionChanged: (s) => setState(() => _crs = s.first),
),
// Tárolás — csak új projektnél
if (!_isEditing) ...[
const SizedBox(height: 16),
const _Label('Adattárolás'),
const SizedBox(height: 6),
SegmentedButton<bool>(
segments: const [
ButtonSegment(
value: true,
label: Text('Csak lokális'),
icon: Icon(Icons.phone_android, size: 14),
),
ButtonSegment(
value: false,
label: Text('Felhős mentés'),
icon: Icon(Icons.cloud_outlined, size: 14),
),
],
selected: {_isLocalOnly},
onSelectionChanged: (s) =>
setState(() => _isLocalOnly = s.first),
),
const SizedBox(height: 6),
Text(
_isLocalOnly
? '📱 Az adatok csak ezen a készüléken maradnak.'
: '☁ Az adatok szinkronizálódnak a szerverre.',
style: TextStyle(fontSize: 12, color: Colors.grey.shade600),
),
],
const SizedBox(height: 16),
// Szín
const _Label('Projekt szín'),
const SizedBox(height: 8),
Wrap(
spacing: 8,
runSpacing: 6,
children: _colorOptions.map((hex) {
final selected = _colorHex == hex;
return GestureDetector(
onTap: () => setState(() => _colorHex = hex),
child: AnimatedContainer(
duration: const Duration(milliseconds: 150),
width: 32,
height: 32,
decoration: BoxDecoration(
color: _hexColor(hex),
shape: BoxShape.circle,
border: selected
? Border.all(
color: Theme.of(context).colorScheme.onSurface,
width: 2.5)
: null,
boxShadow: selected
? [
BoxShadow(
color: _hexColor(hex).withOpacity(0.5),
blurRadius: 6,
spreadRadius: 1)
]
: null,
),
child: selected
? const Icon(Icons.check,
color: Colors.white, size: 16)
: null,
),
);
}).toList(),
),
const SizedBox(height: 24),
// Gombok
Row(
mainAxisAlignment: MainAxisAlignment.end,
children: [
TextButton(
onPressed: _isSubmitting ? null : Get.back,
child: const Text('Mégse'),
),
const SizedBox(width: 12),
FilledButton(
onPressed: _isSubmitting ? null : _submit,
child: _isSubmitting
? const SizedBox(
width: 18,
height: 18,
child: CircularProgressIndicator(strokeWidth: 2))
: Text(_isEditing ? 'Mentés' : 'Létrehozás'),
),
],
),
],
),
),
),
);
}
Future<void> _submit() async {
final name = _nameCtrl.text.trim();
if (name.isEmpty) {
Get.snackbar('Hiányzó adat', 'A projekt neve kötelező.',
snackPosition: SnackPosition.TOP);
return;
}
setState(() => _isSubmitting = true);
try {
final svc = ProjectService.to;
if (_isEditing) {
await svc.updateProject(widget.existing!.copyWith(
name: name,
client: _clientCtrl.text.trim(),
description: _descCtrl.text.trim(),
crs: _crs,
color: _colorHex,
));
Get.back();
Get.snackbar('Projekt frissítve', name,
snackPosition: SnackPosition.BOTTOM);
} else {
final Project created;
if (_isLocalOnly) {
created = await svc.createLocalProject(
name: name,
client: _clientCtrl.text.trim(),
crs: _crs,
color: _colorHex,
);
} else {
created = await svc.createOnlineProject(
name: name,
client: _clientCtrl.text.trim(),
crs: _crs,
color: _colorHex,
);
}
await svc.setActiveProject(created);
Get.snackbar(
'Projekt létrehozva',
'$name — aktív projektnek beállítva.',
snackPosition: SnackPosition.BOTTOM,
duration: const Duration(seconds: 3),
);
}
} catch (e) {
Get.snackbar('Hiba', e.toString(),
backgroundColor: Colors.red,
colorText: Colors.white,
snackPosition: SnackPosition.TOP);
} finally {
if (mounted) setState(() => _isSubmitting = false);
}
}
}
class _Label extends StatelessWidget {
final String text;
const _Label(this.text);
@override
Widget build(BuildContext context) => Text(
text,
style: const TextStyle(fontSize: 12, color: Colors.grey),
);
}