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:dio/dio.dart';
|
||||||
import 'package:superport_v2/core/common/models/paginated_result.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_client.dart';
|
||||||
|
import 'package:superport_v2/core/network/api_routes.dart';
|
||||||
|
|
||||||
import '../../domain/entities/approval.dart';
|
import '../../domain/entities/approval.dart';
|
||||||
|
import '../../domain/entities/approval_proceed_status.dart';
|
||||||
import '../../domain/repositories/approval_repository.dart';
|
import '../../domain/repositories/approval_repository.dart';
|
||||||
import '../dtos/approval_dto.dart';
|
import '../dtos/approval_dto.dart';
|
||||||
|
import '../dtos/approval_proceed_status_dto.dart';
|
||||||
|
|
||||||
/// 결재 API 엔드포인트를 호출하는 원격 저장소 구현체.
|
/// 결재 API 엔드포인트를 호출하는 원격 저장소 구현체.
|
||||||
///
|
///
|
||||||
@@ -15,7 +18,7 @@ class ApprovalRepositoryRemote implements ApprovalRepository {
|
|||||||
|
|
||||||
final ApiClient _api;
|
final ApiClient _api;
|
||||||
|
|
||||||
static const _basePath = '/approvals';
|
static const _basePath = '${ApiRoutes.apiV1}/approvals';
|
||||||
|
|
||||||
/// 결재 목록을 조회한다. 필터 조건이 없으면 최신순 페이지를 반환한다.
|
/// 결재 목록을 조회한다. 필터 조건이 없으면 최신순 페이지를 반환한다.
|
||||||
@override
|
@override
|
||||||
@@ -69,7 +72,7 @@ class ApprovalRepositoryRemote implements ApprovalRepository {
|
|||||||
@override
|
@override
|
||||||
Future<List<ApprovalAction>> listActions({bool activeOnly = true}) async {
|
Future<List<ApprovalAction>> listActions({bool activeOnly = true}) async {
|
||||||
final response = await _api.get<Map<String, dynamic>>(
|
final response = await _api.get<Map<String, dynamic>>(
|
||||||
'/approval-actions',
|
'${ApiRoutes.apiV1}/approval-actions',
|
||||||
query: {'page': 1, 'page_size': 100, if (activeOnly) 'active': true},
|
query: {'page': 1, 'page_size': 100, if (activeOnly) 'active': true},
|
||||||
options: Options(responseType: ResponseType.json),
|
options: Options(responseType: ResponseType.json),
|
||||||
);
|
);
|
||||||
@@ -85,7 +88,7 @@ class ApprovalRepositoryRemote implements ApprovalRepository {
|
|||||||
@override
|
@override
|
||||||
Future<Approval> performStepAction(ApprovalStepActionInput input) async {
|
Future<Approval> performStepAction(ApprovalStepActionInput input) async {
|
||||||
final response = await _api.post<Map<String, dynamic>>(
|
final response = await _api.post<Map<String, dynamic>>(
|
||||||
'/approval-steps/${input.stepId}/actions',
|
'${ApiRoutes.apiV1}/approval-steps/${input.stepId}/actions',
|
||||||
data: input.toPayload(),
|
data: input.toPayload(),
|
||||||
options: Options(responseType: ResponseType.json),
|
options: Options(responseType: ResponseType.json),
|
||||||
);
|
);
|
||||||
@@ -102,7 +105,7 @@ class ApprovalRepositoryRemote implements ApprovalRepository {
|
|||||||
@override
|
@override
|
||||||
Future<Approval> assignSteps(ApprovalStepAssignmentInput input) async {
|
Future<Approval> assignSteps(ApprovalStepAssignmentInput input) async {
|
||||||
final response = await _api.post<Map<String, dynamic>>(
|
final response = await _api.post<Map<String, dynamic>>(
|
||||||
'/approvals/${input.approvalId}/steps',
|
'${ApiRoutes.apiV1}/approvals/${input.approvalId}/steps',
|
||||||
data: input.toPayload(),
|
data: input.toPayload(),
|
||||||
options: Options(responseType: ResponseType.json),
|
options: Options(responseType: ResponseType.json),
|
||||||
);
|
);
|
||||||
@@ -115,6 +118,17 @@ class ApprovalRepositoryRemote implements ApprovalRepository {
|
|||||||
return ApprovalDto.fromJson(approvalJson).toEntity();
|
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
|
@override
|
||||||
Future<Approval> create(ApprovalInput input) async {
|
Future<Approval> create(ApprovalInput input) async {
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ import 'package:dio/dio.dart';
|
|||||||
|
|
||||||
import '../../../../core/common/models/paginated_result.dart';
|
import '../../../../core/common/models/paginated_result.dart';
|
||||||
import '../../../../core/network/api_client.dart';
|
import '../../../../core/network/api_client.dart';
|
||||||
|
import '../../../../core/network/api_routes.dart';
|
||||||
import '../../domain/entities/approval_template.dart';
|
import '../../domain/entities/approval_template.dart';
|
||||||
import '../../domain/repositories/approval_template_repository.dart';
|
import '../../domain/repositories/approval_template_repository.dart';
|
||||||
import '../dtos/approval_template_dto.dart';
|
import '../dtos/approval_template_dto.dart';
|
||||||
@@ -16,7 +17,7 @@ class ApprovalTemplateRepositoryRemote implements ApprovalTemplateRepository {
|
|||||||
|
|
||||||
final ApiClient _api;
|
final ApiClient _api;
|
||||||
|
|
||||||
static const _basePath = '/approval-templates';
|
static const _basePath = '${ApiRoutes.apiV1}/approval-templates';
|
||||||
|
|
||||||
/// 결재 템플릿 목록을 조회한다. 검색/활성 여부 필터를 지원한다.
|
/// 결재 템플릿 목록을 조회한다. 검색/활성 여부 필터를 지원한다.
|
||||||
@override
|
@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 'package:superport_v2/core/common/models/paginated_result.dart';
|
||||||
|
|
||||||
import '../entities/approval.dart';
|
import '../entities/approval.dart';
|
||||||
|
import '../entities/approval_proceed_status.dart';
|
||||||
|
|
||||||
/// 결재 도메인에서 사용하는 저장소 인터페이스.
|
/// 결재 도메인에서 사용하는 저장소 인터페이스.
|
||||||
///
|
///
|
||||||
@@ -34,6 +35,9 @@ abstract class ApprovalRepository {
|
|||||||
/// 결재 단계 일괄 생성/재배치
|
/// 결재 단계 일괄 생성/재배치
|
||||||
Future<Approval> assignSteps(ApprovalStepAssignmentInput input);
|
Future<Approval> assignSteps(ApprovalStepAssignmentInput input);
|
||||||
|
|
||||||
|
/// 결재가 다음 단계로 진행 가능한지 여부를 확인한다.
|
||||||
|
Future<ApprovalProceedStatus> canProceed(int id);
|
||||||
|
|
||||||
/// 결재를 생성한다.
|
/// 결재를 생성한다.
|
||||||
Future<Approval> create(ApprovalInput input);
|
Future<Approval> create(ApprovalInput input);
|
||||||
|
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
import 'package:dio/dio.dart';
|
import 'package:dio/dio.dart';
|
||||||
import 'package:superport_v2/core/common/models/paginated_result.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_client.dart';
|
||||||
|
import 'package:superport_v2/core/network/api_routes.dart';
|
||||||
|
|
||||||
import '../../domain/entities/approval_history_record.dart';
|
import '../../domain/entities/approval_history_record.dart';
|
||||||
import '../../domain/repositories/approval_history_repository.dart';
|
import '../../domain/repositories/approval_history_repository.dart';
|
||||||
@@ -13,7 +14,7 @@ class ApprovalHistoryRepositoryRemote implements ApprovalHistoryRepository {
|
|||||||
|
|
||||||
final ApiClient _api;
|
final ApiClient _api;
|
||||||
|
|
||||||
static const _basePath = '/approval-histories';
|
static const _basePath = '${ApiRoutes.apiV1}/approval-histories';
|
||||||
|
|
||||||
/// 결재 이력 목록을 조회한다.
|
/// 결재 이력 목록을 조회한다.
|
||||||
@override
|
@override
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
import 'package:flutter/foundation.dart';
|
import 'package:flutter/foundation.dart';
|
||||||
import 'package:superport_v2/core/common/models/paginated_result.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/entities/approval_history_record.dart';
|
||||||
import '../../domain/repositories/approval_history_repository.dart';
|
import '../../domain/repositories/approval_history_repository.dart';
|
||||||
@@ -59,8 +60,9 @@ class ApprovalHistoryController extends ChangeNotifier {
|
|||||||
);
|
);
|
||||||
_result = response;
|
_result = response;
|
||||||
_pageSize = response.pageSize;
|
_pageSize = response.pageSize;
|
||||||
} catch (e) {
|
} catch (error) {
|
||||||
_errorMessage = e.toString();
|
final failure = Failure.from(error);
|
||||||
|
_errorMessage = failure.describe();
|
||||||
} finally {
|
} finally {
|
||||||
_isLoading = false;
|
_isLoading = false;
|
||||||
notifyListeners();
|
notifyListeners();
|
||||||
|
|||||||
@@ -1,7 +1,11 @@
|
|||||||
import 'package:flutter/foundation.dart';
|
import 'package:flutter/foundation.dart';
|
||||||
import 'package:superport_v2/core/common/models/paginated_result.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.dart';
|
||||||
|
import '../../domain/entities/approval_proceed_status.dart';
|
||||||
import '../../domain/entities/approval_template.dart';
|
import '../../domain/entities/approval_template.dart';
|
||||||
import '../../domain/repositories/approval_repository.dart';
|
import '../../domain/repositories/approval_repository.dart';
|
||||||
import '../../domain/repositories/approval_template_repository.dart';
|
import '../../domain/repositories/approval_template_repository.dart';
|
||||||
@@ -23,6 +27,14 @@ const Map<ApprovalStepActionType, List<String>> _actionAliases = {
|
|||||||
ApprovalStepActionType.comment: ['comment', '코멘트', '의견'],
|
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({
|
ApprovalController({
|
||||||
required ApprovalRepository approvalRepository,
|
required ApprovalRepository approvalRepository,
|
||||||
required ApprovalTemplateRepository templateRepository,
|
required ApprovalTemplateRepository templateRepository,
|
||||||
|
InventoryLookupRepository? lookupRepository,
|
||||||
}) : _repository = approvalRepository,
|
}) : _repository = approvalRepository,
|
||||||
_templateRepository = templateRepository;
|
_templateRepository = templateRepository,
|
||||||
|
_lookupRepository = lookupRepository;
|
||||||
|
|
||||||
final ApprovalRepository _repository;
|
final ApprovalRepository _repository;
|
||||||
final ApprovalTemplateRepository _templateRepository;
|
final ApprovalTemplateRepository _templateRepository;
|
||||||
|
final InventoryLookupRepository? _lookupRepository;
|
||||||
|
|
||||||
PaginatedResult<Approval>? _result;
|
PaginatedResult<Approval>? _result;
|
||||||
Approval? _selected;
|
Approval? _selected;
|
||||||
@@ -47,6 +62,7 @@ class ApprovalController extends ChangeNotifier {
|
|||||||
bool _isLoadingTemplates = false;
|
bool _isLoadingTemplates = false;
|
||||||
bool _isApplyingTemplate = false;
|
bool _isApplyingTemplate = false;
|
||||||
int? _applyingTemplateId;
|
int? _applyingTemplateId;
|
||||||
|
ApprovalProceedStatus? _proceedStatus;
|
||||||
String? _errorMessage;
|
String? _errorMessage;
|
||||||
String _query = '';
|
String _query = '';
|
||||||
ApprovalStatusFilter _statusFilter = ApprovalStatusFilter.all;
|
ApprovalStatusFilter _statusFilter = ApprovalStatusFilter.all;
|
||||||
@@ -54,6 +70,12 @@ class ApprovalController extends ChangeNotifier {
|
|||||||
DateTime? _toDate;
|
DateTime? _toDate;
|
||||||
List<ApprovalAction> _actions = const [];
|
List<ApprovalAction> _actions = const [];
|
||||||
List<ApprovalTemplate> _templates = 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;
|
PaginatedResult<Approval>? get result => _result;
|
||||||
Approval? get selected => _selected;
|
Approval? get selected => _selected;
|
||||||
@@ -73,6 +95,17 @@ class ApprovalController extends ChangeNotifier {
|
|||||||
bool get isLoadingTemplates => _isLoadingTemplates;
|
bool get isLoadingTemplates => _isLoadingTemplates;
|
||||||
bool get isApplyingTemplate => _isApplyingTemplate;
|
bool get isApplyingTemplate => _isApplyingTemplate;
|
||||||
int? get applyingTemplateId => _applyingTemplateId;
|
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;
|
_errorMessage = null;
|
||||||
notifyListeners();
|
notifyListeners();
|
||||||
try {
|
try {
|
||||||
final statusParam = switch (_statusFilter) {
|
final statusParam = _statusCodeFor(_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(
|
final response = await _repository.list(
|
||||||
page: page,
|
page: page,
|
||||||
pageSize: _result?.pageSize ?? 20,
|
pageSize: _result?.pageSize ?? 20,
|
||||||
@@ -106,10 +132,12 @@ class ApprovalController extends ChangeNotifier {
|
|||||||
final exists = response.items.any((item) => item.id == _selected?.id);
|
final exists = response.items.any((item) => item.id == _selected?.id);
|
||||||
if (!exists) {
|
if (!exists) {
|
||||||
_selected = null;
|
_selected = null;
|
||||||
|
_proceedStatus = null;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} catch (e) {
|
} catch (error) {
|
||||||
_errorMessage = e.toString();
|
final failure = Failure.from(error);
|
||||||
|
_errorMessage = failure.describe();
|
||||||
} finally {
|
} finally {
|
||||||
_isLoadingList = false;
|
_isLoadingList = false;
|
||||||
notifyListeners();
|
notifyListeners();
|
||||||
@@ -129,14 +157,78 @@ class ApprovalController extends ChangeNotifier {
|
|||||||
try {
|
try {
|
||||||
final items = await _repository.listActions(activeOnly: true);
|
final items = await _repository.listActions(activeOnly: true);
|
||||||
_actions = items;
|
_actions = items;
|
||||||
} catch (e) {
|
} catch (error) {
|
||||||
_errorMessage = e.toString();
|
final failure = Failure.from(error);
|
||||||
|
_errorMessage = failure.describe();
|
||||||
} finally {
|
} finally {
|
||||||
_isLoadingActions = false;
|
_isLoadingActions = false;
|
||||||
notifyListeners();
|
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를 다시 호출한다.
|
/// 템플릿이 비어 있거나 [force]가 `true`이면 API를 다시 호출한다.
|
||||||
@@ -154,8 +246,9 @@ class ApprovalController extends ChangeNotifier {
|
|||||||
isActive: true,
|
isActive: true,
|
||||||
);
|
);
|
||||||
_templates = result.items;
|
_templates = result.items;
|
||||||
} catch (e) {
|
} catch (error) {
|
||||||
_errorMessage = e.toString();
|
final failure = Failure.from(error);
|
||||||
|
_errorMessage = failure.describe();
|
||||||
} finally {
|
} finally {
|
||||||
_isLoadingTemplates = false;
|
_isLoadingTemplates = false;
|
||||||
notifyListeners();
|
notifyListeners();
|
||||||
@@ -169,6 +262,7 @@ class ApprovalController extends ChangeNotifier {
|
|||||||
Future<void> selectApproval(int id) async {
|
Future<void> selectApproval(int id) async {
|
||||||
_isLoadingDetail = true;
|
_isLoadingDetail = true;
|
||||||
_errorMessage = null;
|
_errorMessage = null;
|
||||||
|
_proceedStatus = null;
|
||||||
notifyListeners();
|
notifyListeners();
|
||||||
try {
|
try {
|
||||||
final detail = await _repository.fetchDetail(
|
final detail = await _repository.fetchDetail(
|
||||||
@@ -177,8 +271,12 @@ class ApprovalController extends ChangeNotifier {
|
|||||||
includeHistories: true,
|
includeHistories: true,
|
||||||
);
|
);
|
||||||
_selected = detail;
|
_selected = detail;
|
||||||
} catch (e) {
|
if (detail.id != null) {
|
||||||
_errorMessage = e.toString();
|
await _loadProceedStatus(detail.id!);
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
final failure = Failure.from(error);
|
||||||
|
_errorMessage = failure.describe();
|
||||||
} finally {
|
} finally {
|
||||||
_isLoadingDetail = false;
|
_isLoadingDetail = false;
|
||||||
notifyListeners();
|
notifyListeners();
|
||||||
@@ -188,6 +286,7 @@ class ApprovalController extends ChangeNotifier {
|
|||||||
/// 선택된 결재 상세를 비우고 화면을 초기화한다.
|
/// 선택된 결재 상세를 비우고 화면을 초기화한다.
|
||||||
void clearSelection() {
|
void clearSelection() {
|
||||||
_selected = null;
|
_selected = null;
|
||||||
|
_proceedStatus = null;
|
||||||
notifyListeners();
|
notifyListeners();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -200,6 +299,12 @@ class ApprovalController extends ChangeNotifier {
|
|||||||
required ApprovalStepActionType type,
|
required ApprovalStepActionType type,
|
||||||
String? note,
|
String? note,
|
||||||
}) async {
|
}) async {
|
||||||
|
final approvalId = _selected?.id;
|
||||||
|
if (approvalId == null) {
|
||||||
|
_errorMessage = '선택한 결재 정보가 없어 단계를 처리할 수 없습니다.';
|
||||||
|
notifyListeners();
|
||||||
|
return false;
|
||||||
|
}
|
||||||
if (step.id == null) {
|
if (step.id == null) {
|
||||||
_errorMessage = '단계 식별자가 없어 행위를 수행할 수 없습니다.';
|
_errorMessage = '단계 식별자가 없어 행위를 수행할 수 없습니다.';
|
||||||
notifyListeners();
|
notifyListeners();
|
||||||
@@ -217,6 +322,14 @@ class ApprovalController extends ChangeNotifier {
|
|||||||
_errorMessage = null;
|
_errorMessage = null;
|
||||||
notifyListeners();
|
notifyListeners();
|
||||||
try {
|
try {
|
||||||
|
final proceedStatus = await _repository.canProceed(approvalId);
|
||||||
|
_proceedStatus = proceedStatus;
|
||||||
|
if (!proceedStatus.canProceed) {
|
||||||
|
_errorMessage = proceedStatus.reason ??
|
||||||
|
'결재 단계가 현재 상태에서 진행될 수 없습니다.';
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
final sanitizedNote = note?.trim();
|
final sanitizedNote = note?.trim();
|
||||||
final updated = await _repository.performStepAction(
|
final updated = await _repository.performStepAction(
|
||||||
ApprovalStepActionInput(
|
ApprovalStepActionInput(
|
||||||
@@ -232,9 +345,15 @@ class ApprovalController extends ChangeNotifier {
|
|||||||
.toList();
|
.toList();
|
||||||
_result = _result!.copyWith(items: items);
|
_result = _result!.copyWith(items: items);
|
||||||
}
|
}
|
||||||
|
if (updated.id != null) {
|
||||||
|
await _loadProceedStatus(updated.id!);
|
||||||
|
} else {
|
||||||
|
await _loadProceedStatus(approvalId);
|
||||||
|
}
|
||||||
return true;
|
return true;
|
||||||
} catch (e) {
|
} catch (error) {
|
||||||
_errorMessage = e.toString();
|
final failure = Failure.from(error);
|
||||||
|
_errorMessage = failure.describe();
|
||||||
return false;
|
return false;
|
||||||
} finally {
|
} finally {
|
||||||
_isPerformingAction = false;
|
_isPerformingAction = false;
|
||||||
@@ -291,8 +410,9 @@ class ApprovalController extends ChangeNotifier {
|
|||||||
_result = _result!.copyWith(items: items);
|
_result = _result!.copyWith(items: items);
|
||||||
}
|
}
|
||||||
return true;
|
return true;
|
||||||
} catch (e) {
|
} catch (error) {
|
||||||
_errorMessage = e.toString();
|
final failure = Failure.from(error);
|
||||||
|
_errorMessage = failure.describe();
|
||||||
return false;
|
return false;
|
||||||
} finally {
|
} finally {
|
||||||
_isApplyingTemplate = false;
|
_isApplyingTemplate = false;
|
||||||
@@ -348,4 +468,15 @@ class ApprovalController extends ChangeNotifier {
|
|||||||
}
|
}
|
||||||
return null;
|
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/config/environment.dart';
|
||||||
import '../../../../core/constants/app_sections.dart';
|
import '../../../../core/constants/app_sections.dart';
|
||||||
import '../../../../core/permissions/permission_manager.dart';
|
import '../../../../core/permissions/permission_manager.dart';
|
||||||
|
import '../../../../core/permissions/permission_resources.dart';
|
||||||
import '../../../../widgets/app_layout.dart';
|
import '../../../../widgets/app_layout.dart';
|
||||||
import '../../../../widgets/components/feedback.dart';
|
import '../../../../widgets/components/feedback.dart';
|
||||||
import '../../../../widgets/components/filter_bar.dart';
|
import '../../../../widgets/components/filter_bar.dart';
|
||||||
@@ -18,9 +19,10 @@ import '../../domain/entities/approval.dart';
|
|||||||
import '../../domain/entities/approval_template.dart';
|
import '../../domain/entities/approval_template.dart';
|
||||||
import '../../domain/repositories/approval_repository.dart';
|
import '../../domain/repositories/approval_repository.dart';
|
||||||
import '../../domain/repositories/approval_template_repository.dart';
|
import '../../domain/repositories/approval_template_repository.dart';
|
||||||
|
import '../../../inventory/lookups/domain/repositories/inventory_lookup_repository.dart';
|
||||||
import '../controllers/approval_controller.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(
|
_controller = ApprovalController(
|
||||||
approvalRepository: GetIt.I<ApprovalRepository>(),
|
approvalRepository: GetIt.I<ApprovalRepository>(),
|
||||||
templateRepository: GetIt.I<ApprovalTemplateRepository>(),
|
templateRepository: GetIt.I<ApprovalTemplateRepository>(),
|
||||||
|
lookupRepository: GetIt.I.isRegistered<InventoryLookupRepository>()
|
||||||
|
? GetIt.I<InventoryLookupRepository>()
|
||||||
|
: null,
|
||||||
)..addListener(_handleControllerUpdate);
|
)..addListener(_handleControllerUpdate);
|
||||||
WidgetsBinding.instance.addPostFrameCallback((_) async {
|
WidgetsBinding.instance.addPostFrameCallback((_) async {
|
||||||
await Future.wait([
|
await Future.wait([
|
||||||
_controller.loadActionOptions(),
|
_controller.loadActionOptions(),
|
||||||
_controller.loadTemplates(),
|
_controller.loadTemplates(),
|
||||||
|
_controller.loadStatusLookups(),
|
||||||
]);
|
]);
|
||||||
await _controller.fetch();
|
await _controller.fetch();
|
||||||
});
|
});
|
||||||
@@ -335,6 +341,8 @@ class _ApprovalEnabledPageState extends State<_ApprovalEnabledPage> {
|
|||||||
selectedTemplateId: _selectedTemplateId,
|
selectedTemplateId: _selectedTemplateId,
|
||||||
canPerformStepActions: canPerformStepActions,
|
canPerformStepActions: canPerformStepActions,
|
||||||
canApplyTemplate: canManageTemplates,
|
canApplyTemplate: canManageTemplates,
|
||||||
|
canProceed: _controller.canProceedSelected,
|
||||||
|
cannotProceedReason: _controller.cannotProceedReason,
|
||||||
dateFormat: _dateTimeFormat,
|
dateFormat: _dateTimeFormat,
|
||||||
onRefresh: () {
|
onRefresh: () {
|
||||||
final id = selectedApproval?.id;
|
final id = selectedApproval?.id;
|
||||||
@@ -660,22 +668,8 @@ class _ApprovalEnabledPageState extends State<_ApprovalEnabledPage> {
|
|||||||
return confirmed ?? false;
|
return confirmed ?? false;
|
||||||
}
|
}
|
||||||
|
|
||||||
String _statusLabel(ApprovalStatusFilter filter) {
|
String _statusLabel(ApprovalStatusFilter filter) =>
|
||||||
switch (filter) {
|
_controller.statusLabel(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 _dialogTitle(ApprovalStepActionType type) {
|
String _dialogTitle(ApprovalStepActionType type) {
|
||||||
switch (type) {
|
switch (type) {
|
||||||
@@ -827,6 +821,8 @@ class _DetailSection extends StatelessWidget {
|
|||||||
required this.selectedTemplateId,
|
required this.selectedTemplateId,
|
||||||
required this.canPerformStepActions,
|
required this.canPerformStepActions,
|
||||||
required this.canApplyTemplate,
|
required this.canApplyTemplate,
|
||||||
|
required this.canProceed,
|
||||||
|
required this.cannotProceedReason,
|
||||||
required this.dateFormat,
|
required this.dateFormat,
|
||||||
required this.onRefresh,
|
required this.onRefresh,
|
||||||
required this.onClose,
|
required this.onClose,
|
||||||
@@ -849,6 +845,8 @@ class _DetailSection extends StatelessWidget {
|
|||||||
final int? selectedTemplateId;
|
final int? selectedTemplateId;
|
||||||
final bool canPerformStepActions;
|
final bool canPerformStepActions;
|
||||||
final bool canApplyTemplate;
|
final bool canApplyTemplate;
|
||||||
|
final bool canProceed;
|
||||||
|
final String? cannotProceedReason;
|
||||||
final intl.DateFormat dateFormat;
|
final intl.DateFormat dateFormat;
|
||||||
final VoidCallback onRefresh;
|
final VoidCallback onRefresh;
|
||||||
final VoidCallback? onClose;
|
final VoidCallback? onClose;
|
||||||
@@ -929,6 +927,8 @@ class _DetailSection extends StatelessWidget {
|
|||||||
selectedTemplateId: selectedTemplateId,
|
selectedTemplateId: selectedTemplateId,
|
||||||
canPerformStepActions: canPerformStepActions,
|
canPerformStepActions: canPerformStepActions,
|
||||||
canApplyTemplate: canApplyTemplate,
|
canApplyTemplate: canApplyTemplate,
|
||||||
|
canProceed: canProceed,
|
||||||
|
cannotProceedReason: cannotProceedReason,
|
||||||
onSelectTemplate: onSelectTemplate,
|
onSelectTemplate: onSelectTemplate,
|
||||||
onApplyTemplate: onApplyTemplate,
|
onApplyTemplate: onApplyTemplate,
|
||||||
onReloadTemplates: onReloadTemplates,
|
onReloadTemplates: onReloadTemplates,
|
||||||
@@ -1028,6 +1028,8 @@ class _StepTab extends StatelessWidget {
|
|||||||
required this.selectedTemplateId,
|
required this.selectedTemplateId,
|
||||||
required this.canPerformStepActions,
|
required this.canPerformStepActions,
|
||||||
required this.canApplyTemplate,
|
required this.canApplyTemplate,
|
||||||
|
required this.canProceed,
|
||||||
|
required this.cannotProceedReason,
|
||||||
required this.onSelectTemplate,
|
required this.onSelectTemplate,
|
||||||
required this.onApplyTemplate,
|
required this.onApplyTemplate,
|
||||||
required this.onReloadTemplates,
|
required this.onReloadTemplates,
|
||||||
@@ -1048,6 +1050,8 @@ class _StepTab extends StatelessWidget {
|
|||||||
final int? selectedTemplateId;
|
final int? selectedTemplateId;
|
||||||
final bool canPerformStepActions;
|
final bool canPerformStepActions;
|
||||||
final bool canApplyTemplate;
|
final bool canApplyTemplate;
|
||||||
|
final bool canProceed;
|
||||||
|
final String? cannotProceedReason;
|
||||||
final void Function(int?) onSelectTemplate;
|
final void Function(int?) onSelectTemplate;
|
||||||
final void Function(int templateId) onApplyTemplate;
|
final void Function(int templateId) onApplyTemplate;
|
||||||
final VoidCallback onReloadTemplates;
|
final VoidCallback onReloadTemplates;
|
||||||
@@ -1097,6 +1101,14 @@ class _StepTab extends StatelessWidget {
|
|||||||
style: theme.textTheme.muted,
|
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)
|
if (steps.isEmpty)
|
||||||
Expanded(
|
Expanded(
|
||||||
child: Center(
|
child: Center(
|
||||||
@@ -1112,6 +1124,8 @@ class _StepTab extends StatelessWidget {
|
|||||||
final disabledReason = _disabledReason(
|
final disabledReason = _disabledReason(
|
||||||
step,
|
step,
|
||||||
canPerformStepActions,
|
canPerformStepActions,
|
||||||
|
canProceed,
|
||||||
|
cannotProceedReason,
|
||||||
);
|
);
|
||||||
final isProcessingStep =
|
final isProcessingStep =
|
||||||
isPerformingAction && processingStepId == step.id;
|
isPerformingAction && processingStepId == step.id;
|
||||||
@@ -1284,7 +1298,12 @@ class _StepTab extends StatelessWidget {
|
|||||||
return button;
|
return button;
|
||||||
}
|
}
|
||||||
|
|
||||||
String? _disabledReason(ApprovalStep step, bool canPerformStepActions) {
|
String? _disabledReason(
|
||||||
|
ApprovalStep step,
|
||||||
|
bool canPerformStepActions,
|
||||||
|
bool canProceed,
|
||||||
|
String? cannotProceedReason,
|
||||||
|
) {
|
||||||
if (!canPerformStepActions) {
|
if (!canPerformStepActions) {
|
||||||
return '결재 행위를 수행할 권한이 없습니다.';
|
return '결재 행위를 수행할 권한이 없습니다.';
|
||||||
}
|
}
|
||||||
@@ -1294,6 +1313,9 @@ class _StepTab extends StatelessWidget {
|
|||||||
if (!hasActionOptions) {
|
if (!hasActionOptions) {
|
||||||
return '사용 가능한 결재 행위가 없습니다.';
|
return '사용 가능한 결재 행위가 없습니다.';
|
||||||
}
|
}
|
||||||
|
if (!canProceed) {
|
||||||
|
return cannotProceedReason ?? '현재는 결재 단계를 진행할 수 없습니다.';
|
||||||
|
}
|
||||||
if (isPerformingAction && processingStepId != step.id) {
|
if (isPerformingAction && processingStepId != step.id) {
|
||||||
return '다른 결재 단계를 처리 중입니다.';
|
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/common/models/paginated_result.dart';
|
||||||
import 'package:superport_v2/core/network/api_client.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/entities/approval_step_record.dart';
|
||||||
import 'package:superport_v2/features/approvals/step/domain/repositories/approval_step_repository.dart';
|
import 'package:superport_v2/features/approvals/step/domain/repositories/approval_step_repository.dart';
|
||||||
import '../dtos/approval_step_record_dto.dart';
|
import '../dtos/approval_step_record_dto.dart';
|
||||||
@@ -14,7 +15,7 @@ class ApprovalStepRepositoryRemote implements ApprovalStepRepository {
|
|||||||
|
|
||||||
final ApiClient _api;
|
final ApiClient _api;
|
||||||
|
|
||||||
static const _basePath = '/approval-steps';
|
static const _basePath = '${ApiRoutes.apiV1}/approval-steps';
|
||||||
|
|
||||||
/// 결재 단계 목록을 조회한다.
|
/// 결재 단계 목록을 조회한다.
|
||||||
@override
|
@override
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
import 'package:flutter/foundation.dart';
|
import 'package:flutter/foundation.dart';
|
||||||
import 'package:superport_v2/core/common/models/paginated_result.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_input.dart';
|
||||||
import '../../domain/entities/approval_step_record.dart';
|
import '../../domain/entities/approval_step_record.dart';
|
||||||
@@ -49,8 +50,9 @@ class ApprovalStepController extends ChangeNotifier {
|
|||||||
approverId: _approverId,
|
approverId: _approverId,
|
||||||
);
|
);
|
||||||
_result = response;
|
_result = response;
|
||||||
} catch (e) {
|
} catch (error) {
|
||||||
_errorMessage = e.toString();
|
final failure = Failure.from(error);
|
||||||
|
_errorMessage = failure.describe();
|
||||||
} finally {
|
} finally {
|
||||||
_isLoading = false;
|
_isLoading = false;
|
||||||
notifyListeners();
|
notifyListeners();
|
||||||
@@ -86,8 +88,9 @@ class ApprovalStepController extends ChangeNotifier {
|
|||||||
final detail = await _repository.fetchDetail(id);
|
final detail = await _repository.fetchDetail(id);
|
||||||
_selected = detail;
|
_selected = detail;
|
||||||
return detail;
|
return detail;
|
||||||
} catch (e) {
|
} catch (error) {
|
||||||
_errorMessage = e.toString();
|
final failure = Failure.from(error);
|
||||||
|
_errorMessage = failure.describe();
|
||||||
return null;
|
return null;
|
||||||
} finally {
|
} finally {
|
||||||
_isLoadingDetail = false;
|
_isLoadingDetail = false;
|
||||||
@@ -128,8 +131,9 @@ class ApprovalStepController extends ChangeNotifier {
|
|||||||
final nextPage = _result?.page ?? 1;
|
final nextPage = _result?.page ?? 1;
|
||||||
await fetch(page: nextPage);
|
await fetch(page: nextPage);
|
||||||
return created;
|
return created;
|
||||||
} catch (e) {
|
} catch (error) {
|
||||||
_errorMessage = e.toString();
|
final failure = Failure.from(error);
|
||||||
|
_errorMessage = failure.describe();
|
||||||
return null;
|
return null;
|
||||||
} finally {
|
} finally {
|
||||||
_isSaving = false;
|
_isSaving = false;
|
||||||
@@ -150,8 +154,9 @@ class ApprovalStepController extends ChangeNotifier {
|
|||||||
final nextPage = _result?.page ?? 1;
|
final nextPage = _result?.page ?? 1;
|
||||||
await fetch(page: nextPage);
|
await fetch(page: nextPage);
|
||||||
return updated;
|
return updated;
|
||||||
} catch (e) {
|
} catch (error) {
|
||||||
_errorMessage = e.toString();
|
final failure = Failure.from(error);
|
||||||
|
_errorMessage = failure.describe();
|
||||||
return null;
|
return null;
|
||||||
} finally {
|
} finally {
|
||||||
_isSaving = false;
|
_isSaving = false;
|
||||||
@@ -181,8 +186,9 @@ class ApprovalStepController extends ChangeNotifier {
|
|||||||
_result = _result!.copyWith(items: items);
|
_result = _result!.copyWith(items: items);
|
||||||
}
|
}
|
||||||
return true;
|
return true;
|
||||||
} catch (e) {
|
} catch (error) {
|
||||||
_errorMessage = e.toString();
|
final failure = Failure.from(error);
|
||||||
|
_errorMessage = failure.describe();
|
||||||
return false;
|
return false;
|
||||||
} finally {
|
} finally {
|
||||||
_isSaving = false;
|
_isSaving = false;
|
||||||
@@ -210,8 +216,9 @@ class ApprovalStepController extends ChangeNotifier {
|
|||||||
_result = _result!.copyWith(items: items);
|
_result = _result!.copyWith(items: items);
|
||||||
}
|
}
|
||||||
return record;
|
return record;
|
||||||
} catch (e) {
|
} catch (error) {
|
||||||
_errorMessage = e.toString();
|
final failure = Failure.from(error);
|
||||||
|
_errorMessage = failure.describe();
|
||||||
return null;
|
return null;
|
||||||
} finally {
|
} finally {
|
||||||
_isSaving = false;
|
_isSaving = false;
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ import 'package:shadcn_ui/shadcn_ui.dart';
|
|||||||
import '../../../../../core/config/environment.dart';
|
import '../../../../../core/config/environment.dart';
|
||||||
import '../../../../../core/constants/app_sections.dart';
|
import '../../../../../core/constants/app_sections.dart';
|
||||||
import '../../../../../core/permissions/permission_manager.dart';
|
import '../../../../../core/permissions/permission_manager.dart';
|
||||||
|
import '../../../../../core/permissions/permission_resources.dart';
|
||||||
import '../../../../../widgets/app_layout.dart';
|
import '../../../../../widgets/app_layout.dart';
|
||||||
import '../../../../../widgets/components/filter_bar.dart';
|
import '../../../../../widgets/components/filter_bar.dart';
|
||||||
import '../../../../../widgets/components/superport_dialog.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/entities/approval_step_record.dart';
|
||||||
import '../../domain/repositories/approval_step_repository.dart';
|
import '../../domain/repositories/approval_step_repository.dart';
|
||||||
|
|
||||||
const String _stepResourcePath = '/approvals/steps';
|
const String _stepResourcePath = PermissionResources.approvalSteps;
|
||||||
|
|
||||||
/// 결재 단계 관리 진입 페이지. 기능 플래그에 따라 실제 화면 또는 준비중 화면을 노출한다.
|
/// 결재 단계 관리 진입 페이지. 기능 플래그에 따라 실제 화면 또는 준비중 화면을 노출한다.
|
||||||
class ApprovalStepPage extends StatelessWidget {
|
class ApprovalStepPage extends StatelessWidget {
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
import 'package:flutter/foundation.dart';
|
import 'package:flutter/foundation.dart';
|
||||||
import 'package:superport_v2/core/common/models/paginated_result.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/entities/approval_template.dart';
|
||||||
import '../../../domain/repositories/approval_template_repository.dart';
|
import '../../../domain/repositories/approval_template_repository.dart';
|
||||||
@@ -54,8 +55,9 @@ class ApprovalTemplateController extends ChangeNotifier {
|
|||||||
);
|
);
|
||||||
_result = response;
|
_result = response;
|
||||||
_pageSize = response.pageSize;
|
_pageSize = response.pageSize;
|
||||||
} catch (e) {
|
} catch (error) {
|
||||||
_errorMessage = e.toString();
|
final failure = Failure.from(error);
|
||||||
|
_errorMessage = failure.describe();
|
||||||
} finally {
|
} finally {
|
||||||
_isLoading = false;
|
_isLoading = false;
|
||||||
notifyListeners();
|
notifyListeners();
|
||||||
@@ -83,8 +85,9 @@ class ApprovalTemplateController extends ChangeNotifier {
|
|||||||
try {
|
try {
|
||||||
final detail = await _repository.fetchDetail(id, includeSteps: true);
|
final detail = await _repository.fetchDetail(id, includeSteps: true);
|
||||||
return detail;
|
return detail;
|
||||||
} catch (e) {
|
} catch (error) {
|
||||||
_errorMessage = e.toString();
|
final failure = Failure.from(error);
|
||||||
|
_errorMessage = failure.describe();
|
||||||
notifyListeners();
|
notifyListeners();
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
@@ -100,8 +103,9 @@ class ApprovalTemplateController extends ChangeNotifier {
|
|||||||
final created = await _repository.create(input, steps: steps);
|
final created = await _repository.create(input, steps: steps);
|
||||||
await fetch(page: 1);
|
await fetch(page: 1);
|
||||||
return created;
|
return created;
|
||||||
} catch (e) {
|
} catch (error) {
|
||||||
_errorMessage = e.toString();
|
final failure = Failure.from(error);
|
||||||
|
_errorMessage = failure.describe();
|
||||||
notifyListeners();
|
notifyListeners();
|
||||||
return null;
|
return null;
|
||||||
} finally {
|
} finally {
|
||||||
@@ -120,8 +124,9 @@ class ApprovalTemplateController extends ChangeNotifier {
|
|||||||
final updated = await _repository.update(id, input, steps: steps);
|
final updated = await _repository.update(id, input, steps: steps);
|
||||||
await fetch(page: _result?.page ?? 1);
|
await fetch(page: _result?.page ?? 1);
|
||||||
return updated;
|
return updated;
|
||||||
} catch (e) {
|
} catch (error) {
|
||||||
_errorMessage = e.toString();
|
final failure = Failure.from(error);
|
||||||
|
_errorMessage = failure.describe();
|
||||||
notifyListeners();
|
notifyListeners();
|
||||||
return null;
|
return null;
|
||||||
} finally {
|
} finally {
|
||||||
@@ -136,8 +141,9 @@ class ApprovalTemplateController extends ChangeNotifier {
|
|||||||
await _repository.delete(id);
|
await _repository.delete(id);
|
||||||
await fetch(page: _result?.page ?? 1);
|
await fetch(page: _result?.page ?? 1);
|
||||||
return true;
|
return true;
|
||||||
} catch (e) {
|
} catch (error) {
|
||||||
_errorMessage = e.toString();
|
final failure = Failure.from(error);
|
||||||
|
_errorMessage = failure.describe();
|
||||||
notifyListeners();
|
notifyListeners();
|
||||||
return false;
|
return false;
|
||||||
} finally {
|
} finally {
|
||||||
@@ -152,8 +158,9 @@ class ApprovalTemplateController extends ChangeNotifier {
|
|||||||
final restored = await _repository.restore(id);
|
final restored = await _repository.restore(id);
|
||||||
await fetch(page: _result?.page ?? 1);
|
await fetch(page: _result?.page ?? 1);
|
||||||
return restored;
|
return restored;
|
||||||
} catch (e) {
|
} catch (error) {
|
||||||
_errorMessage = e.toString();
|
final failure = Failure.from(error);
|
||||||
|
_errorMessage = failure.describe();
|
||||||
notifyListeners();
|
notifyListeners();
|
||||||
return null;
|
return null;
|
||||||
} finally {
|
} finally {
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
import 'package:dio/dio.dart';
|
import 'package:dio/dio.dart';
|
||||||
import 'package:superport_v2/core/common/models/paginated_result.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_client.dart';
|
||||||
|
import 'package:superport_v2/core/network/api_routes.dart';
|
||||||
|
|
||||||
import '../../domain/entities/customer.dart';
|
import '../../domain/entities/customer.dart';
|
||||||
import '../../domain/repositories/customer_repository.dart';
|
import '../../domain/repositories/customer_repository.dart';
|
||||||
@@ -12,7 +13,7 @@ class CustomerRepositoryRemote implements CustomerRepository {
|
|||||||
|
|
||||||
final ApiClient _api;
|
final ApiClient _api;
|
||||||
|
|
||||||
static const _basePath = '/customers';
|
static const _basePath = '${ApiRoutes.apiV1}/customers';
|
||||||
|
|
||||||
/// 고객 목록을 조회한다.
|
/// 고객 목록을 조회한다.
|
||||||
@override
|
@override
|
||||||
@@ -39,6 +40,17 @@ class CustomerRepositoryRemote implements CustomerRepository {
|
|||||||
return CustomerDto.parsePaginated(response.data ?? const {});
|
return CustomerDto.parsePaginated(response.data ?? const {});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Future<Customer> fetchDetail(int id, {bool includeZipcode = true}) async {
|
||||||
|
final response = await _api.get<Map<String, dynamic>>(
|
||||||
|
'$_basePath/$id',
|
||||||
|
query: {if (includeZipcode) 'include': 'zipcode'},
|
||||||
|
options: Options(responseType: ResponseType.json),
|
||||||
|
);
|
||||||
|
final data = (response.data?['data'] as Map<String, dynamic>?) ?? {};
|
||||||
|
return CustomerDto.fromJson(data).toEntity();
|
||||||
|
}
|
||||||
|
|
||||||
/// 고객을 생성한다.
|
/// 고객을 생성한다.
|
||||||
@override
|
@override
|
||||||
Future<Customer> create(CustomerInput input) async {
|
Future<Customer> create(CustomerInput input) async {
|
||||||
|
|||||||
@@ -14,6 +14,9 @@ abstract class CustomerRepository {
|
|||||||
bool? isActive,
|
bool? isActive,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
/// 고객 단건 상세를 조회한다.
|
||||||
|
Future<Customer> fetchDetail(int id, {bool includeZipcode = true});
|
||||||
|
|
||||||
/// 고객을 생성한다.
|
/// 고객을 생성한다.
|
||||||
Future<Customer> create(CustomerInput input);
|
Future<Customer> create(CustomerInput input);
|
||||||
|
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
import 'package:flutter/foundation.dart';
|
import 'package:flutter/foundation.dart';
|
||||||
import 'package:superport_v2/core/common/models/paginated_result.dart';
|
import 'package:superport_v2/core/common/models/paginated_result.dart';
|
||||||
|
import 'package:superport_v2/core/network/failure.dart';
|
||||||
|
|
||||||
import '../../domain/entities/customer.dart';
|
import '../../domain/entities/customer.dart';
|
||||||
import '../../domain/repositories/customer_repository.dart';
|
import '../../domain/repositories/customer_repository.dart';
|
||||||
@@ -78,8 +79,9 @@ class CustomerController extends ChangeNotifier {
|
|||||||
if (response.pageSize > 0 && response.pageSize != _pageSize) {
|
if (response.pageSize > 0 && response.pageSize != _pageSize) {
|
||||||
_pageSize = response.pageSize;
|
_pageSize = response.pageSize;
|
||||||
}
|
}
|
||||||
} catch (e) {
|
} catch (error) {
|
||||||
_errorMessage = e.toString();
|
final failure = Failure.from(error);
|
||||||
|
_errorMessage = failure.describe();
|
||||||
} finally {
|
} finally {
|
||||||
_isLoading = false;
|
_isLoading = false;
|
||||||
notifyListeners();
|
notifyListeners();
|
||||||
@@ -129,8 +131,9 @@ class CustomerController extends ChangeNotifier {
|
|||||||
final created = await _repository.create(input);
|
final created = await _repository.create(input);
|
||||||
await fetch(page: 1);
|
await fetch(page: 1);
|
||||||
return created;
|
return created;
|
||||||
} catch (e) {
|
} catch (error) {
|
||||||
_errorMessage = e.toString();
|
final failure = Failure.from(error);
|
||||||
|
_errorMessage = failure.describe();
|
||||||
notifyListeners();
|
notifyListeners();
|
||||||
return null;
|
return null;
|
||||||
} finally {
|
} finally {
|
||||||
@@ -145,8 +148,9 @@ class CustomerController extends ChangeNotifier {
|
|||||||
final updated = await _repository.update(id, input);
|
final updated = await _repository.update(id, input);
|
||||||
await fetch(page: _result?.page ?? 1);
|
await fetch(page: _result?.page ?? 1);
|
||||||
return updated;
|
return updated;
|
||||||
} catch (e) {
|
} catch (error) {
|
||||||
_errorMessage = e.toString();
|
final failure = Failure.from(error);
|
||||||
|
_errorMessage = failure.describe();
|
||||||
notifyListeners();
|
notifyListeners();
|
||||||
return null;
|
return null;
|
||||||
} finally {
|
} finally {
|
||||||
@@ -161,8 +165,9 @@ class CustomerController extends ChangeNotifier {
|
|||||||
await _repository.delete(id);
|
await _repository.delete(id);
|
||||||
await fetch(page: _result?.page ?? 1);
|
await fetch(page: _result?.page ?? 1);
|
||||||
return true;
|
return true;
|
||||||
} catch (e) {
|
} catch (error) {
|
||||||
_errorMessage = e.toString();
|
final failure = Failure.from(error);
|
||||||
|
_errorMessage = failure.describe();
|
||||||
notifyListeners();
|
notifyListeners();
|
||||||
return false;
|
return false;
|
||||||
} finally {
|
} finally {
|
||||||
@@ -177,8 +182,9 @@ class CustomerController extends ChangeNotifier {
|
|||||||
final restored = await _repository.restore(id);
|
final restored = await _repository.restore(id);
|
||||||
await fetch(page: _result?.page ?? 1);
|
await fetch(page: _result?.page ?? 1);
|
||||||
return restored;
|
return restored;
|
||||||
} catch (e) {
|
} catch (error) {
|
||||||
_errorMessage = e.toString();
|
final failure = Failure.from(error);
|
||||||
|
_errorMessage = failure.describe();
|
||||||
notifyListeners();
|
notifyListeners();
|
||||||
return null;
|
return null;
|
||||||
} finally {
|
} finally {
|
||||||
|
|||||||
@@ -32,7 +32,9 @@ class GroupDto {
|
|||||||
return GroupDto(
|
return GroupDto(
|
||||||
id: json['id'] as int?,
|
id: json['id'] as int?,
|
||||||
groupName: json['group_name'] as String,
|
groupName: json['group_name'] as String,
|
||||||
description: json['description'] as String?,
|
description:
|
||||||
|
json['description'] as String? ??
|
||||||
|
json['group_description'] as String?,
|
||||||
isDefault: (json['is_default'] as bool?) ?? false,
|
isDefault: (json['is_default'] as bool?) ?? false,
|
||||||
isActive: (json['is_active'] as bool?) ?? true,
|
isActive: (json['is_active'] as bool?) ?? true,
|
||||||
isDeleted: (json['is_deleted'] as bool?) ?? false,
|
isDeleted: (json['is_deleted'] as bool?) ?? false,
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
import 'package:dio/dio.dart';
|
import 'package:dio/dio.dart';
|
||||||
import 'package:superport_v2/core/common/models/paginated_result.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_client.dart';
|
||||||
|
import 'package:superport_v2/core/network/api_routes.dart';
|
||||||
|
|
||||||
import '../../domain/entities/group.dart';
|
import '../../domain/entities/group.dart';
|
||||||
import '../../domain/repositories/group_repository.dart';
|
import '../../domain/repositories/group_repository.dart';
|
||||||
@@ -12,7 +13,7 @@ class GroupRepositoryRemote implements GroupRepository {
|
|||||||
|
|
||||||
final ApiClient _api;
|
final ApiClient _api;
|
||||||
|
|
||||||
static const _basePath = '/groups';
|
static const _basePath = '${ApiRoutes.apiV1}/groups';
|
||||||
|
|
||||||
/// 그룹 목록을 조회한다.
|
/// 그룹 목록을 조회한다.
|
||||||
@override
|
@override
|
||||||
@@ -22,7 +23,16 @@ class GroupRepositoryRemote implements GroupRepository {
|
|||||||
String? query,
|
String? query,
|
||||||
bool? isDefault,
|
bool? isDefault,
|
||||||
bool? isActive,
|
bool? isActive,
|
||||||
|
bool includePermissions = false,
|
||||||
|
bool includeEmployees = false,
|
||||||
}) async {
|
}) async {
|
||||||
|
final includeParts = <String>[];
|
||||||
|
if (includePermissions) {
|
||||||
|
includeParts.add('permissions');
|
||||||
|
}
|
||||||
|
if (includeEmployees) {
|
||||||
|
includeParts.add('employees');
|
||||||
|
}
|
||||||
final response = await _api.get<Map<String, dynamic>>(
|
final response = await _api.get<Map<String, dynamic>>(
|
||||||
_basePath,
|
_basePath,
|
||||||
query: {
|
query: {
|
||||||
@@ -31,6 +41,7 @@ class GroupRepositoryRemote implements GroupRepository {
|
|||||||
if (query != null && query.isNotEmpty) 'q': query,
|
if (query != null && query.isNotEmpty) 'q': query,
|
||||||
if (isDefault != null) 'is_default': isDefault,
|
if (isDefault != null) 'is_default': isDefault,
|
||||||
if (isActive != null) 'is_active': isActive,
|
if (isActive != null) 'is_active': isActive,
|
||||||
|
if (includeParts.isNotEmpty) 'include': includeParts.join(','),
|
||||||
},
|
},
|
||||||
options: Options(responseType: ResponseType.json),
|
options: Options(responseType: ResponseType.json),
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -10,6 +10,8 @@ abstract class GroupRepository {
|
|||||||
String? query,
|
String? query,
|
||||||
bool? isDefault,
|
bool? isDefault,
|
||||||
bool? isActive,
|
bool? isActive,
|
||||||
|
bool includePermissions = false,
|
||||||
|
bool includeEmployees = false,
|
||||||
});
|
});
|
||||||
|
|
||||||
/// 그룹 신규 등록
|
/// 그룹 신규 등록
|
||||||
|
|||||||
@@ -1,8 +1,13 @@
|
|||||||
import 'package:flutter/foundation.dart';
|
import 'package:flutter/foundation.dart';
|
||||||
import 'package:superport_v2/core/common/models/paginated_result.dart';
|
import 'package:superport_v2/core/common/models/paginated_result.dart';
|
||||||
|
import 'package:superport_v2/core/network/failure.dart';
|
||||||
|
|
||||||
|
import 'package:superport_v2/core/permissions/permission_manager.dart';
|
||||||
|
|
||||||
import '../../domain/entities/group.dart';
|
import '../../domain/entities/group.dart';
|
||||||
import '../../domain/repositories/group_repository.dart';
|
import '../../domain/repositories/group_repository.dart';
|
||||||
|
import '../../../group_permission/application/permission_synchronizer.dart';
|
||||||
|
import '../../../group_permission/domain/repositories/group_permission_repository.dart';
|
||||||
|
|
||||||
/// 기본 그룹 여부 필터.
|
/// 기본 그룹 여부 필터.
|
||||||
enum GroupDefaultFilter { all, defaultOnly, nonDefault }
|
enum GroupDefaultFilter { all, defaultOnly, nonDefault }
|
||||||
@@ -15,10 +20,17 @@ enum GroupStatusFilter { all, activeOnly, inactiveOnly }
|
|||||||
/// - 목록 조회 및 필터, 페이징 상태를 담당한다.
|
/// - 목록 조회 및 필터, 페이징 상태를 담당한다.
|
||||||
/// - 생성/수정/삭제/복구 요청을 래핑하여 UI와 통신한다.
|
/// - 생성/수정/삭제/복구 요청을 래핑하여 UI와 통신한다.
|
||||||
class GroupController extends ChangeNotifier {
|
class GroupController extends ChangeNotifier {
|
||||||
GroupController({required GroupRepository repository})
|
GroupController({
|
||||||
: _repository = repository;
|
required GroupRepository repository,
|
||||||
|
GroupPermissionRepository? permissionRepository,
|
||||||
|
PermissionManager? permissionManager,
|
||||||
|
}) : _repository = repository,
|
||||||
|
_permissionRepository = permissionRepository,
|
||||||
|
_permissionManager = permissionManager;
|
||||||
|
|
||||||
final GroupRepository _repository;
|
final GroupRepository _repository;
|
||||||
|
final GroupPermissionRepository? _permissionRepository;
|
||||||
|
final PermissionManager? _permissionManager;
|
||||||
|
|
||||||
PaginatedResult<Group>? _result;
|
PaginatedResult<Group>? _result;
|
||||||
bool _isLoading = false;
|
bool _isLoading = false;
|
||||||
@@ -60,8 +72,9 @@ class GroupController extends ChangeNotifier {
|
|||||||
isActive: isActive,
|
isActive: isActive,
|
||||||
);
|
);
|
||||||
_result = response;
|
_result = response;
|
||||||
} catch (e) {
|
} catch (error) {
|
||||||
_errorMessage = e.toString();
|
final failure = Failure.from(error);
|
||||||
|
_errorMessage = failure.describe();
|
||||||
} finally {
|
} finally {
|
||||||
_isLoading = false;
|
_isLoading = false;
|
||||||
notifyListeners();
|
notifyListeners();
|
||||||
@@ -92,9 +105,11 @@ class GroupController extends ChangeNotifier {
|
|||||||
try {
|
try {
|
||||||
final created = await _repository.create(input);
|
final created = await _repository.create(input);
|
||||||
await fetch(page: 1);
|
await fetch(page: 1);
|
||||||
|
await _maybeSync(created.id);
|
||||||
return created;
|
return created;
|
||||||
} catch (e) {
|
} catch (error) {
|
||||||
_errorMessage = e.toString();
|
final failure = Failure.from(error);
|
||||||
|
_errorMessage = failure.describe();
|
||||||
notifyListeners();
|
notifyListeners();
|
||||||
return null;
|
return null;
|
||||||
} finally {
|
} finally {
|
||||||
@@ -108,9 +123,11 @@ class GroupController extends ChangeNotifier {
|
|||||||
try {
|
try {
|
||||||
final updated = await _repository.update(id, input);
|
final updated = await _repository.update(id, input);
|
||||||
await fetch(page: _result?.page ?? 1);
|
await fetch(page: _result?.page ?? 1);
|
||||||
|
await _maybeSync(updated.id);
|
||||||
return updated;
|
return updated;
|
||||||
} catch (e) {
|
} catch (error) {
|
||||||
_errorMessage = e.toString();
|
final failure = Failure.from(error);
|
||||||
|
_errorMessage = failure.describe();
|
||||||
notifyListeners();
|
notifyListeners();
|
||||||
return null;
|
return null;
|
||||||
} finally {
|
} finally {
|
||||||
@@ -124,9 +141,11 @@ class GroupController extends ChangeNotifier {
|
|||||||
try {
|
try {
|
||||||
await _repository.delete(id);
|
await _repository.delete(id);
|
||||||
await fetch(page: _result?.page ?? 1);
|
await fetch(page: _result?.page ?? 1);
|
||||||
|
await _maybeSync(id);
|
||||||
return true;
|
return true;
|
||||||
} catch (e) {
|
} catch (error) {
|
||||||
_errorMessage = e.toString();
|
final failure = Failure.from(error);
|
||||||
|
_errorMessage = failure.describe();
|
||||||
notifyListeners();
|
notifyListeners();
|
||||||
return false;
|
return false;
|
||||||
} finally {
|
} finally {
|
||||||
@@ -140,9 +159,11 @@ class GroupController extends ChangeNotifier {
|
|||||||
try {
|
try {
|
||||||
final restored = await _repository.restore(id);
|
final restored = await _repository.restore(id);
|
||||||
await fetch(page: _result?.page ?? 1);
|
await fetch(page: _result?.page ?? 1);
|
||||||
|
await _maybeSync(restored.id);
|
||||||
return restored;
|
return restored;
|
||||||
} catch (e) {
|
} catch (error) {
|
||||||
_errorMessage = e.toString();
|
final failure = Failure.from(error);
|
||||||
|
_errorMessage = failure.describe();
|
||||||
notifyListeners();
|
notifyListeners();
|
||||||
return null;
|
return null;
|
||||||
} finally {
|
} finally {
|
||||||
@@ -161,4 +182,28 @@ class GroupController extends ChangeNotifier {
|
|||||||
_isSubmitting = value;
|
_isSubmitting = value;
|
||||||
notifyListeners();
|
notifyListeners();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Future<void> _maybeSync(int? groupId) async {
|
||||||
|
if (groupId == null) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
await _syncPermissionsForGroup(groupId);
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> _syncPermissionsForGroup(int groupId) async {
|
||||||
|
final repository = _permissionRepository;
|
||||||
|
final manager = _permissionManager;
|
||||||
|
if (repository == null || manager == null) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
final synchronizer = PermissionSynchronizer(
|
||||||
|
repository: repository,
|
||||||
|
manager: manager,
|
||||||
|
);
|
||||||
|
await synchronizer.syncForGroup(groupId);
|
||||||
|
} catch (_) {
|
||||||
|
// 권한 동기화 실패는 UI 동작에 영향을 주지 않도록 무시한다.
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,51 @@
|
|||||||
|
import 'package:superport_v2/core/permissions/permission_manager.dart';
|
||||||
|
|
||||||
|
import '../domain/entities/group_permission.dart';
|
||||||
|
import '../domain/mappers/group_permission_mapper.dart';
|
||||||
|
import '../domain/repositories/group_permission_repository.dart';
|
||||||
|
|
||||||
|
/// 서버에서 그룹 메뉴 권한을 조회해 [PermissionManager]에 반영하는 동기화기.
|
||||||
|
class PermissionSynchronizer {
|
||||||
|
PermissionSynchronizer({
|
||||||
|
required GroupPermissionRepository repository,
|
||||||
|
required PermissionManager manager,
|
||||||
|
this.pageSize = 200,
|
||||||
|
}) : _repository = repository,
|
||||||
|
_manager = manager;
|
||||||
|
|
||||||
|
final GroupPermissionRepository _repository;
|
||||||
|
final PermissionManager _manager;
|
||||||
|
final int pageSize;
|
||||||
|
|
||||||
|
/// 지정한 [groupId]의 메뉴 권한을 조회해 [PermissionManager]에 적용한다.
|
||||||
|
Future<void> syncForGroup(int groupId) async {
|
||||||
|
final collected = <GroupPermission>[];
|
||||||
|
var page = 1;
|
||||||
|
|
||||||
|
while (true) {
|
||||||
|
final response = await _repository.list(
|
||||||
|
page: page,
|
||||||
|
pageSize: pageSize,
|
||||||
|
groupId: groupId,
|
||||||
|
includeDeleted: false,
|
||||||
|
isActive: true,
|
||||||
|
);
|
||||||
|
collected.addAll(response.items);
|
||||||
|
|
||||||
|
final currentPageSize = response.pageSize == 0
|
||||||
|
? response.items.length
|
||||||
|
: response.pageSize;
|
||||||
|
if (currentPageSize == 0) {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
final fetched = page * currentPageSize;
|
||||||
|
if (fetched >= response.total) {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
page += 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
final permissionMap = buildPermissionMap(collected);
|
||||||
|
_manager.applyServerPermissions(permissionMap);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -111,22 +111,39 @@ class GroupPermissionGroupDto {
|
|||||||
|
|
||||||
/// 권한 대상 메뉴 정보를 담는 DTO.
|
/// 권한 대상 메뉴 정보를 담는 DTO.
|
||||||
class GroupPermissionMenuDto {
|
class GroupPermissionMenuDto {
|
||||||
GroupPermissionMenuDto({required this.id, required this.menuName});
|
GroupPermissionMenuDto({
|
||||||
|
required this.id,
|
||||||
|
required this.menuCode,
|
||||||
|
required this.menuName,
|
||||||
|
this.path,
|
||||||
|
});
|
||||||
|
|
||||||
final int id;
|
final int id;
|
||||||
|
final String menuCode;
|
||||||
final String menuName;
|
final String menuName;
|
||||||
|
final String? path;
|
||||||
|
|
||||||
/// JSON에서 메뉴 정보를 파싱한다.
|
/// JSON에서 메뉴 정보를 파싱한다.
|
||||||
factory GroupPermissionMenuDto.fromJson(Map<String, dynamic> json) {
|
factory GroupPermissionMenuDto.fromJson(Map<String, dynamic> json) {
|
||||||
|
final fallbackName =
|
||||||
|
json['menu_name'] as String? ?? json['name'] as String? ?? '-';
|
||||||
|
final code =
|
||||||
|
json['menu_code'] as String? ?? json['code'] as String? ?? fallbackName;
|
||||||
return GroupPermissionMenuDto(
|
return GroupPermissionMenuDto(
|
||||||
id: json['id'] as int? ?? json['menu_id'] as int,
|
id: json['id'] as int? ?? json['menu_id'] as int,
|
||||||
menuName: json['menu_name'] as String? ?? json['name'] as String? ?? '-',
|
menuCode: code,
|
||||||
|
menuName: fallbackName,
|
||||||
|
path: json['path'] as String?,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// DTO를 [GroupPermissionMenu] 엔티티로 변환한다.
|
/// DTO를 [GroupPermissionMenu] 엔티티로 변환한다.
|
||||||
GroupPermissionMenu toEntity() =>
|
GroupPermissionMenu toEntity() => GroupPermissionMenu(
|
||||||
GroupPermissionMenu(id: id, menuName: menuName);
|
id: id,
|
||||||
|
menuCode: menuCode,
|
||||||
|
menuName: menuName,
|
||||||
|
path: path,
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// 문자열/DateTime 값을 파싱해 [DateTime]으로 변환한다.
|
/// 문자열/DateTime 값을 파싱해 [DateTime]으로 변환한다.
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
import 'package:dio/dio.dart';
|
import 'package:dio/dio.dart';
|
||||||
import 'package:superport_v2/core/common/models/paginated_result.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_client.dart';
|
||||||
|
import 'package:superport_v2/core/network/api_routes.dart';
|
||||||
|
|
||||||
import '../../domain/entities/group_permission.dart';
|
import '../../domain/entities/group_permission.dart';
|
||||||
import '../../domain/repositories/group_permission_repository.dart';
|
import '../../domain/repositories/group_permission_repository.dart';
|
||||||
@@ -13,7 +14,7 @@ class GroupPermissionRepositoryRemote implements GroupPermissionRepository {
|
|||||||
|
|
||||||
final ApiClient _api;
|
final ApiClient _api;
|
||||||
|
|
||||||
static const _basePath = '/group-menu-permissions';
|
static const _basePath = '${ApiRoutes.apiV1}/group-menu-permissions';
|
||||||
|
|
||||||
/// 그룹 권한 목록을 조회한다.
|
/// 그룹 권한 목록을 조회한다.
|
||||||
@override
|
@override
|
||||||
|
|||||||
@@ -70,10 +70,17 @@ class GroupPermissionGroup {
|
|||||||
}
|
}
|
||||||
|
|
||||||
class GroupPermissionMenu {
|
class GroupPermissionMenu {
|
||||||
GroupPermissionMenu({required this.id, required this.menuName});
|
GroupPermissionMenu({
|
||||||
|
required this.id,
|
||||||
|
required this.menuCode,
|
||||||
|
required this.menuName,
|
||||||
|
this.path,
|
||||||
|
});
|
||||||
|
|
||||||
final int id;
|
final int id;
|
||||||
|
final String menuCode;
|
||||||
final String menuName;
|
final String menuName;
|
||||||
|
final String? path;
|
||||||
}
|
}
|
||||||
|
|
||||||
/// 그룹 권한 생성/수정 입력 모델
|
/// 그룹 권한 생성/수정 입력 모델
|
||||||
|
|||||||
@@ -0,0 +1,34 @@
|
|||||||
|
import 'package:superport_v2/core/permissions/permission_manager.dart';
|
||||||
|
import 'package:superport_v2/core/permissions/permission_resources.dart';
|
||||||
|
|
||||||
|
import '../entities/group_permission.dart';
|
||||||
|
|
||||||
|
/// 그룹-메뉴 권한 목록을 [PermissionManager]에 적용할 수 있는 맵으로 변환한다.
|
||||||
|
///
|
||||||
|
/// - 메뉴 경로([GroupPermissionMenu.path])가 비어 있으면 해당 항목은 건너뛴다.
|
||||||
|
/// - 읽기 권한은 [PermissionAction.view]로 매핑하고, CRUD 권한은 각각 대응한다.
|
||||||
|
Map<String, Set<PermissionAction>> buildPermissionMap(
|
||||||
|
Iterable<GroupPermission> permissions,
|
||||||
|
) {
|
||||||
|
final result = <String, Set<PermissionAction>>{};
|
||||||
|
for (final permission in permissions) {
|
||||||
|
final path = PermissionResources.normalize(permission.menu.path ?? '');
|
||||||
|
if (path.isEmpty) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
final actions = result.putIfAbsent(path, () => <PermissionAction>{});
|
||||||
|
if (permission.canRead) {
|
||||||
|
actions.add(PermissionAction.view);
|
||||||
|
}
|
||||||
|
if (permission.canCreate) {
|
||||||
|
actions.add(PermissionAction.create);
|
||||||
|
}
|
||||||
|
if (permission.canUpdate) {
|
||||||
|
actions.add(PermissionAction.edit);
|
||||||
|
}
|
||||||
|
if (permission.canDelete) {
|
||||||
|
actions.add(PermissionAction.delete);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return result;
|
||||||
|
}
|
||||||
@@ -1,6 +1,9 @@
|
|||||||
import 'package:flutter/foundation.dart';
|
import 'package:flutter/foundation.dart';
|
||||||
import 'package:superport_v2/core/common/models/paginated_result.dart';
|
import 'package:superport_v2/core/common/models/paginated_result.dart';
|
||||||
|
import 'package:superport_v2/core/network/failure.dart';
|
||||||
|
|
||||||
|
import '../../../../../core/permissions/permission_manager.dart';
|
||||||
|
import '../../application/permission_synchronizer.dart';
|
||||||
import '../../../group/domain/entities/group.dart';
|
import '../../../group/domain/entities/group.dart';
|
||||||
import '../../../group/domain/repositories/group_repository.dart';
|
import '../../../group/domain/repositories/group_repository.dart';
|
||||||
import '../../../menu/domain/entities/menu.dart';
|
import '../../../menu/domain/entities/menu.dart';
|
||||||
@@ -20,13 +23,16 @@ class GroupPermissionController extends ChangeNotifier {
|
|||||||
required GroupPermissionRepository permissionRepository,
|
required GroupPermissionRepository permissionRepository,
|
||||||
required GroupRepository groupRepository,
|
required GroupRepository groupRepository,
|
||||||
required MenuRepository menuRepository,
|
required MenuRepository menuRepository,
|
||||||
|
PermissionManager? permissionManager,
|
||||||
}) : _permissionRepository = permissionRepository,
|
}) : _permissionRepository = permissionRepository,
|
||||||
_groupRepository = groupRepository,
|
_groupRepository = groupRepository,
|
||||||
_menuRepository = menuRepository;
|
_menuRepository = menuRepository,
|
||||||
|
_permissionManager = permissionManager;
|
||||||
|
|
||||||
final GroupPermissionRepository _permissionRepository;
|
final GroupPermissionRepository _permissionRepository;
|
||||||
final GroupRepository _groupRepository;
|
final GroupRepository _groupRepository;
|
||||||
final MenuRepository _menuRepository;
|
final MenuRepository _menuRepository;
|
||||||
|
final PermissionManager? _permissionManager;
|
||||||
|
|
||||||
PaginatedResult<GroupPermission>? _result;
|
PaginatedResult<GroupPermission>? _result;
|
||||||
bool _isLoading = false;
|
bool _isLoading = false;
|
||||||
@@ -63,8 +69,9 @@ class GroupPermissionController extends ChangeNotifier {
|
|||||||
_groups
|
_groups
|
||||||
..clear()
|
..clear()
|
||||||
..addAll(response.items);
|
..addAll(response.items);
|
||||||
} catch (e) {
|
} catch (error) {
|
||||||
_errorMessage = e.toString();
|
final failure = Failure.from(error);
|
||||||
|
_errorMessage = failure.describe();
|
||||||
} finally {
|
} finally {
|
||||||
_isLoadingGroups = false;
|
_isLoadingGroups = false;
|
||||||
notifyListeners();
|
notifyListeners();
|
||||||
@@ -84,8 +91,9 @@ class GroupPermissionController extends ChangeNotifier {
|
|||||||
_menus
|
_menus
|
||||||
..clear()
|
..clear()
|
||||||
..addAll(response.items);
|
..addAll(response.items);
|
||||||
} catch (e) {
|
} catch (error) {
|
||||||
_errorMessage = e.toString();
|
final failure = Failure.from(error);
|
||||||
|
_errorMessage = failure.describe();
|
||||||
} finally {
|
} finally {
|
||||||
_isLoadingMenus = false;
|
_isLoadingMenus = false;
|
||||||
notifyListeners();
|
notifyListeners();
|
||||||
@@ -112,8 +120,9 @@ class GroupPermissionController extends ChangeNotifier {
|
|||||||
includeDeleted: _includeDeleted,
|
includeDeleted: _includeDeleted,
|
||||||
);
|
);
|
||||||
_result = response;
|
_result = response;
|
||||||
} catch (e) {
|
} catch (error) {
|
||||||
_errorMessage = e.toString();
|
final failure = Failure.from(error);
|
||||||
|
_errorMessage = failure.describe();
|
||||||
} finally {
|
} finally {
|
||||||
_isLoading = false;
|
_isLoading = false;
|
||||||
notifyListeners();
|
notifyListeners();
|
||||||
@@ -150,9 +159,11 @@ class GroupPermissionController extends ChangeNotifier {
|
|||||||
try {
|
try {
|
||||||
final created = await _permissionRepository.create(input);
|
final created = await _permissionRepository.create(input);
|
||||||
await fetch(page: 1);
|
await fetch(page: 1);
|
||||||
|
await _syncPermissionsForGroup(input.groupId);
|
||||||
return created;
|
return created;
|
||||||
} catch (e) {
|
} catch (error) {
|
||||||
_errorMessage = e.toString();
|
final failure = Failure.from(error);
|
||||||
|
_errorMessage = failure.describe();
|
||||||
notifyListeners();
|
notifyListeners();
|
||||||
return null;
|
return null;
|
||||||
} finally {
|
} finally {
|
||||||
@@ -166,9 +177,11 @@ class GroupPermissionController extends ChangeNotifier {
|
|||||||
try {
|
try {
|
||||||
final updated = await _permissionRepository.update(id, input);
|
final updated = await _permissionRepository.update(id, input);
|
||||||
await fetch(page: _result?.page ?? 1);
|
await fetch(page: _result?.page ?? 1);
|
||||||
|
await _syncPermissionsForGroup(input.groupId);
|
||||||
return updated;
|
return updated;
|
||||||
} catch (e) {
|
} catch (error) {
|
||||||
_errorMessage = e.toString();
|
final failure = Failure.from(error);
|
||||||
|
_errorMessage = failure.describe();
|
||||||
notifyListeners();
|
notifyListeners();
|
||||||
return null;
|
return null;
|
||||||
} finally {
|
} finally {
|
||||||
@@ -180,11 +193,16 @@ class GroupPermissionController extends ChangeNotifier {
|
|||||||
Future<bool> delete(int id) async {
|
Future<bool> delete(int id) async {
|
||||||
_setSubmitting(true);
|
_setSubmitting(true);
|
||||||
try {
|
try {
|
||||||
|
final groupId = _resolveGroupIdForPermission(id);
|
||||||
await _permissionRepository.delete(id);
|
await _permissionRepository.delete(id);
|
||||||
await fetch(page: _result?.page ?? 1);
|
await fetch(page: _result?.page ?? 1);
|
||||||
|
if (groupId != null) {
|
||||||
|
await _syncPermissionsForGroup(groupId);
|
||||||
|
}
|
||||||
return true;
|
return true;
|
||||||
} catch (e) {
|
} catch (error) {
|
||||||
_errorMessage = e.toString();
|
final failure = Failure.from(error);
|
||||||
|
_errorMessage = failure.describe();
|
||||||
notifyListeners();
|
notifyListeners();
|
||||||
return false;
|
return false;
|
||||||
} finally {
|
} finally {
|
||||||
@@ -198,9 +216,11 @@ class GroupPermissionController extends ChangeNotifier {
|
|||||||
try {
|
try {
|
||||||
final restored = await _permissionRepository.restore(id);
|
final restored = await _permissionRepository.restore(id);
|
||||||
await fetch(page: _result?.page ?? 1);
|
await fetch(page: _result?.page ?? 1);
|
||||||
|
await _syncPermissionsForGroup(restored.group.id);
|
||||||
return restored;
|
return restored;
|
||||||
} catch (e) {
|
} catch (error) {
|
||||||
_errorMessage = e.toString();
|
final failure = Failure.from(error);
|
||||||
|
_errorMessage = failure.describe();
|
||||||
notifyListeners();
|
notifyListeners();
|
||||||
return null;
|
return null;
|
||||||
} finally {
|
} finally {
|
||||||
@@ -219,4 +239,33 @@ class GroupPermissionController extends ChangeNotifier {
|
|||||||
_isSubmitting = value;
|
_isSubmitting = value;
|
||||||
notifyListeners();
|
notifyListeners();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Future<void> _syncPermissionsForGroup(int groupId) async {
|
||||||
|
final manager = _permissionManager;
|
||||||
|
if (manager == null) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
final synchronizer = PermissionSynchronizer(
|
||||||
|
repository: _permissionRepository,
|
||||||
|
manager: manager,
|
||||||
|
);
|
||||||
|
await synchronizer.syncForGroup(groupId);
|
||||||
|
} catch (_) {
|
||||||
|
// 권한 동기화 실패는 사용자 경험에 영향이 없도록 무시한다.
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
int? _resolveGroupIdForPermission(int permissionId) {
|
||||||
|
final current = _result?.items;
|
||||||
|
if (current == null) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
for (final item in current) {
|
||||||
|
if (item.id == permissionId) {
|
||||||
|
return item.group.id;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -9,6 +9,7 @@ import 'package:superport_v2/widgets/components/filter_bar.dart';
|
|||||||
import 'package:superport_v2/widgets/components/superport_dialog.dart';
|
import 'package:superport_v2/widgets/components/superport_dialog.dart';
|
||||||
|
|
||||||
import '../../../../../core/config/environment.dart';
|
import '../../../../../core/config/environment.dart';
|
||||||
|
import '../../../../../core/permissions/permission_manager.dart';
|
||||||
import '../../../../../widgets/spec_page.dart';
|
import '../../../../../widgets/spec_page.dart';
|
||||||
import '../../../group/domain/entities/group.dart';
|
import '../../../group/domain/entities/group.dart';
|
||||||
import '../../../group/domain/repositories/group_repository.dart';
|
import '../../../group/domain/repositories/group_repository.dart';
|
||||||
@@ -118,14 +119,27 @@ class _GroupPermissionEnabledPageState
|
|||||||
final intl.DateFormat _dateFormat = intl.DateFormat('yyyy-MM-dd HH:mm');
|
final intl.DateFormat _dateFormat = intl.DateFormat('yyyy-MM-dd HH:mm');
|
||||||
String? _lastError;
|
String? _lastError;
|
||||||
|
|
||||||
|
bool _initialized = false;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
void initState() {
|
void initState() {
|
||||||
super.initState();
|
super.initState();
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
void didChangeDependencies() {
|
||||||
|
super.didChangeDependencies();
|
||||||
|
if (_initialized) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
final permissionManager = PermissionScope.of(context);
|
||||||
_controller = GroupPermissionController(
|
_controller = GroupPermissionController(
|
||||||
permissionRepository: GetIt.I<GroupPermissionRepository>(),
|
permissionRepository: GetIt.I<GroupPermissionRepository>(),
|
||||||
groupRepository: GetIt.I<GroupRepository>(),
|
groupRepository: GetIt.I<GroupRepository>(),
|
||||||
menuRepository: GetIt.I<MenuRepository>(),
|
menuRepository: GetIt.I<MenuRepository>(),
|
||||||
|
permissionManager: permissionManager,
|
||||||
)..addListener(_handleControllerUpdate);
|
)..addListener(_handleControllerUpdate);
|
||||||
|
_initialized = true;
|
||||||
WidgetsBinding.instance.addPostFrameCallback((_) async {
|
WidgetsBinding.instance.addPostFrameCallback((_) async {
|
||||||
await _controller.loadGroups();
|
await _controller.loadGroups();
|
||||||
await _controller.loadMenus();
|
await _controller.loadMenus();
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
import 'package:dio/dio.dart';
|
import 'package:dio/dio.dart';
|
||||||
import 'package:superport_v2/core/common/models/paginated_result.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_client.dart';
|
||||||
|
import 'package:superport_v2/core/network/api_routes.dart';
|
||||||
|
|
||||||
import '../../domain/entities/menu.dart';
|
import '../../domain/entities/menu.dart';
|
||||||
import '../../domain/repositories/menu_repository.dart';
|
import '../../domain/repositories/menu_repository.dart';
|
||||||
@@ -12,7 +13,7 @@ class MenuRepositoryRemote implements MenuRepository {
|
|||||||
|
|
||||||
final ApiClient _api;
|
final ApiClient _api;
|
||||||
|
|
||||||
static const _basePath = '/menus';
|
static const _basePath = '${ApiRoutes.apiV1}/menus';
|
||||||
|
|
||||||
/// 메뉴 목록을 조회한다.
|
/// 메뉴 목록을 조회한다.
|
||||||
@override
|
@override
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
import 'package:flutter/foundation.dart';
|
import 'package:flutter/foundation.dart';
|
||||||
import 'package:superport_v2/core/common/models/paginated_result.dart';
|
import 'package:superport_v2/core/common/models/paginated_result.dart';
|
||||||
|
import 'package:superport_v2/core/network/failure.dart';
|
||||||
|
|
||||||
import '../../domain/entities/menu.dart';
|
import '../../domain/entities/menu.dart';
|
||||||
import '../../domain/repositories/menu_repository.dart';
|
import '../../domain/repositories/menu_repository.dart';
|
||||||
@@ -50,8 +51,9 @@ class MenuController extends ChangeNotifier {
|
|||||||
includeDeleted: false,
|
includeDeleted: false,
|
||||||
);
|
);
|
||||||
_parents = response.items;
|
_parents = response.items;
|
||||||
} catch (e) {
|
} catch (error) {
|
||||||
_errorMessage = e.toString();
|
final failure = Failure.from(error);
|
||||||
|
_errorMessage = failure.describe();
|
||||||
} finally {
|
} finally {
|
||||||
_isLoadingParents = false;
|
_isLoadingParents = false;
|
||||||
notifyListeners();
|
notifyListeners();
|
||||||
@@ -78,8 +80,9 @@ class MenuController extends ChangeNotifier {
|
|||||||
includeDeleted: _includeDeleted,
|
includeDeleted: _includeDeleted,
|
||||||
);
|
);
|
||||||
_result = response;
|
_result = response;
|
||||||
} catch (e) {
|
} catch (error) {
|
||||||
_errorMessage = e.toString();
|
final failure = Failure.from(error);
|
||||||
|
_errorMessage = failure.describe();
|
||||||
} finally {
|
} finally {
|
||||||
_isLoading = false;
|
_isLoading = false;
|
||||||
notifyListeners();
|
notifyListeners();
|
||||||
@@ -118,8 +121,9 @@ class MenuController extends ChangeNotifier {
|
|||||||
await fetch(page: 1);
|
await fetch(page: 1);
|
||||||
await loadParents();
|
await loadParents();
|
||||||
return created;
|
return created;
|
||||||
} catch (e) {
|
} catch (error) {
|
||||||
_errorMessage = e.toString();
|
final failure = Failure.from(error);
|
||||||
|
_errorMessage = failure.describe();
|
||||||
notifyListeners();
|
notifyListeners();
|
||||||
return null;
|
return null;
|
||||||
} finally {
|
} finally {
|
||||||
@@ -135,8 +139,9 @@ class MenuController extends ChangeNotifier {
|
|||||||
await fetch(page: _result?.page ?? 1);
|
await fetch(page: _result?.page ?? 1);
|
||||||
await loadParents();
|
await loadParents();
|
||||||
return updated;
|
return updated;
|
||||||
} catch (e) {
|
} catch (error) {
|
||||||
_errorMessage = e.toString();
|
final failure = Failure.from(error);
|
||||||
|
_errorMessage = failure.describe();
|
||||||
notifyListeners();
|
notifyListeners();
|
||||||
return null;
|
return null;
|
||||||
} finally {
|
} finally {
|
||||||
@@ -152,8 +157,9 @@ class MenuController extends ChangeNotifier {
|
|||||||
await fetch(page: _result?.page ?? 1);
|
await fetch(page: _result?.page ?? 1);
|
||||||
await loadParents();
|
await loadParents();
|
||||||
return true;
|
return true;
|
||||||
} catch (e) {
|
} catch (error) {
|
||||||
_errorMessage = e.toString();
|
final failure = Failure.from(error);
|
||||||
|
_errorMessage = failure.describe();
|
||||||
notifyListeners();
|
notifyListeners();
|
||||||
return false;
|
return false;
|
||||||
} finally {
|
} finally {
|
||||||
@@ -169,8 +175,9 @@ class MenuController extends ChangeNotifier {
|
|||||||
await fetch(page: _result?.page ?? 1);
|
await fetch(page: _result?.page ?? 1);
|
||||||
await loadParents();
|
await loadParents();
|
||||||
return restored;
|
return restored;
|
||||||
} catch (e) {
|
} catch (error) {
|
||||||
_errorMessage = e.toString();
|
final failure = Failure.from(error);
|
||||||
|
_errorMessage = failure.describe();
|
||||||
notifyListeners();
|
notifyListeners();
|
||||||
return null;
|
return null;
|
||||||
} finally {
|
} finally {
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
import 'package:dio/dio.dart';
|
import 'package:dio/dio.dart';
|
||||||
import 'package:superport_v2/core/common/models/paginated_result.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_client.dart';
|
||||||
|
import 'package:superport_v2/core/network/api_routes.dart';
|
||||||
|
|
||||||
import '../../domain/entities/product.dart';
|
import '../../domain/entities/product.dart';
|
||||||
import '../../domain/repositories/product_repository.dart';
|
import '../../domain/repositories/product_repository.dart';
|
||||||
@@ -12,7 +13,7 @@ class ProductRepositoryRemote implements ProductRepository {
|
|||||||
|
|
||||||
final ApiClient _api;
|
final ApiClient _api;
|
||||||
|
|
||||||
static const _basePath = '/products';
|
static const _basePath = '${ApiRoutes.apiV1}/products';
|
||||||
|
|
||||||
/// 제품 목록을 조회한다.
|
/// 제품 목록을 조회한다.
|
||||||
@override
|
@override
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
import 'package:flutter/foundation.dart';
|
import 'package:flutter/foundation.dart';
|
||||||
import 'package:superport_v2/core/common/models/paginated_result.dart';
|
import 'package:superport_v2/core/common/models/paginated_result.dart';
|
||||||
|
import 'package:superport_v2/core/network/failure.dart';
|
||||||
|
|
||||||
import '../../../vendor/domain/entities/vendor.dart';
|
import '../../../vendor/domain/entities/vendor.dart';
|
||||||
import '../../../vendor/domain/repositories/vendor_repository.dart';
|
import '../../../vendor/domain/repositories/vendor_repository.dart';
|
||||||
@@ -77,8 +78,9 @@ class ProductController extends ChangeNotifier {
|
|||||||
if (response.pageSize > 0 && response.pageSize != _pageSize) {
|
if (response.pageSize > 0 && response.pageSize != _pageSize) {
|
||||||
_pageSize = response.pageSize;
|
_pageSize = response.pageSize;
|
||||||
}
|
}
|
||||||
} catch (e) {
|
} catch (error) {
|
||||||
_errorMessage = e.toString();
|
final failure = Failure.from(error);
|
||||||
|
_errorMessage = failure.describe();
|
||||||
} finally {
|
} finally {
|
||||||
_isLoading = false;
|
_isLoading = false;
|
||||||
notifyListeners();
|
notifyListeners();
|
||||||
@@ -94,8 +96,9 @@ class ProductController extends ChangeNotifier {
|
|||||||
final uomResult = await _uomRepository.list(page: 1, pageSize: 100);
|
final uomResult = await _uomRepository.list(page: 1, pageSize: 100);
|
||||||
_vendorOptions = vendorResult.items;
|
_vendorOptions = vendorResult.items;
|
||||||
_uomOptions = uomResult.items;
|
_uomOptions = uomResult.items;
|
||||||
} catch (e) {
|
} catch (error) {
|
||||||
_errorMessage = e.toString();
|
final failure = Failure.from(error);
|
||||||
|
_errorMessage = failure.describe();
|
||||||
} finally {
|
} finally {
|
||||||
_isLoadingLookups = false;
|
_isLoadingLookups = false;
|
||||||
notifyListeners();
|
notifyListeners();
|
||||||
@@ -154,8 +157,9 @@ class ProductController extends ChangeNotifier {
|
|||||||
final created = await _productRepository.create(input);
|
final created = await _productRepository.create(input);
|
||||||
await fetch(page: 1);
|
await fetch(page: 1);
|
||||||
return created;
|
return created;
|
||||||
} catch (e) {
|
} catch (error) {
|
||||||
_errorMessage = e.toString();
|
final failure = Failure.from(error);
|
||||||
|
_errorMessage = failure.describe();
|
||||||
notifyListeners();
|
notifyListeners();
|
||||||
return null;
|
return null;
|
||||||
} finally {
|
} finally {
|
||||||
@@ -170,8 +174,9 @@ class ProductController extends ChangeNotifier {
|
|||||||
final updated = await _productRepository.update(id, input);
|
final updated = await _productRepository.update(id, input);
|
||||||
await fetch(page: _result?.page ?? 1);
|
await fetch(page: _result?.page ?? 1);
|
||||||
return updated;
|
return updated;
|
||||||
} catch (e) {
|
} catch (error) {
|
||||||
_errorMessage = e.toString();
|
final failure = Failure.from(error);
|
||||||
|
_errorMessage = failure.describe();
|
||||||
notifyListeners();
|
notifyListeners();
|
||||||
return null;
|
return null;
|
||||||
} finally {
|
} finally {
|
||||||
@@ -186,8 +191,9 @@ class ProductController extends ChangeNotifier {
|
|||||||
await _productRepository.delete(id);
|
await _productRepository.delete(id);
|
||||||
await fetch(page: _result?.page ?? 1);
|
await fetch(page: _result?.page ?? 1);
|
||||||
return true;
|
return true;
|
||||||
} catch (e) {
|
} catch (error) {
|
||||||
_errorMessage = e.toString();
|
final failure = Failure.from(error);
|
||||||
|
_errorMessage = failure.describe();
|
||||||
notifyListeners();
|
notifyListeners();
|
||||||
return false;
|
return false;
|
||||||
} finally {
|
} finally {
|
||||||
@@ -202,8 +208,9 @@ class ProductController extends ChangeNotifier {
|
|||||||
final restored = await _productRepository.restore(id);
|
final restored = await _productRepository.restore(id);
|
||||||
await fetch(page: _result?.page ?? 1);
|
await fetch(page: _result?.page ?? 1);
|
||||||
return restored;
|
return restored;
|
||||||
} catch (e) {
|
} catch (error) {
|
||||||
_errorMessage = e.toString();
|
final failure = Failure.from(error);
|
||||||
|
_errorMessage = failure.describe();
|
||||||
notifyListeners();
|
notifyListeners();
|
||||||
return null;
|
return null;
|
||||||
} finally {
|
} finally {
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
import 'package:dio/dio.dart';
|
import 'package:dio/dio.dart';
|
||||||
import 'package:superport_v2/core/common/models/paginated_result.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_client.dart';
|
||||||
|
import 'package:superport_v2/core/network/api_routes.dart';
|
||||||
|
|
||||||
import '../../domain/entities/uom.dart';
|
import '../../domain/entities/uom.dart';
|
||||||
import '../../domain/repositories/uom_repository.dart';
|
import '../../domain/repositories/uom_repository.dart';
|
||||||
@@ -12,7 +13,7 @@ class UomRepositoryRemote implements UomRepository {
|
|||||||
|
|
||||||
final ApiClient _api;
|
final ApiClient _api;
|
||||||
|
|
||||||
static const _basePath = '/uoms';
|
static const _basePath = '${ApiRoutes.apiV1}/uoms';
|
||||||
|
|
||||||
/// UOM 목록을 조회한다.
|
/// UOM 목록을 조회한다.
|
||||||
@override
|
@override
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
import 'package:dio/dio.dart';
|
import 'package:dio/dio.dart';
|
||||||
import 'package:superport_v2/core/common/models/paginated_result.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_client.dart';
|
||||||
|
import 'package:superport_v2/core/network/api_routes.dart';
|
||||||
|
|
||||||
import '../../domain/entities/user.dart';
|
import '../../domain/entities/user.dart';
|
||||||
import '../../domain/repositories/user_repository.dart';
|
import '../../domain/repositories/user_repository.dart';
|
||||||
@@ -12,7 +13,7 @@ class UserRepositoryRemote implements UserRepository {
|
|||||||
|
|
||||||
final ApiClient _api;
|
final ApiClient _api;
|
||||||
|
|
||||||
static const _basePath = '/employees';
|
static const _basePath = '${ApiRoutes.apiV1}/employees';
|
||||||
|
|
||||||
/// 사용자 목록을 조회한다.
|
/// 사용자 목록을 조회한다.
|
||||||
@override
|
@override
|
||||||
|
|||||||
@@ -1,8 +1,12 @@
|
|||||||
import 'package:flutter/foundation.dart';
|
import 'package:flutter/foundation.dart';
|
||||||
import 'package:superport_v2/core/common/models/paginated_result.dart';
|
import 'package:superport_v2/core/common/models/paginated_result.dart';
|
||||||
|
import 'package:superport_v2/core/network/failure.dart';
|
||||||
|
|
||||||
|
import '../../../../../core/permissions/permission_manager.dart';
|
||||||
import '../../../group/domain/entities/group.dart';
|
import '../../../group/domain/entities/group.dart';
|
||||||
import '../../../group/domain/repositories/group_repository.dart';
|
import '../../../group/domain/repositories/group_repository.dart';
|
||||||
|
import '../../../group_permission/application/permission_synchronizer.dart';
|
||||||
|
import '../../../group_permission/domain/repositories/group_permission_repository.dart';
|
||||||
import '../../domain/entities/user.dart';
|
import '../../domain/entities/user.dart';
|
||||||
import '../../domain/repositories/user_repository.dart';
|
import '../../domain/repositories/user_repository.dart';
|
||||||
|
|
||||||
@@ -14,11 +18,17 @@ class UserController extends ChangeNotifier {
|
|||||||
UserController({
|
UserController({
|
||||||
required UserRepository userRepository,
|
required UserRepository userRepository,
|
||||||
required GroupRepository groupRepository,
|
required GroupRepository groupRepository,
|
||||||
|
GroupPermissionRepository? permissionRepository,
|
||||||
|
PermissionManager? permissionManager,
|
||||||
}) : _userRepository = userRepository,
|
}) : _userRepository = userRepository,
|
||||||
_groupRepository = groupRepository;
|
_groupRepository = groupRepository,
|
||||||
|
_permissionRepository = permissionRepository,
|
||||||
|
_permissionManager = permissionManager;
|
||||||
|
|
||||||
final UserRepository _userRepository;
|
final UserRepository _userRepository;
|
||||||
final GroupRepository _groupRepository;
|
final GroupRepository _groupRepository;
|
||||||
|
final GroupPermissionRepository? _permissionRepository;
|
||||||
|
final PermissionManager? _permissionManager;
|
||||||
|
|
||||||
PaginatedResult<UserAccount>? _result;
|
PaginatedResult<UserAccount>? _result;
|
||||||
bool _isLoading = false;
|
bool _isLoading = false;
|
||||||
@@ -47,8 +57,9 @@ class UserController extends ChangeNotifier {
|
|||||||
try {
|
try {
|
||||||
final response = await _groupRepository.list(page: 1, pageSize: 100);
|
final response = await _groupRepository.list(page: 1, pageSize: 100);
|
||||||
_groups = response.items;
|
_groups = response.items;
|
||||||
} catch (e) {
|
} catch (error) {
|
||||||
_errorMessage = e.toString();
|
final failure = Failure.from(error);
|
||||||
|
_errorMessage = failure.describe();
|
||||||
} finally {
|
} finally {
|
||||||
_isLoadingGroups = false;
|
_isLoadingGroups = false;
|
||||||
notifyListeners();
|
notifyListeners();
|
||||||
@@ -74,8 +85,9 @@ class UserController extends ChangeNotifier {
|
|||||||
isActive: isActive,
|
isActive: isActive,
|
||||||
);
|
);
|
||||||
_result = response;
|
_result = response;
|
||||||
} catch (e) {
|
} catch (error) {
|
||||||
_errorMessage = e.toString();
|
final failure = Failure.from(error);
|
||||||
|
_errorMessage = failure.describe();
|
||||||
} finally {
|
} finally {
|
||||||
_isLoading = false;
|
_isLoading = false;
|
||||||
notifyListeners();
|
notifyListeners();
|
||||||
@@ -106,9 +118,11 @@ class UserController extends ChangeNotifier {
|
|||||||
try {
|
try {
|
||||||
final created = await _userRepository.create(input);
|
final created = await _userRepository.create(input);
|
||||||
await fetch(page: 1);
|
await fetch(page: 1);
|
||||||
|
await _syncPermissions(input.groupId);
|
||||||
return created;
|
return created;
|
||||||
} catch (e) {
|
} catch (error) {
|
||||||
_errorMessage = e.toString();
|
final failure = Failure.from(error);
|
||||||
|
_errorMessage = failure.describe();
|
||||||
notifyListeners();
|
notifyListeners();
|
||||||
return null;
|
return null;
|
||||||
} finally {
|
} finally {
|
||||||
@@ -122,9 +136,11 @@ class UserController extends ChangeNotifier {
|
|||||||
try {
|
try {
|
||||||
final updated = await _userRepository.update(id, input);
|
final updated = await _userRepository.update(id, input);
|
||||||
await fetch(page: _result?.page ?? 1);
|
await fetch(page: _result?.page ?? 1);
|
||||||
|
await _syncPermissions(input.groupId);
|
||||||
return updated;
|
return updated;
|
||||||
} catch (e) {
|
} catch (error) {
|
||||||
_errorMessage = e.toString();
|
final failure = Failure.from(error);
|
||||||
|
_errorMessage = failure.describe();
|
||||||
notifyListeners();
|
notifyListeners();
|
||||||
return null;
|
return null;
|
||||||
} finally {
|
} finally {
|
||||||
@@ -136,11 +152,16 @@ class UserController extends ChangeNotifier {
|
|||||||
Future<bool> delete(int id) async {
|
Future<bool> delete(int id) async {
|
||||||
_setSubmitting(true);
|
_setSubmitting(true);
|
||||||
try {
|
try {
|
||||||
|
final groupId = _resolveGroupId(id);
|
||||||
await _userRepository.delete(id);
|
await _userRepository.delete(id);
|
||||||
await fetch(page: _result?.page ?? 1);
|
await fetch(page: _result?.page ?? 1);
|
||||||
|
if (groupId != null) {
|
||||||
|
await _syncPermissions(groupId);
|
||||||
|
}
|
||||||
return true;
|
return true;
|
||||||
} catch (e) {
|
} catch (error) {
|
||||||
_errorMessage = e.toString();
|
final failure = Failure.from(error);
|
||||||
|
_errorMessage = failure.describe();
|
||||||
notifyListeners();
|
notifyListeners();
|
||||||
return false;
|
return false;
|
||||||
} finally {
|
} finally {
|
||||||
@@ -154,9 +175,14 @@ class UserController extends ChangeNotifier {
|
|||||||
try {
|
try {
|
||||||
final restored = await _userRepository.restore(id);
|
final restored = await _userRepository.restore(id);
|
||||||
await fetch(page: _result?.page ?? 1);
|
await fetch(page: _result?.page ?? 1);
|
||||||
|
final groupId = restored.group?.id;
|
||||||
|
if (groupId != null) {
|
||||||
|
await _syncPermissions(groupId);
|
||||||
|
}
|
||||||
return restored;
|
return restored;
|
||||||
} catch (e) {
|
} catch (error) {
|
||||||
_errorMessage = e.toString();
|
final failure = Failure.from(error);
|
||||||
|
_errorMessage = failure.describe();
|
||||||
notifyListeners();
|
notifyListeners();
|
||||||
return null;
|
return null;
|
||||||
} finally {
|
} finally {
|
||||||
@@ -175,4 +201,34 @@ class UserController extends ChangeNotifier {
|
|||||||
_isSubmitting = value;
|
_isSubmitting = value;
|
||||||
notifyListeners();
|
notifyListeners();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Future<void> _syncPermissions(int groupId) async {
|
||||||
|
final manager = _permissionManager;
|
||||||
|
final repository = _permissionRepository;
|
||||||
|
if (manager == null || repository == null) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
final synchronizer = PermissionSynchronizer(
|
||||||
|
repository: repository,
|
||||||
|
manager: manager,
|
||||||
|
);
|
||||||
|
await synchronizer.syncForGroup(groupId);
|
||||||
|
} catch (_) {
|
||||||
|
// 권한 동기화 실패는 무시하고 기존 흐름을 유지한다.
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
int? _resolveGroupId(int userId) {
|
||||||
|
final items = _result?.items;
|
||||||
|
if (items == null) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
for (final user in items) {
|
||||||
|
if (user.id == userId) {
|
||||||
|
return user.group?.id;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -8,9 +8,11 @@ import 'package:superport_v2/widgets/components/filter_bar.dart';
|
|||||||
import 'package:superport_v2/widgets/components/superport_dialog.dart';
|
import 'package:superport_v2/widgets/components/superport_dialog.dart';
|
||||||
|
|
||||||
import '../../../../../core/config/environment.dart';
|
import '../../../../../core/config/environment.dart';
|
||||||
|
import '../../../../../core/permissions/permission_manager.dart';
|
||||||
import '../../../../../widgets/spec_page.dart';
|
import '../../../../../widgets/spec_page.dart';
|
||||||
import '../../../group/domain/entities/group.dart';
|
import '../../../group/domain/entities/group.dart';
|
||||||
import '../../../group/domain/repositories/group_repository.dart';
|
import '../../../group/domain/repositories/group_repository.dart';
|
||||||
|
import '../../../group_permission/domain/repositories/group_permission_repository.dart';
|
||||||
import '../../domain/entities/user.dart';
|
import '../../domain/entities/user.dart';
|
||||||
import '../../domain/repositories/user_repository.dart';
|
import '../../domain/repositories/user_repository.dart';
|
||||||
import '../controllers/user_controller.dart';
|
import '../controllers/user_controller.dart';
|
||||||
@@ -96,17 +98,31 @@ class _UserEnabledPageState extends State<_UserEnabledPage> {
|
|||||||
final FocusNode _searchFocus = FocusNode();
|
final FocusNode _searchFocus = FocusNode();
|
||||||
bool _groupsLoaded = false;
|
bool _groupsLoaded = false;
|
||||||
String? _lastError;
|
String? _lastError;
|
||||||
|
bool _initialized = false;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
void initState() {
|
void initState() {
|
||||||
super.initState();
|
super.initState();
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
void didChangeDependencies() {
|
||||||
|
super.didChangeDependencies();
|
||||||
|
if (_initialized) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
final permissionManager = PermissionScope.of(context);
|
||||||
_controller = UserController(
|
_controller = UserController(
|
||||||
userRepository: GetIt.I<UserRepository>(),
|
userRepository: GetIt.I<UserRepository>(),
|
||||||
groupRepository: GetIt.I<GroupRepository>(),
|
groupRepository: GetIt.I<GroupRepository>(),
|
||||||
|
permissionRepository: GetIt.I<GroupPermissionRepository>(),
|
||||||
|
permissionManager: permissionManager,
|
||||||
)..addListener(_handleControllerUpdate);
|
)..addListener(_handleControllerUpdate);
|
||||||
|
_initialized = true;
|
||||||
WidgetsBinding.instance.addPostFrameCallback((_) async {
|
WidgetsBinding.instance.addPostFrameCallback((_) async {
|
||||||
await _controller.loadGroups();
|
await _controller.loadGroups();
|
||||||
await _controller.fetch();
|
await _controller.fetch();
|
||||||
|
if (!mounted) return;
|
||||||
setState(() {
|
setState(() {
|
||||||
_groupsLoaded = true;
|
_groupsLoaded = true;
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ import 'package:dio/dio.dart';
|
|||||||
|
|
||||||
import 'package:superport_v2/core/common/models/paginated_result.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_client.dart';
|
||||||
|
import 'package:superport_v2/core/network/api_routes.dart';
|
||||||
|
|
||||||
import '../../domain/entities/vendor.dart';
|
import '../../domain/entities/vendor.dart';
|
||||||
import '../../domain/repositories/vendor_repository.dart';
|
import '../../domain/repositories/vendor_repository.dart';
|
||||||
@@ -13,7 +14,7 @@ class VendorRepositoryRemote implements VendorRepository {
|
|||||||
|
|
||||||
final ApiClient _api;
|
final ApiClient _api;
|
||||||
|
|
||||||
static const _basePath = '/vendors'; // TODO: 백엔드 경로 확정 시 수정
|
static const _basePath = '${ApiRoutes.apiV1}/vendors';
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Future<PaginatedResult<Vendor>> list({
|
Future<PaginatedResult<Vendor>> list({
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
import 'package:flutter/foundation.dart';
|
import 'package:flutter/foundation.dart';
|
||||||
import 'package:superport_v2/core/common/models/paginated_result.dart';
|
import 'package:superport_v2/core/common/models/paginated_result.dart';
|
||||||
|
import 'package:superport_v2/core/network/failure.dart';
|
||||||
|
|
||||||
import '../../domain/entities/vendor.dart';
|
import '../../domain/entities/vendor.dart';
|
||||||
import '../../domain/repositories/vendor_repository.dart';
|
import '../../domain/repositories/vendor_repository.dart';
|
||||||
@@ -55,8 +56,9 @@ class VendorController extends ChangeNotifier {
|
|||||||
if (response.pageSize > 0 && response.pageSize != _pageSize) {
|
if (response.pageSize > 0 && response.pageSize != _pageSize) {
|
||||||
_pageSize = response.pageSize;
|
_pageSize = response.pageSize;
|
||||||
}
|
}
|
||||||
} catch (e) {
|
} catch (error) {
|
||||||
_errorMessage = e.toString();
|
final failure = Failure.from(error);
|
||||||
|
_errorMessage = failure.describe();
|
||||||
} finally {
|
} finally {
|
||||||
_isLoading = false;
|
_isLoading = false;
|
||||||
notifyListeners();
|
notifyListeners();
|
||||||
@@ -94,8 +96,9 @@ class VendorController extends ChangeNotifier {
|
|||||||
final vendor = await _repository.create(input);
|
final vendor = await _repository.create(input);
|
||||||
await fetch(page: 1);
|
await fetch(page: 1);
|
||||||
return vendor;
|
return vendor;
|
||||||
} catch (e) {
|
} catch (error) {
|
||||||
_errorMessage = e.toString();
|
final failure = Failure.from(error);
|
||||||
|
_errorMessage = failure.describe();
|
||||||
notifyListeners();
|
notifyListeners();
|
||||||
return null;
|
return null;
|
||||||
} finally {
|
} finally {
|
||||||
@@ -110,8 +113,9 @@ class VendorController extends ChangeNotifier {
|
|||||||
final vendor = await _repository.update(id, input);
|
final vendor = await _repository.update(id, input);
|
||||||
await fetch(page: _result?.page ?? 1);
|
await fetch(page: _result?.page ?? 1);
|
||||||
return vendor;
|
return vendor;
|
||||||
} catch (e) {
|
} catch (error) {
|
||||||
_errorMessage = e.toString();
|
final failure = Failure.from(error);
|
||||||
|
_errorMessage = failure.describe();
|
||||||
notifyListeners();
|
notifyListeners();
|
||||||
return null;
|
return null;
|
||||||
} finally {
|
} finally {
|
||||||
@@ -126,8 +130,9 @@ class VendorController extends ChangeNotifier {
|
|||||||
await _repository.delete(id);
|
await _repository.delete(id);
|
||||||
await fetch(page: _result?.page ?? 1);
|
await fetch(page: _result?.page ?? 1);
|
||||||
return true;
|
return true;
|
||||||
} catch (e) {
|
} catch (error) {
|
||||||
_errorMessage = e.toString();
|
final failure = Failure.from(error);
|
||||||
|
_errorMessage = failure.describe();
|
||||||
notifyListeners();
|
notifyListeners();
|
||||||
return false;
|
return false;
|
||||||
} finally {
|
} finally {
|
||||||
@@ -142,8 +147,9 @@ class VendorController extends ChangeNotifier {
|
|||||||
final vendor = await _repository.restore(id);
|
final vendor = await _repository.restore(id);
|
||||||
await fetch(page: _result?.page ?? 1);
|
await fetch(page: _result?.page ?? 1);
|
||||||
return vendor;
|
return vendor;
|
||||||
} catch (e) {
|
} catch (error) {
|
||||||
_errorMessage = e.toString();
|
final failure = Failure.from(error);
|
||||||
|
_errorMessage = failure.describe();
|
||||||
notifyListeners();
|
notifyListeners();
|
||||||
return null;
|
return null;
|
||||||
} finally {
|
} finally {
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
import 'package:dio/dio.dart';
|
import 'package:dio/dio.dart';
|
||||||
import 'package:superport_v2/core/common/models/paginated_result.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_client.dart';
|
||||||
|
import 'package:superport_v2/core/network/api_routes.dart';
|
||||||
|
|
||||||
import '../../domain/entities/warehouse.dart';
|
import '../../domain/entities/warehouse.dart';
|
||||||
import '../../domain/repositories/warehouse_repository.dart';
|
import '../../domain/repositories/warehouse_repository.dart';
|
||||||
@@ -12,7 +13,7 @@ class WarehouseRepositoryRemote implements WarehouseRepository {
|
|||||||
|
|
||||||
final ApiClient _api;
|
final ApiClient _api;
|
||||||
|
|
||||||
static const _basePath = '/warehouses';
|
static const _basePath = '${ApiRoutes.apiV1}/warehouses';
|
||||||
|
|
||||||
/// 창고 목록을 조회한다.
|
/// 창고 목록을 조회한다.
|
||||||
@override
|
@override
|
||||||
@@ -21,6 +22,7 @@ class WarehouseRepositoryRemote implements WarehouseRepository {
|
|||||||
int pageSize = 20,
|
int pageSize = 20,
|
||||||
String? query,
|
String? query,
|
||||||
bool? isActive,
|
bool? isActive,
|
||||||
|
bool includeZipcode = true,
|
||||||
}) async {
|
}) async {
|
||||||
final response = await _api.get<Map<String, dynamic>>(
|
final response = await _api.get<Map<String, dynamic>>(
|
||||||
_basePath,
|
_basePath,
|
||||||
@@ -29,6 +31,7 @@ class WarehouseRepositoryRemote implements WarehouseRepository {
|
|||||||
'page_size': pageSize,
|
'page_size': pageSize,
|
||||||
if (query != null && query.isNotEmpty) 'q': query,
|
if (query != null && query.isNotEmpty) 'q': query,
|
||||||
if (isActive != null) 'is_active': isActive,
|
if (isActive != null) 'is_active': isActive,
|
||||||
|
if (includeZipcode) 'include': 'zipcode',
|
||||||
},
|
},
|
||||||
options: Options(responseType: ResponseType.json),
|
options: Options(responseType: ResponseType.json),
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -10,6 +10,7 @@ abstract class WarehouseRepository {
|
|||||||
int pageSize = 20,
|
int pageSize = 20,
|
||||||
String? query,
|
String? query,
|
||||||
bool? isActive,
|
bool? isActive,
|
||||||
|
bool includeZipcode = true,
|
||||||
});
|
});
|
||||||
|
|
||||||
/// 창고를 생성한다.
|
/// 창고를 생성한다.
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
import 'package:flutter/foundation.dart';
|
import 'package:flutter/foundation.dart';
|
||||||
import 'package:superport_v2/core/common/models/paginated_result.dart';
|
import 'package:superport_v2/core/common/models/paginated_result.dart';
|
||||||
|
import 'package:superport_v2/core/network/failure.dart';
|
||||||
|
|
||||||
import '../../domain/entities/warehouse.dart';
|
import '../../domain/entities/warehouse.dart';
|
||||||
import '../../domain/repositories/warehouse_repository.dart';
|
import '../../domain/repositories/warehouse_repository.dart';
|
||||||
@@ -53,8 +54,9 @@ class WarehouseController extends ChangeNotifier {
|
|||||||
if (response.pageSize > 0 && response.pageSize != _pageSize) {
|
if (response.pageSize > 0 && response.pageSize != _pageSize) {
|
||||||
_pageSize = response.pageSize;
|
_pageSize = response.pageSize;
|
||||||
}
|
}
|
||||||
} catch (e) {
|
} catch (error) {
|
||||||
_errorMessage = e.toString();
|
final failure = Failure.from(error);
|
||||||
|
_errorMessage = failure.describe();
|
||||||
} finally {
|
} finally {
|
||||||
_isLoading = false;
|
_isLoading = false;
|
||||||
notifyListeners();
|
notifyListeners();
|
||||||
@@ -95,8 +97,9 @@ class WarehouseController extends ChangeNotifier {
|
|||||||
final created = await _repository.create(input);
|
final created = await _repository.create(input);
|
||||||
await fetch(page: 1);
|
await fetch(page: 1);
|
||||||
return created;
|
return created;
|
||||||
} catch (e) {
|
} catch (error) {
|
||||||
_errorMessage = e.toString();
|
final failure = Failure.from(error);
|
||||||
|
_errorMessage = failure.describe();
|
||||||
notifyListeners();
|
notifyListeners();
|
||||||
return null;
|
return null;
|
||||||
} finally {
|
} finally {
|
||||||
@@ -111,8 +114,9 @@ class WarehouseController extends ChangeNotifier {
|
|||||||
final updated = await _repository.update(id, input);
|
final updated = await _repository.update(id, input);
|
||||||
await fetch(page: _result?.page ?? 1);
|
await fetch(page: _result?.page ?? 1);
|
||||||
return updated;
|
return updated;
|
||||||
} catch (e) {
|
} catch (error) {
|
||||||
_errorMessage = e.toString();
|
final failure = Failure.from(error);
|
||||||
|
_errorMessage = failure.describe();
|
||||||
notifyListeners();
|
notifyListeners();
|
||||||
return null;
|
return null;
|
||||||
} finally {
|
} finally {
|
||||||
@@ -127,8 +131,9 @@ class WarehouseController extends ChangeNotifier {
|
|||||||
await _repository.delete(id);
|
await _repository.delete(id);
|
||||||
await fetch(page: _result?.page ?? 1);
|
await fetch(page: _result?.page ?? 1);
|
||||||
return true;
|
return true;
|
||||||
} catch (e) {
|
} catch (error) {
|
||||||
_errorMessage = e.toString();
|
final failure = Failure.from(error);
|
||||||
|
_errorMessage = failure.describe();
|
||||||
notifyListeners();
|
notifyListeners();
|
||||||
return false;
|
return false;
|
||||||
} finally {
|
} finally {
|
||||||
@@ -143,8 +148,9 @@ class WarehouseController extends ChangeNotifier {
|
|||||||
final restored = await _repository.restore(id);
|
final restored = await _repository.restore(id);
|
||||||
await fetch(page: _result?.page ?? 1);
|
await fetch(page: _result?.page ?? 1);
|
||||||
return restored;
|
return restored;
|
||||||
} catch (e) {
|
} catch (error) {
|
||||||
_errorMessage = e.toString();
|
final failure = Failure.from(error);
|
||||||
|
_errorMessage = failure.describe();
|
||||||
notifyListeners();
|
notifyListeners();
|
||||||
return null;
|
return null;
|
||||||
} finally {
|
} finally {
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
import 'package:dio/dio.dart';
|
import 'package:dio/dio.dart';
|
||||||
|
|
||||||
import 'package:superport_v2/core/network/api_client.dart';
|
import 'package:superport_v2/core/network/api_client.dart';
|
||||||
|
import 'package:superport_v2/core/network/api_routes.dart';
|
||||||
|
|
||||||
import '../../domain/entities/postal_code.dart';
|
import '../../domain/entities/postal_code.dart';
|
||||||
import '../../domain/repositories/postal_search_repository.dart';
|
import '../../domain/repositories/postal_search_repository.dart';
|
||||||
@@ -13,12 +14,13 @@ class PostalSearchRepositoryRemote implements PostalSearchRepository {
|
|||||||
|
|
||||||
final ApiClient _api;
|
final ApiClient _api;
|
||||||
|
|
||||||
static const _path = '/zipcodes';
|
static const _path = '${ApiRoutes.apiV1}/zipcodes';
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Future<List<PostalCode>> search({
|
Future<List<PostalCode>> search({
|
||||||
required String keyword,
|
required String keyword,
|
||||||
int limit = 20,
|
int limit = 20,
|
||||||
|
int page = 1,
|
||||||
}) async {
|
}) async {
|
||||||
final trimmed = keyword.trim();
|
final trimmed = keyword.trim();
|
||||||
if (trimmed.isEmpty) {
|
if (trimmed.isEmpty) {
|
||||||
@@ -27,12 +29,7 @@ class PostalSearchRepositoryRemote implements PostalSearchRepository {
|
|||||||
|
|
||||||
final response = await _api.get<dynamic>(
|
final response = await _api.get<dynamic>(
|
||||||
_path,
|
_path,
|
||||||
query: {
|
query: {'q': trimmed, 'page': page, 'page_size': limit},
|
||||||
'zipcode': trimmed,
|
|
||||||
'road_name': trimmed,
|
|
||||||
'q': trimmed,
|
|
||||||
'page_size': limit,
|
|
||||||
},
|
|
||||||
options: Options(responseType: ResponseType.json),
|
options: Options(responseType: ResponseType.json),
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|||||||
@@ -5,6 +5,11 @@ abstract class PostalSearchRepository {
|
|||||||
/// 키워드를 기반으로 우편번호 목록을 검색한다.
|
/// 키워드를 기반으로 우편번호 목록을 검색한다.
|
||||||
///
|
///
|
||||||
/// [keyword]는 우편번호/도로명/건물번호 중 하나의 문자열을 전달한다.
|
/// [keyword]는 우편번호/도로명/건물번호 중 하나의 문자열을 전달한다.
|
||||||
/// [limit]을 지정하면 최대 반환 건수를 제한한다.
|
/// [limit]은 페이지 크기(`page_size`)에 대응한다.
|
||||||
Future<List<PostalCode>> search({required String keyword, int limit = 20});
|
/// [page]를 지정하면 서버 페이지네이션을 제어한다.
|
||||||
|
Future<List<PostalCode>> search({
|
||||||
|
required String keyword,
|
||||||
|
int limit = 20,
|
||||||
|
int page = 1,
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ import 'package:flutter/material.dart';
|
|||||||
import 'package:get_it/get_it.dart';
|
import 'package:get_it/get_it.dart';
|
||||||
import 'package:shadcn_ui/shadcn_ui.dart';
|
import 'package:shadcn_ui/shadcn_ui.dart';
|
||||||
|
|
||||||
|
import 'package:superport_v2/core/network/failure.dart';
|
||||||
import 'package:superport_v2/widgets/components/superport_table.dart';
|
import 'package:superport_v2/widgets/components/superport_table.dart';
|
||||||
|
|
||||||
import '../../domain/entities/postal_code.dart';
|
import '../../domain/entities/postal_code.dart';
|
||||||
@@ -102,7 +103,8 @@ class _PostalSearchDialogState extends State<_PostalSearchDialog> {
|
|||||||
if (!mounted) return;
|
if (!mounted) return;
|
||||||
setState(() {
|
setState(() {
|
||||||
_results = const [];
|
_results = const [];
|
||||||
_errorMessage = error.toString();
|
final failure = Failure.from(error);
|
||||||
|
_errorMessage = failure.describe();
|
||||||
});
|
});
|
||||||
} finally {
|
} finally {
|
||||||
if (mounted) {
|
if (mounted) {
|
||||||
|
|||||||
@@ -7,11 +7,15 @@ import 'package:shadcn_ui/shadcn_ui.dart';
|
|||||||
import 'package:superport_v2/core/common/models/paginated_result.dart';
|
import 'package:superport_v2/core/common/models/paginated_result.dart';
|
||||||
import 'package:superport_v2/core/config/environment.dart';
|
import 'package:superport_v2/core/config/environment.dart';
|
||||||
import 'package:superport_v2/core/permissions/permission_manager.dart';
|
import 'package:superport_v2/core/permissions/permission_manager.dart';
|
||||||
|
import 'package:superport_v2/core/permissions/permission_resources.dart';
|
||||||
import 'package:superport_v2/features/approvals/domain/entities/approval.dart';
|
import 'package:superport_v2/features/approvals/domain/entities/approval.dart';
|
||||||
import 'package:superport_v2/features/approvals/domain/entities/approval_template.dart';
|
import 'package:superport_v2/features/approvals/domain/entities/approval_template.dart';
|
||||||
|
import 'package:superport_v2/features/approvals/domain/entities/approval_proceed_status.dart';
|
||||||
import 'package:superport_v2/features/approvals/domain/repositories/approval_repository.dart';
|
import 'package:superport_v2/features/approvals/domain/repositories/approval_repository.dart';
|
||||||
import 'package:superport_v2/features/approvals/domain/repositories/approval_template_repository.dart';
|
import 'package:superport_v2/features/approvals/domain/repositories/approval_template_repository.dart';
|
||||||
import 'package:superport_v2/features/approvals/presentation/pages/approval_page.dart';
|
import 'package:superport_v2/features/approvals/presentation/pages/approval_page.dart';
|
||||||
|
import 'package:superport_v2/features/inventory/lookups/domain/entities/lookup_item.dart';
|
||||||
|
import 'package:superport_v2/features/inventory/lookups/domain/repositories/inventory_lookup_repository.dart';
|
||||||
|
|
||||||
import '../../helpers/test_app.dart';
|
import '../../helpers/test_app.dart';
|
||||||
|
|
||||||
@@ -42,12 +46,14 @@ void main() {
|
|||||||
testWidgets('결재 단계 액션은 승인 권한이 없으면 비활성화된다', (tester) async {
|
testWidgets('결재 단계 액션은 승인 권한이 없으면 비활성화된다', (tester) async {
|
||||||
final repo = _StubApprovalRepository();
|
final repo = _StubApprovalRepository();
|
||||||
final templateRepo = _StubApprovalTemplateRepository();
|
final templateRepo = _StubApprovalTemplateRepository();
|
||||||
|
final lookupRepo = _StubInventoryLookupRepository();
|
||||||
GetIt.I.registerSingleton<ApprovalRepository>(repo);
|
GetIt.I.registerSingleton<ApprovalRepository>(repo);
|
||||||
GetIt.I.registerSingleton<ApprovalTemplateRepository>(templateRepo);
|
GetIt.I.registerSingleton<ApprovalTemplateRepository>(templateRepo);
|
||||||
|
GetIt.I.registerSingleton<InventoryLookupRepository>(lookupRepo);
|
||||||
|
|
||||||
final permissionManager = PermissionManager(
|
final permissionManager = PermissionManager(
|
||||||
overrides: {
|
overrides: {
|
||||||
'/approvals/requests': {PermissionAction.view},
|
PermissionResources.approvals: {PermissionAction.view},
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
|
||||||
@@ -83,12 +89,14 @@ void main() {
|
|||||||
testWidgets('승인 권한이 있으면 단계 액션을 실행할 수 있다', (tester) async {
|
testWidgets('승인 권한이 있으면 단계 액션을 실행할 수 있다', (tester) async {
|
||||||
final repo = _StubApprovalRepository();
|
final repo = _StubApprovalRepository();
|
||||||
final templateRepo = _StubApprovalTemplateRepository();
|
final templateRepo = _StubApprovalTemplateRepository();
|
||||||
|
final lookupRepo = _StubInventoryLookupRepository();
|
||||||
GetIt.I.registerSingleton<ApprovalRepository>(repo);
|
GetIt.I.registerSingleton<ApprovalRepository>(repo);
|
||||||
GetIt.I.registerSingleton<ApprovalTemplateRepository>(templateRepo);
|
GetIt.I.registerSingleton<ApprovalTemplateRepository>(templateRepo);
|
||||||
|
GetIt.I.registerSingleton<InventoryLookupRepository>(lookupRepo);
|
||||||
|
|
||||||
final permissionManager = PermissionManager(
|
final permissionManager = PermissionManager(
|
||||||
overrides: {
|
overrides: {
|
||||||
'/approvals/requests': {
|
PermissionResources.approvals: {
|
||||||
PermissionAction.view,
|
PermissionAction.view,
|
||||||
PermissionAction.approve,
|
PermissionAction.approve,
|
||||||
},
|
},
|
||||||
@@ -115,6 +123,44 @@ void main() {
|
|||||||
expect(approveButton.onPressed, isNotNull);
|
expect(approveButton.onPressed, isNotNull);
|
||||||
expect(find.text('결재 권한이 없어 단계 행위를 실행할 수 없습니다.'), findsNothing);
|
expect(find.text('결재 권한이 없어 단계 행위를 실행할 수 없습니다.'), findsNothing);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
testWidgets('canProceed가 false면 단계 버튼을 비활성화하고 이유를 안내한다', (tester) async {
|
||||||
|
final repo = _BlockingApprovalRepository();
|
||||||
|
final templateRepo = _StubApprovalTemplateRepository();
|
||||||
|
final lookupRepo = _StubInventoryLookupRepository();
|
||||||
|
GetIt.I.registerSingleton<ApprovalRepository>(repo);
|
||||||
|
GetIt.I.registerSingleton<ApprovalTemplateRepository>(templateRepo);
|
||||||
|
GetIt.I.registerSingleton<InventoryLookupRepository>(lookupRepo);
|
||||||
|
|
||||||
|
final permissionManager = PermissionManager(
|
||||||
|
overrides: {
|
||||||
|
PermissionResources.approvals: {
|
||||||
|
PermissionAction.view,
|
||||||
|
PermissionAction.approve,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
await pumpApprovalPage(tester, permissionManager);
|
||||||
|
|
||||||
|
final rowFinder = find.byKey(const ValueKey('approval_row_1'));
|
||||||
|
expect(rowFinder, findsOneWidget);
|
||||||
|
|
||||||
|
await tester.tap(rowFinder);
|
||||||
|
await tester.pumpAndSettle();
|
||||||
|
|
||||||
|
final tabContext = tester.element(find.byType(TabBar));
|
||||||
|
final tabController = DefaultTabController.of(tabContext);
|
||||||
|
tabController.animateTo(1);
|
||||||
|
await tester.pumpAndSettle();
|
||||||
|
|
||||||
|
expect(find.textContaining('선행 단계가 완료되지 않았습니다.'), findsWidgets);
|
||||||
|
|
||||||
|
final approveButton = tester.widget<ShadButton>(
|
||||||
|
find.byKey(const ValueKey('step_action_100_approve')),
|
||||||
|
);
|
||||||
|
expect(approveButton.onPressed, isNull);
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
class _StubApprovalRepository implements ApprovalRepository {
|
class _StubApprovalRepository implements ApprovalRepository {
|
||||||
@@ -194,6 +240,11 @@ class _StubApprovalRepository implements ApprovalRepository {
|
|||||||
return _approval;
|
return _approval;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Future<ApprovalProceedStatus> canProceed(int id) async {
|
||||||
|
return ApprovalProceedStatus(approvalId: id, canProceed: true);
|
||||||
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Future<Approval> create(ApprovalInput input) {
|
Future<Approval> create(ApprovalInput input) {
|
||||||
throw UnimplementedError();
|
throw UnimplementedError();
|
||||||
@@ -215,6 +266,17 @@ class _StubApprovalRepository implements ApprovalRepository {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
class _BlockingApprovalRepository extends _StubApprovalRepository {
|
||||||
|
@override
|
||||||
|
Future<ApprovalProceedStatus> canProceed(int id) async {
|
||||||
|
return ApprovalProceedStatus(
|
||||||
|
approvalId: id,
|
||||||
|
canProceed: false,
|
||||||
|
reason: '선행 단계가 완료되지 않았습니다. 관리자에게 문의하세요.',
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
class _StubApprovalTemplateRepository implements ApprovalTemplateRepository {
|
class _StubApprovalTemplateRepository implements ApprovalTemplateRepository {
|
||||||
_StubApprovalTemplateRepository();
|
_StubApprovalTemplateRepository();
|
||||||
|
|
||||||
@@ -285,3 +347,45 @@ class _StubApprovalTemplateRepository implements ApprovalTemplateRepository {
|
|||||||
throw UnimplementedError();
|
throw UnimplementedError();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
class _StubInventoryLookupRepository implements InventoryLookupRepository {
|
||||||
|
@override
|
||||||
|
Future<List<LookupItem>> fetchTransactionTypes({
|
||||||
|
bool activeOnly = true,
|
||||||
|
}) async {
|
||||||
|
return <LookupItem>[
|
||||||
|
LookupItem(id: 1, name: '입고', code: 'INBOUND'),
|
||||||
|
LookupItem(id: 2, name: '출고', code: 'OUTBOUND'),
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Future<List<LookupItem>> fetchTransactionStatuses({
|
||||||
|
bool activeOnly = true,
|
||||||
|
}) async {
|
||||||
|
return <LookupItem>[
|
||||||
|
LookupItem(id: 10, name: '승인대기', code: 'pending'),
|
||||||
|
LookupItem(id: 11, name: '진행중', code: 'in_progress'),
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Future<List<LookupItem>> fetchApprovalStatuses({
|
||||||
|
bool activeOnly = true,
|
||||||
|
}) async {
|
||||||
|
return <LookupItem>[
|
||||||
|
LookupItem(id: 20, name: '승인대기', code: 'pending'),
|
||||||
|
LookupItem(id: 21, name: '진행중', code: 'in_progress'),
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Future<List<LookupItem>> fetchApprovalActions({
|
||||||
|
bool activeOnly = true,
|
||||||
|
}) async {
|
||||||
|
return <LookupItem>[
|
||||||
|
LookupItem(id: 30, name: '승인', code: 'approve'),
|
||||||
|
LookupItem(id: 31, name: '반려', code: 'reject'),
|
||||||
|
];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ import 'package:mocktail/mocktail.dart';
|
|||||||
import 'package:superport_v2/core/common/models/paginated_result.dart';
|
import 'package:superport_v2/core/common/models/paginated_result.dart';
|
||||||
import 'package:superport_v2/features/approvals/domain/entities/approval.dart';
|
import 'package:superport_v2/features/approvals/domain/entities/approval.dart';
|
||||||
import 'package:superport_v2/features/approvals/domain/entities/approval_template.dart';
|
import 'package:superport_v2/features/approvals/domain/entities/approval_template.dart';
|
||||||
|
import 'package:superport_v2/features/approvals/domain/entities/approval_proceed_status.dart';
|
||||||
import 'package:superport_v2/features/approvals/domain/repositories/approval_repository.dart';
|
import 'package:superport_v2/features/approvals/domain/repositories/approval_repository.dart';
|
||||||
import 'package:superport_v2/features/approvals/domain/repositories/approval_template_repository.dart';
|
import 'package:superport_v2/features/approvals/domain/repositories/approval_template_repository.dart';
|
||||||
import 'package:superport_v2/features/approvals/presentation/controllers/approval_controller.dart';
|
import 'package:superport_v2/features/approvals/presentation/controllers/approval_controller.dart';
|
||||||
@@ -74,6 +75,12 @@ void main() {
|
|||||||
approvalRepository: repository,
|
approvalRepository: repository,
|
||||||
templateRepository: templateRepository,
|
templateRepository: templateRepository,
|
||||||
);
|
);
|
||||||
|
when(() => repository.canProceed(any())).thenAnswer(
|
||||||
|
(_) async => ApprovalProceedStatus(
|
||||||
|
approvalId: sampleApproval.id!,
|
||||||
|
canProceed: true,
|
||||||
|
),
|
||||||
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
// fetch 메서드 관련 시나리오
|
// fetch 메서드 관련 시나리오
|
||||||
@@ -166,6 +173,8 @@ void main() {
|
|||||||
includeHistories: true,
|
includeHistories: true,
|
||||||
),
|
),
|
||||||
).called(1);
|
).called(1);
|
||||||
|
verify(() => repository.canProceed(1)).called(1);
|
||||||
|
expect(controller.canProceedSelected, isTrue);
|
||||||
});
|
});
|
||||||
|
|
||||||
test('에러 발생 시 errorMessage 설정', () async {
|
test('에러 발생 시 errorMessage 설정', () async {
|
||||||
@@ -369,6 +378,30 @@ void main() {
|
|||||||
expect(controller.isPerformingAction, isFalse);
|
expect(controller.isPerformingAction, isFalse);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test('canProceed가 false면 액션을 중단한다', () async {
|
||||||
|
when(() => repository.canProceed(any())).thenAnswer(
|
||||||
|
(_) async => ApprovalProceedStatus(
|
||||||
|
approvalId: sampleApproval.id!,
|
||||||
|
canProceed: false,
|
||||||
|
reason: '선행 단계가 완료되지 않았습니다.',
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
|
await controller.loadActionOptions(force: true);
|
||||||
|
await controller.fetch();
|
||||||
|
await controller.selectApproval(sampleApproval.id!);
|
||||||
|
|
||||||
|
final success = await controller.performStepAction(
|
||||||
|
step: sampleStep,
|
||||||
|
type: ApprovalStepActionType.approve,
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(success, isFalse);
|
||||||
|
expect(controller.errorMessage, contains('선행 단계'));
|
||||||
|
expect(controller.canProceedSelected, isFalse);
|
||||||
|
verifyNever(() => repository.performStepAction(any()));
|
||||||
|
});
|
||||||
|
|
||||||
test('행위를 찾지 못하면 요청하지 않는다', () async {
|
test('행위를 찾지 못하면 요청하지 않는다', () async {
|
||||||
when(
|
when(
|
||||||
() => repository.listActions(activeOnly: any(named: 'activeOnly')),
|
() => repository.listActions(activeOnly: any(named: 'activeOnly')),
|
||||||
|
|||||||
@@ -5,22 +5,38 @@ import 'package:get_it/get_it.dart';
|
|||||||
import 'package:mocktail/mocktail.dart';
|
import 'package:mocktail/mocktail.dart';
|
||||||
import 'package:shadcn_ui/shadcn_ui.dart';
|
import 'package:shadcn_ui/shadcn_ui.dart';
|
||||||
|
|
||||||
|
import 'package:superport_v2/core/common/models/paginated_result.dart';
|
||||||
|
import 'package:superport_v2/core/permissions/permission_manager.dart';
|
||||||
import 'package:superport_v2/features/approvals/domain/entities/approval.dart';
|
import 'package:superport_v2/features/approvals/domain/entities/approval.dart';
|
||||||
|
import 'package:superport_v2/features/approvals/domain/entities/approval_template.dart';
|
||||||
import 'package:superport_v2/features/approvals/domain/repositories/approval_repository.dart';
|
import 'package:superport_v2/features/approvals/domain/repositories/approval_repository.dart';
|
||||||
|
import 'package:superport_v2/features/approvals/domain/repositories/approval_template_repository.dart';
|
||||||
|
import 'package:superport_v2/features/approvals/presentation/controllers/approval_controller.dart';
|
||||||
import 'package:superport_v2/features/approvals/presentation/pages/approval_page.dart';
|
import 'package:superport_v2/features/approvals/presentation/pages/approval_page.dart';
|
||||||
|
import 'package:superport_v2/features/inventory/lookups/domain/entities/lookup_item.dart';
|
||||||
|
import 'package:superport_v2/features/inventory/lookups/domain/repositories/inventory_lookup_repository.dart';
|
||||||
|
|
||||||
class _MockApprovalRepository extends Mock implements ApprovalRepository {}
|
class _MockApprovalRepository extends Mock implements ApprovalRepository {}
|
||||||
|
|
||||||
class _FakeApprovalInput extends Fake implements ApprovalInput {}
|
class _FakeApprovalInput extends Fake implements ApprovalInput {}
|
||||||
|
|
||||||
|
class _MockApprovalTemplateRepository extends Mock
|
||||||
|
implements ApprovalTemplateRepository {}
|
||||||
|
|
||||||
|
class _MockInventoryLookupRepository extends Mock
|
||||||
|
implements InventoryLookupRepository {}
|
||||||
|
|
||||||
Widget _buildApp(Widget child) {
|
Widget _buildApp(Widget child) {
|
||||||
return MaterialApp(
|
return PermissionScope(
|
||||||
home: ShadTheme(
|
manager: PermissionManager(),
|
||||||
data: ShadThemeData(
|
child: MaterialApp(
|
||||||
colorScheme: const ShadSlateColorScheme.light(),
|
home: ShadTheme(
|
||||||
brightness: Brightness.light,
|
data: ShadThemeData(
|
||||||
|
colorScheme: const ShadSlateColorScheme.light(),
|
||||||
|
brightness: Brightness.light,
|
||||||
|
),
|
||||||
|
child: Scaffold(body: child),
|
||||||
),
|
),
|
||||||
child: Scaffold(body: child),
|
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -49,11 +65,75 @@ void main() {
|
|||||||
|
|
||||||
group('플래그 On', () {
|
group('플래그 On', () {
|
||||||
late _MockApprovalRepository repository;
|
late _MockApprovalRepository repository;
|
||||||
|
late _MockApprovalTemplateRepository templateRepository;
|
||||||
|
late _MockInventoryLookupRepository lookupRepository;
|
||||||
|
|
||||||
setUp(() {
|
setUp(() {
|
||||||
dotenv.testLoad(fileInput: 'FEATURE_APPROVALS_ENABLED=true\n');
|
dotenv.testLoad(fileInput: 'FEATURE_APPROVALS_ENABLED=true\n');
|
||||||
repository = _MockApprovalRepository();
|
repository = _MockApprovalRepository();
|
||||||
|
templateRepository = _MockApprovalTemplateRepository();
|
||||||
|
lookupRepository = _MockInventoryLookupRepository();
|
||||||
GetIt.I.registerLazySingleton<ApprovalRepository>(() => repository);
|
GetIt.I.registerLazySingleton<ApprovalRepository>(() => repository);
|
||||||
|
GetIt.I.registerLazySingleton<ApprovalTemplateRepository>(
|
||||||
|
() => templateRepository,
|
||||||
|
);
|
||||||
|
GetIt.I.registerLazySingleton<InventoryLookupRepository>(
|
||||||
|
() => lookupRepository,
|
||||||
|
);
|
||||||
|
when(
|
||||||
|
() => templateRepository.list(
|
||||||
|
page: any(named: 'page'),
|
||||||
|
pageSize: any(named: 'pageSize'),
|
||||||
|
query: any(named: 'query'),
|
||||||
|
isActive: any(named: 'isActive'),
|
||||||
|
),
|
||||||
|
).thenAnswer(
|
||||||
|
(_) async => PaginatedResult<ApprovalTemplate>(
|
||||||
|
items: [],
|
||||||
|
page: 1,
|
||||||
|
pageSize: 20,
|
||||||
|
total: 0,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
when(
|
||||||
|
() => repository.listActions(activeOnly: any(named: 'activeOnly')),
|
||||||
|
).thenAnswer((_) async => const []);
|
||||||
|
when(() => lookupRepository.fetchApprovalStatuses()).thenAnswer(
|
||||||
|
(_) async => [LookupItem(id: 1, name: '승인대기', code: 'pending')],
|
||||||
|
);
|
||||||
|
when(
|
||||||
|
() => repository.list(
|
||||||
|
page: any(named: 'page'),
|
||||||
|
pageSize: any(named: 'pageSize'),
|
||||||
|
query: any(named: 'query'),
|
||||||
|
status: any(named: 'status'),
|
||||||
|
from: any(named: 'from'),
|
||||||
|
to: any(named: 'to'),
|
||||||
|
includeHistories: any(named: 'includeHistories'),
|
||||||
|
includeSteps: any(named: 'includeSteps'),
|
||||||
|
),
|
||||||
|
).thenAnswer(
|
||||||
|
(_) async => PaginatedResult<Approval>(
|
||||||
|
items: const [],
|
||||||
|
page: 1,
|
||||||
|
pageSize: 20,
|
||||||
|
total: 0,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
testWidgets('상태 룩업을 불러와 필터 라벨을 구성한다', (tester) async {
|
||||||
|
await tester.pumpWidget(_buildApp(const ApprovalPage()));
|
||||||
|
await tester.pumpAndSettle();
|
||||||
|
|
||||||
|
verify(() => lookupRepository.fetchApprovalStatuses()).called(1);
|
||||||
|
final statusSelectFinder = find.byKey(
|
||||||
|
const ValueKey(ApprovalStatusFilter.all),
|
||||||
|
);
|
||||||
|
expect(statusSelectFinder, findsOneWidget);
|
||||||
|
await tester.tap(statusSelectFinder);
|
||||||
|
await tester.pumpAndSettle();
|
||||||
|
expect(find.text('승인대기'), findsWidgets);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ import 'package:shadcn_ui/shadcn_ui.dart';
|
|||||||
|
|
||||||
import 'package:superport_v2/core/common/models/paginated_result.dart';
|
import 'package:superport_v2/core/common/models/paginated_result.dart';
|
||||||
import 'package:superport_v2/core/permissions/permission_manager.dart';
|
import 'package:superport_v2/core/permissions/permission_manager.dart';
|
||||||
|
import 'package:superport_v2/core/permissions/permission_resources.dart';
|
||||||
import 'package:superport_v2/features/approvals/domain/entities/approval.dart';
|
import 'package:superport_v2/features/approvals/domain/entities/approval.dart';
|
||||||
import 'package:superport_v2/features/approvals/step/domain/entities/approval_step_input.dart';
|
import 'package:superport_v2/features/approvals/step/domain/entities/approval_step_input.dart';
|
||||||
import 'package:superport_v2/features/approvals/step/domain/entities/approval_step_record.dart';
|
import 'package:superport_v2/features/approvals/step/domain/entities/approval_step_record.dart';
|
||||||
@@ -18,7 +19,9 @@ class _MockApprovalStepRepository extends Mock
|
|||||||
|
|
||||||
Widget _buildApp(Widget child) {
|
Widget _buildApp(Widget child) {
|
||||||
final manager = PermissionManager(
|
final manager = PermissionManager(
|
||||||
overrides: {'/approvals/steps': PermissionAction.values.toSet()},
|
overrides: {
|
||||||
|
PermissionResources.approvalSteps: PermissionAction.values.toSet(),
|
||||||
|
},
|
||||||
);
|
);
|
||||||
return MaterialApp(
|
return MaterialApp(
|
||||||
home: PermissionScope(
|
home: PermissionScope(
|
||||||
|
|||||||
@@ -0,0 +1,119 @@
|
|||||||
|
import 'package:dio/dio.dart';
|
||||||
|
import 'package:flutter_test/flutter_test.dart';
|
||||||
|
import 'package:mocktail/mocktail.dart';
|
||||||
|
|
||||||
|
import 'package:superport_v2/core/network/api_client.dart';
|
||||||
|
import 'package:superport_v2/features/masters/customer/data/repositories/customer_repository_remote.dart';
|
||||||
|
|
||||||
|
class _MockApiClient extends Mock implements ApiClient {}
|
||||||
|
|
||||||
|
void main() {
|
||||||
|
late ApiClient apiClient;
|
||||||
|
late CustomerRepositoryRemote repository;
|
||||||
|
|
||||||
|
setUpAll(() {
|
||||||
|
registerFallbackValue(Options());
|
||||||
|
registerFallbackValue(CancelToken());
|
||||||
|
});
|
||||||
|
|
||||||
|
setUp(() {
|
||||||
|
apiClient = _MockApiClient();
|
||||||
|
repository = CustomerRepositoryRemote(apiClient: apiClient);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('list 호출 시 필터를 쿼리에 포함한다', () async {
|
||||||
|
when(
|
||||||
|
() => apiClient.get<Map<String, dynamic>>(
|
||||||
|
any(),
|
||||||
|
query: any(named: 'query'),
|
||||||
|
options: any(named: 'options'),
|
||||||
|
cancelToken: any(named: 'cancelToken'),
|
||||||
|
),
|
||||||
|
).thenAnswer(
|
||||||
|
(_) async => Response<Map<String, dynamic>>(
|
||||||
|
data: {
|
||||||
|
'items': [
|
||||||
|
{'id': 1, 'customer_code': 'C-001', 'customer_name': '슈퍼포트'},
|
||||||
|
],
|
||||||
|
'page': 2,
|
||||||
|
'page_size': 30,
|
||||||
|
'total': 1,
|
||||||
|
},
|
||||||
|
requestOptions: RequestOptions(path: '/api/v1/customers'),
|
||||||
|
statusCode: 200,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
|
final result = await repository.list(
|
||||||
|
page: 2,
|
||||||
|
pageSize: 30,
|
||||||
|
query: 'sup',
|
||||||
|
isPartner: true,
|
||||||
|
isGeneral: false,
|
||||||
|
isActive: true,
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(result.items, isNotEmpty);
|
||||||
|
|
||||||
|
final verification = verify(
|
||||||
|
() => apiClient.get<Map<String, dynamic>>(
|
||||||
|
captureAny(),
|
||||||
|
query: captureAny(named: 'query'),
|
||||||
|
options: any(named: 'options'),
|
||||||
|
cancelToken: any(named: 'cancelToken'),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
final path = verification.captured[0] as String;
|
||||||
|
final query = verification.captured[1] as Map<String, dynamic>;
|
||||||
|
|
||||||
|
expect(path, equals('/api/v1/customers'));
|
||||||
|
expect(query['page'], 2);
|
||||||
|
expect(query['page_size'], 30);
|
||||||
|
expect(query['q'], 'sup');
|
||||||
|
expect(query['is_partner'], true);
|
||||||
|
expect(query['is_general'], false);
|
||||||
|
expect(query['is_active'], true);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('fetchDetail은 include=zipcode 파라미터를 전달한다', () async {
|
||||||
|
when(
|
||||||
|
() => apiClient.get<Map<String, dynamic>>(
|
||||||
|
any(),
|
||||||
|
query: any(named: 'query'),
|
||||||
|
options: any(named: 'options'),
|
||||||
|
cancelToken: any(named: 'cancelToken'),
|
||||||
|
),
|
||||||
|
).thenAnswer(
|
||||||
|
(_) async => Response<Map<String, dynamic>>(
|
||||||
|
data: {
|
||||||
|
'data': {
|
||||||
|
'id': 10,
|
||||||
|
'customer_code': 'C-010',
|
||||||
|
'customer_name': '테스트 고객',
|
||||||
|
'zipcode': {'zipcode': '06000'},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
requestOptions: RequestOptions(path: '/api/v1/customers/10'),
|
||||||
|
statusCode: 200,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
|
final customer = await repository.fetchDetail(10);
|
||||||
|
|
||||||
|
expect(customer.customerCode, 'C-010');
|
||||||
|
|
||||||
|
final verification = verify(
|
||||||
|
() => apiClient.get<Map<String, dynamic>>(
|
||||||
|
captureAny(),
|
||||||
|
query: captureAny(named: 'query'),
|
||||||
|
options: any(named: 'options'),
|
||||||
|
cancelToken: any(named: 'cancelToken'),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
final path = verification.captured[0] as String;
|
||||||
|
final query = verification.captured[1] as Map<String, dynamic>;
|
||||||
|
|
||||||
|
expect(path, equals('/api/v1/customers/10'));
|
||||||
|
expect(query['include'], 'zipcode');
|
||||||
|
});
|
||||||
|
}
|
||||||
@@ -0,0 +1,65 @@
|
|||||||
|
import 'package:dio/dio.dart';
|
||||||
|
import 'package:flutter_test/flutter_test.dart';
|
||||||
|
import 'package:mocktail/mocktail.dart';
|
||||||
|
|
||||||
|
import 'package:superport_v2/core/network/api_client.dart';
|
||||||
|
import 'package:superport_v2/features/masters/group/data/repositories/group_repository_remote.dart';
|
||||||
|
|
||||||
|
class _MockApiClient extends Mock implements ApiClient {}
|
||||||
|
|
||||||
|
void main() {
|
||||||
|
late ApiClient apiClient;
|
||||||
|
late GroupRepositoryRemote repository;
|
||||||
|
|
||||||
|
setUpAll(() {
|
||||||
|
registerFallbackValue(Options());
|
||||||
|
registerFallbackValue(CancelToken());
|
||||||
|
});
|
||||||
|
|
||||||
|
setUp(() {
|
||||||
|
apiClient = _MockApiClient();
|
||||||
|
repository = GroupRepositoryRemote(apiClient: apiClient);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('include 옵션이 쿼리 파라미터에 반영된다', () async {
|
||||||
|
when(
|
||||||
|
() => apiClient.get<Map<String, dynamic>>(
|
||||||
|
any(),
|
||||||
|
query: any(named: 'query'),
|
||||||
|
options: any(named: 'options'),
|
||||||
|
cancelToken: any(named: 'cancelToken'),
|
||||||
|
),
|
||||||
|
).thenAnswer(
|
||||||
|
(_) async => Response<Map<String, dynamic>>(
|
||||||
|
data: {
|
||||||
|
'items': [
|
||||||
|
{
|
||||||
|
'id': 1,
|
||||||
|
'group_name': '관리자',
|
||||||
|
'is_default': true,
|
||||||
|
'is_active': true,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
requestOptions: RequestOptions(path: '/api/v1/groups'),
|
||||||
|
statusCode: 200,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
|
await repository.list(includePermissions: true, includeEmployees: true);
|
||||||
|
|
||||||
|
final captured = verify(
|
||||||
|
() => apiClient.get<Map<String, dynamic>>(
|
||||||
|
captureAny(),
|
||||||
|
query: captureAny(named: 'query'),
|
||||||
|
options: any(named: 'options'),
|
||||||
|
cancelToken: any(named: 'cancelToken'),
|
||||||
|
),
|
||||||
|
).captured;
|
||||||
|
final path = captured[0] as String;
|
||||||
|
final query = captured[1] as Map<String, dynamic>;
|
||||||
|
|
||||||
|
expect(path, equals('/api/v1/groups'));
|
||||||
|
expect(query['include'], 'permissions,employees');
|
||||||
|
});
|
||||||
|
}
|
||||||
@@ -2,12 +2,18 @@ import 'package:flutter_test/flutter_test.dart';
|
|||||||
import 'package:mocktail/mocktail.dart';
|
import 'package:mocktail/mocktail.dart';
|
||||||
|
|
||||||
import 'package:superport_v2/core/common/models/paginated_result.dart';
|
import 'package:superport_v2/core/common/models/paginated_result.dart';
|
||||||
|
import 'package:superport_v2/core/permissions/permission_manager.dart';
|
||||||
import 'package:superport_v2/features/masters/group/domain/entities/group.dart';
|
import 'package:superport_v2/features/masters/group/domain/entities/group.dart';
|
||||||
import 'package:superport_v2/features/masters/group/domain/repositories/group_repository.dart';
|
import 'package:superport_v2/features/masters/group/domain/repositories/group_repository.dart';
|
||||||
import 'package:superport_v2/features/masters/group/presentation/controllers/group_controller.dart';
|
import 'package:superport_v2/features/masters/group/presentation/controllers/group_controller.dart';
|
||||||
|
import 'package:superport_v2/features/masters/group_permission/domain/entities/group_permission.dart';
|
||||||
|
import 'package:superport_v2/features/masters/group_permission/domain/repositories/group_permission_repository.dart';
|
||||||
|
|
||||||
class _MockGroupRepository extends Mock implements GroupRepository {}
|
class _MockGroupRepository extends Mock implements GroupRepository {}
|
||||||
|
|
||||||
|
class _MockGroupPermissionRepository extends Mock
|
||||||
|
implements GroupPermissionRepository {}
|
||||||
|
|
||||||
class _FakeGroupInput extends Fake implements GroupInput {}
|
class _FakeGroupInput extends Fake implements GroupInput {}
|
||||||
|
|
||||||
void main() {
|
void main() {
|
||||||
@@ -126,6 +132,65 @@ void main() {
|
|||||||
expect(controller.statusFilter, GroupStatusFilter.activeOnly);
|
expect(controller.statusFilter, GroupStatusFilter.activeOnly);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
group('permission sync', () {
|
||||||
|
late _MockGroupPermissionRepository permissionRepository;
|
||||||
|
late PermissionManager permissionManager;
|
||||||
|
|
||||||
|
setUp(() {
|
||||||
|
permissionRepository = _MockGroupPermissionRepository();
|
||||||
|
permissionManager = PermissionManager();
|
||||||
|
when(
|
||||||
|
() => permissionRepository.list(
|
||||||
|
page: any(named: 'page'),
|
||||||
|
pageSize: any(named: 'pageSize'),
|
||||||
|
groupId: any(named: 'groupId'),
|
||||||
|
menuId: any(named: 'menuId'),
|
||||||
|
isActive: any(named: 'isActive'),
|
||||||
|
includeDeleted: any(named: 'includeDeleted'),
|
||||||
|
),
|
||||||
|
).thenAnswer(
|
||||||
|
(_) async => PaginatedResult<GroupPermission>(
|
||||||
|
items: const <GroupPermission>[],
|
||||||
|
page: 1,
|
||||||
|
pageSize: 200,
|
||||||
|
total: 0,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('그룹 생성 후 권한 동기화를 시도한다', () async {
|
||||||
|
when(() => repository.create(any())).thenAnswer((_) async => sampleGroup);
|
||||||
|
when(
|
||||||
|
() => repository.list(
|
||||||
|
page: any(named: 'page'),
|
||||||
|
pageSize: any(named: 'pageSize'),
|
||||||
|
query: any(named: 'query'),
|
||||||
|
isDefault: any(named: 'isDefault'),
|
||||||
|
isActive: any(named: 'isActive'),
|
||||||
|
),
|
||||||
|
).thenAnswer((_) async => createResult());
|
||||||
|
|
||||||
|
final controllerWithSync = GroupController(
|
||||||
|
repository: repository,
|
||||||
|
permissionRepository: permissionRepository,
|
||||||
|
permissionManager: permissionManager,
|
||||||
|
);
|
||||||
|
|
||||||
|
await controllerWithSync.create(GroupInput(groupName: '신규 그룹'));
|
||||||
|
|
||||||
|
verify(
|
||||||
|
() => permissionRepository.list(
|
||||||
|
page: any(named: 'page'),
|
||||||
|
pageSize: any(named: 'pageSize'),
|
||||||
|
groupId: sampleGroup.id,
|
||||||
|
menuId: any(named: 'menuId'),
|
||||||
|
isActive: any(named: 'isActive'),
|
||||||
|
includeDeleted: any(named: 'includeDeleted'),
|
||||||
|
),
|
||||||
|
).called(greaterThanOrEqualTo(1));
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
group('mutations', () {
|
group('mutations', () {
|
||||||
setUp(() {
|
setUp(() {
|
||||||
when(
|
when(
|
||||||
|
|||||||
@@ -0,0 +1,115 @@
|
|||||||
|
import 'package:flutter_test/flutter_test.dart';
|
||||||
|
import 'package:mocktail/mocktail.dart';
|
||||||
|
|
||||||
|
import 'package:superport_v2/core/common/models/paginated_result.dart';
|
||||||
|
import 'package:superport_v2/core/permissions/permission_manager.dart';
|
||||||
|
import 'package:superport_v2/core/permissions/permission_resources.dart';
|
||||||
|
import 'package:superport_v2/features/masters/group_permission/application/permission_synchronizer.dart';
|
||||||
|
import 'package:superport_v2/features/masters/group_permission/domain/entities/group_permission.dart';
|
||||||
|
import 'package:superport_v2/features/masters/group_permission/domain/repositories/group_permission_repository.dart';
|
||||||
|
|
||||||
|
class _MockGroupPermissionRepository extends Mock
|
||||||
|
implements GroupPermissionRepository {}
|
||||||
|
|
||||||
|
void main() {
|
||||||
|
TestWidgetsFlutterBinding.ensureInitialized();
|
||||||
|
|
||||||
|
test('그룹 권한을 모두 불러와 PermissionManager에 적용한다', () async {
|
||||||
|
final repository = _MockGroupPermissionRepository();
|
||||||
|
final manager = PermissionManager();
|
||||||
|
final synchronizer = PermissionSynchronizer(
|
||||||
|
repository: repository,
|
||||||
|
manager: manager,
|
||||||
|
pageSize: 1,
|
||||||
|
);
|
||||||
|
|
||||||
|
final permissionPage1 = GroupPermission(
|
||||||
|
id: 1,
|
||||||
|
group: GroupPermissionGroup(id: 1, groupName: '관리자'),
|
||||||
|
menu: GroupPermissionMenu(
|
||||||
|
id: 10,
|
||||||
|
menuCode: 'INBOUND',
|
||||||
|
menuName: '입고',
|
||||||
|
path: '/inventory/inbound',
|
||||||
|
),
|
||||||
|
canCreate: true,
|
||||||
|
canRead: true,
|
||||||
|
canUpdate: false,
|
||||||
|
canDelete: false,
|
||||||
|
);
|
||||||
|
|
||||||
|
final permissionPage2 = GroupPermission(
|
||||||
|
id: 2,
|
||||||
|
group: GroupPermissionGroup(id: 1, groupName: '관리자'),
|
||||||
|
menu: GroupPermissionMenu(
|
||||||
|
id: 11,
|
||||||
|
menuCode: 'OUTBOUND',
|
||||||
|
menuName: '출고',
|
||||||
|
path: '/inventory/outbound',
|
||||||
|
),
|
||||||
|
canCreate: false,
|
||||||
|
canRead: true,
|
||||||
|
canUpdate: true,
|
||||||
|
canDelete: false,
|
||||||
|
);
|
||||||
|
|
||||||
|
when(
|
||||||
|
() => repository.list(
|
||||||
|
page: any(named: 'page'),
|
||||||
|
pageSize: any(named: 'pageSize'),
|
||||||
|
groupId: any(named: 'groupId'),
|
||||||
|
menuId: any(named: 'menuId'),
|
||||||
|
isActive: any(named: 'isActive'),
|
||||||
|
includeDeleted: any(named: 'includeDeleted'),
|
||||||
|
),
|
||||||
|
).thenAnswer((invocation) async {
|
||||||
|
final page = invocation.namedArguments[#page] as int;
|
||||||
|
if (page == 1) {
|
||||||
|
return PaginatedResult<GroupPermission>(
|
||||||
|
items: [permissionPage1],
|
||||||
|
page: 1,
|
||||||
|
pageSize: 1,
|
||||||
|
total: 2,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return PaginatedResult<GroupPermission>(
|
||||||
|
items: [permissionPage2],
|
||||||
|
page: 2,
|
||||||
|
pageSize: 1,
|
||||||
|
total: 2,
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
await synchronizer.syncForGroup(1);
|
||||||
|
|
||||||
|
verify(
|
||||||
|
() => repository.list(
|
||||||
|
page: any(named: 'page'),
|
||||||
|
pageSize: 1,
|
||||||
|
groupId: 1,
|
||||||
|
menuId: null,
|
||||||
|
isActive: true,
|
||||||
|
includeDeleted: false,
|
||||||
|
),
|
||||||
|
).called(greaterThanOrEqualTo(1));
|
||||||
|
|
||||||
|
expect(
|
||||||
|
manager.can(
|
||||||
|
PermissionResources.stockTransactions,
|
||||||
|
PermissionAction.create,
|
||||||
|
),
|
||||||
|
isTrue,
|
||||||
|
);
|
||||||
|
expect(
|
||||||
|
manager.can(PermissionResources.stockTransactions, PermissionAction.edit),
|
||||||
|
isTrue,
|
||||||
|
);
|
||||||
|
expect(
|
||||||
|
manager.can(
|
||||||
|
PermissionResources.stockTransactions,
|
||||||
|
PermissionAction.delete,
|
||||||
|
),
|
||||||
|
isFalse,
|
||||||
|
);
|
||||||
|
});
|
||||||
|
}
|
||||||
@@ -0,0 +1,71 @@
|
|||||||
|
import 'package:flutter_test/flutter_test.dart';
|
||||||
|
|
||||||
|
import 'package:superport_v2/core/permissions/permission_manager.dart';
|
||||||
|
import 'package:superport_v2/core/permissions/permission_resources.dart';
|
||||||
|
import 'package:superport_v2/features/masters/group_permission/domain/entities/group_permission.dart';
|
||||||
|
import 'package:superport_v2/features/masters/group_permission/domain/mappers/group_permission_mapper.dart';
|
||||||
|
|
||||||
|
void main() {
|
||||||
|
test('메뉴 경로 기준으로 권한 맵을 생성한다', () {
|
||||||
|
final permissions = [
|
||||||
|
GroupPermission(
|
||||||
|
id: 1,
|
||||||
|
group: GroupPermissionGroup(id: 1, groupName: '관리자'),
|
||||||
|
menu: GroupPermissionMenu(
|
||||||
|
id: 10,
|
||||||
|
menuCode: 'INBOUND',
|
||||||
|
menuName: '입고',
|
||||||
|
path: '/inventory/inbound',
|
||||||
|
),
|
||||||
|
canCreate: true,
|
||||||
|
canRead: true,
|
||||||
|
canUpdate: false,
|
||||||
|
canDelete: false,
|
||||||
|
),
|
||||||
|
GroupPermission(
|
||||||
|
id: 2,
|
||||||
|
group: GroupPermissionGroup(id: 1, groupName: '관리자'),
|
||||||
|
menu: GroupPermissionMenu(
|
||||||
|
id: 11,
|
||||||
|
menuCode: 'OUTBOUND',
|
||||||
|
menuName: '출고',
|
||||||
|
path: '/inventory/outbound',
|
||||||
|
),
|
||||||
|
canCreate: false,
|
||||||
|
canRead: true,
|
||||||
|
canUpdate: true,
|
||||||
|
canDelete: true,
|
||||||
|
),
|
||||||
|
GroupPermission(
|
||||||
|
id: 3,
|
||||||
|
group: GroupPermissionGroup(id: 1, groupName: '관리자'),
|
||||||
|
menu: GroupPermissionMenu(
|
||||||
|
id: 12,
|
||||||
|
menuCode: 'NO_PATH',
|
||||||
|
menuName: '경로없음',
|
||||||
|
path: null,
|
||||||
|
),
|
||||||
|
canCreate: true,
|
||||||
|
canRead: true,
|
||||||
|
canUpdate: true,
|
||||||
|
canDelete: true,
|
||||||
|
),
|
||||||
|
];
|
||||||
|
|
||||||
|
final map = buildPermissionMap(permissions);
|
||||||
|
|
||||||
|
expect(map.length, 1);
|
||||||
|
final stockPermissions =
|
||||||
|
map[PermissionResources.stockTransactions] ?? <PermissionAction>{};
|
||||||
|
expect(
|
||||||
|
stockPermissions,
|
||||||
|
containsAll(<PermissionAction>{
|
||||||
|
PermissionAction.view,
|
||||||
|
PermissionAction.create,
|
||||||
|
PermissionAction.edit,
|
||||||
|
PermissionAction.delete,
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
expect(map.containsKey('NO_PATH'), isFalse);
|
||||||
|
});
|
||||||
|
}
|
||||||
@@ -2,6 +2,7 @@ import 'package:flutter_test/flutter_test.dart';
|
|||||||
import 'package:mocktail/mocktail.dart';
|
import 'package:mocktail/mocktail.dart';
|
||||||
|
|
||||||
import 'package:superport_v2/core/common/models/paginated_result.dart';
|
import 'package:superport_v2/core/common/models/paginated_result.dart';
|
||||||
|
import 'package:superport_v2/core/permissions/permission_manager.dart';
|
||||||
import 'package:superport_v2/features/masters/group/domain/entities/group.dart';
|
import 'package:superport_v2/features/masters/group/domain/entities/group.dart';
|
||||||
import 'package:superport_v2/features/masters/group/domain/repositories/group_repository.dart';
|
import 'package:superport_v2/features/masters/group/domain/repositories/group_repository.dart';
|
||||||
import 'package:superport_v2/features/masters/group_permission/domain/entities/group_permission.dart';
|
import 'package:superport_v2/features/masters/group_permission/domain/entities/group_permission.dart';
|
||||||
@@ -24,11 +25,17 @@ void main() {
|
|||||||
late _MockPermissionRepository permissionRepository;
|
late _MockPermissionRepository permissionRepository;
|
||||||
late _MockGroupRepository groupRepository;
|
late _MockGroupRepository groupRepository;
|
||||||
late _MockMenuRepository menuRepository;
|
late _MockMenuRepository menuRepository;
|
||||||
|
late PermissionManager permissionManager;
|
||||||
|
|
||||||
final samplePermission = GroupPermission(
|
final samplePermission = GroupPermission(
|
||||||
id: 1,
|
id: 1,
|
||||||
group: GroupPermissionGroup(id: 1, groupName: '관리자'),
|
group: GroupPermissionGroup(id: 1, groupName: '관리자'),
|
||||||
menu: GroupPermissionMenu(id: 10, menuName: '대시보드'),
|
menu: GroupPermissionMenu(
|
||||||
|
id: 10,
|
||||||
|
menuCode: 'DASHBOARD',
|
||||||
|
menuName: '대시보드',
|
||||||
|
path: '/dashboard',
|
||||||
|
),
|
||||||
canCreate: true,
|
canCreate: true,
|
||||||
canRead: true,
|
canRead: true,
|
||||||
canUpdate: false,
|
canUpdate: false,
|
||||||
@@ -52,10 +59,22 @@ void main() {
|
|||||||
permissionRepository = _MockPermissionRepository();
|
permissionRepository = _MockPermissionRepository();
|
||||||
groupRepository = _MockGroupRepository();
|
groupRepository = _MockGroupRepository();
|
||||||
menuRepository = _MockMenuRepository();
|
menuRepository = _MockMenuRepository();
|
||||||
|
permissionManager = PermissionManager();
|
||||||
|
when(
|
||||||
|
() => permissionRepository.list(
|
||||||
|
page: any(named: 'page'),
|
||||||
|
pageSize: any(named: 'pageSize'),
|
||||||
|
groupId: any(named: 'groupId'),
|
||||||
|
menuId: any(named: 'menuId'),
|
||||||
|
isActive: any(named: 'isActive'),
|
||||||
|
includeDeleted: any(named: 'includeDeleted'),
|
||||||
|
),
|
||||||
|
).thenAnswer((_) async => createResult([samplePermission]));
|
||||||
controller = GroupPermissionController(
|
controller = GroupPermissionController(
|
||||||
permissionRepository: permissionRepository,
|
permissionRepository: permissionRepository,
|
||||||
groupRepository: groupRepository,
|
groupRepository: groupRepository,
|
||||||
menuRepository: menuRepository,
|
menuRepository: menuRepository,
|
||||||
|
permissionManager: permissionManager,
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -68,6 +87,8 @@ void main() {
|
|||||||
query: any(named: 'query'),
|
query: any(named: 'query'),
|
||||||
isDefault: any(named: 'isDefault'),
|
isDefault: any(named: 'isDefault'),
|
||||||
isActive: any(named: 'isActive'),
|
isActive: any(named: 'isActive'),
|
||||||
|
includePermissions: any(named: 'includePermissions'),
|
||||||
|
includeEmployees: any(named: 'includeEmployees'),
|
||||||
),
|
),
|
||||||
).thenAnswer(
|
).thenAnswer(
|
||||||
(_) async => PaginatedResult<Group>(
|
(_) async => PaginatedResult<Group>(
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ import 'package:mocktail/mocktail.dart';
|
|||||||
import 'package:shadcn_ui/shadcn_ui.dart';
|
import 'package:shadcn_ui/shadcn_ui.dart';
|
||||||
|
|
||||||
import 'package:superport_v2/core/common/models/paginated_result.dart';
|
import 'package:superport_v2/core/common/models/paginated_result.dart';
|
||||||
|
import 'package:superport_v2/core/permissions/permission_manager.dart';
|
||||||
import 'package:superport_v2/features/masters/group/domain/entities/group.dart';
|
import 'package:superport_v2/features/masters/group/domain/entities/group.dart';
|
||||||
import 'package:superport_v2/features/masters/group/domain/repositories/group_repository.dart';
|
import 'package:superport_v2/features/masters/group/domain/repositories/group_repository.dart';
|
||||||
import 'package:superport_v2/features/masters/group_permission/domain/entities/group_permission.dart';
|
import 'package:superport_v2/features/masters/group_permission/domain/entities/group_permission.dart';
|
||||||
@@ -24,13 +25,16 @@ class _MockMenuRepository extends Mock implements MenuRepository {}
|
|||||||
class _FakeGroupPermissionInput extends Fake implements GroupPermissionInput {}
|
class _FakeGroupPermissionInput extends Fake implements GroupPermissionInput {}
|
||||||
|
|
||||||
Widget _buildApp(Widget child) {
|
Widget _buildApp(Widget child) {
|
||||||
return MaterialApp(
|
return PermissionScope(
|
||||||
home: ShadTheme(
|
manager: PermissionManager(),
|
||||||
data: ShadThemeData(
|
child: MaterialApp(
|
||||||
colorScheme: const ShadSlateColorScheme.light(),
|
home: ShadTheme(
|
||||||
brightness: Brightness.light,
|
data: ShadThemeData(
|
||||||
|
colorScheme: const ShadSlateColorScheme.light(),
|
||||||
|
brightness: Brightness.light,
|
||||||
|
),
|
||||||
|
child: Scaffold(body: child),
|
||||||
),
|
),
|
||||||
child: Scaffold(body: child),
|
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -80,6 +84,8 @@ void main() {
|
|||||||
query: any(named: 'query'),
|
query: any(named: 'query'),
|
||||||
isDefault: any(named: 'isDefault'),
|
isDefault: any(named: 'isDefault'),
|
||||||
isActive: any(named: 'isActive'),
|
isActive: any(named: 'isActive'),
|
||||||
|
includePermissions: any(named: 'includePermissions'),
|
||||||
|
includeEmployees: any(named: 'includeEmployees'),
|
||||||
),
|
),
|
||||||
).thenAnswer(
|
).thenAnswer(
|
||||||
(_) async => PaginatedResult<Group>(
|
(_) async => PaginatedResult<Group>(
|
||||||
@@ -125,7 +131,12 @@ void main() {
|
|||||||
GroupPermission(
|
GroupPermission(
|
||||||
id: 1,
|
id: 1,
|
||||||
group: GroupPermissionGroup(id: 1, groupName: '관리자'),
|
group: GroupPermissionGroup(id: 1, groupName: '관리자'),
|
||||||
menu: GroupPermissionMenu(id: 10, menuName: '대시보드'),
|
menu: GroupPermissionMenu(
|
||||||
|
id: 10,
|
||||||
|
menuCode: 'DASHBOARD',
|
||||||
|
menuName: '대시보드',
|
||||||
|
path: '/dashboard',
|
||||||
|
),
|
||||||
canCreate: true,
|
canCreate: true,
|
||||||
canRead: true,
|
canRead: true,
|
||||||
),
|
),
|
||||||
@@ -201,7 +212,12 @@ void main() {
|
|||||||
GroupPermission(
|
GroupPermission(
|
||||||
id: 5,
|
id: 5,
|
||||||
group: GroupPermissionGroup(id: 1, groupName: '관리자'),
|
group: GroupPermissionGroup(id: 1, groupName: '관리자'),
|
||||||
menu: GroupPermissionMenu(id: 10, menuName: '대시보드'),
|
menu: GroupPermissionMenu(
|
||||||
|
id: 10,
|
||||||
|
menuCode: 'DASHBOARD',
|
||||||
|
menuName: '대시보드',
|
||||||
|
path: '/dashboard',
|
||||||
|
),
|
||||||
canCreate: true,
|
canCreate: true,
|
||||||
canRead: true,
|
canRead: true,
|
||||||
),
|
),
|
||||||
@@ -226,7 +242,9 @@ void main() {
|
|||||||
),
|
),
|
||||||
menu: GroupPermissionMenu(
|
menu: GroupPermissionMenu(
|
||||||
id: capturedInput!.menuId,
|
id: capturedInput!.menuId,
|
||||||
|
menuCode: 'DASHBOARD',
|
||||||
menuName: '대시보드',
|
menuName: '대시보드',
|
||||||
|
path: '/dashboard',
|
||||||
),
|
),
|
||||||
canCreate: capturedInput!.canCreate,
|
canCreate: capturedInput!.canCreate,
|
||||||
canRead: capturedInput!.canRead,
|
canRead: capturedInput!.canRead,
|
||||||
|
|||||||
@@ -0,0 +1,86 @@
|
|||||||
|
import 'package:dio/dio.dart';
|
||||||
|
import 'package:flutter_test/flutter_test.dart';
|
||||||
|
import 'package:mocktail/mocktail.dart';
|
||||||
|
|
||||||
|
import 'package:superport_v2/core/network/api_client.dart';
|
||||||
|
import 'package:superport_v2/features/masters/product/data/repositories/product_repository_remote.dart';
|
||||||
|
|
||||||
|
class _MockApiClient extends Mock implements ApiClient {}
|
||||||
|
|
||||||
|
void main() {
|
||||||
|
late ApiClient apiClient;
|
||||||
|
late ProductRepositoryRemote repository;
|
||||||
|
|
||||||
|
setUpAll(() {
|
||||||
|
registerFallbackValue(Options());
|
||||||
|
registerFallbackValue(CancelToken());
|
||||||
|
});
|
||||||
|
|
||||||
|
setUp(() {
|
||||||
|
apiClient = _MockApiClient();
|
||||||
|
repository = ProductRepositoryRemote(apiClient: apiClient);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('list는 include 파라미터와 필터를 전달한다', () async {
|
||||||
|
when(
|
||||||
|
() => apiClient.get<Map<String, dynamic>>(
|
||||||
|
any(),
|
||||||
|
query: any(named: 'query'),
|
||||||
|
options: any(named: 'options'),
|
||||||
|
cancelToken: any(named: 'cancelToken'),
|
||||||
|
),
|
||||||
|
).thenAnswer(
|
||||||
|
(_) async => Response<Map<String, dynamic>>(
|
||||||
|
data: {
|
||||||
|
'items': [
|
||||||
|
{
|
||||||
|
'id': 1,
|
||||||
|
'product_code': 'P-001',
|
||||||
|
'product_name': '샘플',
|
||||||
|
'vendor': {
|
||||||
|
'id': 10,
|
||||||
|
'vendor_code': 'V-010',
|
||||||
|
'vendor_name': '테스트 벤더',
|
||||||
|
},
|
||||||
|
'uom': {'id': 5, 'uom_name': 'EA'},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
'page': 1,
|
||||||
|
'page_size': 20,
|
||||||
|
'total': 1,
|
||||||
|
},
|
||||||
|
requestOptions: RequestOptions(path: '/api/v1/products'),
|
||||||
|
statusCode: 200,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
|
final result = await repository.list(
|
||||||
|
page: 3,
|
||||||
|
pageSize: 40,
|
||||||
|
query: 'gear',
|
||||||
|
vendorId: 10,
|
||||||
|
uomId: 5,
|
||||||
|
isActive: false,
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(result.items, isNotEmpty);
|
||||||
|
|
||||||
|
final verification = verify(
|
||||||
|
() => apiClient.get<Map<String, dynamic>>(
|
||||||
|
captureAny(),
|
||||||
|
query: captureAny(named: 'query'),
|
||||||
|
options: any(named: 'options'),
|
||||||
|
cancelToken: any(named: 'cancelToken'),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
final query = verification.captured[1] as Map<String, dynamic>;
|
||||||
|
|
||||||
|
expect(query['include'], 'vendor,uom');
|
||||||
|
expect(query['vendor_id'], 10);
|
||||||
|
expect(query['uom_id'], 5);
|
||||||
|
expect(query['is_active'], false);
|
||||||
|
expect(query['page'], 3);
|
||||||
|
expect(query['page_size'], 40);
|
||||||
|
expect(query['q'], 'gear');
|
||||||
|
});
|
||||||
|
}
|
||||||
@@ -154,6 +154,33 @@ void main() {
|
|||||||
).called(1);
|
).called(1);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
testWidgets('목록이 비어 있으면 안내 문구를 표시한다', (tester) async {
|
||||||
|
when(
|
||||||
|
() => productRepository.list(
|
||||||
|
page: any(named: 'page'),
|
||||||
|
pageSize: any(named: 'pageSize'),
|
||||||
|
query: any(named: 'query'),
|
||||||
|
vendorId: any(named: 'vendorId'),
|
||||||
|
uomId: any(named: 'uomId'),
|
||||||
|
isActive: any(named: 'isActive'),
|
||||||
|
),
|
||||||
|
).thenAnswer(
|
||||||
|
(_) async => PaginatedResult<Product>(
|
||||||
|
items: const [],
|
||||||
|
page: 1,
|
||||||
|
pageSize: 20,
|
||||||
|
total: 0,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
|
await tester.pumpWidget(
|
||||||
|
_buildApp(ProductPage(routeUri: Uri(path: '/masters/products'))),
|
||||||
|
);
|
||||||
|
await tester.pumpAndSettle();
|
||||||
|
|
||||||
|
expect(find.text('조건에 맞는 제품이 없습니다.'), findsOneWidget);
|
||||||
|
});
|
||||||
|
|
||||||
testWidgets('폼 검증: 필수값 미입력 시 에러 메시지를 표시한다', (tester) async {
|
testWidgets('폼 검증: 필수값 미입력 시 에러 메시지를 표시한다', (tester) async {
|
||||||
when(
|
when(
|
||||||
() => productRepository.list(
|
() => productRepository.list(
|
||||||
|
|||||||
@@ -0,0 +1,65 @@
|
|||||||
|
import 'package:dio/dio.dart';
|
||||||
|
import 'package:flutter_test/flutter_test.dart';
|
||||||
|
import 'package:mocktail/mocktail.dart';
|
||||||
|
|
||||||
|
import 'package:superport_v2/core/network/api_client.dart';
|
||||||
|
import 'package:superport_v2/features/masters/user/data/repositories/user_repository_remote.dart';
|
||||||
|
|
||||||
|
class _MockApiClient extends Mock implements ApiClient {}
|
||||||
|
|
||||||
|
void main() {
|
||||||
|
late ApiClient apiClient;
|
||||||
|
late UserRepositoryRemote repository;
|
||||||
|
|
||||||
|
setUpAll(() {
|
||||||
|
registerFallbackValue(Options());
|
||||||
|
registerFallbackValue(CancelToken());
|
||||||
|
});
|
||||||
|
|
||||||
|
setUp(() {
|
||||||
|
apiClient = _MockApiClient();
|
||||||
|
repository = UserRepositoryRemote(apiClient: apiClient);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('목록 조회 시 include=group 파라미터를 전달한다', () async {
|
||||||
|
when(
|
||||||
|
() => apiClient.get<Map<String, dynamic>>(
|
||||||
|
any(),
|
||||||
|
query: any(named: 'query'),
|
||||||
|
options: any(named: 'options'),
|
||||||
|
cancelToken: any(named: 'cancelToken'),
|
||||||
|
),
|
||||||
|
).thenAnswer(
|
||||||
|
(_) async => Response<Map<String, dynamic>>(
|
||||||
|
data: {
|
||||||
|
'items': [
|
||||||
|
{
|
||||||
|
'id': 1,
|
||||||
|
'employee_no': 'E-001',
|
||||||
|
'employee_name': '홍길동',
|
||||||
|
'group': {'id': 2, 'group_name': '관리자'},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
requestOptions: RequestOptions(path: '/api/v1/employees'),
|
||||||
|
statusCode: 200,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
|
await repository.list();
|
||||||
|
|
||||||
|
final captured = verify(
|
||||||
|
() => apiClient.get<Map<String, dynamic>>(
|
||||||
|
captureAny(),
|
||||||
|
query: captureAny(named: 'query'),
|
||||||
|
options: any(named: 'options'),
|
||||||
|
cancelToken: any(named: 'cancelToken'),
|
||||||
|
),
|
||||||
|
).captured;
|
||||||
|
final path = captured[0] as String;
|
||||||
|
final query = captured[1] as Map<String, dynamic>;
|
||||||
|
|
||||||
|
expect(path, equals('/api/v1/employees'));
|
||||||
|
expect(query['include'], 'group');
|
||||||
|
});
|
||||||
|
}
|
||||||
@@ -2,8 +2,11 @@ import 'package:flutter_test/flutter_test.dart';
|
|||||||
import 'package:mocktail/mocktail.dart';
|
import 'package:mocktail/mocktail.dart';
|
||||||
|
|
||||||
import 'package:superport_v2/core/common/models/paginated_result.dart';
|
import 'package:superport_v2/core/common/models/paginated_result.dart';
|
||||||
|
import 'package:superport_v2/core/permissions/permission_manager.dart';
|
||||||
import 'package:superport_v2/features/masters/group/domain/entities/group.dart';
|
import 'package:superport_v2/features/masters/group/domain/entities/group.dart';
|
||||||
import 'package:superport_v2/features/masters/group/domain/repositories/group_repository.dart';
|
import 'package:superport_v2/features/masters/group/domain/repositories/group_repository.dart';
|
||||||
|
import 'package:superport_v2/features/masters/group_permission/domain/entities/group_permission.dart';
|
||||||
|
import 'package:superport_v2/features/masters/group_permission/domain/repositories/group_permission_repository.dart';
|
||||||
import 'package:superport_v2/features/masters/user/domain/entities/user.dart';
|
import 'package:superport_v2/features/masters/user/domain/entities/user.dart';
|
||||||
import 'package:superport_v2/features/masters/user/domain/repositories/user_repository.dart';
|
import 'package:superport_v2/features/masters/user/domain/repositories/user_repository.dart';
|
||||||
import 'package:superport_v2/features/masters/user/presentation/controllers/user_controller.dart';
|
import 'package:superport_v2/features/masters/user/presentation/controllers/user_controller.dart';
|
||||||
@@ -12,12 +15,17 @@ class _MockUserRepository extends Mock implements UserRepository {}
|
|||||||
|
|
||||||
class _MockGroupRepository extends Mock implements GroupRepository {}
|
class _MockGroupRepository extends Mock implements GroupRepository {}
|
||||||
|
|
||||||
|
class _MockGroupPermissionRepository extends Mock
|
||||||
|
implements GroupPermissionRepository {}
|
||||||
|
|
||||||
class _FakeUserInput extends Fake implements UserInput {}
|
class _FakeUserInput extends Fake implements UserInput {}
|
||||||
|
|
||||||
void main() {
|
void main() {
|
||||||
late UserController controller;
|
late UserController controller;
|
||||||
late _MockUserRepository userRepository;
|
late _MockUserRepository userRepository;
|
||||||
late _MockGroupRepository groupRepository;
|
late _MockGroupRepository groupRepository;
|
||||||
|
late _MockGroupPermissionRepository permissionRepository;
|
||||||
|
late PermissionManager permissionManager;
|
||||||
|
|
||||||
final sampleUser = UserAccount(
|
final sampleUser = UserAccount(
|
||||||
id: 1,
|
id: 1,
|
||||||
@@ -44,9 +52,43 @@ void main() {
|
|||||||
setUp(() {
|
setUp(() {
|
||||||
userRepository = _MockUserRepository();
|
userRepository = _MockUserRepository();
|
||||||
groupRepository = _MockGroupRepository();
|
groupRepository = _MockGroupRepository();
|
||||||
|
permissionRepository = _MockGroupPermissionRepository();
|
||||||
|
permissionManager = PermissionManager();
|
||||||
|
when(
|
||||||
|
() => permissionRepository.list(
|
||||||
|
page: any(named: 'page'),
|
||||||
|
pageSize: any(named: 'pageSize'),
|
||||||
|
groupId: any(named: 'groupId'),
|
||||||
|
menuId: any(named: 'menuId'),
|
||||||
|
isActive: any(named: 'isActive'),
|
||||||
|
includeDeleted: any(named: 'includeDeleted'),
|
||||||
|
),
|
||||||
|
).thenAnswer(
|
||||||
|
(_) async => PaginatedResult<GroupPermission>(
|
||||||
|
items: [
|
||||||
|
GroupPermission(
|
||||||
|
id: 1,
|
||||||
|
group: GroupPermissionGroup(id: 1, groupName: '관리자'),
|
||||||
|
menu: GroupPermissionMenu(
|
||||||
|
id: 10,
|
||||||
|
menuCode: 'DASHBOARD',
|
||||||
|
menuName: '대시보드',
|
||||||
|
path: '/dashboard',
|
||||||
|
),
|
||||||
|
canCreate: true,
|
||||||
|
canRead: true,
|
||||||
|
),
|
||||||
|
],
|
||||||
|
page: 1,
|
||||||
|
pageSize: 200,
|
||||||
|
total: 1,
|
||||||
|
),
|
||||||
|
);
|
||||||
controller = UserController(
|
controller = UserController(
|
||||||
userRepository: userRepository,
|
userRepository: userRepository,
|
||||||
groupRepository: groupRepository,
|
groupRepository: groupRepository,
|
||||||
|
permissionRepository: permissionRepository,
|
||||||
|
permissionManager: permissionManager,
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -58,6 +100,8 @@ void main() {
|
|||||||
query: any(named: 'query'),
|
query: any(named: 'query'),
|
||||||
isDefault: any(named: 'isDefault'),
|
isDefault: any(named: 'isDefault'),
|
||||||
isActive: any(named: 'isActive'),
|
isActive: any(named: 'isActive'),
|
||||||
|
includePermissions: any(named: 'includePermissions'),
|
||||||
|
includeEmployees: any(named: 'includeEmployees'),
|
||||||
),
|
),
|
||||||
).thenAnswer(
|
).thenAnswer(
|
||||||
(_) async => PaginatedResult<Group>(
|
(_) async => PaginatedResult<Group>(
|
||||||
@@ -82,6 +126,8 @@ void main() {
|
|||||||
query: any(named: 'query'),
|
query: any(named: 'query'),
|
||||||
isDefault: any(named: 'isDefault'),
|
isDefault: any(named: 'isDefault'),
|
||||||
isActive: any(named: 'isActive'),
|
isActive: any(named: 'isActive'),
|
||||||
|
includePermissions: any(named: 'includePermissions'),
|
||||||
|
includeEmployees: any(named: 'includeEmployees'),
|
||||||
),
|
),
|
||||||
).thenAnswer(
|
).thenAnswer(
|
||||||
(_) async => PaginatedResult<Group>(
|
(_) async => PaginatedResult<Group>(
|
||||||
@@ -195,6 +241,24 @@ void main() {
|
|||||||
verify(() => userRepository.delete(1)).called(1);
|
verify(() => userRepository.delete(1)).called(1);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test('delete 이후 권한 동기화를 시도한다', () async {
|
||||||
|
when(() => userRepository.delete(any())).thenAnswer((_) async {});
|
||||||
|
|
||||||
|
await controller.fetch();
|
||||||
|
await controller.delete(sampleUser.id!);
|
||||||
|
|
||||||
|
verify(
|
||||||
|
() => permissionRepository.list(
|
||||||
|
page: any(named: 'page'),
|
||||||
|
pageSize: any(named: 'pageSize'),
|
||||||
|
groupId: sampleUser.group!.id,
|
||||||
|
menuId: any(named: 'menuId'),
|
||||||
|
isActive: any(named: 'isActive'),
|
||||||
|
includeDeleted: any(named: 'includeDeleted'),
|
||||||
|
),
|
||||||
|
).called(greaterThanOrEqualTo(1));
|
||||||
|
});
|
||||||
|
|
||||||
test('restore 성공', () async {
|
test('restore 성공', () async {
|
||||||
when(
|
when(
|
||||||
() => userRepository.restore(any()),
|
() => userRepository.restore(any()),
|
||||||
|
|||||||
@@ -6,8 +6,11 @@ import 'package:mocktail/mocktail.dart';
|
|||||||
import 'package:shadcn_ui/shadcn_ui.dart';
|
import 'package:shadcn_ui/shadcn_ui.dart';
|
||||||
|
|
||||||
import 'package:superport_v2/core/common/models/paginated_result.dart';
|
import 'package:superport_v2/core/common/models/paginated_result.dart';
|
||||||
|
import 'package:superport_v2/core/permissions/permission_manager.dart';
|
||||||
import 'package:superport_v2/features/masters/group/domain/entities/group.dart';
|
import 'package:superport_v2/features/masters/group/domain/entities/group.dart';
|
||||||
import 'package:superport_v2/features/masters/group/domain/repositories/group_repository.dart';
|
import 'package:superport_v2/features/masters/group/domain/repositories/group_repository.dart';
|
||||||
|
import 'package:superport_v2/features/masters/group_permission/domain/entities/group_permission.dart';
|
||||||
|
import 'package:superport_v2/features/masters/group_permission/domain/repositories/group_permission_repository.dart';
|
||||||
import 'package:superport_v2/features/masters/user/domain/entities/user.dart';
|
import 'package:superport_v2/features/masters/user/domain/entities/user.dart';
|
||||||
import 'package:superport_v2/features/masters/user/domain/repositories/user_repository.dart';
|
import 'package:superport_v2/features/masters/user/domain/repositories/user_repository.dart';
|
||||||
import 'package:superport_v2/features/masters/user/presentation/pages/user_page.dart';
|
import 'package:superport_v2/features/masters/user/presentation/pages/user_page.dart';
|
||||||
@@ -16,16 +19,22 @@ class _MockUserRepository extends Mock implements UserRepository {}
|
|||||||
|
|
||||||
class _MockGroupRepository extends Mock implements GroupRepository {}
|
class _MockGroupRepository extends Mock implements GroupRepository {}
|
||||||
|
|
||||||
|
class _MockGroupPermissionRepository extends Mock
|
||||||
|
implements GroupPermissionRepository {}
|
||||||
|
|
||||||
class _FakeUserInput extends Fake implements UserInput {}
|
class _FakeUserInput extends Fake implements UserInput {}
|
||||||
|
|
||||||
Widget _buildApp(Widget child) {
|
Widget _buildApp(Widget child) {
|
||||||
return MaterialApp(
|
return PermissionScope(
|
||||||
home: ShadTheme(
|
manager: PermissionManager(),
|
||||||
data: ShadThemeData(
|
child: MaterialApp(
|
||||||
colorScheme: const ShadSlateColorScheme.light(),
|
home: ShadTheme(
|
||||||
brightness: Brightness.light,
|
data: ShadThemeData(
|
||||||
|
colorScheme: const ShadSlateColorScheme.light(),
|
||||||
|
brightness: Brightness.light,
|
||||||
|
),
|
||||||
|
child: Scaffold(body: child),
|
||||||
),
|
),
|
||||||
child: Scaffold(body: child),
|
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -55,12 +64,17 @@ void main() {
|
|||||||
group('플래그 On', () {
|
group('플래그 On', () {
|
||||||
late _MockUserRepository userRepository;
|
late _MockUserRepository userRepository;
|
||||||
late _MockGroupRepository groupRepository;
|
late _MockGroupRepository groupRepository;
|
||||||
|
late _MockGroupPermissionRepository permissionRepository;
|
||||||
setUp(() {
|
setUp(() {
|
||||||
dotenv.testLoad(fileInput: 'FEATURE_USERS_ENABLED=true\n');
|
dotenv.testLoad(fileInput: 'FEATURE_USERS_ENABLED=true\n');
|
||||||
userRepository = _MockUserRepository();
|
userRepository = _MockUserRepository();
|
||||||
groupRepository = _MockGroupRepository();
|
groupRepository = _MockGroupRepository();
|
||||||
|
permissionRepository = _MockGroupPermissionRepository();
|
||||||
GetIt.I.registerLazySingleton<UserRepository>(() => userRepository);
|
GetIt.I.registerLazySingleton<UserRepository>(() => userRepository);
|
||||||
GetIt.I.registerLazySingleton<GroupRepository>(() => groupRepository);
|
GetIt.I.registerLazySingleton<GroupRepository>(() => groupRepository);
|
||||||
|
GetIt.I.registerLazySingleton<GroupPermissionRepository>(
|
||||||
|
() => permissionRepository,
|
||||||
|
);
|
||||||
|
|
||||||
when(
|
when(
|
||||||
() => groupRepository.list(
|
() => groupRepository.list(
|
||||||
@@ -69,6 +83,8 @@ void main() {
|
|||||||
query: any(named: 'query'),
|
query: any(named: 'query'),
|
||||||
isDefault: any(named: 'isDefault'),
|
isDefault: any(named: 'isDefault'),
|
||||||
isActive: any(named: 'isActive'),
|
isActive: any(named: 'isActive'),
|
||||||
|
includePermissions: any(named: 'includePermissions'),
|
||||||
|
includeEmployees: any(named: 'includeEmployees'),
|
||||||
),
|
),
|
||||||
).thenAnswer(
|
).thenAnswer(
|
||||||
(_) async => PaginatedResult<Group>(
|
(_) async => PaginatedResult<Group>(
|
||||||
@@ -78,6 +94,37 @@ void main() {
|
|||||||
total: 1,
|
total: 1,
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
|
|
||||||
|
when(
|
||||||
|
() => permissionRepository.list(
|
||||||
|
page: any(named: 'page'),
|
||||||
|
pageSize: any(named: 'pageSize'),
|
||||||
|
groupId: any(named: 'groupId'),
|
||||||
|
menuId: any(named: 'menuId'),
|
||||||
|
isActive: any(named: 'isActive'),
|
||||||
|
includeDeleted: any(named: 'includeDeleted'),
|
||||||
|
),
|
||||||
|
).thenAnswer(
|
||||||
|
(_) async => PaginatedResult<GroupPermission>(
|
||||||
|
items: [
|
||||||
|
GroupPermission(
|
||||||
|
id: 1,
|
||||||
|
group: GroupPermissionGroup(id: 1, groupName: '관리자'),
|
||||||
|
menu: GroupPermissionMenu(
|
||||||
|
id: 10,
|
||||||
|
menuCode: 'DASHBOARD',
|
||||||
|
menuName: '대시보드',
|
||||||
|
path: '/dashboard',
|
||||||
|
),
|
||||||
|
canCreate: true,
|
||||||
|
canRead: true,
|
||||||
|
),
|
||||||
|
],
|
||||||
|
page: 1,
|
||||||
|
pageSize: 200,
|
||||||
|
total: 1,
|
||||||
|
),
|
||||||
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
testWidgets('목록 조회 후 테이블 렌더', (tester) async {
|
testWidgets('목록 조회 후 테이블 렌더', (tester) async {
|
||||||
|
|||||||
@@ -84,6 +84,35 @@ void main() {
|
|||||||
).called(1);
|
).called(1);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
testWidgets('목록이 비어 있으면 안내 문구를 표시한다', (tester) async {
|
||||||
|
dotenv.testLoad(fileInput: 'FEATURE_VENDORS_ENABLED=true\n');
|
||||||
|
final repository = _MockVendorRepository();
|
||||||
|
GetIt.I.registerLazySingleton<VendorRepository>(() => repository);
|
||||||
|
|
||||||
|
when(
|
||||||
|
() => repository.list(
|
||||||
|
page: any(named: 'page'),
|
||||||
|
pageSize: any(named: 'pageSize'),
|
||||||
|
query: any(named: 'query'),
|
||||||
|
isActive: any(named: 'isActive'),
|
||||||
|
),
|
||||||
|
).thenAnswer(
|
||||||
|
(_) async => PaginatedResult<Vendor>(
|
||||||
|
items: const [],
|
||||||
|
page: 1,
|
||||||
|
pageSize: 20,
|
||||||
|
total: 0,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
|
await tester.pumpWidget(
|
||||||
|
_buildApp(VendorPage(routeUri: Uri(path: '/masters/vendors'))),
|
||||||
|
);
|
||||||
|
await tester.pumpAndSettle();
|
||||||
|
|
||||||
|
expect(find.text('조건에 맞는 벤더가 없습니다.'), findsOneWidget);
|
||||||
|
});
|
||||||
|
|
||||||
testWidgets('신규 등록 폼에서 필수값 미입력 시 검증 메시지를 보여준다', (tester) async {
|
testWidgets('신규 등록 폼에서 필수값 미입력 시 검증 메시지를 보여준다', (tester) async {
|
||||||
dotenv.testLoad(fileInput: 'FEATURE_VENDORS_ENABLED=true\n');
|
dotenv.testLoad(fileInput: 'FEATURE_VENDORS_ENABLED=true\n');
|
||||||
final repository = _MockVendorRepository();
|
final repository = _MockVendorRepository();
|
||||||
|
|||||||
@@ -0,0 +1,60 @@
|
|||||||
|
import 'package:dio/dio.dart';
|
||||||
|
import 'package:flutter_test/flutter_test.dart';
|
||||||
|
import 'package:mocktail/mocktail.dart';
|
||||||
|
|
||||||
|
import 'package:superport_v2/core/network/api_client.dart';
|
||||||
|
import 'package:superport_v2/features/masters/warehouse/data/repositories/warehouse_repository_remote.dart';
|
||||||
|
|
||||||
|
class _MockApiClient extends Mock implements ApiClient {}
|
||||||
|
|
||||||
|
void main() {
|
||||||
|
late ApiClient apiClient;
|
||||||
|
late WarehouseRepositoryRemote repository;
|
||||||
|
|
||||||
|
setUpAll(() {
|
||||||
|
registerFallbackValue(Options());
|
||||||
|
registerFallbackValue(CancelToken());
|
||||||
|
});
|
||||||
|
|
||||||
|
setUp(() {
|
||||||
|
apiClient = _MockApiClient();
|
||||||
|
repository = WarehouseRepositoryRemote(apiClient: apiClient);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('include=zipcode 파라미터를 기본으로 전달한다', () async {
|
||||||
|
when(
|
||||||
|
() => apiClient.get<Map<String, dynamic>>(
|
||||||
|
any(),
|
||||||
|
query: any(named: 'query'),
|
||||||
|
options: any(named: 'options'),
|
||||||
|
cancelToken: any(named: 'cancelToken'),
|
||||||
|
),
|
||||||
|
).thenAnswer(
|
||||||
|
(_) async => Response<Map<String, dynamic>>(
|
||||||
|
data: {
|
||||||
|
'items': [
|
||||||
|
{'id': 1, 'warehouse_code': 'WH-001', 'warehouse_name': '1센터'},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
requestOptions: RequestOptions(path: '/api/v1/warehouses'),
|
||||||
|
statusCode: 200,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
|
await repository.list();
|
||||||
|
|
||||||
|
final captured = verify(
|
||||||
|
() => apiClient.get<Map<String, dynamic>>(
|
||||||
|
captureAny(),
|
||||||
|
query: captureAny(named: 'query'),
|
||||||
|
options: any(named: 'options'),
|
||||||
|
cancelToken: any(named: 'cancelToken'),
|
||||||
|
),
|
||||||
|
).captured;
|
||||||
|
final path = captured[0] as String;
|
||||||
|
final query = captured[1] as Map<String, dynamic>;
|
||||||
|
|
||||||
|
expect(path, equals('/api/v1/warehouses'));
|
||||||
|
expect(query['include'], 'zipcode');
|
||||||
|
});
|
||||||
|
}
|
||||||
@@ -48,6 +48,7 @@ void main() {
|
|||||||
pageSize: any(named: 'pageSize'),
|
pageSize: any(named: 'pageSize'),
|
||||||
query: any(named: 'query'),
|
query: any(named: 'query'),
|
||||||
isActive: any(named: 'isActive'),
|
isActive: any(named: 'isActive'),
|
||||||
|
includeZipcode: any(named: 'includeZipcode'),
|
||||||
),
|
),
|
||||||
).thenAnswer((_) async => createResult());
|
).thenAnswer((_) async => createResult());
|
||||||
|
|
||||||
@@ -55,8 +56,13 @@ void main() {
|
|||||||
|
|
||||||
expect(controller.result?.items, isNotEmpty);
|
expect(controller.result?.items, isNotEmpty);
|
||||||
verify(
|
verify(
|
||||||
() =>
|
() => repository.list(
|
||||||
repository.list(page: 1, pageSize: 20, query: null, isActive: null),
|
page: 1,
|
||||||
|
pageSize: 20,
|
||||||
|
query: null,
|
||||||
|
isActive: null,
|
||||||
|
includeZipcode: true,
|
||||||
|
),
|
||||||
).called(1);
|
).called(1);
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -67,6 +73,7 @@ void main() {
|
|||||||
pageSize: any(named: 'pageSize'),
|
pageSize: any(named: 'pageSize'),
|
||||||
query: any(named: 'query'),
|
query: any(named: 'query'),
|
||||||
isActive: any(named: 'isActive'),
|
isActive: any(named: 'isActive'),
|
||||||
|
includeZipcode: any(named: 'includeZipcode'),
|
||||||
),
|
),
|
||||||
).thenThrow(Exception('fail'));
|
).thenThrow(Exception('fail'));
|
||||||
|
|
||||||
@@ -92,6 +99,7 @@ void main() {
|
|||||||
pageSize: any(named: 'pageSize'),
|
pageSize: any(named: 'pageSize'),
|
||||||
query: any(named: 'query'),
|
query: any(named: 'query'),
|
||||||
isActive: any(named: 'isActive'),
|
isActive: any(named: 'isActive'),
|
||||||
|
includeZipcode: any(named: 'includeZipcode'),
|
||||||
),
|
),
|
||||||
).thenAnswer((_) async => createResult());
|
).thenAnswer((_) async => createResult());
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -92,6 +92,7 @@ void main() {
|
|||||||
pageSize: any(named: 'pageSize'),
|
pageSize: any(named: 'pageSize'),
|
||||||
query: any(named: 'query'),
|
query: any(named: 'query'),
|
||||||
isActive: any(named: 'isActive'),
|
isActive: any(named: 'isActive'),
|
||||||
|
includeZipcode: any(named: 'includeZipcode'),
|
||||||
),
|
),
|
||||||
).thenAnswer(
|
).thenAnswer(
|
||||||
(_) async => PaginatedResult<Warehouse>(
|
(_) async => PaginatedResult<Warehouse>(
|
||||||
@@ -116,8 +117,13 @@ void main() {
|
|||||||
|
|
||||||
expect(find.text('WH-001'), findsOneWidget);
|
expect(find.text('WH-001'), findsOneWidget);
|
||||||
verify(
|
verify(
|
||||||
() =>
|
() => repository.list(
|
||||||
repository.list(page: 1, pageSize: 20, query: null, isActive: null),
|
page: 1,
|
||||||
|
pageSize: 20,
|
||||||
|
query: null,
|
||||||
|
isActive: null,
|
||||||
|
includeZipcode: true,
|
||||||
|
),
|
||||||
).called(1);
|
).called(1);
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -128,6 +134,7 @@ void main() {
|
|||||||
pageSize: any(named: 'pageSize'),
|
pageSize: any(named: 'pageSize'),
|
||||||
query: any(named: 'query'),
|
query: any(named: 'query'),
|
||||||
isActive: any(named: 'isActive'),
|
isActive: any(named: 'isActive'),
|
||||||
|
includeZipcode: any(named: 'includeZipcode'),
|
||||||
),
|
),
|
||||||
).thenAnswer(
|
).thenAnswer(
|
||||||
(_) async => PaginatedResult<Warehouse>(
|
(_) async => PaginatedResult<Warehouse>(
|
||||||
|
|||||||
@@ -0,0 +1,77 @@
|
|||||||
|
import 'package:dio/dio.dart';
|
||||||
|
import 'package:flutter_test/flutter_test.dart';
|
||||||
|
import 'package:mocktail/mocktail.dart';
|
||||||
|
|
||||||
|
import 'package:superport_v2/core/network/api_client.dart';
|
||||||
|
import 'package:superport_v2/features/util/postal_search/data/repositories/postal_search_repository_remote.dart';
|
||||||
|
|
||||||
|
class _MockApiClient extends Mock implements ApiClient {}
|
||||||
|
|
||||||
|
void main() {
|
||||||
|
late ApiClient apiClient;
|
||||||
|
late PostalSearchRepositoryRemote repository;
|
||||||
|
|
||||||
|
setUpAll(() {
|
||||||
|
registerFallbackValue(Options());
|
||||||
|
registerFallbackValue(CancelToken());
|
||||||
|
});
|
||||||
|
|
||||||
|
setUp(() {
|
||||||
|
apiClient = _MockApiClient();
|
||||||
|
repository = PostalSearchRepositoryRemote(apiClient: apiClient);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('검색 키워드가 비어 있으면 빈 배열을 반환한다', () async {
|
||||||
|
final result = await repository.search(keyword: ' ');
|
||||||
|
expect(result, isEmpty);
|
||||||
|
verifyNever(() => apiClient.get<dynamic>(any()));
|
||||||
|
});
|
||||||
|
|
||||||
|
test('검색 요청 시 q/page/page_size 파라미터를 전달한다', () async {
|
||||||
|
when(
|
||||||
|
() => apiClient.get<dynamic>(
|
||||||
|
any(),
|
||||||
|
query: any(named: 'query'),
|
||||||
|
options: any(named: 'options'),
|
||||||
|
cancelToken: any(named: 'cancelToken'),
|
||||||
|
),
|
||||||
|
).thenAnswer(
|
||||||
|
(_) async => Response<dynamic>(
|
||||||
|
data: {
|
||||||
|
'items': [
|
||||||
|
{
|
||||||
|
'zipcode': '06000',
|
||||||
|
'sido': '서울특별시',
|
||||||
|
'sigungu': '강남구',
|
||||||
|
'road_name': '테헤란로',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
requestOptions: RequestOptions(path: '/api/v1/zipcodes'),
|
||||||
|
statusCode: 200,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
|
final result = await repository.search(keyword: '테헤란로', limit: 10, page: 2);
|
||||||
|
|
||||||
|
expect(result, hasLength(1));
|
||||||
|
|
||||||
|
final verification = verify(
|
||||||
|
() => apiClient.get<dynamic>(
|
||||||
|
captureAny(),
|
||||||
|
query: captureAny(named: 'query'),
|
||||||
|
options: any(named: 'options'),
|
||||||
|
cancelToken: any(named: 'cancelToken'),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
final path = verification.captured[0] as String;
|
||||||
|
final query = verification.captured[1] as Map<String, dynamic>;
|
||||||
|
|
||||||
|
expect(path, equals('/api/v1/zipcodes'));
|
||||||
|
expect(query['q'], '테헤란로');
|
||||||
|
expect(query['page'], 2);
|
||||||
|
expect(query['page_size'], 10);
|
||||||
|
expect(query.containsKey('zipcode'), isFalse);
|
||||||
|
expect(query.containsKey('road_name'), isFalse);
|
||||||
|
});
|
||||||
|
}
|
||||||
@@ -2,6 +2,8 @@ import 'package:flutter/material.dart';
|
|||||||
import 'package:flutter_test/flutter_test.dart';
|
import 'package:flutter_test/flutter_test.dart';
|
||||||
import 'package:shadcn_ui/shadcn_ui.dart';
|
import 'package:shadcn_ui/shadcn_ui.dart';
|
||||||
|
|
||||||
|
import 'package:superport_v2/core/network/api_error.dart';
|
||||||
|
import 'package:superport_v2/core/network/failure.dart';
|
||||||
import 'package:superport_v2/features/util/postal_search/presentation/models/postal_search_result.dart';
|
import 'package:superport_v2/features/util/postal_search/presentation/models/postal_search_result.dart';
|
||||||
import 'package:superport_v2/features/util/postal_search/presentation/widgets/postal_search_dialog.dart';
|
import 'package:superport_v2/features/util/postal_search/presentation/widgets/postal_search_dialog.dart';
|
||||||
|
|
||||||
@@ -151,4 +153,32 @@ void main() {
|
|||||||
await tester.tap(find.text('닫기'));
|
await tester.tap(find.text('닫기'));
|
||||||
await tester.pumpAndSettle();
|
await tester.pumpAndSettle();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
testWidgets('검색 실패 시 Failure 메시지를 표시한다', (tester) async {
|
||||||
|
final exception = ApiException(
|
||||||
|
code: ApiErrorCode.unprocessableEntity,
|
||||||
|
message: '우편번호 검색에 실패했습니다.',
|
||||||
|
details: {
|
||||||
|
'errors': {
|
||||||
|
'keyword': ['검색어를 다시 확인해 주세요.'],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
await tester.pumpWidget(
|
||||||
|
_PostalSearchHarness(
|
||||||
|
fetcher: (_) => Future<List<PostalSearchResult>>.error(exception),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
|
await tester.pumpAndSettle();
|
||||||
|
|
||||||
|
final inputFinder = find.byType(EditableText);
|
||||||
|
await tester.enterText(inputFinder, '강남대로');
|
||||||
|
await tester.tap(find.text('검색'));
|
||||||
|
await tester.pumpAndSettle();
|
||||||
|
|
||||||
|
final failure = Failure.from(exception);
|
||||||
|
expect(find.text(failure.describe()), findsOneWidget);
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,12 +1,18 @@
|
|||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:flutter_test/flutter_test.dart';
|
import 'package:flutter_test/flutter_test.dart';
|
||||||
|
import 'package:get_it/get_it.dart';
|
||||||
import 'package:go_router/go_router.dart';
|
import 'package:go_router/go_router.dart';
|
||||||
import 'package:shadcn_ui/shadcn_ui.dart';
|
import 'package:shadcn_ui/shadcn_ui.dart';
|
||||||
|
import 'package:superport_v2/core/common/models/paginated_result.dart';
|
||||||
import 'package:superport_v2/core/config/environment.dart';
|
import 'package:superport_v2/core/config/environment.dart';
|
||||||
import 'package:superport_v2/core/constants/app_sections.dart';
|
import 'package:superport_v2/core/constants/app_sections.dart';
|
||||||
import 'package:superport_v2/features/login/presentation/pages/login_page.dart';
|
|
||||||
import 'package:superport_v2/core/theme/superport_shad_theme.dart';
|
|
||||||
import 'package:superport_v2/core/permissions/permission_manager.dart';
|
import 'package:superport_v2/core/permissions/permission_manager.dart';
|
||||||
|
import 'package:superport_v2/core/theme/superport_shad_theme.dart';
|
||||||
|
import 'package:superport_v2/features/login/presentation/pages/login_page.dart';
|
||||||
|
import 'package:superport_v2/features/masters/group/domain/entities/group.dart';
|
||||||
|
import 'package:superport_v2/features/masters/group/domain/repositories/group_repository.dart';
|
||||||
|
import 'package:superport_v2/features/masters/group_permission/domain/entities/group_permission.dart';
|
||||||
|
import 'package:superport_v2/features/masters/group_permission/domain/repositories/group_permission_repository.dart';
|
||||||
|
|
||||||
GoRouter _createTestRouter() {
|
GoRouter _createTestRouter() {
|
||||||
return GoRouter(
|
return GoRouter(
|
||||||
@@ -114,6 +120,108 @@ class _PlaceholderPage extends StatelessWidget {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
class _StubGroupRepository implements GroupRepository {
|
||||||
|
@override
|
||||||
|
Future<Group> create(GroupInput input) {
|
||||||
|
throw UnimplementedError();
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Future<void> delete(int id) {
|
||||||
|
throw UnimplementedError();
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Future<PaginatedResult<Group>> list({
|
||||||
|
int page = 1,
|
||||||
|
int pageSize = 20,
|
||||||
|
String? query,
|
||||||
|
bool? isDefault,
|
||||||
|
bool? isActive,
|
||||||
|
bool includePermissions = false,
|
||||||
|
bool includeEmployees = false,
|
||||||
|
}) async {
|
||||||
|
return PaginatedResult<Group>(
|
||||||
|
items: [
|
||||||
|
Group(
|
||||||
|
id: 1,
|
||||||
|
groupName: '기본 그룹',
|
||||||
|
description: '테스트',
|
||||||
|
isDefault: true,
|
||||||
|
isActive: true,
|
||||||
|
),
|
||||||
|
],
|
||||||
|
page: 1,
|
||||||
|
pageSize: 1,
|
||||||
|
total: 1,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Future<Group> restore(int id) {
|
||||||
|
throw UnimplementedError();
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Future<Group> update(int id, GroupInput input) {
|
||||||
|
throw UnimplementedError();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class _StubGroupPermissionRepository implements GroupPermissionRepository {
|
||||||
|
@override
|
||||||
|
Future<GroupPermission> create(GroupPermissionInput input) {
|
||||||
|
throw UnimplementedError();
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Future<void> delete(int id) {
|
||||||
|
throw UnimplementedError();
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Future<PaginatedResult<GroupPermission>> list({
|
||||||
|
int page = 1,
|
||||||
|
int pageSize = 20,
|
||||||
|
int? groupId,
|
||||||
|
int? menuId,
|
||||||
|
bool? isActive,
|
||||||
|
bool includeDeleted = false,
|
||||||
|
}) async {
|
||||||
|
return PaginatedResult<GroupPermission>(
|
||||||
|
items: [
|
||||||
|
GroupPermission(
|
||||||
|
id: 1,
|
||||||
|
group: GroupPermissionGroup(id: groupId ?? 1, groupName: '기본 그룹'),
|
||||||
|
menu: GroupPermissionMenu(
|
||||||
|
id: 10,
|
||||||
|
menuCode: 'DASHBOARD',
|
||||||
|
menuName: '대시보드',
|
||||||
|
path: dashboardRoutePath,
|
||||||
|
),
|
||||||
|
canCreate: true,
|
||||||
|
canRead: true,
|
||||||
|
canUpdate: true,
|
||||||
|
canDelete: true,
|
||||||
|
),
|
||||||
|
],
|
||||||
|
page: 1,
|
||||||
|
pageSize: 1,
|
||||||
|
total: 1,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Future<GroupPermission> restore(int id) {
|
||||||
|
throw UnimplementedError();
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Future<GroupPermission> update(int id, GroupPermissionInput input) {
|
||||||
|
throw UnimplementedError();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
void main() {
|
void main() {
|
||||||
TestWidgetsFlutterBinding.ensureInitialized();
|
TestWidgetsFlutterBinding.ensureInitialized();
|
||||||
|
|
||||||
@@ -132,6 +240,13 @@ void main() {
|
|||||||
view.resetDevicePixelRatio();
|
view.resetDevicePixelRatio();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
await GetIt.I.reset();
|
||||||
|
GetIt.I.registerSingleton<GroupRepository>(_StubGroupRepository());
|
||||||
|
GetIt.I.registerSingleton<GroupPermissionRepository>(
|
||||||
|
_StubGroupPermissionRepository(),
|
||||||
|
);
|
||||||
|
addTearDown(() async => GetIt.I.reset());
|
||||||
|
|
||||||
final router = _createTestRouter();
|
final router = _createTestRouter();
|
||||||
await tester.pumpWidget(_TestApp(router: router));
|
await tester.pumpWidget(_TestApp(router: router));
|
||||||
await tester.pumpAndSettle();
|
await tester.pumpAndSettle();
|
||||||
|
|||||||
Reference in New Issue
Block a user