approval_actions.dart 5.6 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178
  1. import 'package:flutter/material.dart';
  2. import 'package:tdesign_flutter/tdesign_flutter.dart';
  3. import '../../core/i18n/app_localizations.dart';
  4. import '../../core/theme/app_colors_extension.dart';
  5. import '../models/user_model.dart';
  6. class ApprovalActions extends StatefulWidget {
  7. final String status;
  8. final UserRole userRole;
  9. final VoidCallback onApprove;
  10. final VoidCallback onReject;
  11. final VoidCallback? onEdit;
  12. final VoidCallback? onWithdraw;
  13. final bool isSubmitting;
  14. const ApprovalActions({
  15. super.key,
  16. required this.status,
  17. required this.userRole,
  18. required this.onApprove,
  19. required this.onReject,
  20. this.onEdit,
  21. this.onWithdraw,
  22. this.isSubmitting = false,
  23. });
  24. @override
  25. State<ApprovalActions> createState() => _ApprovalActionsState();
  26. }
  27. class _ApprovalActionsState extends State<ApprovalActions> {
  28. final _opinionCtrl = TextEditingController();
  29. Future<void> _showOpinionDialog(String action) async {
  30. final l10n = AppLocalizations.of(context);
  31. final result = await showDialog<bool>(
  32. context: context,
  33. builder: (ctx) => TDAlertDialog(
  34. title: action == 'approve'
  35. ? l10n.get('confirmApprove')
  36. : l10n.get('confirmReject'),
  37. contentWidget: Column(
  38. mainAxisSize: MainAxisSize.min,
  39. children: [
  40. Text(
  41. l10n.getString(
  42. 'confirmAction',
  43. args: {
  44. 'action': action == 'approve'
  45. ? l10n.get('approve')
  46. : l10n.get('reject'),
  47. },
  48. ),
  49. ),
  50. const SizedBox(height: 12),
  51. TDInput(
  52. controller: _opinionCtrl,
  53. type: TDInputType.longText,
  54. maxLines: 3,
  55. hintText: l10n.get('approvalComment'),
  56. ),
  57. ],
  58. ),
  59. leftBtn: TDDialogButtonOptions(title: l10n.get('cancel'), action: () => Navigator.pop(ctx, false)),
  60. rightBtn: TDDialogButtonOptions(title: l10n.get('confirm'), action: () => Navigator.pop(ctx, true)),
  61. ),
  62. );
  63. if (result == true) {
  64. if (action == 'approve') {
  65. widget.onApprove();
  66. } else {
  67. widget.onReject();
  68. }
  69. }
  70. }
  71. @override
  72. Widget build(BuildContext context) {
  73. final colors = Theme.of(context).extension<AppColorsExtension>()!;
  74. final l10n = AppLocalizations.of(context);
  75. final isPending = widget.status == 'pending';
  76. final isDraft = widget.status == 'draft';
  77. final canApprove =
  78. isPending &&
  79. (widget.userRole == UserRole.approver ||
  80. widget.userRole == UserRole.finance ||
  81. widget.userRole == UserRole.admin);
  82. return Container(
  83. padding: const EdgeInsets.all(12),
  84. decoration: BoxDecoration(
  85. color: Colors.white,
  86. boxShadow: [
  87. BoxShadow(
  88. color: Colors.black.withValues(alpha: 0.05),
  89. blurRadius: 4,
  90. offset: const Offset(0, -1),
  91. ),
  92. ],
  93. ),
  94. child: canApprove
  95. ? Row(
  96. children: [
  97. Expanded(
  98. child: TDButton(
  99. text: l10n.get('reject'),
  100. size: TDButtonSize.large,
  101. type: TDButtonType.outline,
  102. style: TDButtonStyle(
  103. frameColor: colors.danger,
  104. textColor: colors.danger,
  105. ),
  106. onTap: widget.isSubmitting
  107. ? null
  108. : () => _showOpinionDialog('reject'),
  109. ),
  110. ),
  111. const SizedBox(width: 12),
  112. Expanded(
  113. flex: 2,
  114. child: TDButton(
  115. text: widget.isSubmitting ? '' : l10n.get('approve'),
  116. size: TDButtonSize.large,
  117. theme: TDButtonTheme.primary,
  118. onTap: widget.isSubmitting
  119. ? null
  120. : () => _showOpinionDialog('approve'),
  121. iconWidget: widget.isSubmitting
  122. ? const SizedBox(
  123. width: 20,
  124. height: 20,
  125. child: CircularProgressIndicator(
  126. strokeWidth: 2,
  127. color: Colors.white,
  128. ),
  129. )
  130. : null,
  131. ),
  132. ),
  133. ],
  134. )
  135. : Row(
  136. children: [
  137. if (isDraft && widget.onEdit != null) ...[
  138. Expanded(
  139. child: TDButton(
  140. text: l10n.get('edit'),
  141. size: TDButtonSize.large,
  142. type: TDButtonType.outline,
  143. onTap: widget.onEdit,
  144. ),
  145. ),
  146. const SizedBox(width: 12),
  147. ],
  148. if (isPending && widget.onWithdraw != null)
  149. Expanded(
  150. child: TDButton(
  151. text: l10n.get('withdrawAction'),
  152. size: TDButtonSize.large,
  153. type: TDButtonType.outline,
  154. style: TDButtonStyle(
  155. frameColor: colors.warning,
  156. textColor: colors.warning,
  157. ),
  158. onTap: widget.onWithdraw,
  159. ),
  160. ),
  161. ],
  162. ),
  163. );
  164. }
  165. @override
  166. void dispose() {
  167. _opinionCtrl.dispose();
  168. super.dispose();
  169. }
  170. }