Compare commits
3 Commits
fbf18acf05
...
migration
| Author | SHA1 | Date | |
|---|---|---|---|
| 71efc18c05 | |||
| 5214ea9745 | |||
| 1115d2cb87 |
@@ -1,5 +1,8 @@
|
|||||||
plugins {
|
plugins {
|
||||||
id("com.android.application")
|
id("com.android.application")
|
||||||
|
// START: FlutterFire Configuration
|
||||||
|
id("com.google.gms.google-services")
|
||||||
|
// END: FlutterFire Configuration
|
||||||
id("kotlin-android")
|
id("kotlin-android")
|
||||||
// The Flutter Gradle Plugin must be applied after the Android and Kotlin Gradle plugins.
|
// The Flutter Gradle Plugin must be applied after the Android and Kotlin Gradle plugins.
|
||||||
id("dev.flutter.flutter-gradle-plugin")
|
id("dev.flutter.flutter-gradle-plugin")
|
||||||
|
|||||||
86
android/app/google-services.json
Normal file
86
android/app/google-services.json
Normal file
@@ -0,0 +1,86 @@
|
|||||||
|
{
|
||||||
|
"project_info": {
|
||||||
|
"project_number": "872447580790",
|
||||||
|
"project_id": "assistenza-catelli",
|
||||||
|
"storage_bucket": "assistenza-catelli.firebasestorage.app"
|
||||||
|
},
|
||||||
|
"client": [
|
||||||
|
{
|
||||||
|
"client_info": {
|
||||||
|
"mobilesdk_app_id": "1:872447580790:android:193235afcc2920ce5d9d57",
|
||||||
|
"android_client_info": {
|
||||||
|
"package_name": "com.catelli.scans2"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"oauth_client": [],
|
||||||
|
"api_key": [
|
||||||
|
{
|
||||||
|
"current_key": "AIzaSyBSxpdLDlPnN0xjejlX_5JL19BDeSzKOr8"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"services": {
|
||||||
|
"appinvite_service": {
|
||||||
|
"other_platform_oauth_client": []
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"client_info": {
|
||||||
|
"mobilesdk_app_id": "1:872447580790:android:9c6172d77b1d2cae5d9d57",
|
||||||
|
"android_client_info": {
|
||||||
|
"package_name": "com.catellisrl.assistenza"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"oauth_client": [],
|
||||||
|
"api_key": [
|
||||||
|
{
|
||||||
|
"current_key": "AIzaSyBSxpdLDlPnN0xjejlX_5JL19BDeSzKOr8"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"services": {
|
||||||
|
"appinvite_service": {
|
||||||
|
"other_platform_oauth_client": []
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"client_info": {
|
||||||
|
"mobilesdk_app_id": "1:872447580790:android:425d21710d7682005d9d57",
|
||||||
|
"android_client_info": {
|
||||||
|
"package_name": "com.catellisrl.catelli_energy_comparator"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"oauth_client": [],
|
||||||
|
"api_key": [
|
||||||
|
{
|
||||||
|
"current_key": "AIzaSyBSxpdLDlPnN0xjejlX_5JL19BDeSzKOr8"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"services": {
|
||||||
|
"appinvite_service": {
|
||||||
|
"other_platform_oauth_client": []
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"client_info": {
|
||||||
|
"mobilesdk_app_id": "1:872447580790:android:a1d8d57960451f935d9d57",
|
||||||
|
"android_client_info": {
|
||||||
|
"package_name": "com.catellisrl.flux"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"oauth_client": [],
|
||||||
|
"api_key": [
|
||||||
|
{
|
||||||
|
"current_key": "AIzaSyBSxpdLDlPnN0xjejlX_5JL19BDeSzKOr8"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"services": {
|
||||||
|
"appinvite_service": {
|
||||||
|
"other_platform_oauth_client": []
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"configuration_version": "1"
|
||||||
|
}
|
||||||
@@ -2,4 +2,4 @@ distributionBase=GRADLE_USER_HOME
|
|||||||
distributionPath=wrapper/dists
|
distributionPath=wrapper/dists
|
||||||
zipStoreBase=GRADLE_USER_HOME
|
zipStoreBase=GRADLE_USER_HOME
|
||||||
zipStorePath=wrapper/dists
|
zipStorePath=wrapper/dists
|
||||||
distributionUrl=https\://operations.gradle.org/distributions/gradle-8.14-all.zip
|
distributionUrl=https\://services.gradle.org/distributions/gradle-8.14-all.zip
|
||||||
|
|||||||
@@ -20,6 +20,9 @@ pluginManagement {
|
|||||||
plugins {
|
plugins {
|
||||||
id("dev.flutter.flutter-plugin-loader") version "1.0.0"
|
id("dev.flutter.flutter-plugin-loader") version "1.0.0"
|
||||||
id("com.android.application") version "8.11.1" apply false
|
id("com.android.application") version "8.11.1" apply false
|
||||||
|
// START: FlutterFire Configuration
|
||||||
|
id("com.google.gms.google-services") version("4.3.15") apply false
|
||||||
|
// END: FlutterFire Configuration
|
||||||
id("org.jetbrains.kotlin.android") version "2.2.20" apply false
|
id("org.jetbrains.kotlin.android") version "2.2.20" apply false
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
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
firebase.json
Normal file
1
firebase.json
Normal file
@@ -0,0 +1 @@
|
|||||||
|
{"flutter":{"platforms":{"android":{"default":{"projectId":"assistenza-catelli","appId":"1:872447580790:android:a1d8d57960451f935d9d57","fileOutput":"android/app/google-services.json"}},"dart":{"lib/firebase_options.dart":{"projectId":"assistenza-catelli","configurations":{"android":"1:872447580790:android:a1d8d57960451f935d9d57","ios":"1:872447580790:ios:a87d56c718aa61e05d9d57","macos":"1:872447580790:ios:a87d56c718aa61e05d9d57","web":"1:872447580790:web:10745e7f9afb447d5d9d57","windows":"1:872447580790:web:3b1623eda6abdac75d9d57"}}}}}}
|
||||||
@@ -1,10 +1,12 @@
|
|||||||
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';
|
||||||
import 'package:flux/features/customers/blocs/customers_cubit.dart';
|
import 'package:flux/features/customers/blocs/customers_cubit.dart';
|
||||||
import 'package:flux/features/customers/models/customer_model.dart';
|
import 'package:flux/features/customers/models/customer_model.dart';
|
||||||
import 'package:flux/features/customers/ui/customer_form.dart';
|
import 'package:flux/features/customers/ui/customer_form.dart';
|
||||||
|
import 'package:flux/temp/migration_tools.dart';
|
||||||
import 'package:go_router/go_router.dart';
|
import 'package:go_router/go_router.dart';
|
||||||
|
|
||||||
class CustomersContent extends StatefulWidget {
|
class CustomersContent extends StatefulWidget {
|
||||||
@@ -84,6 +86,42 @@ class _CustomersContentState extends State<CustomersContent> {
|
|||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
|
||||||
|
//TODO cancella quando import finito
|
||||||
|
ElevatedButton(
|
||||||
|
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'),
|
||||||
|
),
|
||||||
|
|
||||||
// LISTA CLIENTI
|
// LISTA CLIENTI
|
||||||
Expanded(
|
Expanded(
|
||||||
child: BlocBuilder<CustomersCubit, CustomersState>(
|
child: BlocBuilder<CustomersCubit, CustomersState>(
|
||||||
|
|||||||
88
lib/firebase_options.dart
Normal file
88
lib/firebase_options.dart
Normal file
@@ -0,0 +1,88 @@
|
|||||||
|
// File generated by FlutterFire CLI.
|
||||||
|
// ignore_for_file: type=lint
|
||||||
|
import 'package:firebase_core/firebase_core.dart' show FirebaseOptions;
|
||||||
|
import 'package:flutter/foundation.dart'
|
||||||
|
show defaultTargetPlatform, kIsWeb, TargetPlatform;
|
||||||
|
|
||||||
|
/// Default [FirebaseOptions] for use with your Firebase apps.
|
||||||
|
///
|
||||||
|
/// Example:
|
||||||
|
/// ```dart
|
||||||
|
/// import 'firebase_options.dart';
|
||||||
|
/// // ...
|
||||||
|
/// await Firebase.initializeApp(
|
||||||
|
/// options: DefaultFirebaseOptions.currentPlatform,
|
||||||
|
/// );
|
||||||
|
/// ```
|
||||||
|
class DefaultFirebaseOptions {
|
||||||
|
static FirebaseOptions get currentPlatform {
|
||||||
|
if (kIsWeb) {
|
||||||
|
return web;
|
||||||
|
}
|
||||||
|
switch (defaultTargetPlatform) {
|
||||||
|
case TargetPlatform.android:
|
||||||
|
return android;
|
||||||
|
case TargetPlatform.iOS:
|
||||||
|
return ios;
|
||||||
|
case TargetPlatform.macOS:
|
||||||
|
return macos;
|
||||||
|
case TargetPlatform.windows:
|
||||||
|
return windows;
|
||||||
|
case TargetPlatform.linux:
|
||||||
|
throw UnsupportedError(
|
||||||
|
'DefaultFirebaseOptions have not been configured for linux - '
|
||||||
|
'you can reconfigure this by running the FlutterFire CLI again.',
|
||||||
|
);
|
||||||
|
default:
|
||||||
|
throw UnsupportedError(
|
||||||
|
'DefaultFirebaseOptions are not supported for this platform.',
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
static const FirebaseOptions web = FirebaseOptions(
|
||||||
|
apiKey: 'AIzaSyA8vQbyEt81DoAuRVDc_3W_VIKY-9F-XTw',
|
||||||
|
appId: '1:872447580790:web:10745e7f9afb447d5d9d57',
|
||||||
|
messagingSenderId: '872447580790',
|
||||||
|
projectId: 'assistenza-catelli',
|
||||||
|
authDomain: 'assistenza-catelli.firebaseapp.com',
|
||||||
|
storageBucket: 'assistenza-catelli.firebasestorage.app',
|
||||||
|
measurementId: 'G-HTSSNQJ15P',
|
||||||
|
);
|
||||||
|
|
||||||
|
static const FirebaseOptions android = FirebaseOptions(
|
||||||
|
apiKey: 'AIzaSyBSxpdLDlPnN0xjejlX_5JL19BDeSzKOr8',
|
||||||
|
appId: '1:872447580790:android:a1d8d57960451f935d9d57',
|
||||||
|
messagingSenderId: '872447580790',
|
||||||
|
projectId: 'assistenza-catelli',
|
||||||
|
storageBucket: 'assistenza-catelli.firebasestorage.app',
|
||||||
|
);
|
||||||
|
|
||||||
|
static const FirebaseOptions ios = FirebaseOptions(
|
||||||
|
apiKey: 'AIzaSyCkjOTW6BlckKIxQdp5TPnHuRfXFoVC3bY',
|
||||||
|
appId: '1:872447580790:ios:a87d56c718aa61e05d9d57',
|
||||||
|
messagingSenderId: '872447580790',
|
||||||
|
projectId: 'assistenza-catelli',
|
||||||
|
storageBucket: 'assistenza-catelli.firebasestorage.app',
|
||||||
|
iosBundleId: 'com.catellisrl.flux',
|
||||||
|
);
|
||||||
|
|
||||||
|
static const FirebaseOptions macos = FirebaseOptions(
|
||||||
|
apiKey: 'AIzaSyCkjOTW6BlckKIxQdp5TPnHuRfXFoVC3bY',
|
||||||
|
appId: '1:872447580790:ios:a87d56c718aa61e05d9d57',
|
||||||
|
messagingSenderId: '872447580790',
|
||||||
|
projectId: 'assistenza-catelli',
|
||||||
|
storageBucket: 'assistenza-catelli.firebasestorage.app',
|
||||||
|
iosBundleId: 'com.catellisrl.flux',
|
||||||
|
);
|
||||||
|
|
||||||
|
static const FirebaseOptions windows = FirebaseOptions(
|
||||||
|
apiKey: 'AIzaSyA5uJhb8ksqKqdEWbMD5ra6JYXIGoaIdIM',
|
||||||
|
appId: '1:872447580790:web:3b1623eda6abdac75d9d57',
|
||||||
|
messagingSenderId: '872447580790',
|
||||||
|
projectId: 'assistenza-catelli',
|
||||||
|
authDomain: 'assistenza-catelli.firebaseapp.com',
|
||||||
|
storageBucket: 'assistenza-catelli.firebasestorage.app',
|
||||||
|
measurementId: 'G-J8LZTQ9NHB',
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -1,5 +1,6 @@
|
|||||||
import 'dart:io';
|
import 'dart:io';
|
||||||
|
|
||||||
|
import 'package:firebase_core/firebase_core.dart';
|
||||||
import 'package:flutter/foundation.dart';
|
import 'package:flutter/foundation.dart';
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||||
@@ -7,6 +8,7 @@ import 'package:flutter_dotenv/flutter_dotenv.dart';
|
|||||||
import 'package:flux/features/attachments/data/attachments_repository.dart';
|
import 'package:flux/features/attachments/data/attachments_repository.dart';
|
||||||
import 'package:flux/features/auth/bloc/auth_cubit.dart';
|
import 'package:flux/features/auth/bloc/auth_cubit.dart';
|
||||||
import 'package:flux/features/operations/data/operations_repository.dart';
|
import 'package:flux/features/operations/data/operations_repository.dart';
|
||||||
|
import 'package:flux/firebase_options.dart';
|
||||||
import 'package:flux/l10n/app_localizations.dart';
|
import 'package:flux/l10n/app_localizations.dart';
|
||||||
import 'package:get_it/get_it.dart';
|
import 'package:get_it/get_it.dart';
|
||||||
import 'package:go_router/go_router.dart';
|
import 'package:go_router/go_router.dart';
|
||||||
@@ -102,6 +104,9 @@ Future<void> setupLocator() async {
|
|||||||
getIt.registerSingleton<SessionCubit>(
|
getIt.registerSingleton<SessionCubit>(
|
||||||
SessionCubit(getIt<CoreRepository>(), getIt<SharedPreferences>()),
|
SessionCubit(getIt<CoreRepository>(), getIt<SharedPreferences>()),
|
||||||
);
|
);
|
||||||
|
//TODO rimuovere dopo gli import
|
||||||
|
WidgetsFlutterBinding.ensureInitialized();
|
||||||
|
await Firebase.initializeApp(options: DefaultFirebaseOptions.currentPlatform);
|
||||||
}
|
}
|
||||||
|
|
||||||
class FluxApp extends StatefulWidget {
|
class FluxApp extends StatefulWidget {
|
||||||
|
|||||||
336
lib/temp/migration_tools.dart
Normal file
336
lib/temp/migration_tools.dart
Normal file
@@ -0,0 +1,336 @@
|
|||||||
|
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<void> 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<Map<String, dynamic>> 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<void> 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<dynamic> 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<String, String> 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<Map<String, dynamic>> 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<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 {
|
||||||
|
// 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.');
|
||||||
|
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<String, String> customerMap = {
|
||||||
|
for (var row in customersRes)
|
||||||
|
if (row['legacy_id'] != null)
|
||||||
|
row['legacy_id'].toString(): row['id'].toString(),
|
||||||
|
};
|
||||||
|
|
||||||
|
final Map<String, String> 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<Map<String, dynamic>> ticketsToInsert = [];
|
||||||
|
|
||||||
|
for (var entry in rawData.entries) {
|
||||||
|
final data = entry.value as Map<String, dynamic>;
|
||||||
|
|
||||||
|
// 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<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;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -6,16 +6,22 @@ import FlutterMacOS
|
|||||||
import Foundation
|
import Foundation
|
||||||
|
|
||||||
import app_links
|
import app_links
|
||||||
|
import cloud_firestore
|
||||||
import file_picker
|
import file_picker
|
||||||
import file_selector_macos
|
import file_selector_macos
|
||||||
|
import firebase_auth
|
||||||
|
import firebase_core
|
||||||
import pdfx
|
import pdfx
|
||||||
import shared_preferences_foundation
|
import shared_preferences_foundation
|
||||||
import url_launcher_macos
|
import url_launcher_macos
|
||||||
|
|
||||||
func RegisterGeneratedPlugins(registry: FlutterPluginRegistry) {
|
func RegisterGeneratedPlugins(registry: FlutterPluginRegistry) {
|
||||||
AppLinksMacosPlugin.register(with: registry.registrar(forPlugin: "AppLinksMacosPlugin"))
|
AppLinksMacosPlugin.register(with: registry.registrar(forPlugin: "AppLinksMacosPlugin"))
|
||||||
|
FLTFirebaseFirestorePlugin.register(with: registry.registrar(forPlugin: "FLTFirebaseFirestorePlugin"))
|
||||||
FilePickerPlugin.register(with: registry.registrar(forPlugin: "FilePickerPlugin"))
|
FilePickerPlugin.register(with: registry.registrar(forPlugin: "FilePickerPlugin"))
|
||||||
FileSelectorPlugin.register(with: registry.registrar(forPlugin: "FileSelectorPlugin"))
|
FileSelectorPlugin.register(with: registry.registrar(forPlugin: "FileSelectorPlugin"))
|
||||||
|
FLTFirebaseAuthPlugin.register(with: registry.registrar(forPlugin: "FLTFirebaseAuthPlugin"))
|
||||||
|
FLTFirebaseCorePlugin.register(with: registry.registrar(forPlugin: "FLTFirebaseCorePlugin"))
|
||||||
PdfxPlugin.register(with: registry.registrar(forPlugin: "PdfxPlugin"))
|
PdfxPlugin.register(with: registry.registrar(forPlugin: "PdfxPlugin"))
|
||||||
SharedPreferencesPlugin.register(with: registry.registrar(forPlugin: "SharedPreferencesPlugin"))
|
SharedPreferencesPlugin.register(with: registry.registrar(forPlugin: "SharedPreferencesPlugin"))
|
||||||
UrlLauncherPlugin.register(with: registry.registrar(forPlugin: "UrlLauncherPlugin"))
|
UrlLauncherPlugin.register(with: registry.registrar(forPlugin: "UrlLauncherPlugin"))
|
||||||
|
|||||||
1427
macos/Podfile.lock
1427
macos/Podfile.lock
File diff suppressed because it is too large
Load Diff
80
pubspec.lock
80
pubspec.lock
@@ -1,6 +1,14 @@
|
|||||||
# Generated by pub
|
# Generated by pub
|
||||||
# See https://dart.dev/tools/pub/glossary#lockfile
|
# See https://dart.dev/tools/pub/glossary#lockfile
|
||||||
packages:
|
packages:
|
||||||
|
_flutterfire_internals:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: _flutterfire_internals
|
||||||
|
sha256: bda3b7b55958bfd867addc40d067b4b11f7b8846d57671f5b5a6e7f9a56fe3ad
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "1.3.69"
|
||||||
app_links:
|
app_links:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
@@ -105,6 +113,30 @@ packages:
|
|||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "1.1.2"
|
version: "1.1.2"
|
||||||
|
cloud_firestore:
|
||||||
|
dependency: "direct main"
|
||||||
|
description:
|
||||||
|
name: cloud_firestore
|
||||||
|
sha256: "3ac242332166ae5037bd87bc343744bb96d88d7b13f791492b00958ce5cc6c63"
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "6.3.0"
|
||||||
|
cloud_firestore_platform_interface:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: cloud_firestore_platform_interface
|
||||||
|
sha256: "1bd08b736e1015e8bf5448f5ef67b2087a2380c2c1c7972f8403c1c7b41f5359"
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "7.2.0"
|
||||||
|
cloud_firestore_web:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: cloud_firestore_web
|
||||||
|
sha256: "18617275ffa2331d3ea058c515ef218bcce2ae13a14bee922563ca6ae2507c26"
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "5.3.0"
|
||||||
code_assets:
|
code_assets:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
@@ -241,6 +273,54 @@ packages:
|
|||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "0.9.3+5"
|
version: "0.9.3+5"
|
||||||
|
firebase_auth:
|
||||||
|
dependency: "direct main"
|
||||||
|
description:
|
||||||
|
name: firebase_auth
|
||||||
|
sha256: b12cb1e2e87797d27e0041100b73ebf890dbafcff2e7e991d4593f5e8e309808
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "6.4.0"
|
||||||
|
firebase_auth_platform_interface:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: firebase_auth_platform_interface
|
||||||
|
sha256: c71517b3c78480be42789b05316a7692d69296c17848bd6a9e798300abae1ec7
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "8.1.9"
|
||||||
|
firebase_auth_web:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: firebase_auth_web
|
||||||
|
sha256: "52b0224eb46b09f387e99710707be2d3f48da67c74fe14202e4b942cbe8ce9fd"
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "6.1.5"
|
||||||
|
firebase_core:
|
||||||
|
dependency: "direct main"
|
||||||
|
description:
|
||||||
|
name: firebase_core
|
||||||
|
sha256: d5a94b884dcb1e6d3430298e94bfe002238094cdfd5e29202d536ee2120f9158
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "4.7.0"
|
||||||
|
firebase_core_platform_interface:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: firebase_core_platform_interface
|
||||||
|
sha256: "0ecda14c1bfc9ed8cac303dd0f8d04a320811b479362a9a4efb14fd331a473ce"
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "6.0.3"
|
||||||
|
firebase_core_web:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: firebase_core_web
|
||||||
|
sha256: dc5096257cd67292d34d78ceeb90836f02a4be921b5f3934311a02bb2376118c
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "3.6.0"
|
||||||
fixnum:
|
fixnum:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
|
|||||||
@@ -31,6 +31,9 @@ dependencies:
|
|||||||
uuid: ^4.5.3
|
uuid: ^4.5.3
|
||||||
pdf: ^3.12.0
|
pdf: ^3.12.0
|
||||||
universal_io: ^2.3.1
|
universal_io: ^2.3.1
|
||||||
|
firebase_core: ^4.7.0
|
||||||
|
firebase_auth: ^6.4.0
|
||||||
|
cloud_firestore: ^6.3.0
|
||||||
|
|
||||||
dev_dependencies:
|
dev_dependencies:
|
||||||
flutter_test:
|
flutter_test:
|
||||||
@@ -44,4 +47,5 @@ flutter:
|
|||||||
assets:
|
assets:
|
||||||
- assets/images/
|
- assets/images/
|
||||||
- assets/svg/
|
- assets/svg/
|
||||||
|
- assets/schedeRiparazione-1778021345.json
|
||||||
- .env
|
- .env
|
||||||
|
|||||||
@@ -7,7 +7,10 @@
|
|||||||
#include "generated_plugin_registrant.h"
|
#include "generated_plugin_registrant.h"
|
||||||
|
|
||||||
#include <app_links/app_links_plugin_c_api.h>
|
#include <app_links/app_links_plugin_c_api.h>
|
||||||
|
#include <cloud_firestore/cloud_firestore_plugin_c_api.h>
|
||||||
#include <file_selector_windows/file_selector_windows.h>
|
#include <file_selector_windows/file_selector_windows.h>
|
||||||
|
#include <firebase_auth/firebase_auth_plugin_c_api.h>
|
||||||
|
#include <firebase_core/firebase_core_plugin_c_api.h>
|
||||||
#include <pdfx/pdfx_plugin.h>
|
#include <pdfx/pdfx_plugin.h>
|
||||||
#include <permission_handler_windows/permission_handler_windows_plugin.h>
|
#include <permission_handler_windows/permission_handler_windows_plugin.h>
|
||||||
#include <url_launcher_windows/url_launcher_windows.h>
|
#include <url_launcher_windows/url_launcher_windows.h>
|
||||||
@@ -15,8 +18,14 @@
|
|||||||
void RegisterPlugins(flutter::PluginRegistry* registry) {
|
void RegisterPlugins(flutter::PluginRegistry* registry) {
|
||||||
AppLinksPluginCApiRegisterWithRegistrar(
|
AppLinksPluginCApiRegisterWithRegistrar(
|
||||||
registry->GetRegistrarForPlugin("AppLinksPluginCApi"));
|
registry->GetRegistrarForPlugin("AppLinksPluginCApi"));
|
||||||
|
CloudFirestorePluginCApiRegisterWithRegistrar(
|
||||||
|
registry->GetRegistrarForPlugin("CloudFirestorePluginCApi"));
|
||||||
FileSelectorWindowsRegisterWithRegistrar(
|
FileSelectorWindowsRegisterWithRegistrar(
|
||||||
registry->GetRegistrarForPlugin("FileSelectorWindows"));
|
registry->GetRegistrarForPlugin("FileSelectorWindows"));
|
||||||
|
FirebaseAuthPluginCApiRegisterWithRegistrar(
|
||||||
|
registry->GetRegistrarForPlugin("FirebaseAuthPluginCApi"));
|
||||||
|
FirebaseCorePluginCApiRegisterWithRegistrar(
|
||||||
|
registry->GetRegistrarForPlugin("FirebaseCorePluginCApi"));
|
||||||
PdfxPluginRegisterWithRegistrar(
|
PdfxPluginRegisterWithRegistrar(
|
||||||
registry->GetRegistrarForPlugin("PdfxPlugin"));
|
registry->GetRegistrarForPlugin("PdfxPlugin"));
|
||||||
PermissionHandlerWindowsPluginRegisterWithRegistrar(
|
PermissionHandlerWindowsPluginRegisterWithRegistrar(
|
||||||
|
|||||||
@@ -4,7 +4,10 @@
|
|||||||
|
|
||||||
list(APPEND FLUTTER_PLUGIN_LIST
|
list(APPEND FLUTTER_PLUGIN_LIST
|
||||||
app_links
|
app_links
|
||||||
|
cloud_firestore
|
||||||
file_selector_windows
|
file_selector_windows
|
||||||
|
firebase_auth
|
||||||
|
firebase_core
|
||||||
pdfx
|
pdfx
|
||||||
permission_handler_windows
|
permission_handler_windows
|
||||||
url_launcher_windows
|
url_launcher_windows
|
||||||
|
|||||||
Reference in New Issue
Block a user