pedido_screen.dart 21 KB

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