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.
 
 
 
 
 
 

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