ticket migration
This commit is contained in:
1
assets/schedeRiparazione-1778021345.json
Normal file
1
assets/schedeRiparazione-1778021345.json
Normal file
File diff suppressed because one or more lines are too long
@@ -1,4 +1,5 @@
|
|||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:flutter/services.dart';
|
||||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||||
import 'package:flux/core/blocs/session/session_cubit.dart';
|
import 'package:flux/core/blocs/session/session_cubit.dart';
|
||||||
import 'package:flux/core/theme/theme.dart';
|
import 'package:flux/core/theme/theme.dart';
|
||||||
@@ -87,7 +88,37 @@ class _CustomersContentState extends State<CustomersContent> {
|
|||||||
|
|
||||||
//TODO cancella quando import finito
|
//TODO cancella quando import finito
|
||||||
ElevatedButton(
|
ElevatedButton(
|
||||||
onPressed: () => migrateTicketsToSupabase(),
|
onPressed: () async {
|
||||||
|
try {
|
||||||
|
// 1. Mostra un loading (opzionale ma utile)
|
||||||
|
ScaffoldMessenger.of(context).showSnackBar(
|
||||||
|
const SnackBar(content: Text('Caricamento JSON in corso...')),
|
||||||
|
);
|
||||||
|
|
||||||
|
// 2. Legge tutto il file come stringa
|
||||||
|
final String jsonString = await rootBundle.loadString(
|
||||||
|
'assets/schedeRiparazione-1778021345.json',
|
||||||
|
);
|
||||||
|
|
||||||
|
// 3. Lancia lo script (sostituisci l'UUID con l'ID della tua azienda su Supabase)
|
||||||
|
await TicketMigrationScript().runMigration(jsonString);
|
||||||
|
|
||||||
|
// 4. Successo!
|
||||||
|
if (context.mounted) {
|
||||||
|
ScaffoldMessenger.of(context).showSnackBar(
|
||||||
|
const SnackBar(
|
||||||
|
content: Text('Migrazione Completata! Guarda i log.'),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
if (context.mounted) {
|
||||||
|
ScaffoldMessenger.of(
|
||||||
|
context,
|
||||||
|
).showSnackBar(SnackBar(content: Text('Errore: $e')));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
child: const Text('migra clienti'),
|
child: const Text('migra clienti'),
|
||||||
),
|
),
|
||||||
|
|
||||||
|
|||||||
@@ -1,4 +1,9 @@
|
|||||||
|
import 'dart:convert';
|
||||||
|
|
||||||
import 'package:cloud_firestore/cloud_firestore.dart';
|
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';
|
import 'package:supabase_flutter/supabase_flutter.dart';
|
||||||
|
|
||||||
Future<void> migrateCustomersToSupabase() async {
|
Future<void> migrateCustomersToSupabase() async {
|
||||||
@@ -147,125 +152,185 @@ Future<void> migrateModelsToSupabase() async {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<void> migrateTicketsToSupabase() async {
|
class TicketMigrationScript {
|
||||||
final String myRealCompanyId = '6c4b2323-2a60-4d33-bf21-c5a8eb6b4a5b';
|
final SupabaseClient supabase = Supabase.instance.client;
|
||||||
final String myRealStoreId = '782a4638-1c03-40af-9060-9ef214a3e238';
|
final String companyId = GetIt.I.get<SessionCubit>().state.company!.id!;
|
||||||
|
final String storeId = GetIt.I.get<SessionCubit>().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<void> runMigration(String jsonString) async {
|
||||||
|
debugPrint('🚀 INIZIO MIGRAZIONE TICKET...');
|
||||||
|
|
||||||
try {
|
try {
|
||||||
print("Inizio migrazione Ticket...");
|
// 1. Parsing del JSON
|
||||||
|
final Map<String, dynamic> decoded = jsonDecode(jsonString);
|
||||||
|
// Scendiamo al piano di sotto, direttamente nella "pancia" dei dati!
|
||||||
|
final Map<String, dynamic> rawData = decoded['data'];
|
||||||
|
|
||||||
// ==========================================================
|
debugPrint('Trovati ${rawData.length} elementi alla radice.');
|
||||||
// FASE 1: CREAZIONE DEL DIZIONARIO DI TRADUZIONE (LA MAGIA)
|
if (rawData.isNotEmpty) {
|
||||||
// ==========================================================
|
debugPrint(
|
||||||
print("Scarico i Brand da Supabase per tradurre gli ID...");
|
'Il primo elemento contiene: ${rawData.entries.first.value}',
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
// Chiediamo a Supabase solo 2 colonne: il nuovo UUID e il vecchio ID di Firebase
|
// 2. CREAZIONE DELLA CACHE (IL TRUCCO PER NON IMPAZZIRE CON LE JOIN)
|
||||||
final List<dynamic> customerResponse = await Supabase.instance.client
|
debugPrint('📥 Scarico le mappe dei legacy_id da Supabase...');
|
||||||
|
|
||||||
|
final customersRes = await supabase
|
||||||
.from('customer')
|
.from('customer')
|
||||||
.select('id, legacy_id');
|
.select('id, legacy_id')
|
||||||
|
.not('legacy_id', 'is', null);
|
||||||
|
|
||||||
// Creiamo la mappa: la chiave è il vecchio ID, il valore è il nuovo UUID
|
final modelsRes = await supabase
|
||||||
Map<String, String> customerTranslationMap = {};
|
|
||||||
for (var b in customerResponse) {
|
|
||||||
if (b['legacy_id'] != null) {
|
|
||||||
customerTranslationMap[b['legacy_id']] = b['id'];
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
final List<dynamic> modelResponse = await Supabase.instance.client
|
|
||||||
.from('model')
|
.from('model')
|
||||||
.select('id, legacy_id');
|
.select('id, legacy_id')
|
||||||
|
.not('legacy_id', 'is', null);
|
||||||
|
|
||||||
Map<String, String> modelTranslationMap = {};
|
// Creiamo i dizionari: chiave = legacy_id (Firebase), valore = uuid (Supabase)
|
||||||
for (var b in modelResponse) {
|
final Map<String, String> customerMap = {
|
||||||
if (b['legacy_id'] != null) {
|
for (var row in customersRes)
|
||||||
modelTranslationMap[b['legacy_id']] = b['id'];
|
if (row['legacy_id'] != null)
|
||||||
}
|
row['legacy_id'].toString(): row['id'].toString(),
|
||||||
}
|
};
|
||||||
|
|
||||||
print(
|
final Map<String, String> modelMap = {
|
||||||
"Dizionario pronto! Trovati ${customerTranslationMap.length} clienti e ${modelTranslationMap.length} modelli.",
|
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
|
||||||
// FASE 2: SCARICAMENTO E TRADUZIONE DEI MODELLI
|
List<Map<String, dynamic>> ticketsToInsert = [];
|
||||||
// ==========================================================
|
|
||||||
final snapshot = await FirebaseFirestore.instance
|
|
||||||
.collection('schedeRiparazione')
|
|
||||||
.get(); // Controlla il nome esatto della collection!
|
|
||||||
|
|
||||||
if (snapshot.docs.isEmpty) {
|
for (var entry in rawData.entries) {
|
||||||
print("Nessun scheda trovato su Firebase!");
|
final data = entry.value as Map<String, dynamic>;
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
List<Map<String, dynamic>> supabaseTickets = [];
|
// Recuperiamo le relazioni usando i nostri dizionari
|
||||||
|
final String? customerId = customerMap[data['idCliente']];
|
||||||
|
final String? modelId = modelMap[data['idModello']];
|
||||||
|
|
||||||
for (var doc in snapshot.docs) {
|
// Se non troviamo il cliente o il modello, magari loggiamo e saltiamo (o mettiamo null)
|
||||||
final data = doc.data();
|
// Per ora li mettiamo null, ma almeno non spacca il DB
|
||||||
|
|
||||||
// 1. Prendiamo il vecchio ID del brand salvato su Firebase
|
// Risoluzione Date
|
||||||
String? oldFirebaseCustomerId = data['idCliente'];
|
DateTime? createdAt = _parseFirebaseDate(data['dataAperturaScheda']);
|
||||||
String? oldFirebaseModelId = data['idModello'];
|
//DateTime? closedAt = _parseFirebaseDate(data['dataChiusuraScheda']);
|
||||||
|
//DateTime? returnedAt = _parseFirebaseDate(data['dataRiconsegnaCliente']);
|
||||||
|
|
||||||
// 2. TRADUZIONE ISTANTANEA! Cerchiamo il nuovo UUID nel nostro dizionario
|
// Costruzione del Ticket
|
||||||
String? newSupabaseCustomerUuid;
|
ticketsToInsert.add({
|
||||||
if (oldFirebaseCustomerId != null) {
|
'legacy_id': data['fsId'], // Il vecchio ID del doc Firebase
|
||||||
newSupabaseCustomerUuid = customerTranslationMap[oldFirebaseCustomerId];
|
'company_id': companyId,
|
||||||
}
|
'store_id': storeId,
|
||||||
|
'customer_id': customerId,
|
||||||
String? newSupabaseModelUuid;
|
'target_model_id': modelId,
|
||||||
if (oldFirebaseModelId != null) {
|
'target_sn': data['seriale'] ?? '',
|
||||||
newSupabaseModelUuid = modelTranslationMap[oldFirebaseModelId];
|
'customer_price': data['costoTotaleCliente'] ?? 0.0,
|
||||||
}
|
'internal_cost': data['costoTotaleNostro'] ?? 0.0,
|
||||||
|
|
||||||
// 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 (newSupabaseCustomerUuid == null &&
|
|
||||||
newSupabaseModelUuid == null &&
|
|
||||||
oldFirebaseCustomerId != null &&
|
|
||||||
oldFirebaseModelId != null) {
|
|
||||||
print(
|
|
||||||
"ATTENZIONE: La scheda di riparazione${data['numeroScheda']} ha un customer_id ($oldFirebaseCustomerId) o un model_id ($oldFirebaseModelId) che non esiste su Supabase. Salto o metto null.",
|
|
||||||
);
|
|
||||||
continue; // Decommenta questo se vuoi saltare i modelli orfani
|
|
||||||
}
|
|
||||||
|
|
||||||
// Creiamo la riga per Supabase
|
|
||||||
supabaseTickets.add({
|
|
||||||
'legacy_id': doc.id,
|
|
||||||
|
|
||||||
'customer_id': newSupabaseCustomerUuid,
|
|
||||||
|
|
||||||
// Mappa gli altri campi
|
|
||||||
'created_at':
|
'created_at':
|
||||||
(data['dataCreazione'] as Timestamp?)?.toDate().toIso8601String() ??
|
createdAt?.toUtc().toIso8601String() ??
|
||||||
DateTime.now().toIso8601String(),
|
DateTime.now().toUtc().toIso8601String(),
|
||||||
'company_id': myRealCompanyId,
|
|
||||||
'store_id': myRealStoreId,
|
//'closed_at': closedAt?.toUtc().toIso8601String(),
|
||||||
'ticket_type': 'repair',
|
//'returned_at': returnedAt?.toUtc().toIso8601String(),
|
||||||
'target_model_id': newSupabaseModelUuid,
|
'request': (data['guasto']?.toString() ?? ''),
|
||||||
'returned_at': (data['dataRiconsegnaCliente'] as Timestamp?)
|
'included_accessories': data['accessoriConsegnati'],
|
||||||
?.toDate()
|
|
||||||
.toIso8601String(),
|
|
||||||
'request': (data['guasto'] as String?)?.toLowerCase().trim(),
|
|
||||||
'public_notes': data['note'],
|
'public_notes': data['note'],
|
||||||
'internal_notes': data['noteInterne'],
|
'internal_notes': data['noteInterne'],
|
||||||
'reference_number': data['numeroScheda'],
|
'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)
|
||||||
// FASE 3: INVIO A SUPABASE
|
debugPrint(
|
||||||
// ==========================================================
|
'🚀 Inizio inserimento di ${ticketsToInsert.length} ticket su Supabase...',
|
||||||
print("Sto per inviare ${supabaseTickets.length} tickets a Supabase...");
|
);
|
||||||
|
|
||||||
await Supabase.instance.client
|
const int batchSize = 100;
|
||||||
.from('ticket')
|
for (int i = 0; i < ticketsToInsert.length; i += batchSize) {
|
||||||
.upsert(supabaseTickets, onConflict: 'legacy_id');
|
final end = (i + batchSize < ticketsToInsert.length)
|
||||||
|
? i + batchSize
|
||||||
|
: ticketsToInsert.length;
|
||||||
|
final batch = ticketsToInsert.sublist(i, end);
|
||||||
|
|
||||||
print("BOOM! Migrazione ticket completata con successo! 🚀");
|
await supabase.from('ticket').insert(batch);
|
||||||
} catch (e) {
|
debugPrint('✅ Inseriti ticket da $i a $end');
|
||||||
print("Errore durante la migrazione dei ticket: $e");
|
}
|
||||||
|
|
||||||
|
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<String, dynamic> 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<String, dynamic> 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;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -47,4 +47,5 @@ flutter:
|
|||||||
assets:
|
assets:
|
||||||
- assets/images/
|
- assets/images/
|
||||||
- assets/svg/
|
- assets/svg/
|
||||||
|
- assets/schedeRiparazione-1778021345.json
|
||||||
- .env
|
- .env
|
||||||
|
|||||||
Reference in New Issue
Block a user