approval_actions.dart 6.2 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200
  1. import 'package:flutter/material.dart';
  2. import '../../core/i18n/app_localizations.dart';
  3. import '../../core/theme/app_colors.dart';
  4. import '../models/user_model.dart';
  5. class ApprovalActions extends StatefulWidget {
  6. final String status;
  7. final UserRole userRole;
  8. final VoidCallback onApprove;
  9. final VoidCallback onReject;
  10. final VoidCallback? onEdit;
  11. final VoidCallback? onWithdraw;
  12. final bool isSubmitting;
  13. const ApprovalActions({
  14. super.key,
  15. required this.status,
  16. required this.userRole,
  17. required this.onApprove,
  18. required this.onReject,
  19. this.onEdit,
  20. this.onWithdraw,
  21. this.isSubmitting = false,
  22. });
  23. @override
  24. State<ApprovalActions> createState() => _ApprovalActionsState();
  25. }
  26. class _ApprovalActionsState extends State<ApprovalActions> {
  27. final _opinionCtrl = TextEditingController();
  28. Future<void> _showOpinionDialog(String action) async {
  29. final l10n = AppLocalizations.of(context);
  30. final result = await showDialog<bool>(
  31. context: context,
  32. builder: (ctx) => AlertDialog(
  33. title: Text(
  34. action == 'approve'
  35. ? l10n.get('confirmApprove')
  36. : l10n.get('confirmReject'),
  37. ),
  38. content: Column(
  39. mainAxisSize: MainAxisSize.min,
  40. children: [
  41. Text(
  42. l10n.getString(
  43. 'confirmAction',
  44. args: {
  45. 'action': action == 'approve'
  46. ? l10n.get('approve')
  47. : l10n.get('reject'),
  48. },
  49. ),
  50. ),
  51. const SizedBox(height: 12),
  52. TextField(
  53. controller: _opinionCtrl,
  54. maxLines: 3,
  55. decoration: InputDecoration(
  56. hintText: l10n.get('approvalComment'),
  57. border: OutlineInputBorder(
  58. borderRadius: BorderRadius.circular(8),
  59. ),
  60. ),
  61. ),
  62. ],
  63. ),
  64. actions: [
  65. TextButton(
  66. onPressed: () => Navigator.pop(ctx, false),
  67. child: Text(l10n.get('cancel')),
  68. ),
  69. TextButton(
  70. onPressed: () => Navigator.pop(ctx, true),
  71. child: Text(
  72. l10n.get('confirm'),
  73. style: TextStyle(
  74. color: action == 'approve'
  75. ? AppColors.primary
  76. : AppColors.danger,
  77. ),
  78. ),
  79. ),
  80. ],
  81. ),
  82. );
  83. if (result == true) {
  84. if (action == 'approve') {
  85. widget.onApprove();
  86. } else {
  87. widget.onReject();
  88. }
  89. }
  90. }
  91. @override
  92. Widget build(BuildContext context) {
  93. final l10n = AppLocalizations.of(context);
  94. final isPending = widget.status == 'pending';
  95. final isDraft = widget.status == 'draft';
  96. final canApprove =
  97. isPending &&
  98. (widget.userRole == UserRole.approver ||
  99. widget.userRole == UserRole.finance ||
  100. widget.userRole == UserRole.admin);
  101. return Container(
  102. padding: const EdgeInsets.all(12),
  103. decoration: BoxDecoration(
  104. color: Colors.white,
  105. boxShadow: [
  106. BoxShadow(
  107. color: Colors.black.withValues(alpha: 0.05),
  108. blurRadius: 4,
  109. offset: const Offset(0, -1),
  110. ),
  111. ],
  112. ),
  113. child: canApprove
  114. ? Row(
  115. children: [
  116. Expanded(
  117. child: OutlinedButton(
  118. onPressed: widget.isSubmitting
  119. ? null
  120. : () => _showOpinionDialog('reject'),
  121. style: OutlinedButton.styleFrom(
  122. foregroundColor: AppColors.danger,
  123. side: const BorderSide(color: AppColors.danger),
  124. shape: RoundedRectangleBorder(
  125. borderRadius: BorderRadius.circular(8),
  126. ),
  127. minimumSize: const Size(double.infinity, 48),
  128. ),
  129. child: Text(l10n.get('reject')),
  130. ),
  131. ),
  132. const SizedBox(width: 12),
  133. Expanded(
  134. flex: 2,
  135. child: ElevatedButton(
  136. onPressed: widget.isSubmitting
  137. ? null
  138. : () => _showOpinionDialog('approve'),
  139. style: ElevatedButton.styleFrom(
  140. backgroundColor: AppColors.primary,
  141. foregroundColor: Colors.white,
  142. shape: RoundedRectangleBorder(
  143. borderRadius: BorderRadius.circular(8),
  144. ),
  145. minimumSize: const Size(double.infinity, 48),
  146. ),
  147. child: widget.isSubmitting
  148. ? const SizedBox(
  149. width: 20,
  150. height: 20,
  151. child: CircularProgressIndicator(
  152. strokeWidth: 2,
  153. color: Colors.white,
  154. ),
  155. )
  156. : Text(l10n.get('approve')),
  157. ),
  158. ),
  159. ],
  160. )
  161. : Row(
  162. children: [
  163. if (isDraft && widget.onEdit != null) ...[
  164. Expanded(
  165. child: OutlinedButton(
  166. onPressed: widget.onEdit,
  167. child: Text(l10n.get('edit')),
  168. ),
  169. ),
  170. const SizedBox(width: 12),
  171. ],
  172. if (isPending && widget.onWithdraw != null)
  173. Expanded(
  174. child: OutlinedButton(
  175. onPressed: widget.onWithdraw,
  176. style: OutlinedButton.styleFrom(
  177. foregroundColor: AppColors.warning,
  178. side: const BorderSide(color: AppColors.warning),
  179. ),
  180. child: Text(l10n.get('withdrawAction')),
  181. ),
  182. ),
  183. ],
  184. ),
  185. );
  186. }
  187. @override
  188. void dispose() {
  189. _opinionCtrl.dispose();
  190. super.dispose();
  191. }
  192. }