diff --git a/README.md b/README.md index 1faea9d..6cbba72 100644 --- a/README.md +++ b/README.md @@ -1,4 +1,4 @@ -# Light Vitae +# SIMEVEC This is a simple crud application built with Flutter and Firebase. diff --git a/lib/app.dart b/lib/app.dart index 893bacf..707700f 100644 --- a/lib/app.dart +++ b/lib/app.dart @@ -11,11 +11,11 @@ class MyApp extends StatelessWidget { Widget build(BuildContext context) { return MaterialApp( debugShowCheckedModeBanner: false, - title: 'Light Vitae', + title: 'SIMEVEC', navigatorKey: NavigationService.navigatorKey, home: const SplashScreen(), routes: routes, - // Add localization supportj + // Add localization support localizationsDelegates: const [ GlobalMaterialLocalizations.delegate, GlobalWidgetsLocalizations.delegate, diff --git a/lib/routes.dart b/lib/routes.dart index 4f019d8..363bf5e 100644 --- a/lib/routes.dart +++ b/lib/routes.dart @@ -21,35 +21,24 @@ import 'package:client_service/view/auth/login_admin_screen.dart'; import 'package:flutter/cupertino.dart'; final Map routes = { - // Auth '/splash': (context) => const SplashScreen(), '/login': (context) => const LoginSelectionScreen(), '/login-empleado': (context) => const LoginEmpleadoScreen(), '/login-admin': (context) => const LoginAdminScreen(), - // Registros 'Nuevos Empleados': (context) => const RegistroEmpleadoPage(), 'Nuevos Clientes': (context) => const RegistroClientePage(), - - // Servicios 'Registro de instalación': (context) => const RegistroInstalacion(), 'Mantenimiento de cámaras': (context) => const RegistroCamara(), 'Alquiler vehículos': (context) => const RegistroAlquiler(), - - // Facturación 'Dashboard Facturación': (context) => const DashboardFacturacion(), 'Nuevas Facturas': (context) => const CreateFactura(), 'Reporte de Facturas': (context) => const FacturasListAvanzada(), 'Anular Factura': (context) => const AnularFacturas(), - - // Reportes 'Empleados': (context) => const AsistenciasAdminScreen(), 'Clientes': (context) => const ReportCliente(), 'Reporte de instalaciones': (context) => const ReportInstalacion(), 'Reporte de cámaras': (context) => const ReportCamara(), 'Reporte vehículos': (context) => const ReportVehiculo(), - - // Nuevas funcionalidades - // 'calendario': (context) => const CalendarioScreen(), // REMOVED: CalendarioScreen now requires Empleado 'notificaciones': (context) => const NotificacionesScreen(), 'configuracion': (context) => const ConfiguracionScreen(), }; diff --git a/lib/services/service_locator.dart b/lib/services/service_locator.dart index 7fbb2a9..b81bcb1 100644 --- a/lib/services/service_locator.dart +++ b/lib/services/service_locator.dart @@ -11,7 +11,6 @@ import '../viewmodel/instalacion_viewmodel.dart'; import '../viewmodel/camara_viewmodel.dart'; import '../viewmodel/vehiculo_viewmodel.dart'; import '../viewmodel/factura_viewmodel.dart'; -import '../viewmodel/calendario_viewmodel.dart'; import '../services/notificacion_service.dart'; final GetIt sl = GetIt.instance; diff --git a/lib/utils/events/splash_screen.dart b/lib/utils/events/splash_screen.dart index a5f99f1..1967747 100644 --- a/lib/utils/events/splash_screen.dart +++ b/lib/utils/events/splash_screen.dart @@ -124,7 +124,7 @@ class _SplashScreenState extends State // Título principal Text( - 'LIGHT VITAE', + 'SIMEVEC', style: TextStyle( fontSize: 42, fontWeight: FontWeight.bold, @@ -186,7 +186,7 @@ class _SplashScreenState extends State // Footer Text( - '© 2025 Light Vitae', + '© 2025 SIMEVEC', style: TextStyle( fontSize: 14, color: Colors.white.withOpacity(0.8), diff --git a/lib/utils/excel_export_utility.dart b/lib/utils/excel_export_utility.dart index 4494b75..3ee6b58 100644 --- a/lib/utils/excel_export_utility.dart +++ b/lib/utils/excel_export_utility.dart @@ -1,122 +1,46 @@ -import 'package:cloud_firestore/cloud_firestore.dart'; import 'package:excel/excel.dart'; +import 'package:flutter/foundation.dart'; import 'package:universal_html/html.dart' as html; +import 'dart:io'; +import 'package:path_provider/path_provider.dart'; +import 'package:open_file/open_file.dart'; + +/// Guarda el archivo en la carpeta Descargas y lo abre (no web) +Future saveFileToDownloads(List fileBytes, String fileName) async { + try { + Directory? downloadsDir; + if (Platform.isAndroid) { + downloadsDir = Directory('/storage/emulated/0/Download'); + } else if (Platform.isIOS) { + downloadsDir = await getApplicationDocumentsDirectory(); + } else { + downloadsDir = await getDownloadsDirectory(); + } + if (downloadsDir == null) { + throw Exception('No se pudo obtener la carpeta de descargas.'); + } + final file = File('${downloadsDir.path}/$fileName'); + await file.writeAsBytes(fileBytes, flush: true); + print('Archivo guardado en: ${file.path}'); + await OpenFile.open(file.path); + } catch (e) { + print('Error guardando archivo Excel: $e'); + throw Exception('Error guardando archivo Excel: $e'); + } +} -/// A custom utility class for exporting Firestore collections to Excel files. -/// -/// This utility is designed to be compatible with the latest versions of -/// the excel package and Dart SDK. class ExcelSheetData { final String sheetName; final List headers; final List> rows; - ExcelSheetData( - {required this.sheetName, required this.headers, required this.rows}); + ExcelSheetData({ + required this.sheetName, + required this.headers, + required this.rows, + }); } class ExcelExportUtility { - /// Exports a Firestore collection to an Excel file. - /// - /// [collectionName] - The name of the Firestore collection to fetch data from. - /// [headers] - The list of headers for the Excel file. - /// [mapper] - A function that maps Firestore document data to a list of values for each row. - /// [sheetName] - The name of the Excel sheet. - /// [fileName] - The name of the resulting Excel file (default: 'export.xlsx'). - /// [queryBuilder] - An optional function to customize the Firestore query. - static Future exportToExcel({ - required String collectionName, - required List headers, - required List Function(Map) mapper, - required String sheetName, - String fileName = 'export.xlsx', - Query Function(Query query)? queryBuilder, - }) async { - try { - // Build Firestore query - Query query = FirebaseFirestore.instance.collection(collectionName); - if (queryBuilder != null) { - query = queryBuilder(query); - } - - // Fetch data from Firestore - QuerySnapshot snapshot = await query.get(); - - if (snapshot.docs.isEmpty) { - throw Exception( - 'No data found in the Firestore collection: $collectionName'); - } - - // Initialize Excel - var excel = Excel.createExcel(); - Sheet sheetObject = excel['Sheet1']; - - // Create header cell style - CellStyle headerCellStyle = CellStyle( - backgroundColorHex: ExcelColor.blue, - fontFamily: getFontFamily(FontFamily.Calibri), - fontSize: 12, - horizontalAlign: HorizontalAlign.Center, - verticalAlign: VerticalAlign.Center, - bold: true, - ); - - // Write headers to the first row - for (int i = 0; i < headers.length; i++) { - var cell = sheetObject.cell( - CellIndex.indexByColumnRow(columnIndex: i, rowIndex: 0), - ); - cell.cellStyle = headerCellStyle; - cell.value = TextCellValue(headers[i]); - } - - // Create data cell style - var dataCellStyle = CellStyle( - fontFamily: getFontFamily(FontFamily.Calibri), - fontSize: 12, - horizontalAlign: HorizontalAlign.Left, - verticalAlign: VerticalAlign.Center, - ); - - // Write data rows - int rowIndex = 1; - for (var doc in snapshot.docs) { - final data = doc.data() as Map; - final row = mapper(data); - - for (int i = 0; i < row.length; i++) { - var cell = sheetObject.cell( - CellIndex.indexByColumnRow(columnIndex: i, rowIndex: rowIndex), - ); - cell.cellStyle = dataCellStyle; - cell.value = row[i] != null - ? TextCellValue(row[i].toString()) - : TextCellValue(''); - } - rowIndex++; - } - - // set column widths) - for (int i = 0; i < headers.length; i++) { - sheetObject.setColumnWidth(i, 20.0); - } - - // Rename the default sheet to the desired name - excel.rename(sheetObject.sheetName, sheetName); - - // Encode Excel file - List? fileBytes = excel.encode(); - if (fileBytes == null) { - throw Exception("Failed to encode Excel file."); - } - - // Create and trigger download (for web platform) - await _downloadFile(fileBytes, fileName); - } catch (e) { - throw Exception('Error exporting to Excel: $e'); - } - } - - /// Exporta múltiples hojas a un solo archivo Excel (una hoja por empleado, por ejemplo) static Future exportMultipleSheets({ required List sheets, String fileName = 'export.xlsx', @@ -149,37 +73,30 @@ class ExcelExportUtility { } List? fileBytes = excel.encode(); if (fileBytes == null) throw Exception("Failed to encode Excel file."); - await _downloadFile(fileBytes, fileName); + if (kIsWeb) { + await _downloadFile(fileBytes, fileName); + } else { + await saveFileToDownloads(fileBytes, fileName); + } } catch (e) { throw Exception('Error exporting multiple sheets to Excel: $e'); } } - /// Downloads the Excel file on web platform static Future _downloadFile( List fileBytes, String fileName) async { try { - // Create a Blob from the bytes final blob = html.Blob( [fileBytes], 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet', ); - - // Generate a download URL final url = html.Url.createObjectUrlFromBlob(blob); - - // Create an anchor element and trigger the download final anchor = html.document.createElement('a') as html.AnchorElement; anchor.href = url; anchor.style.display = 'none'; anchor.download = fileName; - html.document.body?.children.add(anchor); - - // Trigger the download anchor.click(); - - // Clean up by removing the anchor and revoking the URL anchor.remove(); html.Url.revokeObjectUrl(url); } catch (e) { diff --git a/lib/view/asistencia/asistencia_screen.dart b/lib/view/asistencia/asistencia_screen.dart index 78a9c78..814069e 100644 --- a/lib/view/asistencia/asistencia_screen.dart +++ b/lib/view/asistencia/asistencia_screen.dart @@ -186,8 +186,8 @@ class _AsistenciaScreenState extends State { }, ) else if (_entrada != null && _salida != null) - Column( - children: const [ + const Column( + children: [ Icon(Icons.check_circle, color: Colors.green, size: 48), SizedBox(height: 12), diff --git a/lib/view/auth/login_admin_screen.dart b/lib/view/auth/login_admin_screen.dart index 6bc8c86..57538e8 100644 --- a/lib/view/auth/login_admin_screen.dart +++ b/lib/view/auth/login_admin_screen.dart @@ -187,7 +187,7 @@ class _LoginAdminScreenBodyState extends State<_LoginAdminScreenBody> { ), const SizedBox(height: 20), const Text( - '© 2025 Light Vitae', + '© 2025 SIMEVEC', style: TextStyle( fontSize: 12, color: Colors.white70, diff --git a/lib/view/auth/login_empleado_screen.dart b/lib/view/auth/login_empleado_screen.dart index dca6a1c..251946c 100644 --- a/lib/view/auth/login_empleado_screen.dart +++ b/lib/view/auth/login_empleado_screen.dart @@ -4,7 +4,6 @@ import 'package:client_service/viewmodel/auth_viewmodel.dart'; import 'package:client_service/providers/empleado_provider.dart'; import 'package:client_service/view/auth/cambiar_password_screen.dart'; import 'package:client_service/view/panel_empleado/panel_empleado_screen.dart'; -import 'package:client_service/view/auth/login_selection_screen.dart'; import 'package:client_service/view/widgets/auth/login_card.dart'; class LoginEmpleadoScreen extends StatefulWidget { @@ -64,12 +63,41 @@ class _LoginEmpleadoScreenState extends State { @override Widget build(BuildContext context) { return Scaffold( - body: Center( - child: SingleChildScrollView( - child: LoginCard( - userType: 'Empleado', - isLoading: _isLoading, - onLogin: (correo, cedula) => _iniciarSesion(correo, cedula), + body: Container( + width: double.infinity, + height: double.infinity, + decoration: const BoxDecoration( + image: DecorationImage( + image: AssetImage('assets/images/bg.png'), + fit: BoxFit.cover, + ), + ), + child: SafeArea( + child: SingleChildScrollView( + child: Column( + children: [ + Row( + children: [ + IconButton( + onPressed: () => Navigator.pop(context), + icon: const Icon( + Icons.arrow_back_ios, + color: Colors.white, + size: 24, + ), + ), + ], + ), + Container( + width: double.infinity, + child: LoginCard( + userType: 'Empleado', + isLoading: _isLoading, + onLogin: (correo, cedula) => _iniciarSesion(correo, cedula), + ), + ), + ], + ), ), ), ), diff --git a/lib/view/auth/login_selection_screen.dart b/lib/view/auth/login_selection_screen.dart index aa7d835..940fa5c 100644 --- a/lib/view/auth/login_selection_screen.dart +++ b/lib/view/auth/login_selection_screen.dart @@ -31,7 +31,7 @@ class LoginSelectionScreen extends StatelessWidget { Column( children: [ Text( - 'Light Vitae', + 'SIMEVEC', style: AppFonts.titleBold.copyWith( fontSize: 48, color: AppColors.primaryColor, @@ -45,38 +45,46 @@ class LoginSelectionScreen extends StatelessWidget { // Subtítulo Text( - 'Sistema de Gestión Integral', + 'Sistema de servicios de instalación, mantenimiento de postes, Cámaras y préstamos de vehículos', style: AppFonts.text.copyWith( color: AppColors.whiteColor, + fontWeight: FontWeight.w500, + height: 1.4, fontSize: 24, + shadows: [ + Shadow( + color: Colors.black.withOpacity(0.2), + offset: const Offset(0, 2), + blurRadius: 12, + ), + ], ), + textAlign: TextAlign.center, ), ], ), ), const Spacer(), - // Mensaje de selección Container( margin: const EdgeInsets.symmetric(horizontal: 30), padding: const EdgeInsets.all(10), - child: Text( - 'Selecciona tu tipo de acceso', - style: AppFonts.subtitleBold.copyWith( - color: AppColors.btnColor, - fontSize: 24, - letterSpacing: 1.2, - ), - textAlign: TextAlign.center, - ), + // child: Text( + // 'Selecciona tu tipo de acceso', + // style: AppFonts.subtitleBold.copyWith( + // color: AppColors.btnColor, + // fontSize: 24, + // letterSpacing: 1.2, + // ), + // textAlign: TextAlign.center, + // ), ), const SizedBox(height: 30), - - // Botones de acceso Padding( - padding: const EdgeInsets.symmetric(horizontal: 40), + padding: + const EdgeInsets.symmetric(horizontal: 42, vertical: 20), child: Column( children: [ // Botón Administrador @@ -115,7 +123,9 @@ class LoginSelectionScreen extends StatelessWidget { ), ), ), - // Botón Usuario (Empleado) + + const SizedBox(height: 32), + SizedBox( width: double.infinity, child: ElevatedButton( @@ -161,7 +171,7 @@ class LoginSelectionScreen extends StatelessWidget { Padding( padding: const EdgeInsets.all(10), child: Text( - '© 2025 Light Vitae - Todos los derechos reservados', + '© 2025 SIMEVEC - Todos los derechos reservados', style: AppFonts.text.copyWith( fontSize: 12, color: Colors.white.withOpacity(0.8), diff --git a/lib/view/home/view.dart b/lib/view/home/view.dart index 22d33b1..1825d14 100644 --- a/lib/view/home/view.dart +++ b/lib/view/home/view.dart @@ -41,7 +41,7 @@ class ContentPage extends StatefulWidget { } class _ContentPageState extends State { - String selectedCategory = 'Registros'; + String selectedCategory = 'Servicios'; void updateCategory(String category) { setState(() { @@ -51,14 +51,15 @@ class _ContentPageState extends State { @override Widget build(BuildContext context) { - return Expanded( - child: Column( - children: [ - // SearchBarPage eliminado - CategoryPage(onCategorySelected: updateCategory), - SectionPage(selectedCategory: selectedCategory), - ], - ), + return Column( + // Remove Expanded, use Column directly + children: [ + CategoryPage(onCategorySelected: updateCategory), + Expanded( + // Move Expanded here if SectionPage needs to fill remaining space + child: SectionPage(selectedCategory: selectedCategory), + ), + ], ); } } diff --git a/lib/view/reports/empleado.dart b/lib/view/reports/empleado.dart index a30a586..fcefe32 100644 --- a/lib/view/reports/empleado.dart +++ b/lib/view/reports/empleado.dart @@ -8,6 +8,8 @@ import 'package:client_service/utils/colors.dart'; import 'package:client_service/view/widgets/shared/apptitle.dart'; import 'package:client_service/view/widgets/shared/button.dart'; import 'package:client_service/view/widgets/shared/toolbar.dart'; +import 'package:client_service/view/registers/employet/edit_employet.dart'; +import 'package:client_service/view/reports/empleado_asistencia.dart'; class AsistenciasAdminScreen extends StatefulWidget { const AsistenciasAdminScreen({super.key}); @@ -46,40 +48,60 @@ class _AsistenciasAdminScreenState extends State { await _fetchAsistencias(); } - Future _fetchAsistencias() async { - if (_empleadoSeleccionado == null) { + Future _fetchAsistencias([Empleado? empleado]) async { + final empleadoActual = empleado ?? _empleadoSeleccionado; + // Validación robusta de empleado y cédula + if (empleadoActual == null || empleadoActual.cedula.trim().isEmpty) { setState(() { _asistencias = []; _loading = false; }); return; } + final cedula = empleadoActual.cedula.trim(); setState(() => _loading = true); final start = DateTime(_selectedMonth.year, _selectedMonth.month, 1); final end = DateTime(_selectedMonth.year, _selectedMonth.month + 1, 1); - // Solo filtra por cedula en Firestore, el resto en memoria para evitar el índice compuesto - final query = await FirebaseFirestore.instance - .collection('asistencias') - .where('cedula', isEqualTo: _empleadoSeleccionado!.cedula) - .get(); - final docsFiltrados = query.docs.where((doc) { - final ts = doc['timestamp']; - if (ts is Timestamp) { - final dt = ts.toDate(); - return dt.isAfter(start.subtract(const Duration(seconds: 1))) && - dt.isBefore(end); + try { + final query = await FirebaseFirestore.instance + .collection('asistencias') + .where('cedula', isEqualTo: cedula) + .get(); + final docsFiltrados = query.docs.where((doc) { + final data = doc.data(); + final ts = data['timestamp']; + if (ts is Timestamp) { + final dt = ts.toDate(); + return dt.isAfter(start.subtract(const Duration(seconds: 1))) && + dt.isBefore(end); + } + return false; + }).toList(); + docsFiltrados.sort((a, b) { + final ta = a.data()['timestamp'] as Timestamp?; + final tb = b.data()['timestamp'] as Timestamp?; + if (ta == null || tb == null) return 0; + return tb.compareTo(ta); // descending + }); + setState(() { + _asistencias = docsFiltrados; + _loading = false; + }); + } catch (e) { + setState(() { + _asistencias = []; + _loading = false; + }); + if (mounted) { + ScaffoldMessenger.of(context).showSnackBar( + const SnackBar( + content: Text( + 'No se pueden mostrar asistencias: el empleado no tiene cédula registrada.'), + backgroundColor: Colors.red, + ), + ); } - return false; - }).toList(); - docsFiltrados.sort((a, b) { - final ta = a['timestamp'] as Timestamp; - final tb = b['timestamp'] as Timestamp; - return tb.compareTo(ta); // descending - }); - setState(() { - _asistencias = docsFiltrados; - _loading = false; - }); + } } void _selectMonth(BuildContext context) async { @@ -261,117 +283,41 @@ class _AsistenciasAdminScreenState extends State { icon: const Icon(Icons.more_vert), onSelected: (value) async { if (value == 'asistencias') { - setState(() { - _empleadoSeleccionado = empleado; - }); - await _fetchAsistencias(); - showModalBottomSheet( - context: context, - isScrollControlled: true, - backgroundColor: Colors.white, - shape: const RoundedRectangleBorder( - borderRadius: BorderRadius.vertical( - top: Radius.circular(24)), - ), - builder: (context) { - return SizedBox( - height: MediaQuery.of(context) - .size - .height * - 0.6, - child: Column( - children: [ - Padding( - padding: const EdgeInsets.all( - 16.0), - child: Text( - 'Asistencias de ${empleado.nombreCompleto}', - style: const TextStyle( - fontSize: 18, - fontWeight: - FontWeight.bold, - color: Colors.deepPurple, - ), - ), - ), - Expanded( - child: _asistencias.isEmpty - ? const Center( - child: Text( - 'No hay asistencias registradas para este mes.'), - ) - : ListView( - padding: - const EdgeInsets - .symmetric( - vertical: 8), - children: _asistencias - .map((doc) { - final data = - doc.data(); - final fecha = - data['fecha'] ?? - ''; - final entrada = - data['horaEntrada'] ?? - '-'; - final salida = data[ - 'horaSalida'] ?? - '-'; - return Card( - margin: - const EdgeInsets - .symmetric( - horizontal: - 12, - vertical: - 4), - child: ListTile( - title: Text( - 'Fecha: $fecha'), - subtitle: Row( - children: [ - const Icon( - Icons - .login, - size: - 16, - color: Colors - .green), - Text( - ' Entrada: $entrada'), - const SizedBox( - width: - 16), - const Icon( - Icons - .logout, - size: - 16, - color: Colors - .red), - Text( - ' Salida: $salida'), - ], - ), - ), - ); - }).toList(), - ), - ), - ], + if (empleado.cedula.trim().isEmpty) { + if (mounted) { + ScaffoldMessenger.of(context) + .showSnackBar( + const SnackBar( + content: Text( + 'No se pueden mostrar asistencias: el empleado no tiene cédula registrada.'), + backgroundColor: Colors.red, ), ); - }, + } + return; + } + Navigator.push( + context, + MaterialPageRoute( + builder: (context) => + EmpleadoAsistenciasPage( + empleado: empleado, + selectedMonth: _selectedMonth, + ), + ), ); } else if (value == 'editar') { - // TODO: Navegar a pantalla de edición de empleado - ScaffoldMessenger.of(context) - .showSnackBar( - const SnackBar( - content: Text( - 'Función editar no implementada')), + final result = await Navigator.push( + context, + MaterialPageRoute( + builder: (context) => + EditEmpleadoPage( + empleado: empleado), + ), ); + if (result == true) { + await _fetchEmpleadosYAsistencias(); + } } else if (value == 'eliminar') { final confirm = await showDialog( context: context, @@ -397,15 +343,29 @@ class _AsistenciasAdminScreenState extends State { ), ); if (confirm == true) { + if (empleado.id == null) { + if (mounted) { + ScaffoldMessenger.of(context) + .showSnackBar( + const SnackBar( + content: Text( + 'No se puede eliminar: el empleado no tiene un ID válido.'), + ), + ); + } + return; + } final repo = EmpleadoRepository(); await repo.delete(empleado.id!); await _fetchEmpleadosYAsistencias(); - ScaffoldMessenger.of(context) - .showSnackBar( - const SnackBar( - content: - Text('Empleado eliminado')), - ); + if (mounted) { + ScaffoldMessenger.of(context) + .showSnackBar( + const SnackBar( + content: + Text('Empleado eliminado')), + ); + } } } }, diff --git a/lib/view/reports/empleado_asistencia.dart b/lib/view/reports/empleado_asistencia.dart new file mode 100644 index 0000000..2141781 --- /dev/null +++ b/lib/view/reports/empleado_asistencia.dart @@ -0,0 +1,122 @@ +import 'package:flutter/material.dart'; +import 'package:client_service/models/empleado.dart'; +import 'package:cloud_firestore/cloud_firestore.dart'; +import 'package:intl/intl.dart'; +import 'package:client_service/utils/colors.dart'; + +class EmpleadoAsistenciasPage extends StatefulWidget { + final Empleado empleado; + final DateTime selectedMonth; + const EmpleadoAsistenciasPage( + {Key? key, required this.empleado, required this.selectedMonth}) + : super(key: key); + + @override + State createState() => + _EmpleadoAsistenciasPageState(); +} + +class _EmpleadoAsistenciasPageState extends State { + List>> _asistencias = []; + bool _loading = true; + + @override + void initState() { + super.initState(); + _fetchAsistencias(); + } + + Future _fetchAsistencias() async { + setState(() => _loading = true); + final cedula = widget.empleado.cedula.trim(); + final start = + DateTime(widget.selectedMonth.year, widget.selectedMonth.month, 1); + final end = + DateTime(widget.selectedMonth.year, widget.selectedMonth.month + 1, 1); + try { + final query = await FirebaseFirestore.instance + .collection('asistencias') + .where('cedula', isEqualTo: cedula) + .get(); + final docsFiltrados = query.docs.where((doc) { + final data = doc.data(); + final ts = data['timestamp']; + if (ts is Timestamp) { + final dt = ts.toDate(); + return dt.isAfter(start.subtract(const Duration(seconds: 1))) && + dt.isBefore(end); + } + return false; + }).toList(); + docsFiltrados.sort((a, b) { + final ta = a.data()['timestamp'] as Timestamp?; + final tb = b.data()['timestamp'] as Timestamp?; + if (ta == null || tb == null) return 0; + return tb.compareTo(ta); // descending + }); + setState(() { + _asistencias = docsFiltrados; + _loading = false; + }); + } catch (e) { + setState(() { + _asistencias = []; + _loading = false; + }); + if (mounted) { + ScaffoldMessenger.of(context).showSnackBar( + SnackBar( + content: Text('Error al cargar asistencias: $e'), + backgroundColor: Colors.red), + ); + } + } + } + + @override + Widget build(BuildContext context) { + final monthLabel = + DateFormat('MMMM yyyy', 'es_ES').format(widget.selectedMonth); + return Scaffold( + appBar: AppBar( + title: Text('Asistencias de ${widget.empleado.nombreCompleto}'), + backgroundColor: AppColors.primaryColor, + ), + body: _loading + ? const Center(child: CircularProgressIndicator()) + : _asistencias.isEmpty + ? const Center( + child: Text('No hay asistencias registradas para este mes.')) + : ListView( + padding: const EdgeInsets.symmetric(vertical: 8), + children: _asistencias.map((doc) { + final data = doc.data(); + final fecha = (data['fecha'] ?? '').toString(); + final entrada = (data['horaEntrada'] ?? '-').toString(); + final salida = (data['horaSalida'] ?? '-').toString(); + return Card( + margin: const EdgeInsets.symmetric( + horizontal: 12, vertical: 4), + child: ListTile( + title: Text( + 'Fecha: ${fecha.isNotEmpty ? fecha : 'Desconocida'}'), + subtitle: Row( + children: [ + const Icon(Icons.login, + size: 16, color: Colors.green), + Text( + ' Entrada: ${entrada.isNotEmpty ? entrada : '-'}'), + const SizedBox(width: 16), + const Icon(Icons.logout, + size: 16, color: Colors.red), + Text( + ' Salida: ${salida.isNotEmpty ? salida : '-'}'), + ], + ), + ), + ); + }).toList(), + ), + ); + } +} diff --git a/lib/view/widgets/auth/login_card.dart b/lib/view/widgets/auth/login_card.dart index 55c8b1a..b5e5716 100644 --- a/lib/view/widgets/auth/login_card.dart +++ b/lib/view/widgets/auth/login_card.dart @@ -3,7 +3,7 @@ import 'package:client_service/utils/colors.dart'; import 'package:client_service/utils/font.dart'; class LoginCard extends StatefulWidget { - final String userType; // 'Usuario' o 'Administrador' + final String userType; final Function(String email, String password) onLogin; final bool isLoading; final bool showHeaderOnly; // Nuevo parámetro para mostrar solo el header @@ -71,46 +71,45 @@ class _LoginCardState extends State { child: Column( children: [ // Logo circular como en el mockup - Container( - width: 120, - height: 120, - decoration: BoxDecoration( - color: Colors.white, - shape: BoxShape.circle, - boxShadow: [ - BoxShadow( - color: Colors.black.withOpacity(0.2), - blurRadius: 15, - offset: const Offset(0, 8), - ), - ], - ), - child: Icon( - isAdmin ? Icons.admin_panel_settings : Icons.person, - size: 60, - color: isAdmin ? AppColors.btnColor : AppColors.primaryColor, - ), - ), + // Container( + // width: 120, + // height: 120, + // decoration: BoxDecoration( + // color: Colors.white, + // shape: BoxShape.circle, + // boxShadow: [ + // BoxShadow( + // color: Colors.black.withOpacity(0.2), + // blurRadius: 15, + // offset: const Offset(0, 8), + // ), + // ], + // ), + // child: Icon( + // isAdmin ? Icons.admin_panel_settings : Icons.person, + // size: 60, + // color: isAdmin ? AppColors.btnColor : AppColors.primaryColor, + // ), + // ), const SizedBox(height: 30), - // Título LIGHT VITAE como en el mockup - Text( - 'LIGHT VITAE', - style: AppFonts.titleBold.copyWith( - fontSize: 32, - color: Colors.white, - letterSpacing: 2, - fontWeight: FontWeight.w900, - shadows: [ - Shadow( - color: Colors.black.withOpacity(0.5), - offset: const Offset(2, 2), - blurRadius: 4, - ), - ], - ), - ), + // Text( + // 'SIMEVEC', + // style: AppFonts.titleBold.copyWith( + // fontSize: 32, + // color: Colors.white, + // letterSpacing: 2, + // fontWeight: FontWeight.w900, + // shadows: [ + // Shadow( + // color: Colors.black.withOpacity(0.5), + // offset: const Offset(2, 2), + // blurRadius: 4, + // ), + // ], + // ), + // ), const SizedBox(height: 15), @@ -140,8 +139,7 @@ class _LoginCardState extends State { margin: widget.showFormOnly ? EdgeInsets.zero : const EdgeInsets.symmetric(horizontal: 25), - padding: const EdgeInsets.all( - 40), // Aumenté el padding para hacer el formulario más alto + padding: const EdgeInsets.all(40), decoration: widget.showFormOnly ? null : BoxDecoration( @@ -161,7 +159,7 @@ class _LoginCardState extends State { children: [ // Texto Bienvenido usuario Text( - 'Bienvenido usuario', + 'Iniciar Sesión', style: AppFonts.titleBold.copyWith( color: Colors.black87, fontSize: 22, @@ -300,21 +298,6 @@ class _LoginCardState extends State { const SizedBox(height: 30), // Aumenté el espaciado - // Texto de olvido de contraseña - TextButton( - onPressed: () { - // Acción para olvidé mi contraseña - }, - child: Text( - 'Olvidé mi contraseña', - style: AppFonts.text.copyWith( - color: Colors.grey[600], - fontSize: 14, - decoration: TextDecoration.underline, - ), - ), - ), - if (isAdmin) ...[ const SizedBox(height: 25), // Mensaje de seguridad para admin más discreto diff --git a/lib/viewmodel/camara_viewmodel.dart b/lib/viewmodel/camara_viewmodel.dart index c186c37..fbcca95 100644 --- a/lib/viewmodel/camara_viewmodel.dart +++ b/lib/viewmodel/camara_viewmodel.dart @@ -57,38 +57,45 @@ class CamaraViewModel extends BaseViewModel { await handleAsyncOperation(() => _repository.getAllForExport()); if (data != null) { - await ExcelExportUtility.exportToExcel( - collectionName: 'camaras', - headers: [ - 'ID', - 'Nombre Comercial', - 'Fecha Mantenimiento', - 'Dirección', - 'Técnico', - 'Tipo', - 'Descripción', - 'Costo', + await ExcelExportUtility.exportMultipleSheets( + sheets: [ + ExcelSheetData( + sheetName: 'Cámaras', + headers: [ + 'ID', + 'Nombre Comercial', + 'Fecha Mantenimiento', + 'Dirección', + 'Técnico', + 'Tipo', + 'Descripción', + 'Costo', + ], + rows: data + .map>((dataItem) => [ + dataItem['id'] ?? '', + dataItem['nombreComercial'] ?? '', + dataItem['fechaMantenimiento'] is Timestamp + ? (dataItem['fechaMantenimiento'] as Timestamp) + .toDate() + .toIso8601String() + : dataItem['fechaMantenimiento']?.toString() ?? '', + dataItem['direccion'] ?? '', + dataItem['tecnico'] ?? '', + dataItem['tipo'] ?? '', + dataItem['descripcion'] ?? '', + (dataItem['costo'] is int + ? (dataItem['costo'] as int).toDouble() + : (dataItem['costo'] is double + ? dataItem['costo'] + : double.tryParse( + dataItem['costo'].toString()) ?? + 0.0)) + .toString(), + ]) + .toList(), + ), ], - mapper: (dataItem) => [ - dataItem['id'] ?? '', - dataItem['nombreComercial'] ?? '', - dataItem['fechaMantenimiento'] is Timestamp - ? (dataItem['fechaMantenimiento'] as Timestamp) - .toDate() - .toIso8601String() - : dataItem['fechaMantenimiento']?.toString() ?? '', - dataItem['direccion'] ?? '', - dataItem['tecnico'] ?? '', - dataItem['tipo'] ?? '', - dataItem['descripcion'] ?? '', - (dataItem['costo'] is int - ? (dataItem['costo'] as int).toDouble() - : (dataItem['costo'] is double - ? dataItem['costo'] - : double.tryParse(dataItem['costo'].toString()) ?? 0.0)) - .toString(), - ], - sheetName: 'Camaras', fileName: 'reporte_camaras.xlsx', ); } @@ -118,29 +125,34 @@ class CamaraViewModel extends BaseViewModel { )); if (data != null) { - await ExcelExportUtility.exportToExcel( - collectionName: 'camaras', - headers: [ - 'ID', - 'Nombre Comercial', - 'Fecha Mantenimiento', - 'Dirección', - 'Técnico', - 'Tipo', - 'Descripción', - 'Costo', - ], - mapper: (dataItem) => [ - dataItem['id'] ?? '', - dataItem['nombreComercial'] ?? '', - dataItem['fechaMantenimiento'] ?? '', - dataItem['direccion'] ?? '', - dataItem['tecnico'] ?? '', - dataItem['tipo'] ?? '', - dataItem['descripcion'] ?? '', - dataItem['costo']?.toString() ?? '0', + await ExcelExportUtility.exportMultipleSheets( + sheets: [ + ExcelSheetData( + sheetName: 'Cámaras', + headers: [ + 'ID', + 'Nombre Comercial', + 'Fecha Mantenimiento', + 'Dirección', + 'Técnico', + 'Tipo', + 'Descripción', + 'Costo', + ], + rows: data + .map>((dataItem) => [ + dataItem['id'] ?? '', + dataItem['nombreComercial'] ?? '', + dataItem['fechaMantenimiento'] ?? '', + dataItem['direccion'] ?? '', + dataItem['tecnico'] ?? '', + dataItem['tipo'] ?? '', + dataItem['descripcion'] ?? '', + dataItem['costo']?.toString() ?? '0', + ]) + .toList(), + ), ], - sheetName: 'Cámaras', fileName: 'reporte_camaras_filtrado.xlsx', ); } diff --git a/lib/viewmodel/cliente_viewmodel.dart b/lib/viewmodel/cliente_viewmodel.dart index 82dba12..77d1b4d 100644 --- a/lib/viewmodel/cliente_viewmodel.dart +++ b/lib/viewmodel/cliente_viewmodel.dart @@ -55,27 +55,32 @@ class ClienteViewModel extends BaseViewModel { await handleAsyncOperation(() => _repository.getAllForExport()); if (data != null) { - await ExcelExportUtility.exportToExcel( - collectionName: 'clientes', - headers: [ - 'ID', - 'Nombre Comercial', - 'Correo', - 'Teléfono', - 'Dirección', - 'Persona de Contacto', - 'Cédula', + await ExcelExportUtility.exportMultipleSheets( + sheets: [ + ExcelSheetData( + sheetName: 'Clientes', + headers: [ + 'ID', + 'Nombre Comercial', + 'Correo', + 'Teléfono', + 'Dirección', + 'Persona de Contacto', + 'Cédula', + ], + rows: data + .map>((dataItem) => [ + dataItem['id'] ?? '', + dataItem['nombreComercial'] ?? '', + dataItem['correo'] ?? '', + dataItem['telefono'] ?? '', + dataItem['direccion'] ?? '', + dataItem['personaContacto'] ?? '', + dataItem['cedula'] ?? '', + ]) + .toList(), + ), ], - mapper: (dataItem) => [ - dataItem['id'] ?? '', - dataItem['nombreComercial'] ?? '', - dataItem['correo'] ?? '', - dataItem['telefono'] ?? '', - dataItem['direccion'] ?? '', - dataItem['personaContacto'] ?? '', - dataItem['cedula'] ?? '', - ], - sheetName: 'Clientes', fileName: 'reporte_clientes.xlsx', ); } diff --git a/lib/viewmodel/empleado_viewmodel.dart b/lib/viewmodel/empleado_viewmodel.dart index 4f66e78..0a8c235 100644 --- a/lib/viewmodel/empleado_viewmodel.dart +++ b/lib/viewmodel/empleado_viewmodel.dart @@ -11,7 +11,7 @@ class EmpleadoViewmodel extends BaseViewModel { List _empleados = []; List get empleados => _empleados; - // Obtener todos los empleados + // Obtener todos los empleados3 Future fetchEmpleados() async { try { setLoading(true); @@ -34,36 +34,41 @@ class EmpleadoViewmodel extends BaseViewModel { // Exportar empleados en excel Future exportEmpleados() async { await handleAsyncOperation(() async { - await ExcelExportUtility.exportToExcel( - sheetName: 'Empleados', - collectionName: 'empleados', - fileName: 'reporte_empleados.xlsx', - headers: [ - 'Nombre', - 'Apellido', - 'Cédula', - 'Dirección', - 'Teléfono', - 'Correo', - 'Cargo', - 'Fecha Contratación', - 'Foto URL' - ], - mapper: (data) => [ - data['nombre'] ?? '', - data['apellido'] ?? '', - data['cedula'] ?? '', - data['direccion'] ?? '', - data['telefono'] ?? '', - data['correo'] ?? '', - data['cargo'] ?? '', - data['fechaContratacion'] != null - ? (data['fechaContratacion'] as Timestamp) - .toDate() - .toIso8601String() - : '', - data['fotoUrl'] ?? '' - ]); + final empleados = await _repository.getAllForExport?.call() ?? []; + await ExcelExportUtility.exportMultipleSheets( + sheets: [ + ExcelSheetData( + sheetName: 'Empleados', + headers: [ + 'Nombre', + 'Apellido', + 'Cédula', + 'Dirección', + 'Teléfono', + 'Correo', + 'Cargo', + 'Fecha Contratación', + 'Foto URL' + ], + rows: empleados.map>((data) => [ + data['nombre'] ?? '', + data['apellido'] ?? '', + data['cedula'] ?? '', + data['direccion'] ?? '', + data['telefono'] ?? '', + data['correo'] ?? '', + data['cargo'] ?? '', + data['fechaContratacion'] != null + ? (data['fechaContratacion'] as Timestamp) + .toDate() + .toIso8601String() + : '', + data['fotoUrl'] ?? '' + ]).toList(), + ), + ], + fileName: 'reporte_empleados.xlsx', + ); }); } diff --git a/lib/viewmodel/factura_viewmodel.dart b/lib/viewmodel/factura_viewmodel.dart index f1f510d..85791b4 100644 --- a/lib/viewmodel/factura_viewmodel.dart +++ b/lib/viewmodel/factura_viewmodel.dart @@ -191,43 +191,48 @@ class FacturaViewModel extends BaseViewModel { )); if (data != null) { - await ExcelExportUtility.exportToExcel( - collectionName: 'facturas', - headers: [ - 'Número Factura', - 'Fecha Emisión', - 'Fecha Vencimiento', - 'Cliente', - 'Dirección', - 'Teléfono', - 'Correo', - 'Tipo Servicio', - 'Estado', - 'Subtotal', - 'Descuentos', - 'Impuesto', - 'Total', - 'Observaciones', - 'Creado Por', + await ExcelExportUtility.exportMultipleSheets( + sheets: [ + ExcelSheetData( + sheetName: 'Facturas', + headers: [ + 'Número Factura', + 'Fecha Emisión', + 'Fecha Vencimiento', + 'Cliente', + 'Dirección', + 'Teléfono', + 'Correo', + 'Tipo Servicio', + 'Estado', + 'Subtotal', + 'Descuentos', + 'Impuesto', + 'Total', + 'Observaciones', + 'Creado Por', + ], + rows: data + .map>((dataItem) => [ + dataItem['numeroFactura'] ?? '', + _formatDateTime(dataItem['fechaEmision']), + _formatDateTime(dataItem['fechaVencimiento']), + dataItem['nombreCliente'] ?? '', + dataItem['direccionCliente'] ?? '', + dataItem['telefonoCliente'] ?? '', + dataItem['correoCliente'] ?? '', + _formatTipoServicio(dataItem['tipoServicio']), + _formatEstado(dataItem['estado']), + dataItem['subtotal']?.toString() ?? '0', + dataItem['totalDescuentos']?.toString() ?? '0', + dataItem['montoImpuesto']?.toString() ?? '0', + dataItem['total']?.toString() ?? '0', + dataItem['observaciones'] ?? '', + dataItem['creadoPor'] ?? '', + ]) + .toList(), + ), ], - mapper: (dataItem) => [ - dataItem['numeroFactura'] ?? '', - _formatDateTime(dataItem['fechaEmision']), - _formatDateTime(dataItem['fechaVencimiento']), - dataItem['nombreCliente'] ?? '', - dataItem['direccionCliente'] ?? '', - dataItem['telefonoCliente'] ?? '', - dataItem['correoCliente'] ?? '', - _formatTipoServicio(dataItem['tipoServicio']), - _formatEstado(dataItem['estado']), - dataItem['subtotal']?.toString() ?? '0', - dataItem['totalDescuentos']?.toString() ?? '0', - dataItem['montoImpuesto']?.toString() ?? '0', - dataItem['total']?.toString() ?? '0', - dataItem['observaciones'] ?? '', - dataItem['creadoPor'] ?? '', - ], - sheetName: 'Facturas', fileName: 'reporte_facturas.xlsx', ); } diff --git a/lib/viewmodel/instalacion_viewmodel.dart b/lib/viewmodel/instalacion_viewmodel.dart index 90ac15c..74ac687 100644 --- a/lib/viewmodel/instalacion_viewmodel.dart +++ b/lib/viewmodel/instalacion_viewmodel.dart @@ -64,44 +64,48 @@ class InstalacionViewModel extends BaseViewModel { Future exportarInstalaciones() async { await handleAsyncOperation(() async { final data = await _repository.getAllForExport(); - - await ExcelExportUtility.exportToExcel( - collectionName: 'instalaciones', - headers: [ - 'ID', - 'Fecha Instalación', - 'Cédula', - 'Nombre Comercial', - 'Dirección', - 'Item', - 'Descripción', - 'Hora Inicio', - 'Hora Fin', - 'Tipo Trabajo', - 'Cargo Puesto', - 'Teléfono', - 'Número Tarea' - ], - mapper: (data) => [ - data['id'] ?? '', - data['fechaInstalacion'] is Timestamp - ? (data['fechaInstalacion'] as Timestamp) - .toDate() - .toIso8601String() - : data['fechaInstalacion']?.toString() ?? '', - data['cedula'] ?? '', - data['nombreComercial'] ?? '', - data['direccion'] ?? '', - data['item'] ?? '', - data['descripcion'] ?? '', - data['horaInicio'] ?? '', - data['horaFin'] ?? '', - data['tipoTrabajo'] ?? '', - data['cargoPuesto'] ?? '', - data['telefono'] ?? '', - data['numeroTarea'] ?? '' + await ExcelExportUtility.exportMultipleSheets( + sheets: [ + ExcelSheetData( + sheetName: 'Instalaciones', + headers: [ + 'ID', + 'Fecha Instalación', + 'Cédula', + 'Nombre Comercial', + 'Dirección', + 'Item', + 'Descripción', + 'Hora Inicio', + 'Hora Fin', + 'Tipo Trabajo', + 'Cargo Puesto', + 'Teléfono', + 'Número Tarea' + ], + rows: data + .map>((data) => [ + data['id'] ?? '', + data['fechaInstalacion'] is Timestamp + ? (data['fechaInstalacion'] as Timestamp) + .toDate() + .toIso8601String() + : data['fechaInstalacion']?.toString() ?? '', + data['cedula'] ?? '', + data['nombreComercial'] ?? '', + data['direccion'] ?? '', + data['item'] ?? '', + data['descripcion'] ?? '', + data['horaInicio'] ?? '', + data['horaFin'] ?? '', + data['tipoTrabajo'] ?? '', + data['cargoPuesto'] ?? '', + data['telefono'] ?? '', + data['numeroTarea'] ?? '' + ]) + .toList(), + ), ], - sheetName: 'Instalaciones', fileName: 'reporte_instalaciones.xlsx', ); }); @@ -131,39 +135,44 @@ class InstalacionViewModel extends BaseViewModel { )); if (data != null) { - await ExcelExportUtility.exportToExcel( - collectionName: 'instalaciones', - headers: [ - 'ID', - 'Fecha Instalación', - 'Cédula', - 'Nombre Comercial', - 'Dirección', - 'Item', - 'Descripción', - 'Hora Inicio', - 'Hora Fin', - 'Tipo Trabajo', - 'Cargo/Puesto', - 'Teléfono', - 'Número Tarea', - ], - mapper: (dataItem) => [ - dataItem['id'] ?? '', - dataItem['fechaInstalacion'] ?? '', - dataItem['cedula'] ?? '', - dataItem['nombreComercial'] ?? '', - dataItem['direccion'] ?? '', - dataItem['item'] ?? '', - dataItem['descripcion'] ?? '', - dataItem['horaInicio'] ?? '', - dataItem['horaFin'] ?? '', - dataItem['tipoTrabajo'] ?? '', - dataItem['cargoPuesto'] ?? '', - dataItem['telefono'] ?? '', - dataItem['numeroTarea'] ?? '', + await ExcelExportUtility.exportMultipleSheets( + sheets: [ + ExcelSheetData( + sheetName: 'Instalaciones', + headers: [ + 'ID', + 'Fecha Instalación', + 'Cédula', + 'Nombre Comercial', + 'Dirección', + 'Item', + 'Descripción', + 'Hora Inicio', + 'Hora Fin', + 'Tipo Trabajo', + 'Cargo/Puesto', + 'Teléfono', + 'Número Tarea', + ], + rows: data + .map>((dataItem) => [ + dataItem['id'] ?? '', + dataItem['fechaInstalacion'] ?? '', + dataItem['cedula'] ?? '', + dataItem['nombreComercial'] ?? '', + dataItem['direccion'] ?? '', + dataItem['item'] ?? '', + dataItem['descripcion'] ?? '', + dataItem['horaInicio'] ?? '', + dataItem['horaFin'] ?? '', + dataItem['tipoTrabajo'] ?? '', + dataItem['cargoPuesto'] ?? '', + dataItem['telefono'] ?? '', + dataItem['numeroTarea'] ?? '', + ]) + .toList(), + ), ], - sheetName: 'Instalaciones', fileName: 'reporte_instalaciones_filtrado.xlsx', ); } diff --git a/lib/viewmodel/vehiculo_viewmodel.dart b/lib/viewmodel/vehiculo_viewmodel.dart index 42c6304..75942ab 100644 --- a/lib/viewmodel/vehiculo_viewmodel.dart +++ b/lib/viewmodel/vehiculo_viewmodel.dart @@ -32,33 +32,38 @@ class AlquilerViewModel extends BaseViewModel { await handleAsyncOperation(() => _repository.getAllForExport()); if (data != null) { - await ExcelExportUtility.exportToExcel( - collectionName: 'alquileres', - headers: [ - 'ID', - 'Cliente', - 'Fecha Registro Reserva', - 'Fecha Trabajo', - 'Correo Cliente', - 'Teléfono Cliente', - 'Dirección Cliente', - 'Vehículo', - 'Monto Total', - 'Personal Asignado', + await ExcelExportUtility.exportMultipleSheets( + sheets: [ + ExcelSheetData( + sheetName: 'Alquileres', + headers: [ + 'ID', + 'Cliente', + 'Fecha Registro Reserva', + 'Fecha Trabajo', + 'Correo Cliente', + 'Teléfono Cliente', + 'Dirección Cliente', + 'Vehículo', + 'Monto Total', + 'Personal Asignado', + ], + rows: data + .map>((dataItem) => [ + dataItem['id'] ?? '', + dataItem['nombreComercial'] ?? '', + dataItem['fechaReserva']?.toDate()?.toString() ?? '', + dataItem['fechaTrabajo']?.toDate()?.toString() ?? '', + dataItem['correo'] ?? '', + dataItem['telefono'] ?? '', + dataItem['direccion'] ?? '', + dataItem['tipoVehiculo'] ?? '', + dataItem['montoAlquiler']?.toString() ?? '', + dataItem['personalAsistio'] ?? '', + ]) + .toList(), + ), ], - mapper: (dataItem) => [ - dataItem['id'] ?? '', - dataItem['nombreComercial'] ?? '', - dataItem['fechaReserva']?.toDate()?.toString() ?? '', - dataItem['fechaTrabajo']?.toDate()?.toString() ?? '', - dataItem['correo'] ?? '', - dataItem['telefono'] ?? '', - dataItem['direccion'] ?? '', - dataItem['tipoVehiculo'] ?? '', - dataItem['montoAlquiler']?.toString() ?? '', - dataItem['personalAsistio'] ?? '', - ], - sheetName: 'Alquileres', fileName: 'reporte_alquileres.xlsx', ); } @@ -179,33 +184,38 @@ class AlquilerViewModel extends BaseViewModel { )); if (data != null) { - await ExcelExportUtility.exportToExcel( - collectionName: 'alquileres_reserva', - headers: [ - 'ID', - 'Nombre Comercial', - 'Dirección', - 'Teléfono', - 'Correo', - 'Tipo Vehículo', - 'Fecha Reserva', - 'Fecha Trabajo', - 'Monto Alquiler', - 'Personal Asistió', - ], - mapper: (dataItem) => [ - dataItem['id'] ?? '', - dataItem['nombreComercial'] ?? '', - dataItem['direccion'] ?? '', - dataItem['telefono'] ?? '', - dataItem['correo'] ?? '', - dataItem['tipoVehiculo'] ?? '', - dataItem['fechaReserva'] ?? '', - dataItem['fechaTrabajo'] ?? '', - dataItem['montoAlquiler']?.toString() ?? '0', - dataItem['personalAsistio'] ?? '', + await ExcelExportUtility.exportMultipleSheets( + sheets: [ + ExcelSheetData( + sheetName: 'Alquileres por Reserva', + headers: [ + 'ID', + 'Nombre Comercial', + 'Dirección', + 'Teléfono', + 'Correo', + 'Tipo Vehículo', + 'Fecha Reserva', + 'Fecha Trabajo', + 'Monto Alquiler', + 'Personal Asistió', + ], + rows: data + .map>((dataItem) => [ + dataItem['id'] ?? '', + dataItem['nombreComercial'] ?? '', + dataItem['direccion'] ?? '', + dataItem['telefono'] ?? '', + dataItem['correo'] ?? '', + dataItem['tipoVehiculo'] ?? '', + dataItem['fechaReserva'] ?? '', + dataItem['fechaTrabajo'] ?? '', + dataItem['montoAlquiler']?.toString() ?? '0', + dataItem['personalAsistio'] ?? '', + ]) + .toList(), + ), ], - sheetName: 'Alquileres por Reserva', fileName: 'reporte_alquileres_reserva_filtrado.xlsx', ); } @@ -223,33 +233,38 @@ class AlquilerViewModel extends BaseViewModel { )); if (data != null) { - await ExcelExportUtility.exportToExcel( - collectionName: 'alquileres_trabajo', - headers: [ - 'ID', - 'Nombre Comercial', - 'Dirección', - 'Teléfono', - 'Correo', - 'Tipo Vehículo', - 'Fecha Reserva', - 'Fecha Trabajo', - 'Monto Alquiler', - 'Personal Asistió', - ], - mapper: (dataItem) => [ - dataItem['id'] ?? '', - dataItem['nombreComercial'] ?? '', - dataItem['direccion'] ?? '', - dataItem['telefono'] ?? '', - dataItem['correo'] ?? '', - dataItem['tipoVehiculo'] ?? '', - dataItem['fechaReserva'] ?? '', - dataItem['fechaTrabajo'] ?? '', - dataItem['montoAlquiler']?.toString() ?? '0', - dataItem['personalAsistio'] ?? '', + await ExcelExportUtility.exportMultipleSheets( + sheets: [ + ExcelSheetData( + sheetName: 'Alquileres por Trabajo', + headers: [ + 'ID', + 'Nombre Comercial', + 'Dirección', + 'Teléfono', + 'Correo', + 'Tipo Vehículo', + 'Fecha Reserva', + 'Fecha Trabajo', + 'Monto Alquiler', + 'Personal Asistió', + ], + rows: data + .map>((dataItem) => [ + dataItem['id'] ?? '', + dataItem['nombreComercial'] ?? '', + dataItem['direccion'] ?? '', + dataItem['telefono'] ?? '', + dataItem['correo'] ?? '', + dataItem['tipoVehiculo'] ?? '', + dataItem['fechaReserva'] ?? '', + dataItem['fechaTrabajo'] ?? '', + dataItem['montoAlquiler']?.toString() ?? '0', + dataItem['personalAsistio'] ?? '', + ]) + .toList(), + ), ], - sheetName: 'Alquileres por Trabajo', fileName: 'reporte_alquileres_trabajo_filtrado.xlsx', ); } diff --git a/linux/flutter/generated_plugin_registrant.cc b/linux/flutter/generated_plugin_registrant.cc index 64a0ece..03231b1 100644 --- a/linux/flutter/generated_plugin_registrant.cc +++ b/linux/flutter/generated_plugin_registrant.cc @@ -7,9 +7,13 @@ #include "generated_plugin_registrant.h" #include +#include void fl_register_plugins(FlPluginRegistry* registry) { g_autoptr(FlPluginRegistrar) file_selector_linux_registrar = fl_plugin_registry_get_registrar_for_plugin(registry, "FileSelectorPlugin"); file_selector_plugin_register_with_registrar(file_selector_linux_registrar); + g_autoptr(FlPluginRegistrar) open_file_linux_registrar = + fl_plugin_registry_get_registrar_for_plugin(registry, "OpenFileLinuxPlugin"); + open_file_linux_plugin_register_with_registrar(open_file_linux_registrar); } diff --git a/linux/flutter/generated_plugins.cmake b/linux/flutter/generated_plugins.cmake index 2db3c22..cd7a7d9 100644 --- a/linux/flutter/generated_plugins.cmake +++ b/linux/flutter/generated_plugins.cmake @@ -4,6 +4,7 @@ list(APPEND FLUTTER_PLUGIN_LIST file_selector_linux + open_file_linux ) list(APPEND FLUTTER_FFI_PLUGIN_LIST diff --git a/macos/Flutter/GeneratedPluginRegistrant.swift b/macos/Flutter/GeneratedPluginRegistrant.swift index 07a961b..d6d7d5e 100644 --- a/macos/Flutter/GeneratedPluginRegistrant.swift +++ b/macos/Flutter/GeneratedPluginRegistrant.swift @@ -10,6 +10,7 @@ import file_selector_macos import firebase_auth import firebase_core import firebase_storage +import open_file_mac import path_provider_foundation import shared_preferences_foundation import sqflite_darwin @@ -21,6 +22,7 @@ func RegisterGeneratedPlugins(registry: FlutterPluginRegistry) { FLTFirebaseAuthPlugin.register(with: registry.registrar(forPlugin: "FLTFirebaseAuthPlugin")) FLTFirebaseCorePlugin.register(with: registry.registrar(forPlugin: "FLTFirebaseCorePlugin")) FLTFirebaseStoragePlugin.register(with: registry.registrar(forPlugin: "FLTFirebaseStoragePlugin")) + OpenFilePlugin.register(with: registry.registrar(forPlugin: "OpenFilePlugin")) PathProviderPlugin.register(with: registry.registrar(forPlugin: "PathProviderPlugin")) SharedPreferencesPlugin.register(with: registry.registrar(forPlugin: "SharedPreferencesPlugin")) SqflitePlugin.register(with: registry.registrar(forPlugin: "SqflitePlugin")) diff --git a/pubspec.lock b/pubspec.lock index c9c61f7..60b8af2 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -757,6 +757,70 @@ packages: url: "https://pub.dev" source: hosted version: "2.1.0" + open_file: + dependency: "direct main" + description: + name: open_file + sha256: d17e2bddf5b278cb2ae18393d0496aa4f162142ba97d1a9e0c30d476adf99c0e + url: "https://pub.dev" + source: hosted + version: "3.5.10" + open_file_android: + dependency: transitive + description: + name: open_file_android + sha256: "58141fcaece2f453a9684509a7275f231ac0e3d6ceb9a5e6de310a7dff9084aa" + url: "https://pub.dev" + source: hosted + version: "1.0.6" + open_file_ios: + dependency: transitive + description: + name: open_file_ios + sha256: "02996f01e5f6863832068e97f8f3a5ef9b613516db6897f373b43b79849e4d07" + url: "https://pub.dev" + source: hosted + version: "1.0.3" + open_file_linux: + dependency: transitive + description: + name: open_file_linux + sha256: d189f799eecbb139c97f8bc7d303f9e720954fa4e0fa1b0b7294767e5f2d7550 + url: "https://pub.dev" + source: hosted + version: "0.0.5" + open_file_mac: + dependency: transitive + description: + name: open_file_mac + sha256: "1440b1e37ceb0642208cfeb2c659c6cda27b25187a90635c9d1acb7d0584d324" + url: "https://pub.dev" + source: hosted + version: "1.0.3" + open_file_platform_interface: + dependency: transitive + description: + name: open_file_platform_interface + sha256: "101b424ca359632699a7e1213e83d025722ab668b9fd1412338221bf9b0e5757" + url: "https://pub.dev" + source: hosted + version: "1.0.3" + open_file_web: + dependency: transitive + description: + name: open_file_web + sha256: e3dbc9584856283dcb30aef5720558b90f88036360bd078e494ab80a80130c4f + url: "https://pub.dev" + source: hosted + version: "0.0.4" + open_file_windows: + dependency: transitive + description: + name: open_file_windows + sha256: d26c31ddf935a94a1a3aa43a23f4fff8a5ff4eea395fe7a8cb819cf55431c875 + url: "https://pub.dev" + source: hosted + version: "0.0.3" package_config: dependency: transitive description: @@ -774,7 +838,7 @@ packages: source: hosted version: "1.9.1" path_provider: - dependency: transitive + dependency: "direct main" description: name: path_provider sha256: "50c5dd5b6e1aaf6fb3a78b33f6aa3afca52bf903a8a5298f53101fdaee55bbcd" diff --git a/pubspec.yaml b/pubspec.yaml index 6f98e08..871dc0a 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -36,9 +36,6 @@ dependencies: sdk: flutter flutter_localizations: sdk: flutter - - # The following adds the Cupertino Icons font to your application. - # Use with the CupertinoIcons class for iOS style icons. cupertino_icons: ^1.0.8 google_fonts: ^6.2.1 intl: ^0.20.2 @@ -60,6 +57,8 @@ dependencies: firebase_auth: ^5.6.0 build_runner: ^2.5.4 mockito: ^5.4.6 + path_provider: ^2.1.5 + open_file: ^3.5.10 dev_dependencies: flutter_test: diff --git a/test/services/auth_service_test.dart b/test/services/auth_service_test.dart index 3e58493..cc92bb6 100644 --- a/test/services/auth_service_test.dart +++ b/test/services/auth_service_test.dart @@ -30,7 +30,6 @@ void main() { test('valida métodos estáticos de AuthService', () { // Verificar que los métodos estáticos están definidos - expect(AuthService.iniciarSesion, isA()); expect(AuthService.cerrarSesion, isA()); }); }); diff --git a/test/test_setup.dart b/test/test_setup.dart index ddc8172..c6c6ee2 100644 --- a/test/test_setup.dart +++ b/test/test_setup.dart @@ -17,8 +17,6 @@ import 'package:client_service/models/factura.dart'; import 'package:client_service/models/camara.dart'; import 'package:client_service/models/instalacion.dart'; import 'package:client_service/models/vehiculo.dart'; -import 'package:mockito/mockito.dart'; -import 'package:mockito/annotations.dart'; import 'mocks.mocks.dart'; import 'firebase_test_setup.dart'; diff --git a/test/views/edit_factura_screen_test.dart b/test/views/edit_factura_screen_test.dart index c5918a7..7ba3588 100644 --- a/test/views/edit_factura_screen_test.dart +++ b/test/views/edit_factura_screen_test.dart @@ -69,7 +69,7 @@ void main() { // Busca el DropdownButtonFormField de tipo de servicio y haz tap en él final tipoServicioDropdown = find.byWidgetPredicate((widget) => widget is DropdownButtonFormField && - widget.decoration?.labelText == 'Tipo de Servicio'); + widget.decoration.labelText == 'Tipo de Servicio'); await tester.ensureVisible(tipoServicioDropdown); await tester.tap(tipoServicioDropdown); await tester.pumpAndSettle(); diff --git a/test/views/register_client_screen_test.dart b/test/views/register_client_screen_test.dart index 4005963..6141230 100644 --- a/test/views/register_client_screen_test.dart +++ b/test/views/register_client_screen_test.dart @@ -20,7 +20,7 @@ void main() { testWidgets('Registro de cliente - flujo básico', (WidgetTester tester) async { - await tester.pumpWidget(MaterialApp(home: RegistroClientePage())); + await tester.pumpWidget(const MaterialApp(home: RegistroClientePage())); await tester.pumpAndSettle(); // Verificar que la pantalla se carga con los elementos principales