feat(approvals): 결재 상세 전표 연동과 스코프 권한 매핑 확장

- 결재 상세 다이얼로그에 전표 요약·라인·고객 섹션을 추가하고 현재 사용자 단계 강조 및 비고 입력 검증을 개선함

- 대시보드·결재 목록에서 전표 리포지토리와 AuthService를 주입해 상세 진입과 결재 관리 이동 버튼을 제공함

- StockTransactionApprovalInput이 template/steps를 config 노드로 직렬화하도록 변경하고 통합 테스트를 갱신함

- scope 권한 문자열을 리소스권으로 변환하는 PermissionScopeMapper와 단위 테스트를 추가하고 AuthPermission을 연동함

- 재고 메뉴 정렬, 상세 컨트롤러 오류 리셋, 요청자 자동완성 상태 동기화 등 주변 UI 버그를 수정하고 테스트를 보강함
This commit is contained in:
JiWoong Sul
2025-11-14 01:57:02 +09:00
parent e3cf068bf8
commit 6d09e72142
12 changed files with 857 additions and 50 deletions

View File

@@ -7,8 +7,19 @@ import 'package:lucide_icons_flutter/lucide_icons.dart' as lucide;
import 'package:shadcn_ui/shadcn_ui.dart';
import 'package:superport_v2/core/network/failure.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/auth/application/auth_service.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/usecases/get_approval_draft_use_case.dart';
import 'package:superport_v2/features/approvals/domain/usecases/list_approval_drafts_use_case.dart';
import 'package:superport_v2/features/approvals/domain/usecases/save_approval_draft_use_case.dart';
import 'package:superport_v2/features/approvals/presentation/controllers/approval_controller.dart';
import 'package:superport_v2/features/approvals/presentation/dialogs/approval_detail_dialog.dart';
import 'package:superport_v2/features/inventory/lookups/domain/repositories/inventory_lookup_repository.dart';
import 'package:superport_v2/features/inventory/transactions/domain/repositories/stock_transaction_repository.dart';
import 'package:superport_v2/widgets/app_layout.dart';
import 'package:superport_v2/widgets/components/empty_state.dart';
import 'package:superport_v2/widgets/components/feedback.dart';
@@ -446,6 +457,8 @@ class _PendingApprovalCard extends StatelessWidget {
return;
}
final repository = GetIt.I<ApprovalRepository>();
final parentContext = context;
final detailNotifier = ValueNotifier<Approval?>(null);
final detailFuture = repository
.fetchDetail(approvalId, includeSteps: true, includeHistories: true)
.catchError((error) {
@@ -462,9 +475,11 @@ class _PendingApprovalCard extends StatelessWidget {
debugPrint(
'[DashboardPage] 결재 상세 조회 성공: id=${detail.id}, approvalNo=${detail.approvalNo}',
);
detailNotifier.value = detail;
return detail;
});
if (!context.mounted) {
detailNotifier.dispose();
return;
}
await SuperportDialog.show<void>(
@@ -474,6 +489,37 @@ class _PendingApprovalCard extends StatelessWidget {
description: '결재번호 ${approval.approvalNo}',
constraints: const BoxConstraints(maxWidth: 760),
actions: [
ValueListenableBuilder<Approval?>(
valueListenable: detailNotifier,
builder: (dialogContext, detail, _) {
return ShadButton.outline(
onPressed: detail == null
? null
: () async {
final approvalDetailId = detail.id;
if (approvalDetailId == null) {
SuperportToast.error(
parentContext,
'결재 ID가 없어 결재 관리 화면을 열 수 없습니다.',
);
return;
}
await Navigator.of(
dialogContext,
rootNavigator: true,
).maybePop();
if (!parentContext.mounted) {
return;
}
await _openApprovalManagement(
parentContext,
approvalDetailId,
);
},
child: const Text('결재 관리'),
);
},
),
ShadButton(
onPressed: () =>
Navigator.of(context, rootNavigator: true).maybePop(),
@@ -509,6 +555,73 @@ class _PendingApprovalCard extends StatelessWidget {
),
),
);
detailNotifier.dispose();
}
Future<void> _openApprovalManagement(
BuildContext context,
int approvalId,
) async {
final controller = ApprovalController(
approvalRepository: GetIt.I<ApprovalRepository>(),
templateRepository: GetIt.I<ApprovalTemplateRepository>(),
transactionRepository: GetIt.I<StockTransactionRepository>(),
lookupRepository: GetIt.I.isRegistered<InventoryLookupRepository>()
? GetIt.I<InventoryLookupRepository>()
: null,
saveDraftUseCase: GetIt.I.isRegistered<SaveApprovalDraftUseCase>()
? GetIt.I<SaveApprovalDraftUseCase>()
: null,
getDraftUseCase: GetIt.I.isRegistered<GetApprovalDraftUseCase>()
? GetIt.I<GetApprovalDraftUseCase>()
: null,
listDraftsUseCase: GetIt.I.isRegistered<ListApprovalDraftsUseCase>()
? GetIt.I<ListApprovalDraftsUseCase>()
: null,
);
final dateFormat = intl.DateFormat('yyyy-MM-dd HH:mm');
final currentUserId = GetIt.I.isRegistered<AuthService>()
? GetIt.I<AuthService>().session?.user.id
: null;
try {
await Future.wait([
controller.loadActionOptions(),
controller.loadTemplates(),
controller.loadStatusLookups(),
]);
await controller.selectApproval(approvalId);
if (controller.selected == null) {
final error = controller.errorMessage ?? '결재 상세 정보를 불러오지 못했습니다.';
if (context.mounted) {
SuperportToast.error(context, error);
}
return;
}
if (!context.mounted) {
return;
}
final permissionScope = PermissionScope.of(context);
final canPerformStepActions = permissionScope.can(
PermissionResources.approvals,
PermissionAction.approve,
);
final canApplyTemplate = permissionScope.can(
PermissionResources.approvals,
PermissionAction.edit,
);
await showApprovalDetailDialog(
context: context,
controller: controller,
dateFormat: dateFormat,
canPerformStepActions: canPerformStepActions,
canApplyTemplate: canApplyTemplate,
currentUserId: currentUserId,
);
} finally {
controller.dispose();
}
}
}