import 'package:flutter/foundation.dart'; import 'package:superport_v2/core/common/models/paginated_result.dart'; import '../../domain/entities/approval.dart'; import '../../domain/entities/approval_template.dart'; import '../../domain/repositories/approval_repository.dart'; import '../../domain/repositories/approval_template_repository.dart'; enum ApprovalStatusFilter { all, pending, inProgress, onHold, approved, rejected, } typedef DateRange = ({DateTime from, DateTime to}); const Map> _actionAliases = { ApprovalStepActionType.approve: ['approve', '승인'], ApprovalStepActionType.reject: ['reject', '반려'], ApprovalStepActionType.comment: ['comment', '코멘트', '의견'], }; /// 결재 목록 및 상세 화면 상태 컨트롤러 /// /// - 목록 조회/필터 상태와 선택된 결재 상세 데이터를 관리한다. /// - 승인/반려 등의 후속 액션은 추후 구현 시 추가한다. class ApprovalController extends ChangeNotifier { ApprovalController({ required ApprovalRepository approvalRepository, required ApprovalTemplateRepository templateRepository, }) : _repository = approvalRepository, _templateRepository = templateRepository; final ApprovalRepository _repository; final ApprovalTemplateRepository _templateRepository; PaginatedResult? _result; Approval? _selected; bool _isLoadingList = false; bool _isLoadingDetail = false; bool _isLoadingActions = false; bool _isPerformingAction = false; int? _processingStepId; bool _isLoadingTemplates = false; bool _isApplyingTemplate = false; int? _applyingTemplateId; String? _errorMessage; String _query = ''; ApprovalStatusFilter _statusFilter = ApprovalStatusFilter.all; DateTime? _fromDate; DateTime? _toDate; List _actions = const []; List _templates = const []; PaginatedResult? get result => _result; Approval? get selected => _selected; bool get isLoadingList => _isLoadingList; bool get isLoadingDetail => _isLoadingDetail; bool get isLoadingActions => _isLoadingActions; bool get isPerformingAction => _isPerformingAction; int? get processingStepId => _processingStepId; String? get errorMessage => _errorMessage; String get query => _query; ApprovalStatusFilter get statusFilter => _statusFilter; DateTime? get fromDate => _fromDate; DateTime? get toDate => _toDate; List get actionOptions => _actions; bool get hasActionOptions => _actions.isNotEmpty; List get templates => _templates; bool get isLoadingTemplates => _isLoadingTemplates; bool get isApplyingTemplate => _isApplyingTemplate; int? get applyingTemplateId => _applyingTemplateId; Future fetch({int page = 1}) async { _isLoadingList = true; _errorMessage = null; notifyListeners(); try { final statusParam = switch (_statusFilter) { ApprovalStatusFilter.all => null, ApprovalStatusFilter.pending => 'pending', ApprovalStatusFilter.inProgress => 'in_progress', ApprovalStatusFilter.onHold => 'on_hold', ApprovalStatusFilter.approved => 'approved', ApprovalStatusFilter.rejected => 'rejected', }; final response = await _repository.list( page: page, pageSize: _result?.pageSize ?? 20, query: _query.isEmpty ? null : _query, status: statusParam, from: _fromDate, to: _toDate, includeSteps: false, includeHistories: false, ); _result = response; if (_selected != null) { final exists = response.items.any((item) => item.id == _selected?.id); if (!exists) { _selected = null; } } } catch (e) { _errorMessage = e.toString(); } finally { _isLoadingList = false; notifyListeners(); } } Future loadActionOptions({bool force = false}) async { if (_actions.isNotEmpty && !force) { return; } _isLoadingActions = true; _errorMessage = null; notifyListeners(); try { final items = await _repository.listActions(activeOnly: true); _actions = items; } catch (e) { _errorMessage = e.toString(); } finally { _isLoadingActions = false; notifyListeners(); } } Future loadTemplates({bool force = false}) async { if (_templates.isNotEmpty && !force) { return; } _isLoadingTemplates = true; _errorMessage = null; notifyListeners(); try { final result = await _templateRepository.list( page: 1, pageSize: 100, isActive: true, ); _templates = result.items; } catch (e) { _errorMessage = e.toString(); } finally { _isLoadingTemplates = false; notifyListeners(); } } Future selectApproval(int id) async { _isLoadingDetail = true; _errorMessage = null; notifyListeners(); try { final detail = await _repository.fetchDetail( id, includeSteps: true, includeHistories: true, ); _selected = detail; } catch (e) { _errorMessage = e.toString(); } finally { _isLoadingDetail = false; notifyListeners(); } } void clearSelection() { _selected = null; notifyListeners(); } Future performStepAction({ required ApprovalStep step, required ApprovalStepActionType type, String? note, }) async { if (step.id == null) { _errorMessage = '단계 식별자가 없어 행위를 수행할 수 없습니다.'; notifyListeners(); return false; } final action = _findActionByType(type); if (action == null) { _errorMessage = '사용 가능한 결재 행위를 찾을 수 없습니다.'; notifyListeners(); return false; } _isPerformingAction = true; _processingStepId = step.id; _errorMessage = null; notifyListeners(); try { final sanitizedNote = note?.trim(); final updated = await _repository.performStepAction( ApprovalStepActionInput( stepId: step.id!, actionId: action.id, note: sanitizedNote?.isEmpty ?? true ? null : sanitizedNote, ), ); _selected = updated; if (_result != null && updated.id != null) { final items = _result!.items .map((item) => item.id == updated.id ? updated : item) .toList(); _result = _result!.copyWith(items: items); } return true; } catch (e) { _errorMessage = e.toString(); return false; } finally { _isPerformingAction = false; _processingStepId = null; notifyListeners(); } } Future applyTemplate(int templateId) async { final approvalId = _selected?.id; if (approvalId == null) { _errorMessage = '선택된 결재가 없어 템플릿을 적용할 수 없습니다.'; notifyListeners(); return false; } _isApplyingTemplate = true; _applyingTemplateId = templateId; _errorMessage = null; notifyListeners(); try { final template = await _templateRepository.fetchDetail( templateId, includeSteps: true, ); if (template.steps.isEmpty) { _errorMessage = '선택한 템플릿에 등록된 단계가 없습니다.'; return false; } final sortedSteps = List.of(template.steps) ..sort((a, b) => a.stepOrder.compareTo(b.stepOrder)); final input = ApprovalStepAssignmentInput( approvalId: approvalId, steps: sortedSteps .map( (step) => ApprovalStepAssignmentItem( stepOrder: step.stepOrder, approverId: step.approver.id, note: step.note, ), ) .toList(), ); final updated = await _repository.assignSteps(input); _selected = updated; if (_result != null && updated.id != null) { final items = _result!.items .map((item) => item.id == updated.id ? updated : item) .toList(); _result = _result!.copyWith(items: items); } return true; } catch (e) { _errorMessage = e.toString(); return false; } finally { _isApplyingTemplate = false; _applyingTemplateId = null; notifyListeners(); } } void updateQuery(String value) { _query = value; notifyListeners(); } void updateStatusFilter(ApprovalStatusFilter filter) { _statusFilter = filter; notifyListeners(); } void updateDateRange(DateTime? from, DateTime? to) { _fromDate = from; _toDate = to; notifyListeners(); } void clearFilters() { _query = ''; _statusFilter = ApprovalStatusFilter.all; _fromDate = null; _toDate = null; notifyListeners(); } void clearError() { _errorMessage = null; notifyListeners(); } ApprovalAction? _findActionByType(ApprovalStepActionType type) { final aliases = _actionAliases[type] ?? [type.code]; for (final action in _actions) { final normalized = action.name.toLowerCase(); for (final alias in aliases) { if (normalized == alias.toLowerCase()) { return action; } } } return null; } }