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

@@ -1,6 +1,7 @@
import 'dart:async';
import 'package:flutter/material.dart';
import 'package:flutter/scheduler.dart';
import 'package:get_it/get_it.dart';
import 'package:go_router/go_router.dart';
import 'package:intl/intl.dart' as intl;
@@ -18,6 +19,7 @@ import '../../../../widgets/components/superport_dialog.dart';
import '../../../../widgets/components/superport_table.dart';
import '../../../../widgets/components/superport_pagination_controls.dart';
import '../../../../widgets/components/feature_disabled_placeholder.dart';
import '../../../auth/application/auth_service.dart';
import '../../domain/entities/approval.dart';
import '../../domain/repositories/approval_repository.dart';
import '../../domain/repositories/approval_template_repository.dart';
@@ -26,6 +28,7 @@ import '../../domain/usecases/list_approval_drafts_use_case.dart';
import '../../domain/usecases/save_approval_draft_use_case.dart';
import '../../../inventory/lookups/domain/repositories/inventory_lookup_repository.dart';
import '../../../inventory/shared/widgets/employee_autocomplete_field.dart';
import '../../../inventory/transactions/domain/repositories/stock_transaction_repository.dart';
import '../controllers/approval_controller.dart';
import '../dialogs/approval_detail_dialog.dart';
@@ -100,6 +103,7 @@ class _ApprovalEnabledPageState extends State<_ApprovalEnabledPage> {
_controller = ApprovalController(
approvalRepository: GetIt.I<ApprovalRepository>(),
templateRepository: GetIt.I<ApprovalTemplateRepository>(),
transactionRepository: GetIt.I<StockTransactionRepository>(),
lookupRepository: GetIt.I.isRegistered<InventoryLookupRepository>()
? GetIt.I<InventoryLookupRepository>()
: null,
@@ -229,6 +233,9 @@ class _ApprovalEnabledPageState extends State<_ApprovalEnabledPage> {
_approvalsResourcePath,
PermissionAction.edit,
);
final currentUserId = GetIt.I.isRegistered<AuthService>()
? GetIt.I<AuthService>().session?.user.id
: null;
await showApprovalDetailDialog(
context: context,
@@ -236,6 +243,7 @@ class _ApprovalEnabledPageState extends State<_ApprovalEnabledPage> {
dateFormat: _dateTimeFormat,
canPerformStepActions: canPerformStepActions,
canApplyTemplate: canApplyTemplate,
currentUserId: currentUserId,
);
if (!mounted) {
@@ -245,6 +253,31 @@ class _ApprovalEnabledPageState extends State<_ApprovalEnabledPage> {
_controller.clearSelection();
}
void _handleRequesterFieldChanged() {
void updateSelection() {
final selectedLabel = _selectedRequester == null
? ''
: '${_selectedRequester!.name} (${_selectedRequester!.employeeNo})';
if (_requesterController.text.trim() != selectedLabel) {
_selectedRequester = null;
}
}
if (!mounted) {
return;
}
final phase = SchedulerBinding.instance.schedulerPhase;
if (phase == SchedulerPhase.persistentCallbacks ||
phase == SchedulerPhase.transientCallbacks) {
WidgetsBinding.instance.addPostFrameCallback((_) {
if (!mounted) return;
setState(updateSelection);
});
return;
}
setState(updateSelection);
}
@override
void dispose() {
_controller.removeListener(_handleControllerUpdate);
@@ -323,16 +356,7 @@ class _ApprovalEnabledPageState extends State<_ApprovalEnabledPage> {
onSuggestionSelected: (suggestion) {
setState(() => _selectedRequester = suggestion);
},
onChanged: () {
setState(() {
final selectedLabel = _selectedRequester == null
? ''
: '${_selectedRequester!.name} (${_selectedRequester!.employeeNo})';
if (_requesterController.text.trim() != selectedLabel) {
_selectedRequester = null;
}
});
},
onChanged: _handleRequesterFieldChanged,
),
),
SizedBox(