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