pedido_screen.dart 24 KB

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