pedido_screen.dart 21 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538
  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 'package:yoshi_papas_app/themes/themes.dart';
  6. import 'package:yoshi_papas_app/views/pedido/pedido_csv.dart';
  7. import 'package:yoshi_papas_app/views/pedido/pedido_detalle_screen.dart';
  8. import '../../models/models.dart';
  9. import '../../viewmodels/viewmodels.dart';
  10. import '../../widgets/app_textfield.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_JoshiPapas_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('Editar'),
  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. AppTheme.primary),
  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(color: AppTheme.secondary),
  196. ),
  197. actions: <Widget>[
  198. IconButton(
  199. icon: const Icon(Icons.save_alt),
  200. onPressed: exportCSV,
  201. tooltip: 'Exportar a CSV',
  202. ),
  203. ],
  204. iconTheme: IconThemeData(color: AppTheme.secondary)),
  205. floatingActionButton: FloatingActionButton.extended(
  206. onPressed: () async {
  207. await Navigator.push(
  208. context,
  209. MaterialPageRoute(
  210. builder: (context) => PedidoForm(),
  211. ),
  212. ).then((_) => Provider.of<PedidoViewModel>(context, listen: false)
  213. .fetchLocalPedidosForScreen());
  214. },
  215. icon: Icon(Icons.add, size: 30, color: AppTheme.quaternary),
  216. label: Text(
  217. "Agregar Pedido",
  218. style: TextStyle(fontSize: 20, color: AppTheme.quaternary),
  219. ),
  220. shape: RoundedRectangleBorder(
  221. borderRadius: BorderRadius.circular(8),
  222. ),
  223. materialTapTargetSize: MaterialTapTargetSize.shrinkWrap,
  224. backgroundColor: AppTheme.tertiary,
  225. ),
  226. body: Column(
  227. children: [
  228. Expanded(
  229. child: ListView(
  230. padding: const EdgeInsets.fromLTRB(8, 0, 8, 0),
  231. children: [
  232. const SizedBox(height: 8),
  233. clase.tarjeta(
  234. Padding(
  235. padding: const EdgeInsets.all(8.0),
  236. child: LayoutBuilder(
  237. builder: (context, constraints) {
  238. if (screenWidth > 1000) {
  239. return Row(
  240. children: [
  241. Expanded(
  242. flex: 7,
  243. child: _buildDateRangePicker(),
  244. ),
  245. SizedBox(width: 5),
  246. botonBuscar()
  247. ],
  248. );
  249. } else {
  250. return Column(
  251. children: [
  252. Row(
  253. children: [_buildDateRangePicker()],
  254. ),
  255. SizedBox(height: 15),
  256. Row(
  257. children: [botonBuscar()],
  258. ),
  259. ],
  260. );
  261. }
  262. },
  263. ),
  264. ),
  265. ),
  266. const SizedBox(height: 8),
  267. pvm.isLoading
  268. ? const Center(child: CircularProgressIndicator())
  269. : Container(),
  270. clase.tarjeta(
  271. Column(
  272. children: [
  273. LayoutBuilder(builder: (context, constraints) {
  274. return SingleChildScrollView(
  275. scrollDirection: Axis.vertical,
  276. child: Scrollbar(
  277. controller: horizontalScrollController,
  278. interactive: true,
  279. thumbVisibility: true,
  280. thickness: 10.0,
  281. child: SingleChildScrollView(
  282. controller: horizontalScrollController,
  283. scrollDirection: Axis.horizontal,
  284. child: ConstrainedBox(
  285. constraints: BoxConstraints(
  286. minWidth: isMobile
  287. ? constraints.maxWidth
  288. : screenWidth),
  289. child: DataTable(
  290. columnSpacing: columnSpacing,
  291. sortAscending: true,
  292. sortColumnIndex: 1,
  293. columns: [
  294. DataColumn(label: Text(" ", style: estilo)),
  295. DataColumn(
  296. label: Text("FOLIO", style: estilo)),
  297. DataColumn(
  298. label: Text("NOMBRE", style: estilo)),
  299. DataColumn(
  300. label:
  301. Text("COMENTARIOS", style: estilo)),
  302. DataColumn(
  303. label: Text("ESTATUS", style: estilo)),
  304. DataColumn(
  305. label: Text("FECHA", style: estilo)),
  306. ],
  307. rows: registros,
  308. ),
  309. ),
  310. ),
  311. ),
  312. );
  313. }),
  314. ],
  315. ),
  316. ),
  317. const SizedBox(height: 15),
  318. if (!pvm.isLoading)
  319. Row(
  320. mainAxisAlignment: MainAxisAlignment.center,
  321. children: [
  322. TextButton(
  323. onPressed:
  324. pvm.currentPage > 1 ? pvm.previousPage : null,
  325. child: Text('Anterior'),
  326. style: ButtonStyle(
  327. backgroundColor:
  328. MaterialStateProperty.resolveWith<Color?>(
  329. (Set<MaterialState> states) {
  330. if (states.contains(MaterialState.disabled)) {
  331. return Colors.grey;
  332. }
  333. return AppTheme.tertiary;
  334. },
  335. ),
  336. foregroundColor:
  337. MaterialStateProperty.resolveWith<Color?>(
  338. (Set<MaterialState> states) {
  339. if (states.contains(MaterialState.disabled)) {
  340. return Colors.black;
  341. }
  342. return Colors.white;
  343. },
  344. ),
  345. ),
  346. ),
  347. SizedBox(width: 15),
  348. Text('Página ${pvm.currentPage} de ${pvm.totalPages}'),
  349. SizedBox(width: 15),
  350. TextButton(
  351. onPressed: pvm.currentPage < pvm.totalPages
  352. ? pvm.nextPage
  353. : null,
  354. child: Text('Siguiente'),
  355. style: ButtonStyle(
  356. backgroundColor:
  357. MaterialStateProperty.resolveWith<Color?>(
  358. (Set<MaterialState> states) {
  359. if (states.contains(MaterialState.disabled)) {
  360. return Colors.grey;
  361. }
  362. return AppTheme.tertiary;
  363. },
  364. ),
  365. foregroundColor:
  366. MaterialStateProperty.resolveWith<Color?>(
  367. (Set<MaterialState> states) {
  368. if (states.contains(MaterialState.disabled)) {
  369. return Colors.black;
  370. }
  371. return Colors.white;
  372. },
  373. ),
  374. ),
  375. ),
  376. ],
  377. ),
  378. const SizedBox(height: 15),
  379. ],
  380. ),
  381. ),
  382. ],
  383. ),
  384. );
  385. }
  386. Widget _buildDateRangePicker() {
  387. return Row(
  388. children: [
  389. Expanded(
  390. flex: 3,
  391. child: AppTextField(
  392. prefixIcon: const Icon(Icons.search),
  393. etiqueta: 'Búsqueda por folio...',
  394. controller: _busqueda,
  395. hintText: 'Búsqueda por folio...',
  396. ),
  397. ),
  398. const SizedBox(width: 5),
  399. Expanded(
  400. flex: 2,
  401. child: clase.tarjeta(
  402. ListTile(
  403. title: Text(
  404. "Fecha Inicial",
  405. style: TextStyle(
  406. color: AppTheme.quaternary,
  407. fontWeight: FontWeight.bold),
  408. ),
  409. subtitle: Text(
  410. fechaInicio == null
  411. ? ""
  412. : DateFormat("dd/MM/yyyy").format(fechaInicio!),
  413. style: TextStyle(
  414. color: AppTheme.quaternary,
  415. fontWeight: FontWeight.bold),
  416. ),
  417. trailing:
  418. Icon(Icons.calendar_month, color: AppTheme.quaternary),
  419. onTap: () async {
  420. DateTime? d = await clase.showDatetimePicker(
  421. context, fechaInicio,
  422. tipo: OmniDateTimePickerType.date, solofecha: true);
  423. if (d == null) return;
  424. setState(() {
  425. fechaInicio = d;
  426. });
  427. }),
  428. color: AppTheme.tertiary)),
  429. const SizedBox(width: 5),
  430. Expanded(
  431. flex: 2,
  432. child: clase.tarjeta(
  433. ListTile(
  434. title: Text(
  435. "Fecha Final",
  436. style: TextStyle(
  437. color: AppTheme.quaternary,
  438. fontWeight: FontWeight.bold),
  439. ),
  440. subtitle: Text(
  441. fechaFin == null
  442. ? ""
  443. : DateFormat("dd/MM/yyyy").format(fechaFin!),
  444. style: TextStyle(
  445. color: AppTheme.quaternary,
  446. fontWeight: FontWeight.bold),
  447. ),
  448. trailing:
  449. Icon(Icons.calendar_month, color: AppTheme.quaternary),
  450. onTap: () async {
  451. DateTime? d = await clase.showDatetimePicker(
  452. context, fechaInicio,
  453. inicia: fechaInicio,
  454. tipo: OmniDateTimePickerType.date,
  455. solofecha: true);
  456. if (d == null) return;
  457. setState(() {
  458. fechaFin = d;
  459. });
  460. }),
  461. color: AppTheme.tertiary)),
  462. ],
  463. );
  464. }
  465. Widget botonBuscar() {
  466. return Expanded(
  467. flex: 2,
  468. child: Row(
  469. children: [
  470. Expanded(
  471. flex: 2,
  472. child: Padding(
  473. padding: const EdgeInsets.only(top: 0),
  474. child: ElevatedButton(
  475. onPressed: clearSearchAndReset,
  476. style: ElevatedButton.styleFrom(
  477. shape: RoundedRectangleBorder(
  478. borderRadius: BorderRadius.circular(20.0),
  479. ),
  480. primary: AppTheme.tertiary,
  481. padding: const EdgeInsets.symmetric(vertical: 25),
  482. ),
  483. child: Text('Limpiar',
  484. style: TextStyle(color: AppTheme.quaternary)),
  485. ),
  486. ),
  487. ),
  488. const SizedBox(width: 8),
  489. Expanded(
  490. flex: 2,
  491. child: Padding(
  492. padding: const EdgeInsets.only(top: 0),
  493. child: ElevatedButton(
  494. onPressed: () async {
  495. if (_busqueda.text.isNotEmpty) {
  496. await Provider.of<PedidoViewModel>(context, listen: false)
  497. .buscarPedidosPorFolio(_busqueda.text.trim());
  498. } else if (fechaInicio != null && fechaFin != null) {
  499. await Provider.of<PedidoViewModel>(context, listen: false)
  500. .buscarPedidosPorFecha(fechaInicio!, fechaFin!);
  501. } else {
  502. ScaffoldMessenger.of(context).showSnackBar(const SnackBar(
  503. content: Text(
  504. 'Introduce un folio o selecciona un rango de fechas para buscar.')));
  505. }
  506. },
  507. style: ElevatedButton.styleFrom(
  508. shape: RoundedRectangleBorder(
  509. borderRadius: BorderRadius.circular(20.0),
  510. ),
  511. primary: AppTheme.tertiary,
  512. padding: const EdgeInsets.symmetric(vertical: 25),
  513. ),
  514. child: Text('Buscar',
  515. style: TextStyle(color: AppTheme.quaternary)),
  516. ),
  517. ),
  518. ),
  519. ],
  520. ));
  521. }
  522. }