import 'dart:convert'; import 'package:cloud_firestore/cloud_firestore.dart'; import 'package:flutter/material.dart'; import 'package:flux/core/blocs/session/session_cubit.dart'; import 'package:get_it/get_it.dart'; import 'package:supabase_flutter/supabase_flutter.dart'; Future migrateCustomersToSupabase() async { // 1. IL TUO COMPANY ID REALE SU SUPABASE // Vai nel database Supabase, copia l'UUID della tua azienda e incollalo qui final String myRealCompanyId = '6c4b2323-2a60-4d33-bf21-c5a8eb6b4a5b'; try { print("Inizio download modello da Firebase..."); // 2. Scarichiamo TUTTI i clienti da Firebase final snapshot = await FirebaseFirestore.instance.collection('marca').get(); if (snapshot.docs.isEmpty) { print("Nessun marca trovato su Firebase!"); return; } // Questa lista conterrà i dati formattati pronti per Supabase List> supabaseBrands = []; // 3. Cicliamo i documenti di Firebase e li trasformiamo for (var doc in snapshot.docs) { final data = doc.data(); // Creiamo la riga per Supabase supabaseBrands.add({ 'legacy_id': doc.id, // L'ID vecchio di Firebase //'company_id': myRealCompanyId, // ECCO IL TUO COMPANY ID! // Mappa i campi (attento a far combaciare i nomi esatti delle colonne Supabase!) 'name': (data['nome'] as String).trim().toLowerCase(), 'company_id': myRealCompanyId, // Se avevi una data di creazione su Firebase, convertila, altrimenti ignorala // e Supabase userà il suo 'default now()' // 'created_at': (data['createdAt'] as Timestamp?)?.toDate().toIso8601String(), }); } print("Sto per inviare ${supabaseBrands.length} brand a Supabase..."); // 4. Invio a Supabase con UPSERT await Supabase.instance.client .from('brand') .upsert( supabaseBrands, onConflict: 'legacy_id', // Se il legacy_id c'è già, aggiorna invece di duplicare ); print("BOOM! Migrazione brand completata con successo! 🚀"); } catch (e) { print("Porca miseria, errore durante la migrazione: $e"); } } Future migrateModelsToSupabase() async { final String myRealCompanyId = '6c4b2323-2a60-4d33-bf21-c5a8eb6b4a5b'; try { print("Inizio migrazione Modelli..."); // ========================================================== // FASE 1: CREAZIONE DEL DIZIONARIO DI TRADUZIONE (LA MAGIA) // ========================================================== print("Scarico i Brand da Supabase per tradurre gli ID..."); // Chiediamo a Supabase solo 2 colonne: il nuovo UUID e il vecchio ID di Firebase final List brandResponse = await Supabase.instance.client .from('brand') .select('id, legacy_id'); // Creiamo la mappa: la chiave è il vecchio ID, il valore è il nuovo UUID Map brandTranslationMap = {}; for (var b in brandResponse) { if (b['legacy_id'] != null) { brandTranslationMap[b['legacy_id']] = b['id']; } } print("Dizionario pronto! Trovati ${brandTranslationMap.length} Brand."); // ========================================================== // FASE 2: SCARICAMENTO E TRADUZIONE DEI MODELLI // ========================================================== final snapshot = await FirebaseFirestore.instance .collection('modello') .get(); // Controlla il nome esatto della collection! if (snapshot.docs.isEmpty) { print("Nessun modello trovato su Firebase!"); return; } List> supabaseModels = []; for (var doc in snapshot.docs) { final data = doc.data(); // 1. Prendiamo il vecchio ID del brand salvato su Firebase String? oldFirebaseBrandId = data['idMarca']; // 2. TRADUZIONE ISTANTANEA! Cerchiamo il nuovo UUID nel nostro dizionario String? newSupabaseBrandUuid; if (oldFirebaseBrandId != null) { newSupabaseBrandUuid = brandTranslationMap[oldFirebaseBrandId]; } // 3. Controllo di sicurezza: se il brand non esiste su Supabase, saltiamo il record o mettiamo null? // Se nella tua tabella 'model' il 'brand_id' NON PUÒ essere null, devi per forza avere un match! if (newSupabaseBrandUuid == null && oldFirebaseBrandId != null) { print( "ATTENZIONE: Il modello ${data['nome']} ha un brand_id ($oldFirebaseBrandId) che non esiste su Supabase. Salto o metto null.", ); continue; // Decommenta questo se vuoi saltare i modelli orfani } // Creiamo la riga per Supabase supabaseModels.add({ 'legacy_id': doc.id, // ECCO LA CHIAVE ESTERNA TRADOTTA! 'brand_id': newSupabaseBrandUuid, // Mappa gli altri campi 'name': (data['nome'] as String).trim().toLowerCase(), 'name_with_brand': (data['nomeConMarca'] as String) .toLowerCase() .trim(), }); } // ========================================================== // FASE 3: INVIO A SUPABASE // ========================================================== print("Sto per inviare ${supabaseModels.length} modelli a Supabase..."); await Supabase.instance.client .from('model') .upsert(supabaseModels, onConflict: 'legacy_id'); print("BOOM! Migrazione modelli completata con successo! 🚀"); } catch (e) { print("Errore durante la migrazione dei modelli: $e"); } } class TicketMigrationScript { final SupabaseClient supabase = Supabase.instance.client; final String companyId = GetIt.I.get().state.company!.id!; final String storeId = GetIt.I.get().state.currentStore!.id!; /// Esegui questa funzione passandole la stringa JSON grezza (es. copiata da un file) /// e l'ID della tua Company su Supabase (visto che Firebase non lo aveva). Future runMigration(String jsonString) async { debugPrint('🚀 INIZIO MIGRAZIONE TICKET...'); try { // 1. Parsing del JSON final Map decoded = jsonDecode(jsonString); // Scendiamo al piano di sotto, direttamente nella "pancia" dei dati! final Map rawData = decoded['data']; debugPrint('Trovati ${rawData.length} elementi alla radice.'); if (rawData.isNotEmpty) { debugPrint( 'Il primo elemento contiene: ${rawData.entries.first.value}', ); } // 2. CREAZIONE DELLA CACHE (IL TRUCCO PER NON IMPAZZIRE CON LE JOIN) debugPrint('📥 Scarico le mappe dei legacy_id da Supabase...'); final customersRes = await supabase .from('customer') .select('id, legacy_id') .not('legacy_id', 'is', null); final modelsRes = await supabase .from('model') .select('id, legacy_id') .not('legacy_id', 'is', null); // Creiamo i dizionari: chiave = legacy_id (Firebase), valore = uuid (Supabase) final Map customerMap = { for (var row in customersRes) if (row['legacy_id'] != null) row['legacy_id'].toString(): row['id'].toString(), }; final Map modelMap = { for (var row in modelsRes) if (row['legacy_id'] != null) row['legacy_id'].toString(): row['id'].toString(), }; debugPrint( '✅ Mappe pronte: ${customerMap.length} clienti, ${modelMap.length} modelli.', ); // 3. MAPPATURA DEI DATI List> ticketsToInsert = []; for (var entry in rawData.entries) { final data = entry.value as Map; // Recuperiamo le relazioni usando i nostri dizionari final String? customerId = customerMap[data['idCliente']]; final String? modelId = modelMap[data['idModello']]; // Se non troviamo il cliente o il modello, magari loggiamo e saltiamo (o mettiamo null) // Per ora li mettiamo null, ma almeno non spacca il DB // Risoluzione Date DateTime? createdAt = _parseFirebaseDate(data['dataAperturaScheda']); //DateTime? closedAt = _parseFirebaseDate(data['dataChiusuraScheda']); //DateTime? returnedAt = _parseFirebaseDate(data['dataRiconsegnaCliente']); // Costruzione del Ticket ticketsToInsert.add({ 'legacy_id': data['fsId'], // Il vecchio ID del doc Firebase 'company_id': companyId, 'store_id': storeId, 'customer_id': customerId, 'target_model_id': modelId, 'target_sn': data['seriale'] ?? '', 'customer_price': data['costoTotaleCliente'] ?? 0.0, 'internal_cost': data['costoTotaleNostro'] ?? 0.0, 'created_at': createdAt?.toUtc().toIso8601String() ?? DateTime.now().toUtc().toIso8601String(), //'closed_at': closedAt?.toUtc().toIso8601String(), //'returned_at': returnedAt?.toUtc().toIso8601String(), 'request': (data['guasto']?.toString() ?? ''), 'included_accessories': data['accessoriConsegnati'], 'public_notes': data['note'], 'internal_notes': data['noteInterne'], 'resolution_notes': data['operazioneEffettuata'], // Il nuovo campo di cui parlavamo! 'alternative_phone_number': data['recapitoCliente'], 'has_courtesy_device': data['prestatoMuletto'] ?? false, // Mappatura Enums 'ticket_type': _mapTicketType(data), 'ticket_status': _mapTicketStatus(data), 'ticket_result': _mapTicketResult(data['risultato']), // 'warranty_type': _mapWarranty(data['nomeTipoGaranzia']), // De-commenta se hai la logica pronta }); } // 4. INSERIMENTO BATCH (A botte di 100 per non far arrabbiare Postgres) debugPrint( '🚀 Inizio inserimento di ${ticketsToInsert.length} ticket su Supabase...', ); const int batchSize = 100; for (int i = 0; i < ticketsToInsert.length; i += batchSize) { final end = (i + batchSize < ticketsToInsert.length) ? i + batchSize : ticketsToInsert.length; final batch = ticketsToInsert.sublist(i, end); await supabase.from('ticket').insert(batch); debugPrint('✅ Inseriti ticket da $i a $end'); } debugPrint('🎉 MIGRAZIONE COMPLETATA CON SUCCESSO!'); } catch (e, stacktrace) { debugPrint('❌ ERRORE DURANTE LA MIGRAZIONE: $e'); debugPrint(stacktrace.toString()); } } // --- FUNZIONI DI AIUTO (PARSER E MAPPER) --- /// Estrae la data dalla fastidiosa struttura {"__time__": "..."} di Firestore export DateTime? _parseFirebaseDate(dynamic dateData) { if (dateData == null) return null; if (dateData is Map && dateData.containsKey('__time__')) { return DateTime.tryParse(dateData['__time__'].toString()); } if (dateData is String) { return DateTime.tryParse(dateData); } return null; } /// Converte i boolean di Firebase nel tuo Enum TicketType String _mapTicketType(Map data) { if (data['tipoLavorazionePassaggioDati'] == true) return 'data_transfer'; if (data['tipoLavorazioneRiparazione'] == true) return 'repair'; if (data['tipoLavorazioneConfigurazione'] == true) return 'software_setup'; return 'other'; // Include tipoLavorazioneAltro o fallback } /// Converte la logica di stato di Firebase nel tuo Enum TicketStatus String _mapTicketStatus(Map data) { // Se è stato riconsegnato al cliente o ritirato, è chiuso/consegnato if (data['riconsegnato'] == true || data['nomeStatoScheda'] == 'Ritirato da cliente') { return 'closed'; // o 'closed', in base alla tua logica } // Altrimenti valutiamo le stringhe final String statoFirebase = data['nomeStatoScheda']?.toString().toLowerCase() ?? ''; if (statoFirebase.contains('accettazione')) return 'open'; if (statoFirebase.contains('da inviare centro esterno')) return 'waiting_for_shipping'; if (statoFirebase.contains('attesa ricambi')) return 'waiting_for_parts'; if (statoFirebase.contains('pronto')) return 'ready'; if (data['daLavorare'] == true) return 'in_progress'; return 'closed'; // Fallback } String? _mapTicketResult(dynamic risultato) { if (risultato == null || risultato.toString().isEmpty) return null; final r = risultato.toString().toUpperCase(); if (r == 'OK') return 'success'; if (r == 'KO' || r == 'NON RIPARATO') return 'failure'; return null; } }