pedido_screen.dart 25 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633
  1. import 'package:flutter/material.dart';
  2. import 'package:intl/intl.dart';
  3. import 'package:omni_datetime_picker/omni_datetime_picker.dart';
  4. import 'package:provider/provider.dart';
  5. import '../pedido/pedido_csv.dart';
  6. import '../pedido/pedido_detalle_screen.dart';
  7. import '../../widgets/widgets.dart';
  8. import '../../themes/themes.dart';
  9. import '../../models/models.dart';
  10. import '../../viewmodels/viewmodels.dart';
  11. import '../../widgets/widgets_components.dart' as clase;
  12. import 'pedido_form.dart';
  13. class PedidoScreen extends StatefulWidget {
  14. const PedidoScreen({Key? key}) : super(key: key);
  15. @override
  16. State<PedidoScreen> createState() => _PedidoScreenState();
  17. }
  18. class _PedidoScreenState extends State<PedidoScreen> {
  19. final _busqueda = TextEditingController(text: '');
  20. DateTime? fechaInicio;
  21. DateTime? fechaFin;
  22. ScrollController horizontalScrollController = ScrollController();
  23. bool _isMesasActive = false;
  24. @override
  25. void initState() {
  26. super.initState();
  27. WidgetsBinding.instance.addPostFrameCallback((_) {
  28. Provider.of<PedidoViewModel>(context, listen: false)
  29. .fetchLocalPedidosForScreen();
  30. });
  31. Future.microtask(() async {
  32. bool isMesasActive =
  33. await Provider.of<VariableViewModel>(context, listen: false)
  34. .isVariableActive('MESAS');
  35. setState(() {
  36. _isMesasActive = isMesasActive;
  37. });
  38. });
  39. }
  40. void exportCSV() async {
  41. final pedidosViewModel =
  42. Provider.of<PedidoViewModel>(context, listen: false);
  43. List<Pedido> pedidosConProductos = [];
  44. for (Pedido pedido in pedidosViewModel.pedidos) {
  45. Pedido? pedidoConProductos =
  46. await pedidosViewModel.fetchPedidoConProductos(pedido.id);
  47. if (pedidoConProductos != null) {
  48. pedidosConProductos.add(pedidoConProductos);
  49. }
  50. }
  51. if (pedidosConProductos.isNotEmpty) {
  52. String fileName = 'Pedidos_Turquessa_POS';
  53. if (fechaInicio != null && fechaFin != null) {
  54. String startDateStr = DateFormat('dd-MM-yyyy').format(fechaInicio!);
  55. String endDateStr = DateFormat('dd-MM-yyyy').format(fechaFin!);
  56. fileName += '_${startDateStr}_al_${endDateStr}';
  57. }
  58. fileName += '.csv';
  59. await exportarPedidosACSV(pedidosConProductos, fileName);
  60. ScaffoldMessenger.of(context).showSnackBar(SnackBar(
  61. content: Text('Archivo CSV descargado! Archivo: $fileName')));
  62. } else {
  63. ScaffoldMessenger.of(context).showSnackBar(
  64. SnackBar(content: Text('No hay pedidos para exportar.')));
  65. }
  66. }
  67. void clearSearchAndReset() {
  68. setState(() {
  69. _busqueda.clear();
  70. fechaInicio = null;
  71. fechaFin = null;
  72. Provider.of<PedidoViewModel>(context, listen: false)
  73. .fetchLocalPedidosForScreen();
  74. });
  75. }
  76. void go(Pedido item) async {
  77. Pedido? pedidoCompleto =
  78. await Provider.of<PedidoViewModel>(context, listen: false)
  79. .fetchPedidoConProductos(item.id);
  80. if (pedidoCompleto != null) {
  81. if (pedidoCompleto.estatus == 'TERMINADO' || !_isMesasActive) {
  82. Navigator.push(
  83. context,
  84. MaterialPageRoute(
  85. builder: (context) => PedidoDetalleScreen(pedido: pedidoCompleto),
  86. ),
  87. );
  88. } else {
  89. Navigator.push(
  90. context,
  91. MaterialPageRoute(
  92. builder: (context) => PedidoForm(
  93. pedidoExistente: pedidoCompleto,
  94. ),
  95. ),
  96. ).then((value) async {
  97. await Provider.of<PedidoViewModel>(context, listen: false)
  98. .fetchLocalPedidosForScreen();
  99. });
  100. }
  101. } else {
  102. print("Error al cargar el pedido con productos");
  103. }
  104. }
  105. @override
  106. Widget build(BuildContext context) {
  107. final pvm = Provider.of<PedidoViewModel>(context);
  108. double screenWidth = MediaQuery.of(context).size.width;
  109. final isMobile = screenWidth < 1250;
  110. final double? columnSpacing = isMobile ? null : 0;
  111. TextStyle estilo = const TextStyle(fontWeight: FontWeight.bold);
  112. List<DataRow> registros = [];
  113. final permisoViewModel = Provider.of<PermisoViewModel>(context);
  114. List<String> userPermisos = permisoViewModel.userPermisos;
  115. for (Pedido item in pvm.pedidos) {
  116. final sincronizadoStatus =
  117. item.sincronizado == null || item.sincronizado!.isEmpty
  118. ? "No Sincronizado"
  119. : _formatDateTime(item.sincronizado);
  120. registros.add(DataRow(cells: [
  121. DataCell(
  122. Row(mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [
  123. PopupMenuButton(
  124. itemBuilder: (context) => [
  125. PopupMenuItem(
  126. child: const Text('Detalle'),
  127. onTap: () => go(item),
  128. ),
  129. if (userPermisos.contains(Usuario.CANCELAR_PEDIDO))
  130. PopupMenuItem(
  131. child: const Text('Cancelar Pedido'),
  132. onTap: () async {
  133. bool confirmado = await showDialog<bool>(
  134. context: context,
  135. builder: (context) {
  136. return AlertDialog(
  137. title: const Text("Cancelar Pedido",
  138. style: TextStyle(
  139. fontWeight: FontWeight.w500,
  140. fontSize: 22)),
  141. content: const Text(
  142. '¿Estás seguro de que deseas cancelar este pedido?',
  143. style: TextStyle(fontSize: 18)),
  144. actions: [
  145. Row(
  146. mainAxisAlignment:
  147. MainAxisAlignment.spaceBetween,
  148. children: [
  149. TextButton(
  150. onPressed: () =>
  151. Navigator.of(context).pop(false),
  152. child: const Text('No',
  153. style: TextStyle(fontSize: 18)),
  154. style: ButtonStyle(
  155. padding: MaterialStatePropertyAll(
  156. EdgeInsets.fromLTRB(
  157. 20, 10, 20, 10)),
  158. backgroundColor:
  159. MaterialStatePropertyAll(
  160. Colors.red),
  161. foregroundColor:
  162. MaterialStatePropertyAll(
  163. AppTheme.secondary)),
  164. ),
  165. TextButton(
  166. onPressed: () =>
  167. Navigator.of(context).pop(true),
  168. child: const Text('Sí',
  169. style: TextStyle(fontSize: 18)),
  170. style: ButtonStyle(
  171. padding: MaterialStatePropertyAll(
  172. EdgeInsets.fromLTRB(
  173. 20, 10, 20, 10)),
  174. backgroundColor:
  175. MaterialStatePropertyAll(
  176. AppTheme.tertiary),
  177. foregroundColor:
  178. MaterialStatePropertyAll(
  179. AppTheme.quaternary)),
  180. ),
  181. ],
  182. )
  183. ],
  184. );
  185. },
  186. ) ??
  187. false;
  188. if (confirmado) {
  189. await Provider.of<PedidoViewModel>(context, listen: false)
  190. .cancelarPedido(item.id);
  191. ScaffoldMessenger.of(context).showSnackBar(SnackBar(
  192. content: Text("Pedido cancelado correctamente")));
  193. }
  194. },
  195. )
  196. ],
  197. icon: const Icon(Icons.more_vert),
  198. )
  199. ])),
  200. DataCell(
  201. Text(item.folio.toString()),
  202. onTap: () => go(item),
  203. ),
  204. DataCell(
  205. Text(item.nombreCliente ?? "Sin nombre"),
  206. onTap: () => go(item),
  207. ),
  208. DataCell(
  209. Text(item.comentarios ?? "Sin comentarios"),
  210. onTap: () => go(item),
  211. ),
  212. DataCell(
  213. Text(item.estatus ?? "Sin Estatus"),
  214. onTap: () => go(item),
  215. ),
  216. DataCell(
  217. Text(_formatDateTime(item.peticion)),
  218. onTap: () => go(item),
  219. ),
  220. DataCell(
  221. Text(sincronizadoStatus),
  222. onTap: () => go(item),
  223. ),
  224. ]));
  225. }
  226. return Scaffold(
  227. appBar: AppBar(
  228. title: Text(
  229. 'Pedidos',
  230. style: TextStyle(
  231. color: AppTheme.secondary, fontWeight: FontWeight.w500),
  232. ),
  233. actions: <Widget>[
  234. if (userPermisos.contains(Usuario.VER_REPORTE))
  235. IconButton(
  236. icon: const Icon(Icons.save_alt),
  237. onPressed: exportCSV,
  238. tooltip: 'Exportar a CSV',
  239. ),
  240. ],
  241. iconTheme: IconThemeData(color: AppTheme.secondary)),
  242. body: Stack(
  243. children: [
  244. Column(
  245. children: [
  246. Expanded(
  247. child: ListView(
  248. padding: const EdgeInsets.fromLTRB(8, 0, 8, 0),
  249. children: [
  250. const SizedBox(height: 8),
  251. clase.tarjeta(
  252. Padding(
  253. padding: const EdgeInsets.all(8.0),
  254. child: LayoutBuilder(
  255. builder: (context, constraints) {
  256. if (screenWidth > 1000) {
  257. return Row(
  258. crossAxisAlignment: CrossAxisAlignment.end,
  259. children: [
  260. Expanded(
  261. flex: 7,
  262. child: _buildDateRangePicker(),
  263. ),
  264. const SizedBox(width: 5),
  265. botonBuscar()
  266. ],
  267. );
  268. } else {
  269. return Column(
  270. children: [
  271. Row(
  272. children: [_buildDateRangePicker()],
  273. ),
  274. Row(
  275. children: [botonBuscar()],
  276. ),
  277. ],
  278. );
  279. }
  280. },
  281. ),
  282. ),
  283. ),
  284. const SizedBox(height: 8),
  285. pvm.isLoading
  286. ? const Center(child: CircularProgressIndicator())
  287. : Container(),
  288. clase.tarjeta(
  289. Column(
  290. children: [
  291. LayoutBuilder(builder: (context, constraints) {
  292. return SingleChildScrollView(
  293. scrollDirection: Axis.vertical,
  294. child: Scrollbar(
  295. controller: horizontalScrollController,
  296. interactive: true,
  297. thumbVisibility: true,
  298. thickness: 10.0,
  299. child: SingleChildScrollView(
  300. controller: horizontalScrollController,
  301. scrollDirection: Axis.horizontal,
  302. child: ConstrainedBox(
  303. constraints: BoxConstraints(
  304. minWidth: isMobile
  305. ? constraints.maxWidth
  306. : screenWidth),
  307. child: DataTable(
  308. columnSpacing: columnSpacing,
  309. sortAscending: true,
  310. sortColumnIndex: 1,
  311. columns: [
  312. DataColumn(
  313. label: Text(" ", style: estilo)),
  314. DataColumn(
  315. label:
  316. Text("FOLIO", style: estilo)),
  317. DataColumn(
  318. label:
  319. Text("NOMBRE", style: estilo)),
  320. DataColumn(
  321. label: Text("COMENTARIOS",
  322. style: estilo)),
  323. DataColumn(
  324. label:
  325. Text("ESTATUS", style: estilo)),
  326. DataColumn(
  327. label:
  328. Text("FECHA", style: estilo)),
  329. DataColumn(
  330. label: Text("SINCRONIZADO",
  331. style: estilo)),
  332. ],
  333. rows: registros,
  334. ),
  335. ),
  336. ),
  337. ),
  338. );
  339. }),
  340. ],
  341. ),
  342. ),
  343. const SizedBox(height: 15),
  344. if (!pvm.isLoading)
  345. Row(
  346. mainAxisAlignment: MainAxisAlignment.center,
  347. children: [
  348. TextButton(
  349. onPressed:
  350. pvm.currentPage > 1 ? pvm.previousPage : null,
  351. child: Text('Anterior'),
  352. style: ButtonStyle(
  353. backgroundColor:
  354. MaterialStateProperty.resolveWith<Color?>(
  355. (Set<MaterialState> states) {
  356. if (states.contains(MaterialState.disabled)) {
  357. return Colors.grey;
  358. }
  359. return AppTheme.tertiary;
  360. },
  361. ),
  362. foregroundColor:
  363. MaterialStateProperty.resolveWith<Color?>(
  364. (Set<MaterialState> states) {
  365. if (states.contains(MaterialState.disabled)) {
  366. return Colors.black;
  367. }
  368. return Colors.white;
  369. },
  370. ),
  371. ),
  372. ),
  373. SizedBox(width: 15),
  374. Text(
  375. 'Página ${pvm.currentPage} de ${pvm.totalPages}'),
  376. SizedBox(width: 15),
  377. TextButton(
  378. onPressed: pvm.currentPage < pvm.totalPages
  379. ? pvm.nextPage
  380. : null,
  381. child: Text('Siguiente'),
  382. style: ButtonStyle(
  383. backgroundColor:
  384. MaterialStateProperty.resolveWith<Color?>(
  385. (Set<MaterialState> states) {
  386. if (states.contains(MaterialState.disabled)) {
  387. return Colors.grey;
  388. }
  389. return AppTheme.tertiary;
  390. },
  391. ),
  392. foregroundColor:
  393. MaterialStateProperty.resolveWith<Color?>(
  394. (Set<MaterialState> states) {
  395. if (states.contains(MaterialState.disabled)) {
  396. return Colors.black;
  397. }
  398. return Colors.white;
  399. },
  400. ),
  401. ),
  402. ),
  403. ],
  404. ),
  405. const SizedBox(height: 15),
  406. ],
  407. ),
  408. ),
  409. ],
  410. ),
  411. Positioned(
  412. bottom: 16,
  413. right: 16,
  414. child: FloatingActionButton.extended(
  415. heroTag: 'addPedido',
  416. onPressed: () async {
  417. final corteCajaViewModel =
  418. Provider.of<CorteCajaViewModel>(context, listen: false);
  419. if (corteCajaViewModel.hasOpenCorteCaja()) {
  420. await Navigator.push(
  421. context,
  422. MaterialPageRoute(
  423. builder: (context) => PedidoForm(),
  424. ),
  425. ).then((_) =>
  426. Provider.of<PedidoViewModel>(context, listen: false)
  427. .fetchLocalPedidosForScreen());
  428. } else {
  429. alerta(context,
  430. etiqueta:
  431. 'Solo se pueden realizar pedidos cuando se encuentre la caja abierta.');
  432. }
  433. },
  434. icon: Icon(Icons.add, size: 30, color: AppTheme.quaternary),
  435. label: Text(
  436. "Agregar Pedido",
  437. style: TextStyle(fontSize: 20, color: AppTheme.quaternary),
  438. ),
  439. shape: RoundedRectangleBorder(
  440. borderRadius: BorderRadius.circular(8),
  441. ),
  442. materialTapTargetSize: MaterialTapTargetSize.shrinkWrap,
  443. backgroundColor: AppTheme.tertiary,
  444. ),
  445. ),
  446. ],
  447. ),
  448. );
  449. }
  450. Widget _buildDateRangePicker() {
  451. return Row(
  452. children: [
  453. Expanded(
  454. flex: 3,
  455. child: AppTextField(
  456. prefixIcon: const Icon(Icons.search),
  457. etiqueta: 'Búsqueda por folio...',
  458. controller: _busqueda,
  459. hintText: 'Búsqueda por folio...',
  460. ),
  461. ),
  462. const SizedBox(width: 5),
  463. Expanded(
  464. flex: 3,
  465. child: clase.FechaSelectWidget(
  466. fecha: fechaInicio,
  467. onFechaChanged: (d) {
  468. setState(() {
  469. fechaInicio = d;
  470. });
  471. },
  472. etiqueta: "Fecha Inicial",
  473. context: context,
  474. ),
  475. ),
  476. const SizedBox(width: 5),
  477. Expanded(
  478. flex: 3,
  479. child: clase.FechaSelectWidget(
  480. fecha: fechaFin,
  481. onFechaChanged: (d) {
  482. setState(() {
  483. fechaFin = d;
  484. });
  485. },
  486. etiqueta: "Fecha Final",
  487. context: context,
  488. ),
  489. ),
  490. ],
  491. );
  492. }
  493. Widget botonBuscar() {
  494. return Expanded(
  495. flex: 2,
  496. child: Row(
  497. children: [
  498. Expanded(
  499. flex: 2,
  500. child: Padding(
  501. padding: const EdgeInsets.only(bottom: 5),
  502. child: ElevatedButton(
  503. onPressed: clearSearchAndReset,
  504. style: ElevatedButton.styleFrom(
  505. shape: RoundedRectangleBorder(
  506. borderRadius: BorderRadius.circular(20.0),
  507. ),
  508. primary: AppTheme.tertiary,
  509. padding: const EdgeInsets.symmetric(vertical: 25),
  510. ),
  511. child: Text('Limpiar',
  512. style: TextStyle(color: AppTheme.quaternary)),
  513. ),
  514. ),
  515. ),
  516. const SizedBox(width: 8),
  517. Expanded(
  518. flex: 2,
  519. child: Padding(
  520. padding: const EdgeInsets.only(bottom: 5),
  521. child: ElevatedButton(
  522. onPressed: () async {
  523. if (_busqueda.text.isNotEmpty) {
  524. await Provider.of<PedidoViewModel>(context, listen: false)
  525. .buscarPedidosPorFolio(_busqueda.text.trim());
  526. } else if (fechaInicio != null && fechaFin != null) {
  527. DateTime fechaInicioUTC = DateTime(fechaInicio!.year,
  528. fechaInicio!.month, fechaInicio!.day)
  529. .toUtc();
  530. DateTime fechaFinUTC = DateTime(fechaFin!.year,
  531. fechaFin!.month, fechaFin!.day, 23, 59, 59)
  532. .toUtc();
  533. await Provider.of<PedidoViewModel>(context, listen: false)
  534. .buscarPedidosPorFecha(fechaInicioUTC, fechaFinUTC);
  535. } else {
  536. ScaffoldMessenger.of(context).showSnackBar(const SnackBar(
  537. content: Text(
  538. 'Introduce un folio o selecciona un rango de fechas para buscar.')));
  539. }
  540. },
  541. style: ElevatedButton.styleFrom(
  542. shape: RoundedRectangleBorder(
  543. borderRadius: BorderRadius.circular(20.0),
  544. ),
  545. primary: AppTheme.tertiary,
  546. padding: const EdgeInsets.symmetric(vertical: 25),
  547. ),
  548. child: Text('Buscar',
  549. style: TextStyle(color: AppTheme.quaternary)),
  550. ),
  551. ),
  552. ),
  553. ],
  554. ),
  555. );
  556. }
  557. void alertaSync(BuildContext context) {
  558. showDialog(
  559. context: context,
  560. builder: (BuildContext context) {
  561. return AlertDialog(
  562. title: const Text('Confirmación',
  563. style: TextStyle(fontWeight: FontWeight.w500, fontSize: 22)),
  564. content: const Text('¿Deseas sincronizar todos los pedidos de nuevo?',
  565. style: TextStyle(fontSize: 18)),
  566. actions: [
  567. Row(
  568. mainAxisAlignment: MainAxisAlignment.spaceBetween,
  569. children: [
  570. TextButton(
  571. child: const Text('No', style: TextStyle(fontSize: 18)),
  572. style: ButtonStyle(
  573. padding: MaterialStatePropertyAll(
  574. EdgeInsets.fromLTRB(20, 10, 20, 10)),
  575. backgroundColor: MaterialStatePropertyAll(Colors.red),
  576. foregroundColor:
  577. MaterialStatePropertyAll(AppTheme.secondary)),
  578. onPressed: () {
  579. Navigator.of(context).pop();
  580. },
  581. ),
  582. TextButton(
  583. child: const Text('Sí', style: TextStyle(fontSize: 18)),
  584. style: ButtonStyle(
  585. padding: MaterialStatePropertyAll(
  586. EdgeInsets.fromLTRB(20, 10, 20, 10)),
  587. backgroundColor:
  588. MaterialStatePropertyAll(AppTheme.tertiary),
  589. foregroundColor:
  590. MaterialStatePropertyAll(AppTheme.quaternary)),
  591. onPressed: () {
  592. // No hace nada por el momento
  593. Navigator.of(context).pop();
  594. },
  595. ),
  596. ],
  597. )
  598. ],
  599. );
  600. },
  601. );
  602. }
  603. String _formatDateTime(String? dateTimeString) {
  604. if (dateTimeString == null) return "Sin fecha";
  605. DateTime parsedDate = DateTime.parse(dateTimeString);
  606. var formatter = DateFormat('dd-MM-yyyy HH:mm:ss');
  607. return formatter.format(parsedDate.toLocal());
  608. }
  609. }