25개 이상의 토픽을 선택하실 수 없습니다. Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 
 
 
 
 

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