pedido_screen.dart 24 KB

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