You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 
 
 
 
 

593 rivejä
21 KiB

  1. // ignore_for_file: public_member_api_docs, sort_constructors_first
  2. import 'dart:io';
  3. import 'package:file_picker/file_picker.dart';
  4. import 'package:flutter/material.dart';
  5. import 'package:flutter_gen/gen_l10n/app_localizations.dart';
  6. import 'package:go_router/go_router.dart';
  7. import 'package:open_file/open_file.dart';
  8. import 'package:permission_handler/permission_handler.dart';
  9. import 'package:provider/provider.dart';
  10. import 'package:qadirneyriz/config/config.dart';
  11. import 'package:qadirneyriz/models/private_meeting/private_meetings_model.dart';
  12. import 'package:qadirneyriz/screens/private_meeting_summary/state.dart';
  13. import 'package:qadirneyriz/setting/setting.dart';
  14. import 'package:qadirneyriz/utils/enums/status.dart';
  15. import 'package:qadirneyriz/utils/tools/tools.dart';
  16. import 'package:qadirneyriz/widgets/card_meeting.dart';
  17. import 'package:qadirneyriz/widgets/custom_appbar.dart';
  18. import 'package:qadirneyriz/widgets/custom_button.dart';
  19. import 'package:qadirneyriz/widgets/error_widget.dart';
  20. import 'package:qadirneyriz/widgets/loading_widget.dart';
  21. class PrivateMeetingSummaryScreen extends StatefulWidget {
  22. final DatumInPrivateMeeting itemInPrivateMeeting;
  23. const PrivateMeetingSummaryScreen({
  24. Key? key,
  25. required this.itemInPrivateMeeting,
  26. }) : super(key: key);
  27. @override
  28. State<PrivateMeetingSummaryScreen> createState() =>
  29. _PrivateMeetingSummaryScreenState();
  30. }
  31. class _PrivateMeetingSummaryScreenState
  32. extends State<PrivateMeetingSummaryScreen> {
  33. late TextEditingController _textControllerDescription;
  34. late PrivateMeetingSummaryState state;
  35. @override
  36. void initState() {
  37. super.initState();
  38. _textControllerDescription = TextEditingController();
  39. if (widget.itemInPrivateMeeting.description != null) {
  40. _textControllerDescription.text =
  41. widget.itemInPrivateMeeting.description ?? '';
  42. }
  43. Future.delayed(Duration.zero, () async {
  44. state = Provider.of<PrivateMeetingSummaryState>(context, listen: false);
  45. await state.getStringFiles(id: widget.itemInPrivateMeeting.id ?? 0);
  46. });
  47. }
  48. @override
  49. void dispose() {
  50. _textControllerDescription.dispose();
  51. super.dispose();
  52. }
  53. @override
  54. Widget build(BuildContext context) {
  55. return Scaffold(
  56. body: Consumer<PrivateMeetingSummaryState>(
  57. builder: (context, value, child) {
  58. switch (value.stringsFilsStatus[widget.itemInPrivateMeeting.id]) {
  59. case Status.ready:
  60. return CustomScrollView(
  61. slivers: <Widget>[
  62. CustomAppbar(
  63. title: AppLocalizations.of(context)!.meetingsummary,
  64. ),
  65. SliverToBoxAdapter(
  66. child: CustomCardMeeting(
  67. status: widget.itemInPrivateMeeting.accepted ?? 0,
  68. titel: widget.itemInPrivateMeeting.subject != null
  69. ? widget.itemInPrivateMeeting.subject!.subject ?? ''
  70. : '',
  71. date: widget.itemInPrivateMeeting.dateJalali ?? '',
  72. location: widget.itemInPrivateMeeting.location != null
  73. ? widget.itemInPrivateMeeting.location!.address ?? ''
  74. : '',
  75. fromTime: widget.itemInPrivateMeeting.azHour ?? '',
  76. toTime: widget.itemInPrivateMeeting.taHour ?? '',
  77. cardId: widget.itemInPrivateMeeting.id ?? -1,
  78. ),
  79. ),
  80. SliverToBoxAdapter(
  81. child: Column(
  82. children: [
  83. Padding(
  84. padding: const EdgeInsets.all(10.0),
  85. child: Container(
  86. decoration: BoxDecoration(
  87. color: const Color(0xffF4F9F6),
  88. boxShadow: [
  89. BoxShadow(
  90. color: config.ui.mainGray.withOpacity(.1),
  91. spreadRadius: .1,
  92. offset: const Offset(0, 2),
  93. blurRadius: 6,
  94. ),
  95. ],
  96. borderRadius:
  97. const BorderRadius.all(Radius.circular(12)),
  98. ),
  99. child: CustomTextArea(
  100. hintText: AppLocalizations.of(context)!
  101. .descriptionofthemeeting,
  102. controller: _textControllerDescription,
  103. ),
  104. ),
  105. ),
  106. if (state.filesStringModel[
  107. widget.itemInPrivateMeeting.id] !=
  108. null &&
  109. state
  110. .filesStringModel[
  111. widget.itemInPrivateMeeting.id]!
  112. .isNotEmpty)
  113. Padding(
  114. padding: EdgeInsets.all(10),
  115. child: Container(
  116. decoration: BoxDecoration(
  117. color: const Color(0xffF4F9F6),
  118. boxShadow: [
  119. BoxShadow(
  120. color: config.ui.mainGray.withOpacity(.1),
  121. spreadRadius: .1,
  122. offset: const Offset(0, 2),
  123. blurRadius: 6,
  124. ),
  125. ],
  126. borderRadius:
  127. const BorderRadius.all(Radius.circular(12)),
  128. ),
  129. child: Column(
  130. mainAxisAlignment: MainAxisAlignment.start,
  131. crossAxisAlignment: CrossAxisAlignment.start,
  132. children: [
  133. Padding(
  134. padding: const EdgeInsets.symmetric(
  135. horizontal: 10, vertical: 20),
  136. child: Text(
  137. AppLocalizations.of(context)!.files,
  138. style: TextStyle(
  139. fontSize: 16,
  140. fontWeight: FontWeight.bold,
  141. color: config.ui.mainGreen,
  142. ),
  143. ),
  144. ),
  145. ListView.builder(
  146. physics: NeverScrollableScrollPhysics(),
  147. shrinkWrap: true,
  148. padding: EdgeInsets.all(0),
  149. itemCount: state
  150. .filesStringModel[
  151. widget.itemInPrivateMeeting.id]!
  152. .length,
  153. itemBuilder:
  154. (BuildContext context, int index) {
  155. return Padding(
  156. padding: const EdgeInsets.symmetric(
  157. horizontal: 20, vertical: 10),
  158. child: deleteFilesButton(
  159. value,
  160. widget.itemInPrivateMeeting.id ??
  161. -1,
  162. state.filesStringModel[widget
  163. .itemInPrivateMeeting
  164. .id]![index]),
  165. );
  166. },
  167. ),
  168. ],
  169. ),
  170. ),
  171. ),
  172. Padding(
  173. padding: const EdgeInsets.all(10.0),
  174. child: ReceiptUploadDialog(
  175. state: value,
  176. ),
  177. ),
  178. Padding(
  179. padding: const EdgeInsets.symmetric(vertical: 10),
  180. child: submitSummaryButton(context, value),
  181. ),
  182. ],
  183. ),
  184. ),
  185. if (widget.itemInPrivateMeeting.description != null &&
  186. state.filesStringModel[widget.itemInPrivateMeeting.id!] !=
  187. null &&
  188. state.filesStringModel[widget.itemInPrivateMeeting.id!]!
  189. .isNotEmpty)
  190. SliverToBoxAdapter(
  191. child: Padding(
  192. padding: const EdgeInsets.only(
  193. top: 5, bottom: 40, left: 10, right: 10),
  194. child: downloadButton(
  195. value, widget.itemInPrivateMeeting.id ?? -1),
  196. ),
  197. )
  198. ],
  199. );
  200. case Status.loading:
  201. return const LoadingWidget();
  202. case Status.error:
  203. return CustomErrorWidget(
  204. onPressed: () async {
  205. await state.getStringFiles(
  206. id: widget.itemInPrivateMeeting.id ?? 0);
  207. },
  208. );
  209. default:
  210. return Container();
  211. }
  212. },
  213. ),
  214. );
  215. }
  216. Widget deleteFilesButton(
  217. PrivateMeetingSummaryState state, int id, String text) {
  218. switch (state.statusDeleteFile) {
  219. case Status.loading:
  220. return Row(
  221. mainAxisAlignment: MainAxisAlignment.spaceBetween,
  222. children: [
  223. Icon(Icons.cancel_outlined),
  224. Text(text),
  225. ],
  226. );
  227. default:
  228. return InkWell(
  229. onTap: () async {
  230. final shouldProceed = await showDialog<bool>(
  231. context: context,
  232. builder: (BuildContext context) {
  233. return AlertDialog(
  234. title: Text(
  235. AppLocalizations.of(context)!.acceptoperetion,
  236. ),
  237. content: Text(
  238. AppLocalizations.of(context)!.areusuretodeletfile,
  239. ),
  240. actions: [
  241. TextButton(
  242. onPressed: () {
  243. // لغو عملیات
  244. Navigator.of(context).pop(false);
  245. },
  246. child: Text(
  247. AppLocalizations.of(context)!.cancel,
  248. ),
  249. ),
  250. TextButton(
  251. onPressed: () {
  252. // تأیید عملیات
  253. Navigator.of(context).pop(true);
  254. },
  255. child: Text(
  256. AppLocalizations.of(context)!.accept,
  257. ),
  258. ),
  259. ],
  260. );
  261. },
  262. );
  263. // اگر کاربر تأیید کرد، عملیات انجام شود
  264. if (shouldProceed == true) {
  265. final status = await state.deleteFileSummary(id: id, text: text);
  266. if (status == Status.ready) {
  267. await state.getStringFiles(id: id);
  268. // context.pop();
  269. }
  270. }
  271. },
  272. child: state.filesStringModel[id] != null
  273. ? Row(
  274. mainAxisAlignment: MainAxisAlignment.spaceBetween,
  275. children: [
  276. Icon(Icons.cancel_outlined),
  277. Text(text),
  278. ],
  279. )
  280. : Container(),
  281. );
  282. }
  283. }
  284. CustomButton submitSummaryButton(
  285. BuildContext context, PrivateMeetingSummaryState state) {
  286. switch (state.statusMinuteMeeting) {
  287. case Status.loading:
  288. return CustomButton(
  289. hieght: 50, text: AppLocalizations.of(context)!.loading);
  290. default:
  291. return CustomButton(
  292. hieght: 50,
  293. text: AppLocalizations.of(context)!.submitsummarymeeting,
  294. onPressed: () async {
  295. if (_textControllerDescription.text == '') {
  296. // call add new subject
  297. Tools.showCustomSnackBar(
  298. text: AppLocalizations.of(context)!.enterdescription,
  299. isError: true,
  300. context,
  301. );
  302. }
  303. // else if (state.selectedFiles == null) {
  304. // // call add new subject
  305. // Tools.showCustomSnackBar(
  306. // text: AppLocalizations.of(context)!.enterfile,
  307. // isError: true,
  308. // context,
  309. // );
  310. // }
  311. else {
  312. final status = await state.addMinuteMeeting(
  313. id: widget.itemInPrivateMeeting.id ?? -1,
  314. description: _textControllerDescription.text,
  315. meetingFiles: state.selectedFiles ?? []);
  316. if (status == Status.ready) {
  317. await state.getStringFiles(
  318. id: widget.itemInPrivateMeeting.id ?? -1);
  319. context.pop();
  320. Tools.showCustomSnackBar(
  321. text: AppLocalizations.of(context)!.donesummary,
  322. isError: false,
  323. context,
  324. );
  325. } else {
  326. Tools.showCustomSnackBar(
  327. text: state.errorsMinuteMeeting == null
  328. ? state.messageMinuteMeeting ??
  329. AppLocalizations.of(context)!.haserror
  330. : Tools.combineErrorMessages(
  331. state.errorsMinuteMeeting ?? {}),
  332. isError: true,
  333. context,
  334. );
  335. }
  336. }
  337. },
  338. );
  339. }
  340. }
  341. Future<bool> hasStoragePermission() async {
  342. if (Platform.isAndroid) {
  343. final status = await Permission.storage.status;
  344. if (status != PermissionStatus.granted) {
  345. final result = await Permission.manageExternalStorage.request();
  346. if (result == PermissionStatus.granted) {
  347. return true;
  348. }
  349. } else {
  350. return true;
  351. }
  352. } else {
  353. return true;
  354. }
  355. return false;
  356. }
  357. CustomButton downloadButton(PrivateMeetingSummaryState state, int id) {
  358. switch (state.statusDownload) {
  359. case Status.loading:
  360. return CustomButton(
  361. borderRadius: 15,
  362. hieght: 50,
  363. text: AppLocalizations.of(context)!.loading,
  364. width: double.infinity,
  365. );
  366. default:
  367. return CustomButton(
  368. borderRadius: 15,
  369. hieght: 50,
  370. text: AppLocalizations.of(context)!.downloadreport,
  371. width: double.infinity,
  372. onPressed: () async {
  373. bool hasPermission = await hasStoragePermission();
  374. if (!hasPermission) {
  375. Tools.showCustomSnackBar(context,
  376. text: AppLocalizations.of(context)!.needpermission,
  377. isError: true);
  378. return;
  379. }
  380. // Download the file
  381. await state.downloadSummary(id: id);
  382. if (state.statusDownload == Status.ready) {
  383. try {
  384. await OpenFile.open(state.messageDownload);
  385. } catch (e) {
  386. Tools.showCustomSnackBar(
  387. context,
  388. text: AppLocalizations.of(context)!.needzipapp,
  389. isError: true,
  390. );
  391. }
  392. } else {
  393. Tools.showCustomSnackBar(
  394. context,
  395. text: AppLocalizations.of(context)!.error,
  396. isError: true,
  397. );
  398. }
  399. },
  400. );
  401. }
  402. }
  403. }
  404. class CustomTextArea extends StatelessWidget {
  405. final String hintText;
  406. final TextEditingController controller;
  407. final int maxLines;
  408. final int minLines;
  409. const CustomTextArea({
  410. Key? key,
  411. required this.hintText,
  412. required this.controller,
  413. this.maxLines = 20,
  414. this.minLines = 4,
  415. }) : super(key: key);
  416. @override
  417. Widget build(BuildContext context) {
  418. return TextField(
  419. controller: controller,
  420. maxLines: maxLines,
  421. minLines: minLines,
  422. decoration: InputDecoration(
  423. hintText: hintText,
  424. hintStyle: TextStyle(color: Colors.black.withOpacity(.4), fontSize: 13),
  425. border: InputBorder.none,
  426. contentPadding: const EdgeInsets.all(12.0),
  427. ),
  428. );
  429. }
  430. }
  431. class ReceiptUploadDialog extends StatefulWidget {
  432. final PrivateMeetingSummaryState state;
  433. const ReceiptUploadDialog({
  434. Key? key,
  435. required this.state,
  436. }) : super(key: key);
  437. @override
  438. _ReceiptUploadDialogState createState() => _ReceiptUploadDialogState();
  439. }
  440. class _ReceiptUploadDialogState extends State<ReceiptUploadDialog> {
  441. @override
  442. Widget build(BuildContext context) {
  443. return Container(
  444. decoration: BoxDecoration(
  445. color: const Color(0xffF4F9F6),
  446. boxShadow: [
  447. BoxShadow(
  448. color: config.ui.mainGray.withOpacity(.1),
  449. spreadRadius: .1,
  450. offset: const Offset(0, 2),
  451. blurRadius: 6,
  452. ),
  453. ],
  454. borderRadius: const BorderRadius.all(Radius.circular(12)),
  455. ),
  456. child: Padding(
  457. padding: const EdgeInsets.symmetric(vertical: 25, horizontal: 10),
  458. child: Column(
  459. mainAxisSize: MainAxisSize.min,
  460. crossAxisAlignment: CrossAxisAlignment.start,
  461. children: [
  462. Text(
  463. AppLocalizations.of(context)!.fileupload,
  464. style: TextStyle(
  465. fontSize: 16,
  466. fontWeight: FontWeight.bold,
  467. color: config.ui.mainGreen,
  468. ),
  469. ),
  470. const SizedBox(height: 20),
  471. // Upload box
  472. FileBorderBox(
  473. child: GestureDetector(
  474. onTap: widget.state.pickFiles,
  475. child: Column(
  476. children: [
  477. Icon(Icons.cloud_upload_outlined,
  478. size: 30, color: config.ui.mainGreen),
  479. Text(
  480. AppLocalizations.of(context)!.selectfile,
  481. style:
  482. TextStyle(fontSize: 12, color: config.ui.mainGreen),
  483. ),
  484. ],
  485. ),
  486. ),
  487. ),
  488. // File preview
  489. if (widget.state.selectedFiles != null)
  490. FilePreview(
  491. files: widget.state.selectedFiles!,
  492. onDelete: widget.state.removeFile,
  493. ),
  494. ],
  495. ),
  496. ),
  497. );
  498. }
  499. }
  500. class FilePreview extends StatelessWidget {
  501. final List<PlatformFile> files;
  502. final void Function(int) onDelete;
  503. const FilePreview({
  504. super.key,
  505. required this.files,
  506. required this.onDelete,
  507. });
  508. @override
  509. Widget build(BuildContext context) {
  510. return ListView.builder(
  511. shrinkWrap: true,
  512. itemCount: files.length,
  513. physics: const NeverScrollableScrollPhysics(),
  514. itemBuilder: (BuildContext context, int index) {
  515. return Container(
  516. padding: const EdgeInsets.symmetric(horizontal: 10, vertical: 5),
  517. margin: const EdgeInsets.symmetric(vertical: 5, horizontal: 10),
  518. decoration: BoxDecoration(
  519. borderRadius: BorderRadius.circular(10),
  520. border: Border(bottom: BorderSide(color: config.ui.secendGreen)),
  521. ),
  522. child: Row(
  523. mainAxisAlignment: MainAxisAlignment.start,
  524. children: [
  525. const Icon(Icons.file_present_outlined, color: Color(0xffD0D5ED)),
  526. const SizedBox(width: 10),
  527. Expanded(
  528. child: Text(
  529. files[index].name,
  530. style: const TextStyle(fontSize: 12),
  531. ),
  532. ),
  533. IconButton(
  534. icon: Icon(Icons.delete, color: config.ui.secendGreen),
  535. onPressed: () => onDelete(index),
  536. ),
  537. ],
  538. ),
  539. );
  540. },
  541. );
  542. }
  543. }
  544. class FileBorderBox extends StatelessWidget {
  545. final Widget child;
  546. const FileBorderBox({super.key, required this.child});
  547. @override
  548. Widget build(BuildContext context) {
  549. return Container(
  550. padding: const EdgeInsets.all(20),
  551. decoration: BoxDecoration(
  552. borderRadius: BorderRadius.circular(10),
  553. border: Border.all(
  554. color: config.ui.mainGreen,
  555. style: BorderStyle.solid,
  556. width: .5,
  557. ),
  558. ),
  559. child: Center(child: child),
  560. );
  561. }
  562. }