feat(approvals): 결재 접근 차단 대응과 전표 전이 메모 전달 강화
- approvals 모듈에서 APPROVAL_ACCESS_DENIED 응답을 포착하여 ApprovalAccessDeniedException으로 변환하고 접근 거부 시 토스트·대시보드 리다이렉트를 처리 - approval history 조회가 서버 action id에 맞춰 필터링되도록 repository·controller·테스트를 보강 - 재고 트랜잭션 상태 전이 API 호출에 note를 전달하도록 repository·컨트롤러·통합/단위 테스트를 업데이트 - 승인 플로우 QA 체크리스트 및 연동 문서를 최신 계약과 테스트 흐름으로 업데이트
This commit is contained in:
@@ -11,6 +11,7 @@ import '../../domain/entities/approval.dart';
|
||||
import '../../domain/entities/approval_draft.dart';
|
||||
import '../../domain/entities/approval_proceed_status.dart';
|
||||
import '../../domain/entities/approval_template.dart';
|
||||
import '../../domain/errors/approval_access_denied_exception.dart';
|
||||
import '../../domain/repositories/approval_repository.dart';
|
||||
import '../../domain/repositories/approval_template_repository.dart';
|
||||
import '../../domain/usecases/get_approval_draft_use_case.dart';
|
||||
@@ -90,6 +91,8 @@ class ApprovalController extends ChangeNotifier {
|
||||
ApprovalProceedStatus? _proceedStatus;
|
||||
ApprovalSubmissionInput? _submissionDraft;
|
||||
String? _errorMessage;
|
||||
bool _accessDenied = false;
|
||||
String? _accessDeniedMessage;
|
||||
ApprovalStatusFilter _statusFilter = ApprovalStatusFilter.all;
|
||||
int? _transactionIdFilter;
|
||||
int? _requestedById;
|
||||
@@ -114,6 +117,8 @@ class ApprovalController extends ChangeNotifier {
|
||||
bool get isPerformingAction => _isPerformingAction;
|
||||
int? get processingStepId => _processingStepId;
|
||||
String? get errorMessage => _errorMessage;
|
||||
bool get isAccessDenied => _accessDenied;
|
||||
String? get accessDeniedMessage => _accessDeniedMessage;
|
||||
ApprovalStatusFilter get statusFilter => _statusFilter;
|
||||
int? get transactionIdFilter => _transactionIdFilter;
|
||||
int? get requestedById => _requestedById;
|
||||
@@ -169,6 +174,12 @@ class ApprovalController extends ChangeNotifier {
|
||||
|
||||
Map<String, LookupItem> get statusLookup => _statusLookup;
|
||||
|
||||
/// 결재 열람 제한 플래그를 초기화한다.
|
||||
void acknowledgeAccessDenied() {
|
||||
_accessDenied = false;
|
||||
_accessDeniedMessage = null;
|
||||
}
|
||||
|
||||
/// 필터 조건과 페이지 정보를 기반으로 결재 목록을 조회한다.
|
||||
///
|
||||
/// [page]가 1보다 작으면 1페이지로 보정한다. 조회 실패 시 [_errorMessage]에
|
||||
@@ -211,8 +222,16 @@ class ApprovalController extends ChangeNotifier {
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
final failure = Failure.from(error);
|
||||
_errorMessage = failure.describe();
|
||||
if (error is ApprovalAccessDeniedException) {
|
||||
_accessDenied = true;
|
||||
_accessDeniedMessage = error.message;
|
||||
_result = null;
|
||||
_selected = null;
|
||||
_proceedStatus = null;
|
||||
} else {
|
||||
final failure = Failure.from(error);
|
||||
_errorMessage = failure.describe();
|
||||
}
|
||||
} finally {
|
||||
_isLoadingList = false;
|
||||
notifyListeners();
|
||||
@@ -391,11 +410,18 @@ class ApprovalController extends ChangeNotifier {
|
||||
await _loadProceedStatus(detail.id!);
|
||||
}
|
||||
} catch (error) {
|
||||
final failure = Failure.from(error);
|
||||
debugPrint(
|
||||
'[ApprovalController] 결재 상세 조회 실패: ${failure.describe()}',
|
||||
); // 에러 발생 시 콘솔에 남겨 즉시 파악할 수 있도록 한다.
|
||||
_errorMessage = failure.describe();
|
||||
if (error is ApprovalAccessDeniedException) {
|
||||
_accessDenied = true;
|
||||
_accessDeniedMessage = error.message;
|
||||
_selected = null;
|
||||
_proceedStatus = null;
|
||||
} else {
|
||||
final failure = Failure.from(error);
|
||||
debugPrint(
|
||||
'[ApprovalController] 결재 상세 조회 실패: ${failure.describe()}',
|
||||
); // 에러 발생 시 콘솔에 남겨 즉시 파악할 수 있도록 한다.
|
||||
_errorMessage = failure.describe();
|
||||
}
|
||||
} finally {
|
||||
_isLoadingDetail = false;
|
||||
notifyListeners();
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:get_it/get_it.dart';
|
||||
import 'package:go_router/go_router.dart';
|
||||
import 'package:intl/intl.dart' as intl;
|
||||
import 'package:lucide_icons_flutter/lucide_icons.dart' as lucide;
|
||||
import 'package:shadcn_ui/shadcn_ui.dart';
|
||||
@@ -88,6 +89,7 @@ class _ApprovalEnabledPageState extends State<_ApprovalEnabledPage> {
|
||||
InventoryEmployeeSuggestion? _selectedRequester;
|
||||
final intl.DateFormat _dateTimeFormat = intl.DateFormat('yyyy-MM-dd HH:mm');
|
||||
String? _lastError;
|
||||
String? _lastAccessDeniedMessage;
|
||||
int? _selectedTemplateId;
|
||||
String? _pendingRouteSelection;
|
||||
|
||||
@@ -138,6 +140,21 @@ class _ApprovalEnabledPageState extends State<_ApprovalEnabledPage> {
|
||||
|
||||
void _handleControllerUpdate() {
|
||||
final error = _controller.errorMessage;
|
||||
if (_controller.isAccessDenied) {
|
||||
final message = _controller.accessDeniedMessage ?? '결재를 조회할 권한이 없습니다.';
|
||||
if (mounted) {
|
||||
if (_lastAccessDeniedMessage != message) {
|
||||
SuperportToast.warning(context, message);
|
||||
_lastAccessDeniedMessage = message;
|
||||
}
|
||||
final router = GoRouter.maybeOf(context);
|
||||
router?.go(dashboardRoutePath);
|
||||
}
|
||||
_controller.acknowledgeAccessDenied();
|
||||
return;
|
||||
} else {
|
||||
_lastAccessDeniedMessage = null;
|
||||
}
|
||||
if (error != null && error != _lastError && mounted) {
|
||||
_lastError = error;
|
||||
SuperportToast.error(context, error);
|
||||
|
||||
Reference in New Issue
Block a user