pedido_screen.dart 19 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503
  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. registros.add(DataRow(cells: [
  92. DataCell(
  93. Row(mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [
  94. PopupMenuButton(
  95. itemBuilder: (context) => [
  96. PopupMenuItem(
  97. child: const Text('Detalle'),
  98. onTap: () => go(item),
  99. ),
  100. PopupMenuItem(
  101. child: const Text('Cancelar Pedido'),
  102. onTap: () async {
  103. bool confirmado = await showDialog<bool>(
  104. context: context,
  105. builder: (context) {
  106. return AlertDialog(
  107. title: const Text("Cancelar Pedido",
  108. style: TextStyle(
  109. fontWeight: FontWeight.w500, fontSize: 22)),
  110. content: const Text(
  111. '¿Estás seguro de que deseas cancelar este pedido?',
  112. style: TextStyle(fontSize: 18)),
  113. actions: [
  114. Row(
  115. mainAxisAlignment:
  116. MainAxisAlignment.spaceBetween,
  117. children: [
  118. TextButton(
  119. onPressed: () =>
  120. Navigator.of(context).pop(false),
  121. child: const Text('No',
  122. style: TextStyle(fontSize: 18)),
  123. style: ButtonStyle(
  124. padding: MaterialStatePropertyAll(
  125. EdgeInsets.fromLTRB(
  126. 20, 10, 20, 10)),
  127. backgroundColor:
  128. MaterialStatePropertyAll(
  129. Colors.red),
  130. foregroundColor:
  131. MaterialStatePropertyAll(
  132. AppTheme.secondary)),
  133. ),
  134. TextButton(
  135. onPressed: () =>
  136. Navigator.of(context).pop(true),
  137. child: const Text('Sí',
  138. style: TextStyle(fontSize: 18)),
  139. style: ButtonStyle(
  140. padding: MaterialStatePropertyAll(
  141. EdgeInsets.fromLTRB(
  142. 20, 10, 20, 10)),
  143. backgroundColor:
  144. MaterialStatePropertyAll(
  145. AppTheme.tertiary),
  146. foregroundColor:
  147. MaterialStatePropertyAll(
  148. AppTheme.quaternary)),
  149. ),
  150. ],
  151. )
  152. ],
  153. );
  154. },
  155. ) ??
  156. false;
  157. if (confirmado) {
  158. await Provider.of<PedidoViewModel>(context, listen: false)
  159. .cancelarPedido(item.id);
  160. ScaffoldMessenger.of(context).showSnackBar(SnackBar(
  161. content: Text("Pedido cancelado correctamente")));
  162. }
  163. },
  164. )
  165. ],
  166. icon: const Icon(Icons.more_vert),
  167. )
  168. ])),
  169. DataCell(
  170. Text(item.folio.toString()),
  171. onTap: () => go(item),
  172. ),
  173. DataCell(
  174. Text(item.nombreCliente ?? "Sin nombre"),
  175. onTap: () => go(item),
  176. ),
  177. DataCell(
  178. Text(item.comentarios ?? "Sin comentarios"),
  179. onTap: () => go(item),
  180. ),
  181. DataCell(
  182. Text(item.estatus ?? "Sin Estatus"),
  183. onTap: () => go(item),
  184. ),
  185. DataCell(
  186. Text(item.peticion ?? "Sin fecha"),
  187. onTap: () => go(item),
  188. ),
  189. ]));
  190. }
  191. return Scaffold(
  192. appBar: AppBar(
  193. title: Text(
  194. 'Pedidos',
  195. style: TextStyle(
  196. color: AppTheme.secondary, fontWeight: FontWeight.w500),
  197. ),
  198. actions: <Widget>[
  199. IconButton(
  200. icon: const Icon(Icons.save_alt),
  201. onPressed: exportCSV,
  202. tooltip: 'Exportar a CSV',
  203. ),
  204. ],
  205. iconTheme: IconThemeData(color: AppTheme.secondary)),
  206. floatingActionButton: FloatingActionButton.extended(
  207. onPressed: () async {
  208. await Navigator.push(
  209. context,
  210. MaterialPageRoute(
  211. builder: (context) => PedidoForm(),
  212. ),
  213. ).then((_) => Provider.of<PedidoViewModel>(context, listen: false)
  214. .fetchLocalPedidosForScreen());
  215. },
  216. icon: Icon(Icons.add, size: 30, color: AppTheme.quaternary),
  217. label: Text(
  218. "Agregar Pedido",
  219. style: TextStyle(fontSize: 20, color: AppTheme.quaternary),
  220. ),
  221. shape: RoundedRectangleBorder(
  222. borderRadius: BorderRadius.circular(8),
  223. ),
  224. materialTapTargetSize: MaterialTapTargetSize.shrinkWrap,
  225. backgroundColor: AppTheme.tertiary,
  226. ),
  227. body: 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(label: Text(" ", style: estilo)),
  296. DataColumn(
  297. label: Text("FOLIO", style: estilo)),
  298. DataColumn(
  299. label: Text("NOMBRE", style: estilo)),
  300. DataColumn(
  301. label:
  302. Text("COMENTARIOS", style: estilo)),
  303. DataColumn(
  304. label: Text("ESTATUS", style: estilo)),
  305. DataColumn(
  306. label: Text("FECHA", style: estilo)),
  307. ],
  308. rows: registros,
  309. ),
  310. ),
  311. ),
  312. ),
  313. );
  314. }),
  315. ],
  316. ),
  317. ),
  318. const SizedBox(height: 15),
  319. if (!pvm.isLoading)
  320. Row(
  321. mainAxisAlignment: MainAxisAlignment.center,
  322. children: [
  323. TextButton(
  324. onPressed:
  325. pvm.currentPage > 1 ? pvm.previousPage : null,
  326. child: Text('Anterior'),
  327. style: ButtonStyle(
  328. backgroundColor:
  329. MaterialStateProperty.resolveWith<Color?>(
  330. (Set<MaterialState> states) {
  331. if (states.contains(MaterialState.disabled)) {
  332. return Colors.grey;
  333. }
  334. return AppTheme.tertiary;
  335. },
  336. ),
  337. foregroundColor:
  338. MaterialStateProperty.resolveWith<Color?>(
  339. (Set<MaterialState> states) {
  340. if (states.contains(MaterialState.disabled)) {
  341. return Colors.black;
  342. }
  343. return Colors.white;
  344. },
  345. ),
  346. ),
  347. ),
  348. SizedBox(width: 15),
  349. Text('Página ${pvm.currentPage} de ${pvm.totalPages}'),
  350. SizedBox(width: 15),
  351. TextButton(
  352. onPressed: pvm.currentPage < pvm.totalPages
  353. ? pvm.nextPage
  354. : null,
  355. child: Text('Siguiente'),
  356. style: ButtonStyle(
  357. backgroundColor:
  358. MaterialStateProperty.resolveWith<Color?>(
  359. (Set<MaterialState> states) {
  360. if (states.contains(MaterialState.disabled)) {
  361. return Colors.grey;
  362. }
  363. return AppTheme.tertiary;
  364. },
  365. ),
  366. foregroundColor:
  367. MaterialStateProperty.resolveWith<Color?>(
  368. (Set<MaterialState> states) {
  369. if (states.contains(MaterialState.disabled)) {
  370. return Colors.black;
  371. }
  372. return Colors.white;
  373. },
  374. ),
  375. ),
  376. ),
  377. ],
  378. ),
  379. const SizedBox(height: 15),
  380. ],
  381. ),
  382. ),
  383. ],
  384. ),
  385. );
  386. }
  387. Widget _buildDateRangePicker() {
  388. return Row(
  389. children: [
  390. Expanded(
  391. flex: 3,
  392. child: AppTextField(
  393. prefixIcon: const Icon(Icons.search),
  394. etiqueta: 'Búsqueda por folio...',
  395. controller: _busqueda,
  396. hintText: 'Búsqueda por folio...',
  397. ),
  398. ),
  399. const SizedBox(width: 5),
  400. Expanded(
  401. flex: 3,
  402. child: clase.FechaSelectWidget(
  403. fecha: fechaInicio,
  404. onFechaChanged: (d) {
  405. setState(() {
  406. fechaInicio = d;
  407. });
  408. },
  409. etiqueta: "Fecha Inicial",
  410. context: context,
  411. ),
  412. ),
  413. const SizedBox(width: 5),
  414. Expanded(
  415. flex: 3,
  416. child: clase.FechaSelectWidget(
  417. fecha: fechaFin,
  418. onFechaChanged: (d) {
  419. setState(() {
  420. fechaFin = d;
  421. });
  422. },
  423. etiqueta: "Fecha Final",
  424. context: context,
  425. ),
  426. ),
  427. ],
  428. );
  429. }
  430. Widget botonBuscar() {
  431. return Expanded(
  432. flex: 2,
  433. child: Row(
  434. children: [
  435. Expanded(
  436. flex: 2,
  437. child: Padding(
  438. padding: const EdgeInsets.only(bottom: 5),
  439. child: ElevatedButton(
  440. onPressed: clearSearchAndReset,
  441. style: ElevatedButton.styleFrom(
  442. shape: RoundedRectangleBorder(
  443. borderRadius: BorderRadius.circular(20.0),
  444. ),
  445. backgroundColor: AppTheme.tertiary,
  446. padding: const EdgeInsets.symmetric(vertical: 25),
  447. ),
  448. child: Text('Limpiar',
  449. style: TextStyle(color: AppTheme.quaternary)),
  450. ),
  451. ),
  452. ),
  453. const SizedBox(width: 8),
  454. Expanded(
  455. flex: 2,
  456. child: Padding(
  457. padding: const EdgeInsets.only(bottom: 5),
  458. child: ElevatedButton(
  459. onPressed: () async {
  460. if (_busqueda.text.isNotEmpty) {
  461. await Provider.of<PedidoViewModel>(context, listen: false)
  462. .buscarPedidosPorFolio(_busqueda.text.trim());
  463. } else if (fechaInicio != null && fechaFin != null) {
  464. await Provider.of<PedidoViewModel>(context, listen: false)
  465. .buscarPedidosPorFecha(fechaInicio!, fechaFin!);
  466. } else {
  467. ScaffoldMessenger.of(context).showSnackBar(const SnackBar(
  468. content: Text(
  469. 'Introduce un folio o selecciona un rango de fechas para buscar.')));
  470. }
  471. },
  472. style: ElevatedButton.styleFrom(
  473. shape: RoundedRectangleBorder(
  474. borderRadius: BorderRadius.circular(20.0),
  475. ),
  476. backgroundColor: AppTheme.tertiary,
  477. padding: const EdgeInsets.symmetric(vertical: 25),
  478. ),
  479. child: Text('Buscar',
  480. style: TextStyle(color: AppTheme.quaternary)),
  481. ),
  482. ),
  483. ),
  484. ],
  485. ));
  486. }
  487. }