From 3063970f67a89abb980fb3b248c514feb589382e Mon Sep 17 00:00:00 2001 From: = <24mdavid25@gmail.com> Date: Tue, 6 Aug 2024 15:12:01 -0600 Subject: [PATCH] Avances de información de usuario --- assets/img/ordenador.png | Bin 0 -> 9779 bytes devtools_options.yaml | 3 +++ lib/main.dart | 27 ++++++++++++++++----------- lib/src/config/routes.dart | 5 ++++- lib/src/controllers/articles_controller.dart | 1 + lib/src/controllers/category_controller.dart | 31 +++++++++++++++++++++++++++---- lib/src/controllers/category_service.dart | 28 ---------------------------- lib/src/delegates/custom_search_delegate.dart | 95 +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ lib/src/http_api/article_service.dart | 39 +++++++++++++++++++++++++++++++++++++++ lib/src/http_api/articles_api.dart | 3 ++- lib/src/http_api/category_api.dart | 33 +++++++++++++++++++++++++++++---- lib/src/models/article.dart | 13 +++++++++++++ lib/src/models/carrito_model.dart | 9 +++++++++ lib/src/models/category_model.dart | 12 ++++++------ lib/src/pages/article_form.dart | 147 +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ lib/src/pages/articles_page.dart | 14 +++++++++++--- lib/src/pages/carrito_page.dart | 91 +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ lib/src/pages/category/category_widget.dart | 4 ++-- lib/src/pages/category_form.dart | 84 +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++------------------------- lib/src/pages/home_page.dart | 74 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++-------- lib/src/pages/login_page.dart | 5 +++-- lib/src/providers/carrito_provider.dart | 59 +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ pubspec.lock | 24 ++++++++++++++++++++++++ pubspec.yaml | 2 ++ 24 files changed, 708 insertions(+), 95 deletions(-) create mode 100644 assets/img/ordenador.png create mode 100644 devtools_options.yaml delete mode 100644 lib/src/controllers/category_service.dart create mode 100644 lib/src/delegates/custom_search_delegate.dart create mode 100644 lib/src/http_api/article_service.dart create mode 100644 lib/src/models/article.dart create mode 100644 lib/src/models/carrito_model.dart create mode 100644 lib/src/pages/article_form.dart create mode 100644 lib/src/pages/carrito_page.dart create mode 100644 lib/src/providers/carrito_provider.dart diff --git a/assets/img/ordenador.png b/assets/img/ordenador.png new file mode 100644 index 0000000..2cfdb75 Binary files /dev/null and b/assets/img/ordenador.png differ diff --git a/devtools_options.yaml b/devtools_options.yaml new file mode 100644 index 0000000..fa0b357 --- /dev/null +++ b/devtools_options.yaml @@ -0,0 +1,3 @@ +description: This file stores settings for Dart & Flutter DevTools. +documentation: https://docs.flutter.dev/tools/devtools/extensions#configure-extension-enablement-states +extensions: diff --git a/lib/main.dart b/lib/main.dart index a379f1f..6040ac3 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -1,6 +1,7 @@ import 'package:flutter/material.dart'; +import 'package:provider/provider.dart'; import 'package:practica1_flutter/src/config/routes.dart'; -import 'package:practica1_flutter/src/config/routes.dart'; // Importa el archivo de rutas +import 'package:practica1_flutter/src/providers/carrito_provider.dart'; void main() { runApp(const MyApp()); @@ -11,17 +12,21 @@ class MyApp extends StatelessWidget { @override Widget build(BuildContext context) { - return MaterialApp( - debugShowCheckedModeBanner: false, - title: 'Practica Flutter', - theme: ThemeData( - colorScheme: ColorScheme.fromSeed(seedColor: Colors.deepPurple), - useMaterial3: true, + return MultiProvider( + providers: [ + ChangeNotifierProvider(create: (_) => CarritoProvider()), + ], + child: MaterialApp( + debugShowCheckedModeBanner: false, + title: 'Practica Flutter', + theme: ThemeData( + colorScheme: ColorScheme.fromSeed(seedColor: Colors.deepPurple), + useMaterial3: true, + ), + //home: myWidget() : Se quita la propiedad home para permitir la navegación basada en rutas + initialRoute: 'login', // se define la ruta inicial + routes: getApplicationRoutes(),// Se usa las rutas definidas en routes.dart y se quito el myWidget ), - //home: myWidget() : Se quita la propiedad home para permitir la navegación basada en rutas - initialRoute: 'login', // se define la ruta inicial - routes: getApplicationRoutes(), // Se usa las rutas definidas en routes.dart y se quito el myWidget ); } } - diff --git a/lib/src/config/routes.dart b/lib/src/config/routes.dart index 4a265ba..7716d51 100644 --- a/lib/src/config/routes.dart +++ b/lib/src/config/routes.dart @@ -1,5 +1,6 @@ import 'package:flutter/material.dart'; import 'package:practica1_flutter/src/config/routes.dart'; +import 'package:practica1_flutter/src/pages/article_form.dart'; import '../pages/category/category_widget.dart'; import '../pages/category_form.dart'; import '../pages/home_page.dart'; @@ -9,9 +10,11 @@ import '../pages/articles_page.dart'; Map getApplicationRoutes() { return { 'login': (BuildContext context) => const LoginPage(), - 'homeSeller': (BuildContext context) => const HomeSellerPage(), + 'homeSeller': (BuildContext context) => HomeSellerPage(), //'articulos': (BuildContext context) => const ArticlesPage(), 'category_form': (BuildContext context) => CategoryForm(), + //'cart': (BuildContext context) => CarritoPage(), + 'article_form': (BuildContext context) => ArticleFormPage(), // Define la ruta }; } diff --git a/lib/src/controllers/articles_controller.dart b/lib/src/controllers/articles_controller.dart index 4079bbe..612c18e 100644 --- a/lib/src/controllers/articles_controller.dart +++ b/lib/src/controllers/articles_controller.dart @@ -3,6 +3,7 @@ import 'package:connectivity_plus/connectivity_plus.dart'; import 'package:practica1_flutter/src/models/article_model.dart'; import '../http_api/articles_api.dart'; +import '../models/article.dart'; class ArticleController { final Connectivity _connectivity = Connectivity(); diff --git a/lib/src/controllers/category_controller.dart b/lib/src/controllers/category_controller.dart index 63fe49c..5cbdb65 100644 --- a/lib/src/controllers/category_controller.dart +++ b/lib/src/controllers/category_controller.dart @@ -4,22 +4,22 @@ import 'package:connectivity_plus/connectivity_plus.dart'; import 'package:practica1_flutter/src/http_api/category_api.dart'; import 'package:practica1_flutter/src/models/category_model.dart'; - -//en este manejaremos la información que vamos a presentar visualmente al usuario. class CategoryController { final Connectivity _connectivity = Connectivity(); + final CategoryService _categoryService = CategoryService(); + // Método para obtener las categorías Future> getCategories() async { Map mapResp = { 'ok': false, 'message': 'No hay categorías', 'data': null }; + ConnectivityResult connectivityResult = await _connectivity.checkConnectivity(); if (connectivityResult != ConnectivityResult.none) { if (connectivityResult == ConnectivityResult.wifi || connectivityResult == ConnectivityResult.mobile) { - CategoryService categoryApi = CategoryService(); - Map respGet = await categoryApi.getCategories(); + Map respGet = await _categoryService.getCategories(); if (respGet['statusCode'] == 200) { try { var decodeResp = json.decode(respGet['body']); @@ -36,7 +36,30 @@ class CategoryController { } } } + return mapResp; + } + + // Método para agregar una categoría + Future> addCategory(CategoryModel category) async { + Map mapResp = { + 'ok': false, + 'message': 'No se pudo agregar la categoría', + 'data': null + }; + ConnectivityResult connectivityResult = await _connectivity.checkConnectivity(); + if (connectivityResult != ConnectivityResult.none) { + if (connectivityResult == ConnectivityResult.wifi || connectivityResult == ConnectivityResult.mobile) { + Map respPost = await _categoryService.addCategory(category); + if (respPost['statusCode'] == 200) { + mapResp['ok'] = true; + mapResp['message'] = "Categoría agregada exitosamente"; + mapResp['data'] = json.decode(respPost['body']); + } else { + mapResp['message'] = "${respPost['body']}"; + } + } + } return mapResp; } } diff --git a/lib/src/controllers/category_service.dart b/lib/src/controllers/category_service.dart deleted file mode 100644 index a23015a..0000000 --- a/lib/src/controllers/category_service.dart +++ /dev/null @@ -1,28 +0,0 @@ -import 'dart:convert'; -import 'package:http/http.dart' as http; -import 'package:practica1_flutter/src/models/category_model.dart'; - -class CategoryService { - static const String _baseUrl = 'https://basic2.visorus.com.mx'; - Future> addCategory(CategoryModel category) async { - final String url = '$_baseUrl/categoria'; - - final Map categoryData = category.toJson(); - - try { - final http.Response response = await http.post( - Uri.parse(url), - body: json.encode({"data": [categoryData]}), - headers: {'Content-Type': 'application/json'}, - ); - - if (response.statusCode == 200) { - return {"statusCode": response.statusCode, "body": json.decode(response.body)}; - } else { - return {"statusCode": response.statusCode, "body": response.body}; - } - } catch (e) { - return {"statusCode": 501, "body": '$e'}; - } - } -} diff --git a/lib/src/delegates/custom_search_delegate.dart b/lib/src/delegates/custom_search_delegate.dart new file mode 100644 index 0000000..3490f2c --- /dev/null +++ b/lib/src/delegates/custom_search_delegate.dart @@ -0,0 +1,95 @@ +import 'package:flutter/material.dart'; +import 'package:provider/provider.dart'; +import '../models/article_model.dart'; +import '../providers/carrito_provider.dart'; + +class CustomSearchDelegate extends SearchDelegate { + final List searchList; + + CustomSearchDelegate(this.searchList); + + @override + List buildActions(BuildContext context) { + return [ + IconButton( + icon: Icon(Icons.clear), + onPressed: () { + query = ''; + }, + ), + ]; + } + + @override + Widget buildLeading(BuildContext context) { + return IconButton( + icon: Icon(Icons.arrow_back), + onPressed: () => Navigator.of(context).pop(), + ); + } + + @override + Widget buildResults(BuildContext context) { + final List searchResults = searchList + .where((item) => item.nombre.toLowerCase().contains(query.toLowerCase())) + .toList(); + + //AGREGAR PRODUCTO AL CARRITO + return Scaffold( + body: ListView.builder( + itemCount: searchResults.length, + itemBuilder: (context, index) { + final article = searchResults[index]; + return ListTile( + title: Text(article.nombre), + subtitle: Text('Precio: ${article.precios[0].precio}'), + trailing: IconButton( + //icono del carrito + icon: Icon(Icons.add_shopping_cart, color: Colors.indigo), + onPressed: () { + //Llamamos el provider carrito para agregar un articulo + final carritoProvider = Provider.of(context, listen: false); + carritoProvider.addToCart(article); + ScaffoldMessenger.of(context).showSnackBar( + SnackBar(content: Text('Artículo agregado al carrito')), + ); + }, + ), + ); + }, + ), + ); + } + + @override + Widget buildSuggestions(BuildContext context) { + final List suggestionList = query.isEmpty + ? [] + : searchList + .where((item) => item.nombre.toLowerCase().contains(query.toLowerCase())) + .toList(); + + return ListView.builder( + itemCount: suggestionList.length, + itemBuilder: (context, index) { + return Container( + color: Colors.grey[200], + child: ListTile( + leading: Icon(Icons.search, color: Colors.blue), + title: Text( + suggestionList[index].nombre, + style: TextStyle( + color: Colors.black, + fontWeight: FontWeight.bold, + ), + ), + onTap: () { + query = suggestionList[index].nombre; + showResults(context); + }, + ), + ); + }, + ); + } +} diff --git a/lib/src/http_api/article_service.dart b/lib/src/http_api/article_service.dart new file mode 100644 index 0000000..2b82c04 --- /dev/null +++ b/lib/src/http_api/article_service.dart @@ -0,0 +1,39 @@ +import 'package:http/http.dart' as http; +import 'dart:convert'; + +import '../models/article_model.dart'; + +//METODO DE AGREGAR NUEVOS ARTICULOS +class ArticleService { + final String _baseUrl = 'https://basic2.visorus.com.mx/articulo'; + + Future addArticle(Map articleData) async { + final response = await http.post( + Uri.parse(_baseUrl), + headers: {'Content-Type': 'application/json'}, + body: json.encode(articleData), + ); + + if (response.statusCode == 200) { + return true; + } else { + return false; + } + } +} + + + +class ArticleService1 { + Future> getArticles() async { + final response = await http.get( + Uri.parse('https://basic2.visorus.com.mx/articulo?offset=0&max=100')); + if (response.statusCode == 200) { + final Map data = json.decode(response.body); + List articlesData = data['data']; + return articlesData.map((json) => ArticleModel.fromJson(json)).toList(); + } else { + throw Exception('Error al cargar los artículos'); + } + } +} \ No newline at end of file diff --git a/lib/src/http_api/articles_api.dart b/lib/src/http_api/articles_api.dart index ef3832d..278a21f 100644 --- a/lib/src/http_api/articles_api.dart +++ b/lib/src/http_api/articles_api.dart @@ -3,6 +3,7 @@ import 'package:http/http.dart' as http; import 'dart:convert'; import '../../environments/api_app.dart'; +import '../models/article_model.dart'; class ArticleApi { Future> getArticles() async{ @@ -18,4 +19,4 @@ class ArticleApi { return {"statusCode": 501, "body": '$e'}; } } -} \ No newline at end of file +} diff --git a/lib/src/http_api/category_api.dart b/lib/src/http_api/category_api.dart index 7a76ec4..f3439e0 100644 --- a/lib/src/http_api/category_api.dart +++ b/lib/src/http_api/category_api.dart @@ -4,17 +4,16 @@ import 'package:http/http.dart' as http; import 'package:flutter/foundation.dart'; import 'package:connectivity_plus/connectivity_plus.dart'; import 'package:practica1_flutter/environments/api_app.dart'; +import 'package:practica1_flutter/src/models/category_model.dart'; class CategoryService { + // Método para obtener las categorías desde la API Future> getCategories() async { - final ConnectivityResult connectivityResult = await (Connectivity().checkConnectivity()); - //El Connectivity().checkConnectivity() verifica el estado actual de la conexión a Internet if (connectivityResult == ConnectivityResult.none) { return {"statusCode": 0, "body": "No internet connection"}; - } // y Si no hay conexión a Internet se retorna - // statusCode 0 y un mensaje de sin conexion a internet + } String url = '${apiApp}/categoria?offset=0&max=100'; if (kDebugMode) { @@ -27,4 +26,30 @@ class CategoryService { return {"statusCode": 501, "body": '$e'}; } } + + // metodo para agregar una nueva categoría a la API + Future> addCategory(CategoryModel category) async { + final ConnectivityResult connectivityResult = await (Connectivity().checkConnectivity()); + + if (connectivityResult == ConnectivityResult.none) { + return {"statusCode": 0, "body": "No internet connection"}; + } + + String url = '${apiApp}/categoria'; + if (kDebugMode) { + print('Url -> $url'); + } + + try { + final response = await http.post( + Uri.parse(url), + headers: {"Content-Type": "application/json"}, + body: json.encode(category.toJson()), // Enviar directamente el JSON del objeto + ); + + return {"statusCode": response.statusCode, "body": response.body}; + } catch (e) { + return {"statusCode": 501, "body": '$e'}; + } + } } diff --git a/lib/src/models/article.dart b/lib/src/models/article.dart new file mode 100644 index 0000000..4eaf843 --- /dev/null +++ b/lib/src/models/article.dart @@ -0,0 +1,13 @@ +class Article { + final String name; + final List prices; +//Para mi carrito + Article({required this.name, required this.prices}); + + factory Article.fromJson(Map json) { + return Article( + name: json['name'], + prices: List.from(json['prices'].map((x) => x.toDouble())), + ); + } +} diff --git a/lib/src/models/carrito_model.dart b/lib/src/models/carrito_model.dart new file mode 100644 index 0000000..cd92475 --- /dev/null +++ b/lib/src/models/carrito_model.dart @@ -0,0 +1,9 @@ +import 'article.dart'; + +class CarritoModel { + final Article articulo; + int cantidad; + double precio; + + CarritoModel({required this.articulo, required this.cantidad, required this.precio}); +} diff --git a/lib/src/models/category_model.dart b/lib/src/models/category_model.dart index 7f80f3d..c2cdc6a 100644 --- a/lib/src/models/category_model.dart +++ b/lib/src/models/category_model.dart @@ -44,18 +44,18 @@ class CategoryModel { Map toJson() { return { "id": id, - "version": version, - "clave": key, - "nombre": name, - "fechaCreado": createdDate, - "categoria": parentCategory?.toJson(), + "clave": key ?? '', + "nombre": name ?? '', + "fechaCreado": createdDate ?? 0, "categorias": subCategories != null ? CategoryModel.toJsonArray(subCategories!) : [], - "activo": active, + "activo": active ?? true, }; } + + // Método para crear una lista de CategoryModel desde un JSON static List fromJsonArray(json) { if (json == null) return []; diff --git a/lib/src/pages/article_form.dart b/lib/src/pages/article_form.dart new file mode 100644 index 0000000..194e4c1 --- /dev/null +++ b/lib/src/pages/article_form.dart @@ -0,0 +1,147 @@ +import 'package:flutter/material.dart'; +import 'package:http/http.dart' as http; +import 'dart:convert'; +import '../http_api/article_service.dart'; + +class ArticleFormPage extends StatefulWidget { + @override + _ArticleFormPageState createState() => _ArticleFormPageState(); +} + +class _ArticleFormPageState extends State { + final _formKey = GlobalKey(); + String clave = ''; + int categoria = 1; + String nombre = ''; + List precios = [0.0, 0.0]; + bool activo = true; + + Future _submitForm() async { + if (_formKey.currentState?.validate() ?? false) { + _formKey.currentState?.save(); + + final payload = { + "clave": clave, + "categoria": categoria, + "nombre": nombre, + "precios": precios.map((precio) => {"precio": precio}).toList(), + "activo": activo, + }; + + final articleService = ArticleService(); + final success = await articleService.addArticle(payload); + + if (success) { + Navigator.pop(context); // Regresa a la lista de artículos + } else { + // Maneja el error + ScaffoldMessenger.of(context).showSnackBar( + SnackBar(content: Text('Error al guardar el artículo')), + ); + } + } + } + + @override + Widget build(BuildContext context) { + return Scaffold( + appBar: AppBar( + title: Text('Formulario de Artículo'), + centerTitle: true, + foregroundColor: Colors.white, + backgroundColor: Colors.teal, + ), + body: Padding( + padding: EdgeInsets.all(60.0), + child: Form( + key: _formKey, + child: ListView( + children: [ + TextFormField( + decoration: InputDecoration(labelText: 'Clave', + border: OutlineInputBorder()), + // + onSaved: (value) => clave = value ?? '', + validator: (value) { + if (value == null || value.isEmpty) { + return 'Por favor ingresa la clave'; + } + return null; + }, + ), + SizedBox(height: 20), + + TextFormField( + decoration: InputDecoration(labelText: 'Categoría', + border: OutlineInputBorder()), + keyboardType: TextInputType.number, + // + onSaved: (value) => categoria = int.parse(value ?? '1'), + validator: (value) { + if (value == null || value.isEmpty) { + return 'Por favor ingresa la categoría'; + } + return null; + }, + ), + SizedBox(height: 20), + + TextFormField( + decoration: InputDecoration(labelText: 'Nombre', border: OutlineInputBorder()), + onSaved: (value) => nombre = value ?? '', + validator: (value) { + if (value == null || value.isEmpty) { + return 'Por favor ingresa el nombre'; + } + return null; + }, + ), + SizedBox(height: 20), + + TextFormField( + decoration: InputDecoration(labelText: 'Precio 1', border: OutlineInputBorder()), + keyboardType: TextInputType.number, + onSaved: (value) => precios[0] = double.parse(value ?? '0.0'), + validator: (value) { + if (value == null || value.isEmpty) { + return 'Por favor ingresa el precio'; + } + return null; + }, + ), + SizedBox(height: 20), + + TextFormField( + decoration: InputDecoration(labelText: 'Precio 2', border: OutlineInputBorder()), + keyboardType: TextInputType.number, + onSaved: (value) => precios[1] = double.parse(value ?? '0.0'), + validator: (value) { + if (value == null || value.isEmpty) { + return 'Por favor ingresa el precio'; + } + return null; + }, + ), + SizedBox(height: 20), + + SwitchListTile( + title: Text('Activo'), + value: activo, + onChanged: (value) { + setState(() { + activo = value; + }); + }, + ), + SizedBox(height: 20), + ElevatedButton( + onPressed: _submitForm, + child: Text('Guardar'), + ), + ], + ), + ), + ), + ); + } +} diff --git a/lib/src/pages/articles_page.dart b/lib/src/pages/articles_page.dart index 15c1b3a..9e66915 100644 --- a/lib/src/pages/articles_page.dart +++ b/lib/src/pages/articles_page.dart @@ -32,8 +32,16 @@ class _ArticlePageState extends State { title: const Text('Artículos'), backgroundColor: Colors.indigoAccent, foregroundColor: Colors.white, - ), + ), + //BOTON DE AGREGAR ARTICULO + floatingActionButton: FloatingActionButton( + onPressed: () { + Navigator.pushNamed(context, 'article_form'); + }, + child: Icon(Icons.add), + tooltip: 'Agregar Artículo', + ), body: FutureBuilder>( future: _articlesFuture, builder: (context, snapshot) { @@ -42,7 +50,7 @@ class _ArticlePageState extends State { if (snapshot.data!['data'] != null && (snapshot.data!['data'] as List).isNotEmpty) { List articles = snapshot.data!['data'] as List; - //Lista de articulos + //Lista de articulos return ListView.builder( // Definimos el número de elementos en la lista. itemCount: articles.length, @@ -51,7 +59,7 @@ class _ArticlePageState extends State { return Card( child: ListTile( title: Text(article.nombre), - subtitle: Text('id :${article.categoriaId}'), + subtitle: Text('id :${article.clave}'), //subtitle: Text('Clave: ${article.clave}'), trailing: Text('\$${article.precios.isNotEmpty ? article.precios.first.precio.toStringAsFixed(2) : 'N/A'}'), ), diff --git a/lib/src/pages/carrito_page.dart b/lib/src/pages/carrito_page.dart new file mode 100644 index 0000000..b23132a --- /dev/null +++ b/lib/src/pages/carrito_page.dart @@ -0,0 +1,91 @@ +import 'package:flutter/material.dart'; +import 'package:provider/provider.dart'; + +import '../providers/carrito_provider.dart'; + +class CarritoPage extends StatelessWidget { + @override + Widget build(BuildContext context) { + return Scaffold( + appBar: AppBar( + title: Text('Mi carrito de compras'), + backgroundColor: Colors.amber + ), + + body: Consumer( + builder: (context, carritoProvider, child) { + final carrito = carritoProvider.carrito; + return ListView.builder( + itemCount: carrito.length, + itemBuilder: (context, index) { + final item = carrito[index]; + return Card( + child: ListTile( + leading:Image.asset('assets/img/logo_visorus.jpg', height: 15), // Imagen del artículo + title: Text('${item.articulo.nombre}'),// TITULO DEL ARTICULO + subtitle: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Row( + //mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + //BOTON DE ELIMINAR - + IconButton( + icon: Icon(Icons.remove,color: Colors.red), + onPressed: () { + carritoProvider.updateQuantity(item.articulo, item.cantidad - 1); + }, + ), + Text('${item.cantidad}'), + //AGREGAR + PRODUCTO + IconButton( + icon: Icon(Icons.add, color: Colors.blue), + onPressed: () { + carritoProvider.updateQuantity(item.articulo, item.cantidad + 1); + }, + ), + //CALCULAR PERCIO TOTAL DEL PRODUCTO + Text('Subtotal: \$${(item.precio * item.cantidad).toStringAsFixed(2)}'), + ], + ), + ], + ), + trailing: IconButton( + icon: Icon(Icons.delete, color: Colors.red), + onPressed: () { + carritoProvider.removeFromCart(item.articulo); + }, + ), + ), + ); + }, + ); + }, + ), + bottomNavigationBar: Consumer( + builder: (context, carritoProvider, child) { + return Container( + padding: EdgeInsets.all(8.0), + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Text( + 'Total: \$${carritoProvider.totalAmount.toStringAsFixed(2)}', + style: TextStyle(fontSize: 20.0, fontWeight: FontWeight.bold), + ), + ElevatedButton( + onPressed: () { + ScaffoldMessenger.of(context).showSnackBar( + SnackBar(content: Text('Compra confirmada')), + ); + }, + child: Text('COMPRAR'), + ), + ], + ), + ); + }, + ), + ); + } +} diff --git a/lib/src/pages/category/category_widget.dart b/lib/src/pages/category/category_widget.dart index d06826c..5fb346d 100644 --- a/lib/src/pages/category/category_widget.dart +++ b/lib/src/pages/category/category_widget.dart @@ -12,6 +12,7 @@ class _CategoryWidgetState extends State { final CategoryController _categoryCtrl = CategoryController(); @override + //MOSTRAR ARTICULOS Widget build(BuildContext context) { return SafeArea( child: Padding( @@ -34,8 +35,7 @@ class _CategoryWidgetState extends State { // Navegar al listado de artículos y pasa el ID de la categoría Navigator.push( //El push lo que hace es empujar la ruta dada por medio de la clase - // MaterialPageRoute, en esta clase se envía el contexto y el widget - // de la nueva pantalla que va a mostrar. + // MaterialPageRoute, en esta clase se envía el contexto y el widget de la nueva pantalla que va a mostrar. context, MaterialPageRoute( builder: (context) => ArticlePage( diff --git a/lib/src/pages/category_form.dart b/lib/src/pages/category_form.dart index 85c8fa7..3b1f71d 100644 --- a/lib/src/pages/category_form.dart +++ b/lib/src/pages/category_form.dart @@ -1,4 +1,8 @@ +import 'dart:convert'; + import 'package:flutter/material.dart'; +import 'package:practica1_flutter/src/models/category_model.dart'; +import '../http_api/category_api.dart'; class CategoryForm extends StatelessWidget { final GlobalKey _formKey = GlobalKey(); @@ -6,24 +10,67 @@ class CategoryForm extends StatelessWidget { final TextEditingController _fechaCreadoController = TextEditingController(); final TextEditingController _nombreController = TextEditingController(); - // Función para obtener la fecha en milisegundos int _getFechaCreadoEnMilisegundos(String fecha) { try { - // convertimos el String a DateTime y luego obtenemos el tiempo en milisegundos DateTime fechaCreado = DateTime.parse(fecha); return fechaCreado.millisecondsSinceEpoch; } catch (e) { - // Si hay un error en la conversión, devolvemos 0 return 0; } } + // Función para guardar la categoría + void _saveCategory(BuildContext context) async { + + if (_formKey.currentState!.validate()) { + final clave = _claveController.text.trim(); + final nombre = _nombreController.text.trim(); + final fechaCreadoStr = _fechaCreadoController.text.trim(); + + if (clave.isEmpty || nombre.isEmpty || fechaCreadoStr.isEmpty) { + ScaffoldMessenger.of(context).showSnackBar( + SnackBar(content: Text('Todos los campos deben estar completos')), + ); + return; + } + + final fechaCreado = _getFechaCreadoEnMilisegundos(fechaCreadoStr); + + CategoryModel category = CategoryModel( + id: 0, + version: 0, + key: clave, + name: nombre, + createdDate: fechaCreado, + active: true, + ); + + final categoryService = CategoryService(); + final response = await categoryService.addCategory(category); + + print('Response Status Code: ${response['statusCode']}'); + print('Response Body: ${response['body']}'); + + if (response['statusCode'] == 200) { + ScaffoldMessenger.of(context).showSnackBar( + SnackBar(content: Text('Categoría guardada exitosamente')), + ); + Navigator.pushReplacementNamed(context, 'homeSeller'); + } else { + final responseBody = json.decode(response['body']); + final errorMessage = responseBody['error'] ?? 'Error desconocido'; + ScaffoldMessenger.of(context).showSnackBar( + SnackBar(content: Text('Error al guardar categoría: $errorMessage')), + ); + } + } + } + @override Widget build(BuildContext context) { return Scaffold( appBar: AppBar( title: const Text('Agregar Categoría'), - backgroundColor: Colors.red ), body: Padding( padding: const EdgeInsets.all(16.0), @@ -31,11 +78,11 @@ class CategoryForm extends StatelessWidget { key: _formKey, child: Column( children: [ + // ----- AGREGAR CLAVE ---- TextFormField( controller: _claveController, decoration: InputDecoration(labelText: 'Clave', - border: OutlineInputBorder() - ), + border: OutlineInputBorder()), validator: (value) { if (value == null || value.isEmpty) { return 'Por favor ingrese la clave'; @@ -45,16 +92,15 @@ class CategoryForm extends StatelessWidget { ), SizedBox(height: 20), + // ----- AGREGAR FECHA ---- TextFormField( controller: _fechaCreadoController, - decoration: InputDecoration( - labelText: 'Fecha Creado (YYYY-MM-DD)', + decoration: InputDecoration(labelText: 'Fecha FORMATO(YYYY-MM-DD)', border: OutlineInputBorder()), validator: (value) { if (value == null || value.isEmpty) { return 'Por favor ingrese la fecha en formato YYYY-MM-DD'; } - // Validación de la fecha try { DateTime.parse(value); } catch (e) { @@ -64,14 +110,11 @@ class CategoryForm extends StatelessWidget { }, ), SizedBox(height: 20), - + // ----- AGREGAR NOMBRE DEL ARTICULO ---- TextFormField( - //padding: const EdgeInsets.symmetric(horizontal: 80.0), // añado mi margen horizontal controller: _nombreController, - decoration: InputDecoration( - labelText: 'Nombre', - border: OutlineInputBorder(), - ), + decoration: InputDecoration(labelText: 'Nombre', + border: OutlineInputBorder()), validator: (value) { if (value == null || value.isEmpty) { return 'Por favor ingrese el nombre'; @@ -81,16 +124,7 @@ class CategoryForm extends StatelessWidget { ), SizedBox(height: 20), ElevatedButton( - onPressed: () { - if (_formKey.currentState!.validate()) { - // Guardar los valores y mostrar mensaje en consola - print({ - "clave": _claveController.text, - "fechaCreado": _getFechaCreadoEnMilisegundos(_fechaCreadoController.text), - "nombre": _nombreController.text, - }); - } - }, + onPressed: () => _saveCategory(context), child: Text('GUARDAR'), ), ], diff --git a/lib/src/pages/home_page.dart b/lib/src/pages/home_page.dart index 1cb7490..64de5f4 100644 --- a/lib/src/pages/home_page.dart +++ b/lib/src/pages/home_page.dart @@ -1,9 +1,34 @@ import 'package:flutter/material.dart'; -import 'package:practica1_flutter/src/pages/category/category_widget.dart'; // Asegúrate de importar la página de categorías -import 'package:practica1_flutter/src/pages/category_form.dart'; // Asegúrate de importar la página del formulario +import 'package:badges/badges.dart' as badges; +import 'package:provider/provider.dart'; +import 'package:practica1_flutter/src/pages/category/category_widget.dart'; +import '../delegates/custom_search_delegate.dart'; +import '../http_api/article_service.dart'; +import '../http_api/articles_api.dart'; +import '../models/article_model.dart'; +import '../providers/carrito_provider.dart'; +import 'carrito_page.dart'; -class HomeSellerPage extends StatelessWidget { - const HomeSellerPage({Key? key}) : super(key: key); +class HomeSellerPage extends StatefulWidget { + @override + _HomeSellerPageState createState() => _HomeSellerPageState(); +} + +class _HomeSellerPageState extends State { + List articles = []; + + @override + void initState() { + super.initState(); + _fetchArticles(); + } + + _fetchArticles() async { + var fetchedArticles = await ArticleService1().getArticles(); + setState(() { + articles = fetchedArticles; + }); + } @override Widget build(BuildContext context) { @@ -11,10 +36,43 @@ class HomeSellerPage extends StatelessWidget { length: 2, child: Scaffold( appBar: AppBar( - title: const Text("ShopMaster"), + title: const Text("Tienda Prueba"), centerTitle: true, foregroundColor: Colors.white, backgroundColor: Colors.indigoAccent, + actions: [ + IconButton( + icon: Icon(Icons.search, color: Colors.green), + onPressed: () { + showSearch( + context: context, + delegate: CustomSearchDelegate(articles), // Pasando la lista de artículos + ); + }, + ), + // AGREGAR PRODUCTO AL CARRITO + Consumer( + builder: (context, carrito, child) { + return badges.Badge( + badgeContent: Text( + carrito.totalItems.toString(), + style: TextStyle(color: Colors.white)//Color num notificación + + ), + position: badges.BadgePosition.topEnd(top: 0, end: 3), + child: IconButton( + icon: Icon(Icons.card_travel_outlined), + onPressed: () { + Navigator.push( + context, + MaterialPageRoute(builder: (context) => CarritoPage()), + ); + }, + ), + ); + }, + ), + ], bottom: const TabBar( tabs: [ Tab(icon: Icon(Icons.article_outlined), text: "Artículos"), @@ -23,9 +81,10 @@ class HomeSellerPage extends StatelessWidget { ), ), drawer: MenuLateral(), + body: TabBarView( children: [ - CategoryWidget(), // Página de categorías + CategoryWidget(), ], ), floatingActionButton: FloatingActionButton( @@ -33,14 +92,13 @@ class HomeSellerPage extends StatelessWidget { Navigator.pushNamed(context, 'category_form'); }, child: Icon(Icons.add), - tooltip: 'Agregar Categoría', // Mensaje para el botón + tooltip: 'Agregar Categoría', ), ), ); } } -// Clase del menú lateral class MenuLateral extends StatelessWidget { @override Widget build(BuildContext context) { diff --git a/lib/src/pages/login_page.dart b/lib/src/pages/login_page.dart index 9de2584..f0dc90e 100644 --- a/lib/src/pages/login_page.dart +++ b/lib/src/pages/login_page.dart @@ -8,7 +8,7 @@ class LoginPage extends StatelessWidget { return Scaffold( appBar: AppBar( title: const Text('Estoy en Login'), - backgroundColor: Colors.lightBlue, // Agregar color en la appBar + backgroundColor: Colors.lightBlue, ), body: Center( child: Column( @@ -29,7 +29,7 @@ class LoginPage extends StatelessWidget { const SizedBox(height: 20), Padding( - padding: const EdgeInsets.symmetric(horizontal: 80.0), // Añado mi margen horizontal + padding: const EdgeInsets.symmetric(horizontal: 80.0), child: TextField( decoration: const InputDecoration( labelText: 'Contraseña', @@ -46,6 +46,7 @@ class LoginPage extends StatelessWidget { }, child: const Text('Acceder'), ), + ], ), ), diff --git a/lib/src/providers/carrito_provider.dart b/lib/src/providers/carrito_provider.dart new file mode 100644 index 0000000..8e8da6f --- /dev/null +++ b/lib/src/providers/carrito_provider.dart @@ -0,0 +1,59 @@ +import 'package:flutter/material.dart'; +import '../models/article_model.dart'; + +class CarritoModel { + final ArticleModel articulo; + int cantidad; + double precio; + + CarritoModel({required this.articulo, required this.cantidad, required this.precio}); +} + +class CarritoProvider with ChangeNotifier { + List _carrito = []; + + List get carrito => _carrito; + + void addToCart(ArticleModel article) { + var existingItem = _carrito.firstWhere( // firstWhere busca un artículo en el carrito que tenga el mismo ID que el artículo + (item) => item.articulo.id == article.id, + orElse: () => CarritoModel(articulo: article, cantidad: 0, precio: 0), + ); + + if (existingItem.cantidad == 0) { + CarritoModel item = CarritoModel( + articulo: article, + cantidad: 1, + precio: article.precios[0].precio, + ); + _carrito.add(item); + } else { + existingItem.cantidad += 1; + } + notifyListeners(); // esto nos notifica los widgets que escuchan cambios en el carrito de compras, hace que se actualicen y reflejen los cambios. + } + + void removeFromCart(ArticleModel article) { + _carrito.removeWhere((item) => item.articulo.id == article.id); + notifyListeners(); + } + + void updateQuantity(ArticleModel article, int quantity) { + var existingItem = _carrito.firstWhere( + (item) => item.articulo.id == article.id, + orElse: () => CarritoModel(articulo: article, cantidad: 0, precio: 0), + ); + + if (existingItem.cantidad != 0) { + existingItem.cantidad = quantity; + if (existingItem.cantidad <= 0) { + removeFromCart(article); + } + } + notifyListeners(); + } + + double get totalAmount => _carrito.fold(0.0, (total, current) => total + (current.precio * current.cantidad)); + + int get totalItems => _carrito.fold(0, (total, current) => total + current.cantidad); +} diff --git a/pubspec.lock b/pubspec.lock index 923be8e..d056412 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -17,6 +17,14 @@ packages: url: "https://pub.dev" source: hosted version: "2.11.0" + badges: + dependency: "direct main" + description: + name: badges + sha256: a7b6bbd60dce418df0db3058b53f9d083c22cdb5132a052145dc267494df0b84 + url: "https://pub.dev" + source: hosted + version: "3.1.2" boolean_selector: dependency: transitive description: @@ -232,6 +240,14 @@ packages: url: "https://pub.dev" source: hosted version: "1.12.0" + nested: + dependency: transitive + description: + name: nested + sha256: "03bac4c528c64c95c722ec99280375a6f2fc708eec17c7b3f07253b626cd2a20" + url: "https://pub.dev" + source: hosted + version: "1.0.0" nm: dependency: transitive description: @@ -264,6 +280,14 @@ packages: url: "https://pub.dev" source: hosted version: "2.1.8" + provider: + dependency: "direct dev" + description: + name: provider + sha256: c8a055ee5ce3fd98d6fc872478b03823ffdb448699c6ebdbbc71d59b596fd48c + url: "https://pub.dev" + source: hosted + version: "6.1.2" sky_engine: dependency: transitive description: flutter diff --git a/pubspec.yaml b/pubspec.yaml index b0b87f3..b63fb79 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -27,6 +27,7 @@ dependencies: sdk: flutter http: ^1.2.2 cupertino_icons: ^1.0.6 + badges: ^3.1.2 dev_dependencies: flutter_test: @@ -34,6 +35,7 @@ dev_dependencies: flutter_lints: ^4.0.0 connectivity_plus: ^2.3.0 + provider: ^6.0.2 flutter: # The following line ensures that the Material Icons font is -- libgit2 0.27.1