feat: 결재·마스터 실연동 업데이트
This commit is contained in:
@@ -0,0 +1,28 @@
|
||||
import '../../domain/entities/approval_proceed_status.dart';
|
||||
|
||||
/// 결재 진행 가능 여부(can-proceed) 응답 DTO.
|
||||
class ApprovalProceedStatusDto {
|
||||
ApprovalProceedStatusDto({
|
||||
required this.approvalId,
|
||||
required this.canProceed,
|
||||
this.reason,
|
||||
});
|
||||
|
||||
final int approvalId;
|
||||
final bool canProceed;
|
||||
final String? reason;
|
||||
|
||||
factory ApprovalProceedStatusDto.fromJson(Map<String, dynamic> json) {
|
||||
return ApprovalProceedStatusDto(
|
||||
approvalId: json['id'] as int? ?? json['approval_id'] as int? ?? 0,
|
||||
canProceed: json['can_proceed'] as bool? ?? false,
|
||||
reason: json['reason'] as String?,
|
||||
);
|
||||
}
|
||||
|
||||
ApprovalProceedStatus toEntity() => ApprovalProceedStatus(
|
||||
approvalId: approvalId,
|
||||
canProceed: canProceed,
|
||||
reason: reason,
|
||||
);
|
||||
}
|
||||
@@ -1,10 +1,13 @@
|
||||
import 'package:dio/dio.dart';
|
||||
import 'package:superport_v2/core/common/models/paginated_result.dart';
|
||||
import 'package:superport_v2/core/network/api_client.dart';
|
||||
import 'package:superport_v2/core/network/api_routes.dart';
|
||||
|
||||
import '../../domain/entities/approval.dart';
|
||||
import '../../domain/entities/approval_proceed_status.dart';
|
||||
import '../../domain/repositories/approval_repository.dart';
|
||||
import '../dtos/approval_dto.dart';
|
||||
import '../dtos/approval_proceed_status_dto.dart';
|
||||
|
||||
/// 결재 API 엔드포인트를 호출하는 원격 저장소 구현체.
|
||||
///
|
||||
@@ -15,7 +18,7 @@ class ApprovalRepositoryRemote implements ApprovalRepository {
|
||||
|
||||
final ApiClient _api;
|
||||
|
||||
static const _basePath = '/approvals';
|
||||
static const _basePath = '${ApiRoutes.apiV1}/approvals';
|
||||
|
||||
/// 결재 목록을 조회한다. 필터 조건이 없으면 최신순 페이지를 반환한다.
|
||||
@override
|
||||
@@ -69,7 +72,7 @@ class ApprovalRepositoryRemote implements ApprovalRepository {
|
||||
@override
|
||||
Future<List<ApprovalAction>> listActions({bool activeOnly = true}) async {
|
||||
final response = await _api.get<Map<String, dynamic>>(
|
||||
'/approval-actions',
|
||||
'${ApiRoutes.apiV1}/approval-actions',
|
||||
query: {'page': 1, 'page_size': 100, if (activeOnly) 'active': true},
|
||||
options: Options(responseType: ResponseType.json),
|
||||
);
|
||||
@@ -85,7 +88,7 @@ class ApprovalRepositoryRemote implements ApprovalRepository {
|
||||
@override
|
||||
Future<Approval> performStepAction(ApprovalStepActionInput input) async {
|
||||
final response = await _api.post<Map<String, dynamic>>(
|
||||
'/approval-steps/${input.stepId}/actions',
|
||||
'${ApiRoutes.apiV1}/approval-steps/${input.stepId}/actions',
|
||||
data: input.toPayload(),
|
||||
options: Options(responseType: ResponseType.json),
|
||||
);
|
||||
@@ -102,7 +105,7 @@ class ApprovalRepositoryRemote implements ApprovalRepository {
|
||||
@override
|
||||
Future<Approval> assignSteps(ApprovalStepAssignmentInput input) async {
|
||||
final response = await _api.post<Map<String, dynamic>>(
|
||||
'/approvals/${input.approvalId}/steps',
|
||||
'${ApiRoutes.apiV1}/approvals/${input.approvalId}/steps',
|
||||
data: input.toPayload(),
|
||||
options: Options(responseType: ResponseType.json),
|
||||
);
|
||||
@@ -115,6 +118,17 @@ class ApprovalRepositoryRemote implements ApprovalRepository {
|
||||
return ApprovalDto.fromJson(approvalJson).toEntity();
|
||||
}
|
||||
|
||||
/// 결재가 다음 단계로 진행 가능한지 확인한다.
|
||||
@override
|
||||
Future<ApprovalProceedStatus> canProceed(int id) async {
|
||||
final response = await _api.get<Map<String, dynamic>>(
|
||||
'$_basePath/$id/can-proceed',
|
||||
options: Options(responseType: ResponseType.json),
|
||||
);
|
||||
final data = (response.data?['data'] as Map<String, dynamic>?) ?? {};
|
||||
return ApprovalProceedStatusDto.fromJson(data).toEntity();
|
||||
}
|
||||
|
||||
/// 새로운 결재를 생성한다.
|
||||
@override
|
||||
Future<Approval> create(ApprovalInput input) async {
|
||||
|
||||
@@ -2,6 +2,7 @@ import 'package:dio/dio.dart';
|
||||
|
||||
import '../../../../core/common/models/paginated_result.dart';
|
||||
import '../../../../core/network/api_client.dart';
|
||||
import '../../../../core/network/api_routes.dart';
|
||||
import '../../domain/entities/approval_template.dart';
|
||||
import '../../domain/repositories/approval_template_repository.dart';
|
||||
import '../dtos/approval_template_dto.dart';
|
||||
@@ -16,7 +17,7 @@ class ApprovalTemplateRepositoryRemote implements ApprovalTemplateRepository {
|
||||
|
||||
final ApiClient _api;
|
||||
|
||||
static const _basePath = '/approval-templates';
|
||||
static const _basePath = '${ApiRoutes.apiV1}/approval-templates';
|
||||
|
||||
/// 결재 템플릿 목록을 조회한다. 검색/활성 여부 필터를 지원한다.
|
||||
@override
|
||||
|
||||
@@ -0,0 +1,14 @@
|
||||
/// 결재 진행 가능 여부(can-proceed) 응답 엔티티.
|
||||
///
|
||||
/// - 백엔드 `GET /approvals/{id}/can-proceed` 결과를 표현한다.
|
||||
class ApprovalProceedStatus {
|
||||
const ApprovalProceedStatus({
|
||||
required this.approvalId,
|
||||
required this.canProceed,
|
||||
this.reason,
|
||||
});
|
||||
|
||||
final int approvalId;
|
||||
final bool canProceed;
|
||||
final String? reason;
|
||||
}
|
||||
@@ -1,6 +1,7 @@
|
||||
import 'package:superport_v2/core/common/models/paginated_result.dart';
|
||||
|
||||
import '../entities/approval.dart';
|
||||
import '../entities/approval_proceed_status.dart';
|
||||
|
||||
/// 결재 도메인에서 사용하는 저장소 인터페이스.
|
||||
///
|
||||
@@ -34,6 +35,9 @@ abstract class ApprovalRepository {
|
||||
/// 결재 단계 일괄 생성/재배치
|
||||
Future<Approval> assignSteps(ApprovalStepAssignmentInput input);
|
||||
|
||||
/// 결재가 다음 단계로 진행 가능한지 여부를 확인한다.
|
||||
Future<ApprovalProceedStatus> canProceed(int id);
|
||||
|
||||
/// 결재를 생성한다.
|
||||
Future<Approval> create(ApprovalInput input);
|
||||
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import 'package:dio/dio.dart';
|
||||
import 'package:superport_v2/core/common/models/paginated_result.dart';
|
||||
import 'package:superport_v2/core/network/api_client.dart';
|
||||
import 'package:superport_v2/core/network/api_routes.dart';
|
||||
|
||||
import '../../domain/entities/approval_history_record.dart';
|
||||
import '../../domain/repositories/approval_history_repository.dart';
|
||||
@@ -13,7 +14,7 @@ class ApprovalHistoryRepositoryRemote implements ApprovalHistoryRepository {
|
||||
|
||||
final ApiClient _api;
|
||||
|
||||
static const _basePath = '/approval-histories';
|
||||
static const _basePath = '${ApiRoutes.apiV1}/approval-histories';
|
||||
|
||||
/// 결재 이력 목록을 조회한다.
|
||||
@override
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:superport_v2/core/common/models/paginated_result.dart';
|
||||
import 'package:superport_v2/core/network/failure.dart';
|
||||
|
||||
import '../../domain/entities/approval_history_record.dart';
|
||||
import '../../domain/repositories/approval_history_repository.dart';
|
||||
@@ -59,8 +60,9 @@ class ApprovalHistoryController extends ChangeNotifier {
|
||||
);
|
||||
_result = response;
|
||||
_pageSize = response.pageSize;
|
||||
} catch (e) {
|
||||
_errorMessage = e.toString();
|
||||
} catch (error) {
|
||||
final failure = Failure.from(error);
|
||||
_errorMessage = failure.describe();
|
||||
} finally {
|
||||
_isLoading = false;
|
||||
notifyListeners();
|
||||
|
||||
@@ -1,7 +1,11 @@
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:superport_v2/core/common/models/paginated_result.dart';
|
||||
import 'package:superport_v2/core/network/failure.dart';
|
||||
|
||||
import '../../../inventory/lookups/domain/entities/lookup_item.dart';
|
||||
import '../../../inventory/lookups/domain/repositories/inventory_lookup_repository.dart';
|
||||
import '../../domain/entities/approval.dart';
|
||||
import '../../domain/entities/approval_proceed_status.dart';
|
||||
import '../../domain/entities/approval_template.dart';
|
||||
import '../../domain/repositories/approval_repository.dart';
|
||||
import '../../domain/repositories/approval_template_repository.dart';
|
||||
@@ -23,6 +27,14 @@ const Map<ApprovalStepActionType, List<String>> _actionAliases = {
|
||||
ApprovalStepActionType.comment: ['comment', '코멘트', '의견'],
|
||||
};
|
||||
|
||||
const Map<ApprovalStatusFilter, String> _defaultStatusCodes = {
|
||||
ApprovalStatusFilter.pending: 'pending',
|
||||
ApprovalStatusFilter.inProgress: 'in_progress',
|
||||
ApprovalStatusFilter.onHold: 'on_hold',
|
||||
ApprovalStatusFilter.approved: 'approved',
|
||||
ApprovalStatusFilter.rejected: 'rejected',
|
||||
};
|
||||
|
||||
/// 결재 목록 및 상세 화면 상태 컨트롤러
|
||||
///
|
||||
/// - 목록 조회/필터 상태와 선택된 결재 상세 데이터를 관리한다.
|
||||
@@ -31,11 +43,14 @@ class ApprovalController extends ChangeNotifier {
|
||||
ApprovalController({
|
||||
required ApprovalRepository approvalRepository,
|
||||
required ApprovalTemplateRepository templateRepository,
|
||||
InventoryLookupRepository? lookupRepository,
|
||||
}) : _repository = approvalRepository,
|
||||
_templateRepository = templateRepository;
|
||||
_templateRepository = templateRepository,
|
||||
_lookupRepository = lookupRepository;
|
||||
|
||||
final ApprovalRepository _repository;
|
||||
final ApprovalTemplateRepository _templateRepository;
|
||||
final InventoryLookupRepository? _lookupRepository;
|
||||
|
||||
PaginatedResult<Approval>? _result;
|
||||
Approval? _selected;
|
||||
@@ -47,6 +62,7 @@ class ApprovalController extends ChangeNotifier {
|
||||
bool _isLoadingTemplates = false;
|
||||
bool _isApplyingTemplate = false;
|
||||
int? _applyingTemplateId;
|
||||
ApprovalProceedStatus? _proceedStatus;
|
||||
String? _errorMessage;
|
||||
String _query = '';
|
||||
ApprovalStatusFilter _statusFilter = ApprovalStatusFilter.all;
|
||||
@@ -54,6 +70,12 @@ class ApprovalController extends ChangeNotifier {
|
||||
DateTime? _toDate;
|
||||
List<ApprovalAction> _actions = const [];
|
||||
List<ApprovalTemplate> _templates = const [];
|
||||
final Map<String, LookupItem> _statusLookup = {};
|
||||
final Map<String, String> _statusCodeAliases = Map.fromEntries(
|
||||
_defaultStatusCodes.entries.map(
|
||||
(entry) => MapEntry(entry.value, entry.value),
|
||||
),
|
||||
);
|
||||
|
||||
PaginatedResult<Approval>? get result => _result;
|
||||
Approval? get selected => _selected;
|
||||
@@ -73,6 +95,17 @@ class ApprovalController extends ChangeNotifier {
|
||||
bool get isLoadingTemplates => _isLoadingTemplates;
|
||||
bool get isApplyingTemplate => _isApplyingTemplate;
|
||||
int? get applyingTemplateId => _applyingTemplateId;
|
||||
ApprovalProceedStatus? get proceedStatus => _proceedStatus;
|
||||
bool get canProceedSelected => _proceedStatus?.canProceed ?? true;
|
||||
String? get cannotProceedReason {
|
||||
final reason = _proceedStatus?.reason?.trim();
|
||||
if (reason == null || reason.isEmpty) {
|
||||
return null;
|
||||
}
|
||||
return reason;
|
||||
}
|
||||
|
||||
Map<String, LookupItem> get statusLookup => _statusLookup;
|
||||
|
||||
/// 필터 조건과 페이지 정보를 기반으로 결재 목록을 조회한다.
|
||||
///
|
||||
@@ -83,14 +116,7 @@ class ApprovalController extends ChangeNotifier {
|
||||
_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 statusParam = _statusCodeFor(_statusFilter);
|
||||
final response = await _repository.list(
|
||||
page: page,
|
||||
pageSize: _result?.pageSize ?? 20,
|
||||
@@ -106,10 +132,12 @@ class ApprovalController extends ChangeNotifier {
|
||||
final exists = response.items.any((item) => item.id == _selected?.id);
|
||||
if (!exists) {
|
||||
_selected = null;
|
||||
_proceedStatus = null;
|
||||
}
|
||||
}
|
||||
} catch (e) {
|
||||
_errorMessage = e.toString();
|
||||
} catch (error) {
|
||||
final failure = Failure.from(error);
|
||||
_errorMessage = failure.describe();
|
||||
} finally {
|
||||
_isLoadingList = false;
|
||||
notifyListeners();
|
||||
@@ -129,14 +157,78 @@ class ApprovalController extends ChangeNotifier {
|
||||
try {
|
||||
final items = await _repository.listActions(activeOnly: true);
|
||||
_actions = items;
|
||||
} catch (e) {
|
||||
_errorMessage = e.toString();
|
||||
} catch (error) {
|
||||
final failure = Failure.from(error);
|
||||
_errorMessage = failure.describe();
|
||||
} finally {
|
||||
_isLoadingActions = false;
|
||||
notifyListeners();
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> loadStatusLookups() async {
|
||||
final repository = _lookupRepository;
|
||||
if (repository == null) {
|
||||
return;
|
||||
}
|
||||
try {
|
||||
final items = await repository.fetchApprovalStatuses();
|
||||
_statusLookup
|
||||
..clear()
|
||||
..addEntries(
|
||||
items.map(
|
||||
(item) => MapEntry(
|
||||
(item.code ?? item.name).toLowerCase(),
|
||||
item,
|
||||
),
|
||||
),
|
||||
);
|
||||
for (final entry in _defaultStatusCodes.entries) {
|
||||
final code = entry.value.toLowerCase();
|
||||
final lookup = _statusLookup[code];
|
||||
if (lookup != null) {
|
||||
_statusCodeAliases[entry.value] = lookup.code?.toLowerCase() ?? code;
|
||||
}
|
||||
}
|
||||
notifyListeners();
|
||||
} catch (_) {
|
||||
// 실패 시 기본 라벨 사용
|
||||
}
|
||||
}
|
||||
|
||||
String statusLabel(ApprovalStatusFilter filter) {
|
||||
if (filter == ApprovalStatusFilter.all) {
|
||||
return '전체 상태';
|
||||
}
|
||||
final code = _statusCodeFor(filter);
|
||||
if (code != null) {
|
||||
final normalized = code.toLowerCase();
|
||||
final lookup = _statusLookup[normalized];
|
||||
if (lookup != null && lookup.name.isNotEmpty) {
|
||||
return lookup.name;
|
||||
}
|
||||
}
|
||||
return switch (filter) {
|
||||
ApprovalStatusFilter.pending => '승인대기',
|
||||
ApprovalStatusFilter.inProgress => '진행중',
|
||||
ApprovalStatusFilter.onHold => '보류',
|
||||
ApprovalStatusFilter.approved => '승인완료',
|
||||
ApprovalStatusFilter.rejected => '반려',
|
||||
ApprovalStatusFilter.all => '전체 상태',
|
||||
};
|
||||
}
|
||||
|
||||
String? _statusCodeFor(ApprovalStatusFilter filter) {
|
||||
if (filter == ApprovalStatusFilter.all) {
|
||||
return null;
|
||||
}
|
||||
final defaultCode = _defaultStatusCodes[filter];
|
||||
if (defaultCode == null) {
|
||||
return null;
|
||||
}
|
||||
return _statusCodeAliases[defaultCode] ?? defaultCode;
|
||||
}
|
||||
|
||||
/// 활성화된 결재 템플릿 목록을 조회해 캐싱한다.
|
||||
///
|
||||
/// 템플릿이 비어 있거나 [force]가 `true`이면 API를 다시 호출한다.
|
||||
@@ -154,8 +246,9 @@ class ApprovalController extends ChangeNotifier {
|
||||
isActive: true,
|
||||
);
|
||||
_templates = result.items;
|
||||
} catch (e) {
|
||||
_errorMessage = e.toString();
|
||||
} catch (error) {
|
||||
final failure = Failure.from(error);
|
||||
_errorMessage = failure.describe();
|
||||
} finally {
|
||||
_isLoadingTemplates = false;
|
||||
notifyListeners();
|
||||
@@ -169,6 +262,7 @@ class ApprovalController extends ChangeNotifier {
|
||||
Future<void> selectApproval(int id) async {
|
||||
_isLoadingDetail = true;
|
||||
_errorMessage = null;
|
||||
_proceedStatus = null;
|
||||
notifyListeners();
|
||||
try {
|
||||
final detail = await _repository.fetchDetail(
|
||||
@@ -177,8 +271,12 @@ class ApprovalController extends ChangeNotifier {
|
||||
includeHistories: true,
|
||||
);
|
||||
_selected = detail;
|
||||
} catch (e) {
|
||||
_errorMessage = e.toString();
|
||||
if (detail.id != null) {
|
||||
await _loadProceedStatus(detail.id!);
|
||||
}
|
||||
} catch (error) {
|
||||
final failure = Failure.from(error);
|
||||
_errorMessage = failure.describe();
|
||||
} finally {
|
||||
_isLoadingDetail = false;
|
||||
notifyListeners();
|
||||
@@ -188,6 +286,7 @@ class ApprovalController extends ChangeNotifier {
|
||||
/// 선택된 결재 상세를 비우고 화면을 초기화한다.
|
||||
void clearSelection() {
|
||||
_selected = null;
|
||||
_proceedStatus = null;
|
||||
notifyListeners();
|
||||
}
|
||||
|
||||
@@ -200,6 +299,12 @@ class ApprovalController extends ChangeNotifier {
|
||||
required ApprovalStepActionType type,
|
||||
String? note,
|
||||
}) async {
|
||||
final approvalId = _selected?.id;
|
||||
if (approvalId == null) {
|
||||
_errorMessage = '선택한 결재 정보가 없어 단계를 처리할 수 없습니다.';
|
||||
notifyListeners();
|
||||
return false;
|
||||
}
|
||||
if (step.id == null) {
|
||||
_errorMessage = '단계 식별자가 없어 행위를 수행할 수 없습니다.';
|
||||
notifyListeners();
|
||||
@@ -217,6 +322,14 @@ class ApprovalController extends ChangeNotifier {
|
||||
_errorMessage = null;
|
||||
notifyListeners();
|
||||
try {
|
||||
final proceedStatus = await _repository.canProceed(approvalId);
|
||||
_proceedStatus = proceedStatus;
|
||||
if (!proceedStatus.canProceed) {
|
||||
_errorMessage = proceedStatus.reason ??
|
||||
'결재 단계가 현재 상태에서 진행될 수 없습니다.';
|
||||
return false;
|
||||
}
|
||||
|
||||
final sanitizedNote = note?.trim();
|
||||
final updated = await _repository.performStepAction(
|
||||
ApprovalStepActionInput(
|
||||
@@ -232,9 +345,15 @@ class ApprovalController extends ChangeNotifier {
|
||||
.toList();
|
||||
_result = _result!.copyWith(items: items);
|
||||
}
|
||||
if (updated.id != null) {
|
||||
await _loadProceedStatus(updated.id!);
|
||||
} else {
|
||||
await _loadProceedStatus(approvalId);
|
||||
}
|
||||
return true;
|
||||
} catch (e) {
|
||||
_errorMessage = e.toString();
|
||||
} catch (error) {
|
||||
final failure = Failure.from(error);
|
||||
_errorMessage = failure.describe();
|
||||
return false;
|
||||
} finally {
|
||||
_isPerformingAction = false;
|
||||
@@ -291,8 +410,9 @@ class ApprovalController extends ChangeNotifier {
|
||||
_result = _result!.copyWith(items: items);
|
||||
}
|
||||
return true;
|
||||
} catch (e) {
|
||||
_errorMessage = e.toString();
|
||||
} catch (error) {
|
||||
final failure = Failure.from(error);
|
||||
_errorMessage = failure.describe();
|
||||
return false;
|
||||
} finally {
|
||||
_isApplyingTemplate = false;
|
||||
@@ -348,4 +468,15 @@ class ApprovalController extends ChangeNotifier {
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
Future<void> _loadProceedStatus(int approvalId) async {
|
||||
try {
|
||||
final status = await _repository.canProceed(approvalId);
|
||||
_proceedStatus = status;
|
||||
} catch (error) {
|
||||
_proceedStatus = null;
|
||||
final failure = Failure.from(error);
|
||||
_errorMessage ??= failure.describe();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -7,6 +7,7 @@ import 'package:shadcn_ui/shadcn_ui.dart';
|
||||
import '../../../../core/config/environment.dart';
|
||||
import '../../../../core/constants/app_sections.dart';
|
||||
import '../../../../core/permissions/permission_manager.dart';
|
||||
import '../../../../core/permissions/permission_resources.dart';
|
||||
import '../../../../widgets/app_layout.dart';
|
||||
import '../../../../widgets/components/feedback.dart';
|
||||
import '../../../../widgets/components/filter_bar.dart';
|
||||
@@ -18,9 +19,10 @@ import '../../domain/entities/approval.dart';
|
||||
import '../../domain/entities/approval_template.dart';
|
||||
import '../../domain/repositories/approval_repository.dart';
|
||||
import '../../domain/repositories/approval_template_repository.dart';
|
||||
import '../../../inventory/lookups/domain/repositories/inventory_lookup_repository.dart';
|
||||
import '../controllers/approval_controller.dart';
|
||||
|
||||
const _approvalsResourcePath = '/approvals/requests';
|
||||
const _approvalsResourcePath = PermissionResources.approvals;
|
||||
|
||||
/// 결재 관리 최상위 페이지.
|
||||
///
|
||||
@@ -85,11 +87,15 @@ class _ApprovalEnabledPageState extends State<_ApprovalEnabledPage> {
|
||||
_controller = ApprovalController(
|
||||
approvalRepository: GetIt.I<ApprovalRepository>(),
|
||||
templateRepository: GetIt.I<ApprovalTemplateRepository>(),
|
||||
lookupRepository: GetIt.I.isRegistered<InventoryLookupRepository>()
|
||||
? GetIt.I<InventoryLookupRepository>()
|
||||
: null,
|
||||
)..addListener(_handleControllerUpdate);
|
||||
WidgetsBinding.instance.addPostFrameCallback((_) async {
|
||||
await Future.wait([
|
||||
_controller.loadActionOptions(),
|
||||
_controller.loadTemplates(),
|
||||
_controller.loadStatusLookups(),
|
||||
]);
|
||||
await _controller.fetch();
|
||||
});
|
||||
@@ -335,6 +341,8 @@ class _ApprovalEnabledPageState extends State<_ApprovalEnabledPage> {
|
||||
selectedTemplateId: _selectedTemplateId,
|
||||
canPerformStepActions: canPerformStepActions,
|
||||
canApplyTemplate: canManageTemplates,
|
||||
canProceed: _controller.canProceedSelected,
|
||||
cannotProceedReason: _controller.cannotProceedReason,
|
||||
dateFormat: _dateTimeFormat,
|
||||
onRefresh: () {
|
||||
final id = selectedApproval?.id;
|
||||
@@ -660,22 +668,8 @@ class _ApprovalEnabledPageState extends State<_ApprovalEnabledPage> {
|
||||
return confirmed ?? false;
|
||||
}
|
||||
|
||||
String _statusLabel(ApprovalStatusFilter filter) {
|
||||
switch (filter) {
|
||||
case ApprovalStatusFilter.all:
|
||||
return '전체 상태';
|
||||
case ApprovalStatusFilter.pending:
|
||||
return '대기';
|
||||
case ApprovalStatusFilter.inProgress:
|
||||
return '진행중';
|
||||
case ApprovalStatusFilter.onHold:
|
||||
return '보류';
|
||||
case ApprovalStatusFilter.approved:
|
||||
return '승인';
|
||||
case ApprovalStatusFilter.rejected:
|
||||
return '반려';
|
||||
}
|
||||
}
|
||||
String _statusLabel(ApprovalStatusFilter filter) =>
|
||||
_controller.statusLabel(filter);
|
||||
|
||||
String _dialogTitle(ApprovalStepActionType type) {
|
||||
switch (type) {
|
||||
@@ -827,6 +821,8 @@ class _DetailSection extends StatelessWidget {
|
||||
required this.selectedTemplateId,
|
||||
required this.canPerformStepActions,
|
||||
required this.canApplyTemplate,
|
||||
required this.canProceed,
|
||||
required this.cannotProceedReason,
|
||||
required this.dateFormat,
|
||||
required this.onRefresh,
|
||||
required this.onClose,
|
||||
@@ -849,6 +845,8 @@ class _DetailSection extends StatelessWidget {
|
||||
final int? selectedTemplateId;
|
||||
final bool canPerformStepActions;
|
||||
final bool canApplyTemplate;
|
||||
final bool canProceed;
|
||||
final String? cannotProceedReason;
|
||||
final intl.DateFormat dateFormat;
|
||||
final VoidCallback onRefresh;
|
||||
final VoidCallback? onClose;
|
||||
@@ -929,6 +927,8 @@ class _DetailSection extends StatelessWidget {
|
||||
selectedTemplateId: selectedTemplateId,
|
||||
canPerformStepActions: canPerformStepActions,
|
||||
canApplyTemplate: canApplyTemplate,
|
||||
canProceed: canProceed,
|
||||
cannotProceedReason: cannotProceedReason,
|
||||
onSelectTemplate: onSelectTemplate,
|
||||
onApplyTemplate: onApplyTemplate,
|
||||
onReloadTemplates: onReloadTemplates,
|
||||
@@ -1028,6 +1028,8 @@ class _StepTab extends StatelessWidget {
|
||||
required this.selectedTemplateId,
|
||||
required this.canPerformStepActions,
|
||||
required this.canApplyTemplate,
|
||||
required this.canProceed,
|
||||
required this.cannotProceedReason,
|
||||
required this.onSelectTemplate,
|
||||
required this.onApplyTemplate,
|
||||
required this.onReloadTemplates,
|
||||
@@ -1048,6 +1050,8 @@ class _StepTab extends StatelessWidget {
|
||||
final int? selectedTemplateId;
|
||||
final bool canPerformStepActions;
|
||||
final bool canApplyTemplate;
|
||||
final bool canProceed;
|
||||
final String? cannotProceedReason;
|
||||
final void Function(int?) onSelectTemplate;
|
||||
final void Function(int templateId) onApplyTemplate;
|
||||
final VoidCallback onReloadTemplates;
|
||||
@@ -1097,6 +1101,14 @@ class _StepTab extends StatelessWidget {
|
||||
style: theme.textTheme.muted,
|
||||
),
|
||||
),
|
||||
if (!canProceed)
|
||||
Padding(
|
||||
padding: const EdgeInsets.fromLTRB(20, 0, 20, 12),
|
||||
child: Text(
|
||||
cannotProceedReason ?? '현재는 결재 단계를 진행할 수 없습니다.',
|
||||
style: theme.textTheme.muted,
|
||||
),
|
||||
),
|
||||
if (steps.isEmpty)
|
||||
Expanded(
|
||||
child: Center(
|
||||
@@ -1112,6 +1124,8 @@ class _StepTab extends StatelessWidget {
|
||||
final disabledReason = _disabledReason(
|
||||
step,
|
||||
canPerformStepActions,
|
||||
canProceed,
|
||||
cannotProceedReason,
|
||||
);
|
||||
final isProcessingStep =
|
||||
isPerformingAction && processingStepId == step.id;
|
||||
@@ -1284,7 +1298,12 @@ class _StepTab extends StatelessWidget {
|
||||
return button;
|
||||
}
|
||||
|
||||
String? _disabledReason(ApprovalStep step, bool canPerformStepActions) {
|
||||
String? _disabledReason(
|
||||
ApprovalStep step,
|
||||
bool canPerformStepActions,
|
||||
bool canProceed,
|
||||
String? cannotProceedReason,
|
||||
) {
|
||||
if (!canPerformStepActions) {
|
||||
return '결재 행위를 수행할 권한이 없습니다.';
|
||||
}
|
||||
@@ -1294,6 +1313,9 @@ class _StepTab extends StatelessWidget {
|
||||
if (!hasActionOptions) {
|
||||
return '사용 가능한 결재 행위가 없습니다.';
|
||||
}
|
||||
if (!canProceed) {
|
||||
return cannotProceedReason ?? '현재는 결재 단계를 진행할 수 없습니다.';
|
||||
}
|
||||
if (isPerformingAction && processingStepId != step.id) {
|
||||
return '다른 결재 단계를 처리 중입니다.';
|
||||
}
|
||||
|
||||
@@ -2,6 +2,7 @@ import 'package:dio/dio.dart';
|
||||
|
||||
import 'package:superport_v2/core/common/models/paginated_result.dart';
|
||||
import 'package:superport_v2/core/network/api_client.dart';
|
||||
import 'package:superport_v2/core/network/api_routes.dart';
|
||||
import 'package:superport_v2/features/approvals/step/domain/entities/approval_step_record.dart';
|
||||
import 'package:superport_v2/features/approvals/step/domain/repositories/approval_step_repository.dart';
|
||||
import '../dtos/approval_step_record_dto.dart';
|
||||
@@ -14,7 +15,7 @@ class ApprovalStepRepositoryRemote implements ApprovalStepRepository {
|
||||
|
||||
final ApiClient _api;
|
||||
|
||||
static const _basePath = '/approval-steps';
|
||||
static const _basePath = '${ApiRoutes.apiV1}/approval-steps';
|
||||
|
||||
/// 결재 단계 목록을 조회한다.
|
||||
@override
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:superport_v2/core/common/models/paginated_result.dart';
|
||||
import 'package:superport_v2/core/network/failure.dart';
|
||||
|
||||
import '../../domain/entities/approval_step_input.dart';
|
||||
import '../../domain/entities/approval_step_record.dart';
|
||||
@@ -49,8 +50,9 @@ class ApprovalStepController extends ChangeNotifier {
|
||||
approverId: _approverId,
|
||||
);
|
||||
_result = response;
|
||||
} catch (e) {
|
||||
_errorMessage = e.toString();
|
||||
} catch (error) {
|
||||
final failure = Failure.from(error);
|
||||
_errorMessage = failure.describe();
|
||||
} finally {
|
||||
_isLoading = false;
|
||||
notifyListeners();
|
||||
@@ -86,8 +88,9 @@ class ApprovalStepController extends ChangeNotifier {
|
||||
final detail = await _repository.fetchDetail(id);
|
||||
_selected = detail;
|
||||
return detail;
|
||||
} catch (e) {
|
||||
_errorMessage = e.toString();
|
||||
} catch (error) {
|
||||
final failure = Failure.from(error);
|
||||
_errorMessage = failure.describe();
|
||||
return null;
|
||||
} finally {
|
||||
_isLoadingDetail = false;
|
||||
@@ -128,8 +131,9 @@ class ApprovalStepController extends ChangeNotifier {
|
||||
final nextPage = _result?.page ?? 1;
|
||||
await fetch(page: nextPage);
|
||||
return created;
|
||||
} catch (e) {
|
||||
_errorMessage = e.toString();
|
||||
} catch (error) {
|
||||
final failure = Failure.from(error);
|
||||
_errorMessage = failure.describe();
|
||||
return null;
|
||||
} finally {
|
||||
_isSaving = false;
|
||||
@@ -150,8 +154,9 @@ class ApprovalStepController extends ChangeNotifier {
|
||||
final nextPage = _result?.page ?? 1;
|
||||
await fetch(page: nextPage);
|
||||
return updated;
|
||||
} catch (e) {
|
||||
_errorMessage = e.toString();
|
||||
} catch (error) {
|
||||
final failure = Failure.from(error);
|
||||
_errorMessage = failure.describe();
|
||||
return null;
|
||||
} finally {
|
||||
_isSaving = false;
|
||||
@@ -181,8 +186,9 @@ class ApprovalStepController extends ChangeNotifier {
|
||||
_result = _result!.copyWith(items: items);
|
||||
}
|
||||
return true;
|
||||
} catch (e) {
|
||||
_errorMessage = e.toString();
|
||||
} catch (error) {
|
||||
final failure = Failure.from(error);
|
||||
_errorMessage = failure.describe();
|
||||
return false;
|
||||
} finally {
|
||||
_isSaving = false;
|
||||
@@ -210,8 +216,9 @@ class ApprovalStepController extends ChangeNotifier {
|
||||
_result = _result!.copyWith(items: items);
|
||||
}
|
||||
return record;
|
||||
} catch (e) {
|
||||
_errorMessage = e.toString();
|
||||
} catch (error) {
|
||||
final failure = Failure.from(error);
|
||||
_errorMessage = failure.describe();
|
||||
return null;
|
||||
} finally {
|
||||
_isSaving = false;
|
||||
|
||||
@@ -6,6 +6,7 @@ import 'package:shadcn_ui/shadcn_ui.dart';
|
||||
import '../../../../../core/config/environment.dart';
|
||||
import '../../../../../core/constants/app_sections.dart';
|
||||
import '../../../../../core/permissions/permission_manager.dart';
|
||||
import '../../../../../core/permissions/permission_resources.dart';
|
||||
import '../../../../../widgets/app_layout.dart';
|
||||
import '../../../../../widgets/components/filter_bar.dart';
|
||||
import '../../../../../widgets/components/superport_dialog.dart';
|
||||
@@ -15,7 +16,7 @@ import '../../domain/entities/approval_step_input.dart';
|
||||
import '../../domain/entities/approval_step_record.dart';
|
||||
import '../../domain/repositories/approval_step_repository.dart';
|
||||
|
||||
const String _stepResourcePath = '/approvals/steps';
|
||||
const String _stepResourcePath = PermissionResources.approvalSteps;
|
||||
|
||||
/// 결재 단계 관리 진입 페이지. 기능 플래그에 따라 실제 화면 또는 준비중 화면을 노출한다.
|
||||
class ApprovalStepPage extends StatelessWidget {
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:superport_v2/core/common/models/paginated_result.dart';
|
||||
import 'package:superport_v2/core/network/failure.dart';
|
||||
|
||||
import '../../../domain/entities/approval_template.dart';
|
||||
import '../../../domain/repositories/approval_template_repository.dart';
|
||||
@@ -54,8 +55,9 @@ class ApprovalTemplateController extends ChangeNotifier {
|
||||
);
|
||||
_result = response;
|
||||
_pageSize = response.pageSize;
|
||||
} catch (e) {
|
||||
_errorMessage = e.toString();
|
||||
} catch (error) {
|
||||
final failure = Failure.from(error);
|
||||
_errorMessage = failure.describe();
|
||||
} finally {
|
||||
_isLoading = false;
|
||||
notifyListeners();
|
||||
@@ -83,8 +85,9 @@ class ApprovalTemplateController extends ChangeNotifier {
|
||||
try {
|
||||
final detail = await _repository.fetchDetail(id, includeSteps: true);
|
||||
return detail;
|
||||
} catch (e) {
|
||||
_errorMessage = e.toString();
|
||||
} catch (error) {
|
||||
final failure = Failure.from(error);
|
||||
_errorMessage = failure.describe();
|
||||
notifyListeners();
|
||||
return null;
|
||||
}
|
||||
@@ -100,8 +103,9 @@ class ApprovalTemplateController extends ChangeNotifier {
|
||||
final created = await _repository.create(input, steps: steps);
|
||||
await fetch(page: 1);
|
||||
return created;
|
||||
} catch (e) {
|
||||
_errorMessage = e.toString();
|
||||
} catch (error) {
|
||||
final failure = Failure.from(error);
|
||||
_errorMessage = failure.describe();
|
||||
notifyListeners();
|
||||
return null;
|
||||
} finally {
|
||||
@@ -120,8 +124,9 @@ class ApprovalTemplateController extends ChangeNotifier {
|
||||
final updated = await _repository.update(id, input, steps: steps);
|
||||
await fetch(page: _result?.page ?? 1);
|
||||
return updated;
|
||||
} catch (e) {
|
||||
_errorMessage = e.toString();
|
||||
} catch (error) {
|
||||
final failure = Failure.from(error);
|
||||
_errorMessage = failure.describe();
|
||||
notifyListeners();
|
||||
return null;
|
||||
} finally {
|
||||
@@ -136,8 +141,9 @@ class ApprovalTemplateController extends ChangeNotifier {
|
||||
await _repository.delete(id);
|
||||
await fetch(page: _result?.page ?? 1);
|
||||
return true;
|
||||
} catch (e) {
|
||||
_errorMessage = e.toString();
|
||||
} catch (error) {
|
||||
final failure = Failure.from(error);
|
||||
_errorMessage = failure.describe();
|
||||
notifyListeners();
|
||||
return false;
|
||||
} finally {
|
||||
@@ -152,8 +158,9 @@ class ApprovalTemplateController extends ChangeNotifier {
|
||||
final restored = await _repository.restore(id);
|
||||
await fetch(page: _result?.page ?? 1);
|
||||
return restored;
|
||||
} catch (e) {
|
||||
_errorMessage = e.toString();
|
||||
} catch (error) {
|
||||
final failure = Failure.from(error);
|
||||
_errorMessage = failure.describe();
|
||||
notifyListeners();
|
||||
return null;
|
||||
} finally {
|
||||
|
||||
Reference in New Issue
Block a user