Aktív projekt kiválasztása, projekt létrehozása
This commit is contained in:
parent
ad46429aa9
commit
644201dc8a
@ -65,8 +65,8 @@ class Project {
|
|||||||
'crs': crs.name,
|
'crs': crs.name,
|
||||||
'color': color,
|
'color': color,
|
||||||
'status': status.name,
|
'status': status.name,
|
||||||
'is_default': isDefault,
|
'is_default': isDefault ? 1 : 0,
|
||||||
'is_local_only': isLocalOnly,
|
'is_local_only': isLocalOnly ? 1 : 0,
|
||||||
if (lastSyncedAt != null)
|
if (lastSyncedAt != null)
|
||||||
'last_synced_at': lastSyncedAt!.toIso8601String(),
|
'last_synced_at': lastSyncedAt!.toIso8601String(),
|
||||||
'created_at': createdAt.toIso8601String(),
|
'created_at': createdAt.toIso8601String(),
|
||||||
|
|||||||
@ -158,7 +158,7 @@ class AppDatabase {
|
|||||||
await db.insert('projects', {
|
await db.insert('projects', {
|
||||||
'uuid': const Uuid().v4(),
|
'uuid': const Uuid().v4(),
|
||||||
'name': 'Alapértelmezett projekt',
|
'name': 'Alapértelmezett projekt',
|
||||||
'is_default': true,
|
'is_default': 1,
|
||||||
'is_local_only': 0,
|
'is_local_only': 0,
|
||||||
'status': 'active',
|
'status': 'active',
|
||||||
'created_at': now,
|
'created_at': now,
|
||||||
|
|||||||
@ -98,7 +98,9 @@ class ProjectService extends GetxService {
|
|||||||
final id = await AppDatabase.instance.insertProject(project);
|
final id = await AppDatabase.instance.insertProject(project);
|
||||||
|
|
||||||
// Supabase-be is
|
// Supabase-be is
|
||||||
await Supabase.instance.client.from('projects').insert(project.toMap());
|
await Supabase.instance.client
|
||||||
|
.from('TerepiSeged_Projects')
|
||||||
|
.insert(project.toMap());
|
||||||
|
|
||||||
await _loadProjects();
|
await _loadProjects();
|
||||||
return await AppDatabase.instance.getProject(id) ?? project;
|
return await AppDatabase.instance.getProject(id) ?? project;
|
||||||
@ -127,4 +129,27 @@ class ProjectService extends GetxService {
|
|||||||
await _loadProjects();
|
await _loadProjects();
|
||||||
return await AppDatabase.instance.getProject(id) ?? project;
|
return await AppDatabase.instance.getProject(id) ?? project;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Future<void> reloadProjects() => _loadProjects();
|
||||||
|
|
||||||
|
Future<Map<String, int>> getStats(int projectId) =>
|
||||||
|
AppDatabase.instance.getProjectStats(projectId);
|
||||||
|
|
||||||
|
Future<void> archiveProject(int id) async {
|
||||||
|
await AppDatabase.instance.archiveProject(id);
|
||||||
|
if (activeProject.value?.id == id) {
|
||||||
|
activeProject.value = projects.isNotEmpty
|
||||||
|
? projects.firstWhereOrNull((p) => p.id != id)
|
||||||
|
: null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> updateProject(Project project) async {
|
||||||
|
await AppDatabase.instance.updateProject(project);
|
||||||
|
await _loadProjects();
|
||||||
|
|
||||||
|
if (activeProject.value?.id == project.id) {
|
||||||
|
activeProject.value = project;
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,11 +1,13 @@
|
|||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:get/get.dart';
|
import 'package:get/get.dart';
|
||||||
import 'package:package_info_plus/package_info_plus.dart';
|
import 'package:package_info_plus/package_info_plus.dart';
|
||||||
|
import 'package:terepi_seged/services/project_service.dart';
|
||||||
|
|
||||||
import '../services/gnss/gnss_connection.dart';
|
import '../services/gnss/gnss_connection.dart';
|
||||||
import '../services/gnss/gnss_device_service.dart';
|
import '../services/gnss/gnss_device_service.dart';
|
||||||
import '../services/gnss/gnss_service.dart';
|
import '../services/gnss/gnss_service.dart';
|
||||||
import 'gnss_device_picker_dialog.dart';
|
import 'gnss_device_picker_dialog.dart';
|
||||||
|
import 'project/project_picker_view.dart';
|
||||||
|
|
||||||
class AppDrawer extends StatelessWidget {
|
class AppDrawer extends StatelessWidget {
|
||||||
const AppDrawer({super.key});
|
const AppDrawer({super.key});
|
||||||
@ -32,19 +34,25 @@ class AppDrawer extends StatelessWidget {
|
|||||||
padding: EdgeInsets.zero,
|
padding: EdgeInsets.zero,
|
||||||
children: [
|
children: [
|
||||||
// Projekt
|
// Projekt
|
||||||
ListTile(
|
Obx(() {
|
||||||
leading: const Icon(Icons.folder_outlined),
|
final project = ProjectService.to.activeProject.value;
|
||||||
|
return ListTile(
|
||||||
|
leading: Icon(Icons.folder_outlined,
|
||||||
|
color:
|
||||||
|
project != null ? _hexColor(project.color) : null),
|
||||||
title: const Text('Aktív projekt'),
|
title: const Text('Aktív projekt'),
|
||||||
subtitle: const Text(
|
subtitle: Text(
|
||||||
'Nincs projekt',
|
project?.name ?? 'Nincs projekt',
|
||||||
style: TextStyle(fontSize: 12),
|
style: TextStyle(fontSize: 12),
|
||||||
),
|
),
|
||||||
trailing: const Icon(Icons.arrow_forward_ios, size: 14),
|
trailing: const Icon(Icons.arrow_forward_ios, size: 14),
|
||||||
onTap: () {
|
onTap: () {
|
||||||
Get.back();
|
Get.back();
|
||||||
// Get.to(() => const ProjectPickerView());
|
Get.to(() => const ProjectPickerView(),
|
||||||
|
transition: Transition.rightToLeft);
|
||||||
},
|
},
|
||||||
),
|
);
|
||||||
|
}),
|
||||||
|
|
||||||
// GNSS eszköz
|
// GNSS eszköz
|
||||||
Obx(() {
|
Obx(() {
|
||||||
@ -283,3 +291,11 @@ class _SectionLabel extends StatelessWidget {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Color _hexColor(String hex) {
|
||||||
|
final h = hex.replaceFirst('#', '');
|
||||||
|
return Color(int.parse(
|
||||||
|
h.length == 6 ? 'FF$h' : h,
|
||||||
|
radix: 16,
|
||||||
|
));
|
||||||
|
}
|
||||||
|
|||||||
608
lib/widgets/project/project_picker_view.dart
Normal file
608
lib/widgets/project/project_picker_view.dart
Normal file
@ -0,0 +1,608 @@
|
|||||||
|
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),
|
||||||
|
);
|
||||||
|
}
|
||||||
Loading…
Reference in New Issue
Block a user