pedido_form.dart 20 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585
  1. import 'dart:async';
  2. import 'package:flutter/foundation.dart';
  3. import 'package:flutter/material.dart';
  4. import 'package:intl/intl.dart';
  5. import 'package:pdf/pdf.dart';
  6. import 'package:printing/printing.dart';
  7. import 'package:provider/provider.dart';
  8. import 'package:sqflite/sqflite.dart';
  9. import 'package:yoshi_papas_app/models/pedido_producto_model.dart';
  10. import 'package:yoshi_papas_app/views/pedido/pedido_ticket.dart';
  11. import 'package:yoshi_papas_app/widgets/widgets_components.dart';
  12. import 'package:yoshi_papas_app/models/item_carrito_model.dart';
  13. import '../../themes/themes.dart';
  14. import '../../models/models.dart';
  15. import '../../viewmodels/viewmodels.dart';
  16. import 'package:collection/collection.dart';
  17. import '../../widgets/widgets.dart';
  18. class PedidoForm extends StatefulWidget {
  19. @override
  20. _PedidoFormState createState() => _PedidoFormState();
  21. }
  22. class _PedidoFormState extends State<PedidoForm> {
  23. final _busqueda = TextEditingController(text: '');
  24. CategoriaProductoViewModel cvm = CategoriaProductoViewModel();
  25. ProductoViewModel pvm = ProductoViewModel();
  26. PedidoViewModel pedvm = PedidoViewModel();
  27. bool _isLoading = false;
  28. CategoriaProducto? categoriaSeleccionada;
  29. List<CategoriaProducto> categorias = [];
  30. List<Producto> productos = [];
  31. List<ItemCarrito> carrito = [];
  32. Producto? _productoActual;
  33. bool _estadoBusqueda = false;
  34. Pedido? pedidoActual;
  35. ScrollController _gridViewController = ScrollController();
  36. final _searchController = TextEditingController();
  37. double calcularTotalPedido() {
  38. double total = 0;
  39. for (var item in carrito) {
  40. total += double.parse(item.producto.precio!) * item.cantidad;
  41. }
  42. return total;
  43. }
  44. @override
  45. void initState() {
  46. super.initState();
  47. cargarCategoriasIniciales();
  48. cargarProductosIniciales();
  49. }
  50. _onSearchChanged(String value) {
  51. if (value.isEmpty) {
  52. cargarProductosIniciales();
  53. } else {
  54. Provider.of<ProductoViewModel>(context, listen: false)
  55. .fetchLocalByName(nombre: value);
  56. }
  57. }
  58. void _finalizeOrder() async {
  59. if (carrito.isEmpty) {
  60. showDialog(
  61. context: context,
  62. builder: (BuildContext context) {
  63. return AlertDialog(
  64. title: const Text('Pedido vacío'),
  65. content: const Text(
  66. 'No puedes finalizar un pedido sin productos. Por favor, agrega al menos un producto.'),
  67. shape:
  68. RoundedRectangleBorder(borderRadius: BorderRadius.circular(12)),
  69. actions: <Widget>[
  70. TextButton(
  71. style: TextButton.styleFrom(
  72. foregroundColor: Colors.black,
  73. backgroundColor: AppTheme.primary // Color del texto
  74. ),
  75. onPressed: () {
  76. Navigator.of(context).pop(); // Cerrar el diálogo
  77. },
  78. child: const Text('Aceptar'),
  79. ),
  80. ],
  81. );
  82. },
  83. );
  84. return;
  85. } else {
  86. // Suponiendo que `pedidoActual` es tu pedido actual que ya tiene un ID asignado
  87. int? pedidoId = pedidoActual?.id;
  88. if (pedidoId != null) {
  89. Navigator.of(context)
  90. .pop(); // Regresar a la pantalla anterior o cerrar diálogo
  91. }
  92. }
  93. await _promptForCustomerName();
  94. }
  95. Future<void> _promptForCustomerName() async {
  96. TextEditingController nombreController = TextEditingController();
  97. TextEditingController comentarioController = TextEditingController();
  98. String errorMessage = ''; // Variable para almacenar el mensaje de error
  99. bool? shouldSave = await showDialog<bool>(
  100. context: context,
  101. builder: (BuildContext context) {
  102. return StatefulBuilder(
  103. // Usar StatefulBuilder para actualizar el contenido del diálogo
  104. builder: (context, setState) {
  105. return AlertDialog(
  106. title: const Text('Finalizar Pedido'),
  107. content: Column(
  108. mainAxisSize: MainAxisSize.min,
  109. children: [
  110. TextField(
  111. controller: nombreController,
  112. decoration: InputDecoration(
  113. hintText: "Nombre del Cliente",
  114. errorText: errorMessage.isEmpty
  115. ? null
  116. : errorMessage, // Mostrar el mensaje de error aquí
  117. ),
  118. autofocus: true,
  119. onChanged: (value) {
  120. if (value.trim().isEmpty) {
  121. setState(() => errorMessage =
  122. "El nombre del cliente es obligatorio.");
  123. } else {
  124. setState(() => errorMessage = '');
  125. }
  126. },
  127. ),
  128. TextField(
  129. controller: comentarioController,
  130. decoration: const InputDecoration(
  131. hintText: "Comentarios (opcional)"),
  132. maxLines: 3,
  133. ),
  134. ],
  135. ),
  136. actions: <Widget>[
  137. TextButton(
  138. child: const Text('Cancelar'),
  139. onPressed: () {
  140. Navigator.of(context).pop(false); // Return false on cancel
  141. },
  142. ),
  143. TextButton(
  144. child: const Text('Guardar'),
  145. onPressed: () {
  146. if (nombreController.text.trim().isEmpty) {
  147. // Actualizar el mensaje de error si el campo está vacío
  148. setState(() => errorMessage =
  149. "El nombre del cliente es obligatorio.");
  150. return; // No cerrar el diálogo
  151. }
  152. Navigator.of(context).pop(true); // Return true on save
  153. },
  154. ),
  155. ],
  156. );
  157. },
  158. );
  159. },
  160. );
  161. if (shouldSave ?? false) {
  162. // Preparar y guardar el pedido sólo si el usuario no canceló el diálogo
  163. prepararPedidoActual(nombreController.text, comentarioController.text);
  164. }
  165. }
  166. void prepararPedidoActual(String nombreCliente, String comentarios) async {
  167. DateTime now = DateTime.now();
  168. String formattedDate = DateFormat('yyyy-MM-dd kk:mm:ss').format(now);
  169. // Crear una instancia de Pedido
  170. Pedido nuevoPedido = Pedido(
  171. peticion: formattedDate,
  172. nombreCliente: nombreCliente,
  173. comentarios: comentarios,
  174. estatus: "NUEVO",
  175. );
  176. // Preparar la lista de PedidoProducto a partir del carrito
  177. List<PedidoProducto> listaPedidoProducto = carrito.map((item) {
  178. return PedidoProducto(
  179. idProducto: item.producto.id,
  180. producto:
  181. item.producto, // Esto debe tener todos los detalles del producto.
  182. costoUnitario: item.producto.precio,
  183. cantidad: item.cantidad,
  184. comentario: comentarios,
  185. );
  186. }).toList();
  187. // Asignar la lista de productos al pedido
  188. nuevoPedido.productos = listaPedidoProducto;
  189. // Usar el ViewModel para guardar el pedido en la base de datos local
  190. bool result = await Provider.of<PedidoViewModel>(context, listen: false)
  191. .guardarPedidoLocal(pedido: nuevoPedido);
  192. if (result) {
  193. printTickets(nuevoPedido);
  194. Navigator.of(context).pop(); // Volver a la pantalla anterior.
  195. } else {
  196. print("Error al guardar el pedido");
  197. }
  198. }
  199. void _limpiarBusqueda() async {
  200. setState(() {
  201. _busqueda.text = '';
  202. _estadoBusqueda = false;
  203. });
  204. // Carga nuevamente las categorías y productos iniciales.
  205. await cargarCategoriasIniciales();
  206. // Opcionalmente, puedes llamar a una función específica para cargar productos iniciales si tienes una.
  207. }
  208. Future<void> cargarProductosIniciales() async {
  209. setState(() => _isLoading = true);
  210. // Llama al método para obtener todos los productos desde el ViewModel local
  211. await Provider.of<ProductoViewModel>(context, listen: false)
  212. .fetchLocalAll();
  213. // Obtiene la lista de productos del ViewModel
  214. productos =
  215. Provider.of<ProductoViewModel>(context, listen: false).productos;
  216. setState(() => _isLoading = false);
  217. }
  218. @override
  219. void dispose() {
  220. _gridViewController.dispose();
  221. _searchController.dispose();
  222. super.dispose();
  223. }
  224. Future<void> cargarCategoriasIniciales() async {
  225. setState(() => _isLoading = true);
  226. await Provider.of<CategoriaProductoViewModel>(context, listen: false)
  227. .fetchLocalAll();
  228. categorias = Provider.of<CategoriaProductoViewModel>(context, listen: false)
  229. .categoriaProductos;
  230. if (categorias.isNotEmpty) {
  231. categoriaSeleccionada = categorias.first;
  232. cargarProductosPorCategoria(categoriaSeleccionada!.id);
  233. }
  234. setState(() => _isLoading = false);
  235. if (categorias.isNotEmpty) {
  236. categoriaSeleccionada = categorias.first;
  237. }
  238. }
  239. void cargarProductosPorCategoria(int categoriaId) async {
  240. setState(() => _isLoading = true);
  241. await Provider.of<ProductoViewModel>(context, listen: false)
  242. .fetchLocalByID(idCategoria: categoriaId);
  243. productos =
  244. Provider.of<ProductoViewModel>(context, listen: false).productos;
  245. // Restablece la posición de desplazamiento
  246. _gridViewController.jumpTo(_gridViewController.position.minScrollExtent);
  247. setState(() => _isLoading = false);
  248. if (_gridViewController.hasClients) {
  249. _gridViewController.jumpTo(0.0);
  250. }
  251. }
  252. void agregarAlCarrito(Producto producto) {
  253. setState(() {
  254. var existente =
  255. carrito.firstWhereOrNull((item) => item.producto.id == producto.id);
  256. if (existente != null) {
  257. existente.cantidad++;
  258. } else {
  259. carrito.add(ItemCarrito(producto: producto, cantidad: 1));
  260. }
  261. });
  262. }
  263. void quitarDelCarrito(Producto producto) {
  264. setState(() {
  265. // Comienza con setState por la misma razón
  266. var indice =
  267. carrito.indexWhere((item) => item.producto.id == producto.id);
  268. if (indice != -1) {
  269. if (carrito[indice].cantidad > 1) {
  270. carrito[indice].cantidad--;
  271. } else {
  272. carrito.removeAt(indice);
  273. }
  274. }
  275. });
  276. }
  277. void addToCart(Producto producto, {List<Toping>? toppings}) {
  278. // Revisa si hay un producto en el carrito con los mismos toppings
  279. var existingIndex = carrito.indexWhere((item) =>
  280. item.producto.id == producto.id && listEquals(item.toppings, toppings));
  281. if (existingIndex != -1) {
  282. carrito[existingIndex].cantidad++;
  283. } else {
  284. carrito.add(ItemCarrito(
  285. producto: producto, cantidad: 1, toppings: toppings ?? []));
  286. }
  287. setState(() {});
  288. }
  289. void finalizeCustomization() {
  290. if (_productoActual != null) {
  291. addToCart(_productoActual!);
  292. setState(() {
  293. _productoActual = null;
  294. });
  295. }
  296. }
  297. @override
  298. Widget build(BuildContext context) {
  299. return Scaffold(
  300. appBar: AppBar(
  301. title: const Text("Crear Pedido"),
  302. ),
  303. body: Row(
  304. children: [
  305. Flexible(
  306. flex: 3,
  307. child: _buildCartSection(),
  308. ),
  309. SizedBox(width: 35),
  310. Flexible(flex: 7, child: _buildProductsSection()),
  311. ],
  312. ),
  313. );
  314. }
  315. Widget _buildTotalSection() {
  316. double total =
  317. calcularTotalPedido(); // Aquí llamarías a la función calcularTotalPedido
  318. return Padding(
  319. padding: const EdgeInsets.symmetric(horizontal: 8.0),
  320. child: Row(
  321. mainAxisAlignment: MainAxisAlignment.spaceBetween,
  322. children: [
  323. const Text('Total',
  324. style: TextStyle(fontWeight: FontWeight.bold, fontSize: 18)),
  325. Text("\$${total.toStringAsFixed(2)}",
  326. style:
  327. const TextStyle(fontWeight: FontWeight.bold, fontSize: 18)),
  328. ],
  329. ),
  330. );
  331. }
  332. List<Widget> _buildToppingList(Map<String, dynamic>? customizations) {
  333. List<Widget> list = [];
  334. customizations?.forEach((category, toppingsAsString) {
  335. if (toppingsAsString is String && toppingsAsString.isNotEmpty) {
  336. // Divide la string por comas para obtener los nombres individuales de los toppings
  337. List<String> toppingNames = toppingsAsString.split(', ');
  338. for (var toppingName in toppingNames) {
  339. list.add(ListTile(
  340. title: Text(toppingName),
  341. subtitle: Text(category), // Muestra la categoría como subtítulo
  342. ));
  343. }
  344. }
  345. });
  346. return list;
  347. }
  348. Widget _buildCartSection() {
  349. return Card(
  350. margin: const EdgeInsets.all(8.0),
  351. child: Column(
  352. children: [
  353. const Padding(
  354. padding: EdgeInsets.symmetric(horizontal: 8.0, vertical: 16.0),
  355. child: Row(
  356. mainAxisAlignment: MainAxisAlignment.spaceBetween,
  357. children: [
  358. Text('Producto',
  359. style:
  360. TextStyle(fontWeight: FontWeight.bold, fontSize: 18)),
  361. Text('Cantidad',
  362. style:
  363. TextStyle(fontWeight: FontWeight.bold, fontSize: 18)),
  364. ],
  365. ),
  366. ),
  367. Expanded(
  368. child: ListView.builder(
  369. itemCount: carrito.length,
  370. itemBuilder: (context, index) {
  371. final item = carrito[index];
  372. // Concatena los nombres de los toppings en una sola línea
  373. String toppingsList =
  374. item.toppings.map((topping) => topping.nombre).join(', ');
  375. return Column(
  376. children: [
  377. ListTile(
  378. title: Text(item.producto.nombre!,
  379. style: const TextStyle(fontWeight: FontWeight.w600)),
  380. subtitle: Text('\$${item.producto.precio}',
  381. style: const TextStyle(
  382. fontWeight: FontWeight.bold,
  383. color: Color(0xFF008000))),
  384. trailing: Row(
  385. mainAxisSize: MainAxisSize.min,
  386. children: [
  387. IconButton(
  388. icon: const Icon(Icons.delete, color: Colors.red),
  389. onPressed: () =>
  390. eliminarProductoDelCarrito(index)),
  391. IconButton(
  392. icon: const Icon(Icons.remove),
  393. onPressed: () => quitarDelCarrito(item.producto)),
  394. const SizedBox(width: 5),
  395. Text('${item.cantidad}',
  396. style: const TextStyle(
  397. fontWeight: FontWeight.bold, fontSize: 14)),
  398. const SizedBox(width: 5),
  399. IconButton(
  400. icon: const Icon(Icons.add),
  401. onPressed: () => agregarAlCarrito(item.producto)),
  402. ],
  403. ),
  404. ),
  405. Padding(
  406. padding: const EdgeInsets.only(left: 16.0, top: 4.0),
  407. child: Text(toppingsList, // Usa la lista concatenada aquí
  408. style: const TextStyle(
  409. fontWeight: FontWeight.w500, fontSize: 14.0)),
  410. ),
  411. Divider(), // Opcional: Un divisor visual entre los elementos.
  412. ],
  413. );
  414. },
  415. ),
  416. ),
  417. const Divider(thickness: 5),
  418. _buildTotalSection(),
  419. const SizedBox(height: 25),
  420. Padding(
  421. padding: const EdgeInsets.all(8.0),
  422. child: ElevatedButton(
  423. onPressed: _finalizeOrder,
  424. style: ElevatedButton.styleFrom(
  425. primary: AppTheme.primary,
  426. onPrimary: AppTheme.secondary,
  427. textStyle: const TextStyle(fontSize: 22),
  428. fixedSize: const Size(250, 50)),
  429. child: const Text('Finalizar Pedido'),
  430. ),
  431. ),
  432. ],
  433. ),
  434. );
  435. }
  436. void eliminarProductoDelCarrito(int index) {
  437. setState(() {
  438. carrito.removeAt(index);
  439. });
  440. }
  441. Widget _buildProductsSection() {
  442. return Column(
  443. children: [
  444. // _buildSearchBar(),
  445. const SizedBox(height: 10),
  446. _buildCategoryButtons(),
  447. const SizedBox(height: 15),
  448. Expanded(
  449. child: Consumer<ProductoViewModel>(builder: (context, model, child) {
  450. productos = model.productos;
  451. return GridView.builder(
  452. controller: _gridViewController,
  453. key: ValueKey<int>(categoriaSeleccionada?.id ?? 0),
  454. gridDelegate: const SliverGridDelegateWithFixedCrossAxisCount(
  455. crossAxisCount: 3,
  456. childAspectRatio: 3 / 2,
  457. ),
  458. itemCount: productos.length,
  459. itemBuilder: (context, index) {
  460. final producto = productos[index];
  461. if (producto.idCategoria != categoriaSeleccionada?.id) {
  462. return Container();
  463. }
  464. return Card(
  465. child: InkWell(
  466. onTap: () => agregarAlCarrito(producto),
  467. child: Column(
  468. mainAxisAlignment: MainAxisAlignment.center,
  469. children: [
  470. const Icon(Icons.fastfood, size: 80),
  471. const SizedBox(height: 8),
  472. Padding(
  473. padding: const EdgeInsets.symmetric(horizontal: 8.0),
  474. child: Text(
  475. producto.nombre ?? '',
  476. style: const TextStyle(
  477. fontSize: 16,
  478. fontWeight: FontWeight.bold,
  479. ),
  480. textAlign: TextAlign.center,
  481. ),
  482. ),
  483. const SizedBox(height: 8),
  484. Text(
  485. '\$${producto.precio}',
  486. style: const TextStyle(
  487. fontSize: 16,
  488. fontWeight: FontWeight.bold,
  489. color: Color(0xFF008000),
  490. ),
  491. ),
  492. ],
  493. ),
  494. ),
  495. );
  496. },
  497. );
  498. }),
  499. )
  500. ],
  501. );
  502. }
  503. Widget _buildCategoryButtons() {
  504. return Container(
  505. height: 50, // Define una altura fija para los botones
  506. child: ListView.builder(
  507. scrollDirection: Axis.horizontal,
  508. itemCount: categorias.length,
  509. itemBuilder: (context, index) {
  510. final categoria = categorias[index];
  511. bool isSelected = categoriaSeleccionada?.id == categoria.id;
  512. return Padding(
  513. padding: const EdgeInsets.symmetric(horizontal: 4.0),
  514. child: ElevatedButton(
  515. onPressed: () {
  516. cargarProductosPorCategoria(categoria.id);
  517. setState(() {
  518. categoriaSeleccionada = categoria;
  519. });
  520. },
  521. style: ElevatedButton.styleFrom(
  522. primary: isSelected ? AppTheme.primary : Colors.grey,
  523. onPrimary: Colors.white,
  524. ),
  525. child: Text(categoria.nombre!),
  526. ),
  527. );
  528. },
  529. ),
  530. );
  531. }
  532. Widget _buildSearchBar() {
  533. return Padding(
  534. padding: const EdgeInsets.all(8.0),
  535. child: TextField(
  536. controller: _searchController,
  537. decoration: InputDecoration(
  538. hintText: 'Buscar producto...',
  539. prefixIcon: const Icon(Icons.search),
  540. border: OutlineInputBorder(
  541. borderRadius: BorderRadius.circular(12.0),
  542. ),
  543. ),
  544. onChanged: _onSearchChanged, // Usar el método de búsqueda aquí
  545. ),
  546. );
  547. }
  548. }