pedido_screen.dart 27 KB

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