Vous ne pouvez pas sélectionner plus de 25 sujets Les noms de sujets doivent commencer par une lettre ou un nombre, peuvent contenir des tirets ('-') et peuvent comporter jusqu'à 35 caractères.
 
 
 
 
 
 

591 lignes
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. } else if (state.selectedFiles == null) {
  303. // call add new subject
  304. Tools.showCustomSnackBar(
  305. text: AppLocalizations.of(context)!.enterfile,
  306. isError: true,
  307. context,
  308. );
  309. } else {
  310. final status = await state.addMinuteMeeting(
  311. id: widget.itemInPrivateMeeting.id ?? -1,
  312. description: _textControllerDescription.text,
  313. meetingFiles: state.selectedFiles ?? []);
  314. if (status == Status.ready) {
  315. await state.getStringFiles(
  316. id: widget.itemInPrivateMeeting.id ?? -1);
  317. context.pop();
  318. Tools.showCustomSnackBar(
  319. text: AppLocalizations.of(context)!.donesummary,
  320. isError: false,
  321. context,
  322. );
  323. } else {
  324. Tools.showCustomSnackBar(
  325. text: state.errorsMinuteMeeting == null
  326. ? state.messageMinuteMeeting ??
  327. AppLocalizations.of(context)!.haserror
  328. : Tools.combineErrorMessages(
  329. state.errorsMinuteMeeting ?? {}),
  330. isError: true,
  331. context,
  332. );
  333. }
  334. }
  335. },
  336. );
  337. }
  338. }
  339. Future<bool> hasStoragePermission() async {
  340. if (Platform.isAndroid) {
  341. final status = await Permission.storage.status;
  342. if (status != PermissionStatus.granted) {
  343. final result = await Permission.manageExternalStorage.request();
  344. if (result == PermissionStatus.granted) {
  345. return true;
  346. }
  347. } else {
  348. return true;
  349. }
  350. } else {
  351. return true;
  352. }
  353. return false;
  354. }
  355. CustomButton downloadButton(PrivateMeetingSummaryState state, int id) {
  356. switch (state.statusDownload) {
  357. case Status.loading:
  358. return CustomButton(
  359. borderRadius: 15,
  360. hieght: 50,
  361. text: AppLocalizations.of(context)!.loading,
  362. width: double.infinity,
  363. );
  364. default:
  365. return CustomButton(
  366. borderRadius: 15,
  367. hieght: 50,
  368. text: AppLocalizations.of(context)!.downloadreport,
  369. width: double.infinity,
  370. onPressed: () async {
  371. bool hasPermission = await hasStoragePermission();
  372. if (!hasPermission) {
  373. Tools.showCustomSnackBar(context,
  374. text: AppLocalizations.of(context)!.needpermission,
  375. isError: true);
  376. return;
  377. }
  378. // Download the file
  379. await state.downloadSummary(id: id);
  380. if (state.statusDownload == Status.ready) {
  381. try {
  382. await OpenFile.open(state.messageDownload);
  383. } catch (e) {
  384. Tools.showCustomSnackBar(
  385. context,
  386. text: AppLocalizations.of(context)!.needzipapp,
  387. isError: true,
  388. );
  389. }
  390. } else {
  391. Tools.showCustomSnackBar(
  392. context,
  393. text: AppLocalizations.of(context)!.error,
  394. isError: true,
  395. );
  396. }
  397. },
  398. );
  399. }
  400. }
  401. }
  402. class CustomTextArea extends StatelessWidget {
  403. final String hintText;
  404. final TextEditingController controller;
  405. final int maxLines;
  406. final int minLines;
  407. const CustomTextArea({
  408. Key? key,
  409. required this.hintText,
  410. required this.controller,
  411. this.maxLines = 20,
  412. this.minLines = 4,
  413. }) : super(key: key);
  414. @override
  415. Widget build(BuildContext context) {
  416. return TextField(
  417. controller: controller,
  418. maxLines: maxLines,
  419. minLines: minLines,
  420. decoration: InputDecoration(
  421. hintText: hintText,
  422. hintStyle: TextStyle(color: Colors.black.withOpacity(.4), fontSize: 13),
  423. border: InputBorder.none,
  424. contentPadding: const EdgeInsets.all(12.0),
  425. ),
  426. );
  427. }
  428. }
  429. class ReceiptUploadDialog extends StatefulWidget {
  430. final PrivateMeetingSummaryState state;
  431. const ReceiptUploadDialog({
  432. Key? key,
  433. required this.state,
  434. }) : super(key: key);
  435. @override
  436. _ReceiptUploadDialogState createState() => _ReceiptUploadDialogState();
  437. }
  438. class _ReceiptUploadDialogState extends State<ReceiptUploadDialog> {
  439. @override
  440. Widget build(BuildContext context) {
  441. return Container(
  442. decoration: BoxDecoration(
  443. color: const Color(0xffF4F9F6),
  444. boxShadow: [
  445. BoxShadow(
  446. color: config.ui.mainGray.withOpacity(.1),
  447. spreadRadius: .1,
  448. offset: const Offset(0, 2),
  449. blurRadius: 6,
  450. ),
  451. ],
  452. borderRadius: const BorderRadius.all(Radius.circular(12)),
  453. ),
  454. child: Padding(
  455. padding: const EdgeInsets.symmetric(vertical: 25, horizontal: 10),
  456. child: Column(
  457. mainAxisSize: MainAxisSize.min,
  458. crossAxisAlignment: CrossAxisAlignment.start,
  459. children: [
  460. Text(
  461. AppLocalizations.of(context)!.fileupload,
  462. style: TextStyle(
  463. fontSize: 16,
  464. fontWeight: FontWeight.bold,
  465. color: config.ui.mainGreen,
  466. ),
  467. ),
  468. const SizedBox(height: 20),
  469. // Upload box
  470. FileBorderBox(
  471. child: GestureDetector(
  472. onTap: widget.state.pickFiles,
  473. child: Column(
  474. children: [
  475. Icon(Icons.cloud_upload_outlined,
  476. size: 30, color: config.ui.mainGreen),
  477. Text(
  478. AppLocalizations.of(context)!.selectfile,
  479. style:
  480. TextStyle(fontSize: 12, color: config.ui.mainGreen),
  481. ),
  482. ],
  483. ),
  484. ),
  485. ),
  486. // File preview
  487. if (widget.state.selectedFiles != null)
  488. FilePreview(
  489. files: widget.state.selectedFiles!,
  490. onDelete: widget.state.removeFile,
  491. ),
  492. ],
  493. ),
  494. ),
  495. );
  496. }
  497. }
  498. class FilePreview extends StatelessWidget {
  499. final List<PlatformFile> files;
  500. final void Function(int) onDelete;
  501. const FilePreview({
  502. super.key,
  503. required this.files,
  504. required this.onDelete,
  505. });
  506. @override
  507. Widget build(BuildContext context) {
  508. return ListView.builder(
  509. shrinkWrap: true,
  510. itemCount: files.length,
  511. physics: const NeverScrollableScrollPhysics(),
  512. itemBuilder: (BuildContext context, int index) {
  513. return Container(
  514. padding: const EdgeInsets.symmetric(horizontal: 10, vertical: 5),
  515. margin: const EdgeInsets.symmetric(vertical: 5, horizontal: 10),
  516. decoration: BoxDecoration(
  517. borderRadius: BorderRadius.circular(10),
  518. border: Border(bottom: BorderSide(color: config.ui.secendGreen)),
  519. ),
  520. child: Row(
  521. mainAxisAlignment: MainAxisAlignment.start,
  522. children: [
  523. const Icon(Icons.file_present_outlined, color: Color(0xffD0D5ED)),
  524. const SizedBox(width: 10),
  525. Expanded(
  526. child: Text(
  527. files[index].name,
  528. style: const TextStyle(fontSize: 12),
  529. ),
  530. ),
  531. IconButton(
  532. icon: Icon(Icons.delete, color: config.ui.secendGreen),
  533. onPressed: () => onDelete(index),
  534. ),
  535. ],
  536. ),
  537. );
  538. },
  539. );
  540. }
  541. }
  542. class FileBorderBox extends StatelessWidget {
  543. final Widget child;
  544. const FileBorderBox({super.key, required this.child});
  545. @override
  546. Widget build(BuildContext context) {
  547. return Container(
  548. padding: const EdgeInsets.all(20),
  549. decoration: BoxDecoration(
  550. borderRadius: BorderRadius.circular(10),
  551. border: Border.all(
  552. color: config.ui.mainGreen,
  553. style: BorderStyle.solid,
  554. width: .5,
  555. ),
  556. ),
  557. child: Center(child: child),
  558. );
  559. }
  560. }