2026-06-10 15:17:26 +02:00
|
|
|
import 'package:get/get.dart';
|
|
|
|
|
import 'package:shared_preferences/shared_preferences.dart';
|
|
|
|
|
import 'package:supabase_flutter/supabase_flutter.dart';
|
|
|
|
|
import 'package:uuid/uuid.dart';
|
|
|
|
|
import '../models/project.dart';
|
|
|
|
|
import 'app_database.dart';
|
2026-07-07 02:21:08 +02:00
|
|
|
import 'ts_sync_service.dart';
|
2026-06-10 15:17:26 +02:00
|
|
|
|
2026-08-07 21:25:31 +02:00
|
|
|
class ProjectRequiresLoginException implements Exception {
|
|
|
|
|
final String message;
|
|
|
|
|
ProjectRequiresLoginException(
|
|
|
|
|
[this.message = 'Felhős projekt használatához be kell jelentkezni.']);
|
|
|
|
|
|
|
|
|
|
@override
|
|
|
|
|
String toString() => message;
|
|
|
|
|
}
|
|
|
|
|
|
2026-06-10 15:17:26 +02:00
|
|
|
class ProjectService extends GetxService {
|
|
|
|
|
static ProjectService get to => Get.find();
|
|
|
|
|
|
|
|
|
|
final activeProject = Rxn<Project>();
|
|
|
|
|
final projects = <Project>[].obs;
|
|
|
|
|
|
|
|
|
|
int? get activeProjectId => activeProject.value?.id;
|
|
|
|
|
|
|
|
|
|
@override
|
|
|
|
|
Future<void> onInit() async {
|
|
|
|
|
super.onInit();
|
|
|
|
|
await _loadProjects();
|
|
|
|
|
await _restoreActiveProject();
|
2026-08-07 21:25:31 +02:00
|
|
|
|
|
|
|
|
Supabase.instance.client.auth.onAuthStateChange.listen((data) {
|
|
|
|
|
final loggedOut = data.session == null;
|
|
|
|
|
final active = activeProject.value;
|
|
|
|
|
|
|
|
|
|
if (loggedOut && active != null && !active.isLocalOnly) {
|
|
|
|
|
final fallback = projects.firstWhereOrNull((p) => p.isLocalOnly);
|
|
|
|
|
if (fallback != null) {
|
|
|
|
|
setActiveProject(fallback);
|
|
|
|
|
} else {
|
|
|
|
|
activeProject.value = null;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
});
|
2026-06-10 15:17:26 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
Future<void> _loadProjects() async {
|
|
|
|
|
projects.value = await AppDatabase.instance.listProjects();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Az utoljára aktív projektet tárolja a SharedPreferences
|
|
|
|
|
Future<void> _restoreActiveProject() async {
|
|
|
|
|
final prefs = await SharedPreferences.getInstance();
|
|
|
|
|
final savedId = prefs.getInt('active_project_id');
|
|
|
|
|
|
|
|
|
|
if (savedId != null) {
|
|
|
|
|
final project = await AppDatabase.instance.getProject(savedId);
|
|
|
|
|
if (project != null && project.status == ProjectStatus.active) {
|
|
|
|
|
activeProject.value = project;
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
// Fallback: az első aktív projekt
|
|
|
|
|
if (projects.isNotEmpty) {
|
|
|
|
|
await setActiveProject(projects.first);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
Future<void> setActiveProject(Project project) async {
|
2026-08-07 21:25:31 +02:00
|
|
|
if (!project.isLocalOnly &&
|
|
|
|
|
Supabase.instance.client.auth.currentUser == null) {
|
|
|
|
|
throw ProjectRequiresLoginException();
|
|
|
|
|
}
|
|
|
|
|
|
2026-06-10 15:17:26 +02:00
|
|
|
activeProject.value = project;
|
|
|
|
|
final prefs = await SharedPreferences.getInstance();
|
|
|
|
|
await prefs.setInt('active_project_id', project.id!);
|
|
|
|
|
|
|
|
|
|
// Frissítjük az updated_at-et hogy a lista tetejére kerüljön
|
|
|
|
|
await AppDatabase.instance.updateProject(project.copyWith());
|
|
|
|
|
await _loadProjects();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
Future<Project> createProject({
|
|
|
|
|
required String name,
|
|
|
|
|
String client = '',
|
|
|
|
|
String description = '',
|
|
|
|
|
ProjectCrs crs = ProjectCrs.eov,
|
|
|
|
|
String color = '#185FA5',
|
|
|
|
|
}) async {
|
|
|
|
|
final now = DateTime.now();
|
|
|
|
|
final project = Project(
|
|
|
|
|
uuid: const Uuid().v4(),
|
|
|
|
|
name: name,
|
|
|
|
|
client: client,
|
|
|
|
|
description: description,
|
|
|
|
|
crs: crs,
|
|
|
|
|
color: color,
|
|
|
|
|
createdAt: now,
|
|
|
|
|
updatedAt: now,
|
|
|
|
|
);
|
|
|
|
|
final id = await AppDatabase.instance.insertProject(project);
|
|
|
|
|
final saved = project.copyWith();
|
|
|
|
|
await _loadProjects();
|
|
|
|
|
return await AppDatabase.instance.getProject(id) ?? saved;
|
|
|
|
|
}
|
|
|
|
|
|
2026-07-07 02:21:08 +02:00
|
|
|
/// Közös (szinkronizált) projekt.
|
|
|
|
|
///
|
|
|
|
|
/// OFFLINE-FIRST: csak lokálisan jön létre (sync_status = 'pending'),
|
|
|
|
|
/// a feltöltést a TsSyncService végzi, amint van hálózat — így a
|
|
|
|
|
/// projekt-létrehozás net nélkül is működik, és nem ír többé
|
|
|
|
|
/// közvetlenül a régi Supabase-táblába.
|
|
|
|
|
|
2026-06-10 15:17:26 +02:00
|
|
|
Future<Project> createOnlineProject({
|
|
|
|
|
required String name,
|
|
|
|
|
String client = '',
|
|
|
|
|
ProjectCrs crs = ProjectCrs.eov,
|
|
|
|
|
String color = '#185FA5',
|
|
|
|
|
}) async {
|
|
|
|
|
final project = Project(
|
|
|
|
|
uuid: const Uuid().v4(),
|
|
|
|
|
name: name,
|
|
|
|
|
client: client,
|
|
|
|
|
crs: crs,
|
|
|
|
|
color: color,
|
|
|
|
|
isLocalOnly: false, // ← szinkronizált
|
|
|
|
|
createdAt: DateTime.now(),
|
|
|
|
|
updatedAt: DateTime.now(),
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
// Lokálisan mentjük
|
|
|
|
|
final id = await AppDatabase.instance.insertProject(project);
|
|
|
|
|
|
2026-07-07 02:21:08 +02:00
|
|
|
// // Supabase-be is
|
|
|
|
|
// await Supabase.instance.client
|
|
|
|
|
// .from('TerepiSeged_Projects')
|
|
|
|
|
// .insert(project.toMap());
|
2026-06-10 15:17:26 +02:00
|
|
|
|
|
|
|
|
await _loadProjects();
|
2026-07-07 02:21:08 +02:00
|
|
|
|
|
|
|
|
// Ha van net, azonnal fel is megy (és owner-tagság is létrejön).
|
|
|
|
|
if (Get.isRegistered<TsSyncService>()) {
|
|
|
|
|
TsSyncService.to.syncNow();
|
|
|
|
|
}
|
|
|
|
|
|
2026-06-10 15:17:26 +02:00
|
|
|
return await AppDatabase.instance.getProject(id) ?? project;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/// Lokális projekt — soha nem kerül Supabase-be
|
|
|
|
|
Future<Project> createLocalProject({
|
|
|
|
|
required String name,
|
|
|
|
|
String client = '',
|
|
|
|
|
ProjectCrs crs = ProjectCrs.eov,
|
|
|
|
|
String color = '#185FA5',
|
|
|
|
|
}) async {
|
|
|
|
|
final project = Project(
|
|
|
|
|
uuid: const Uuid().v4(),
|
|
|
|
|
name: name,
|
|
|
|
|
client: client,
|
|
|
|
|
crs: crs,
|
|
|
|
|
color: color,
|
|
|
|
|
isLocalOnly: true, // ← csak lokális
|
|
|
|
|
createdAt: DateTime.now(),
|
|
|
|
|
updatedAt: DateTime.now(),
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
// Csak lokálisan
|
|
|
|
|
final id = await AppDatabase.instance.insertProject(project);
|
|
|
|
|
await _loadProjects();
|
|
|
|
|
return await AppDatabase.instance.getProject(id) ?? project;
|
|
|
|
|
}
|
2026-06-12 10:59:38 +02:00
|
|
|
|
2026-07-07 02:21:08 +02:00
|
|
|
// ═════════════════════════════════════════════════════════════════
|
|
|
|
|
// Közös projektek — listázás és csatlakozás
|
|
|
|
|
// ═════════════════════════════════════════════════════════════════
|
|
|
|
|
|
|
|
|
|
/// A Supabase-en elérhető közös projektek listája
|
|
|
|
|
/// (név, tulajdonos, tagszám, is_member) — a ts_shared_projects view-ból.
|
|
|
|
|
Future<List<Map<String, dynamic>>> fetchSharedProjects() async {
|
|
|
|
|
final rows = await Supabase.instance.client
|
|
|
|
|
.from('terepi_seged_shared_projects')
|
|
|
|
|
.select()
|
|
|
|
|
.order('name', ascending: true);
|
|
|
|
|
return List<Map<String, dynamic>>.from(rows);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/// Csatlakozás egy közös projekthez (egyszerűsített modell):
|
|
|
|
|
/// 1. tagság-sor beszúrása (az RLS csak saját magunkat engedi felvenni),
|
|
|
|
|
/// 2. a projekt lokális sora létrejön a TÁVOLI uuid-dal — innentől a
|
|
|
|
|
/// szinkron ugyanúgy kezeli, mint a sajátokat,
|
|
|
|
|
/// 3. azonnali szinkron, hogy a projekt eddigi adatai lejöjjenek.
|
|
|
|
|
Future<Project> joinSharedProject(Map<String, dynamic> sharedRow) async {
|
|
|
|
|
final client = Supabase.instance.client;
|
2026-08-07 21:25:31 +02:00
|
|
|
final user = client.auth.currentUser;
|
|
|
|
|
if (user == null) {
|
|
|
|
|
throw ProjectRequiresLoginException(
|
|
|
|
|
'Közös projekthez csatlakozáshoz be kell jelentkezni.');
|
|
|
|
|
}
|
2026-07-07 02:21:08 +02:00
|
|
|
final projectUuid = sharedRow['id'] as String;
|
|
|
|
|
|
|
|
|
|
// 1. Tagság (idempotens: ha már tag, nem hiba).
|
|
|
|
|
await client.from('terepi_seged_project_members').upsert(
|
|
|
|
|
{
|
|
|
|
|
'project_id': projectUuid,
|
2026-08-07 21:25:31 +02:00
|
|
|
'user_id': user.id,
|
2026-07-07 02:21:08 +02:00
|
|
|
'role': 'editor',
|
|
|
|
|
},
|
|
|
|
|
ignoreDuplicates: true,
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
// 2. Lokális projekt-sor a távoli uuid-dal.
|
|
|
|
|
final localId =
|
|
|
|
|
await AppDatabase.instance.upsertProjectFromRemote(sharedRow);
|
|
|
|
|
await _loadProjects();
|
|
|
|
|
|
|
|
|
|
// 3. Adatok letöltése háttérben.
|
|
|
|
|
if (Get.isRegistered<TsSyncService>()) {
|
|
|
|
|
TsSyncService.to.syncNow();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return (await AppDatabase.instance.getProject(localId))!;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/// Kilépés egy közös projektből: a tagság törlődik a szerveren,
|
|
|
|
|
/// a lokális adat megmarad (archiválható külön).
|
|
|
|
|
Future<void> leaveSharedProject(Project project) async {
|
|
|
|
|
final client = Supabase.instance.client;
|
2026-08-07 21:25:31 +02:00
|
|
|
final user = client.auth.currentUser;
|
|
|
|
|
if (user == null) return; // nincs bejelentkezve — nincs mit tenni
|
2026-07-07 02:21:08 +02:00
|
|
|
await client
|
|
|
|
|
.from('terepi_seged_project_members')
|
|
|
|
|
.delete()
|
|
|
|
|
.eq('project_id', project.uuid)
|
2026-08-07 21:25:31 +02:00
|
|
|
.eq('user_id', user.id);
|
2026-07-07 02:21:08 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// ═════════════════════════════════════════════════════════════════
|
2026-06-12 10:59:38 +02:00
|
|
|
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;
|
|
|
|
|
}
|
|
|
|
|
}
|
2026-06-10 15:17:26 +02:00
|
|
|
}
|