대시보드 결재 상세 진입 지원

This commit is contained in:
JiWoong Sul
2025-10-23 20:19:59 +09:00
parent 7e933a2dda
commit 9d6cbb1ab2
14 changed files with 543 additions and 76 deletions

View File

@@ -345,6 +345,9 @@ class ApprovalController extends ChangeNotifier {
}
} catch (error) {
final failure = Failure.from(error);
debugPrint(
'[ApprovalController] 결재 상세 조회 실패: ${failure.describe()}',
); // 에러 발생 시 콘솔에 남겨 즉시 파악할 수 있도록 한다.
_errorMessage = failure.describe();
} finally {
_isLoadingDetail = false;

View File

@@ -28,7 +28,9 @@ const _approvalsResourcePath = PermissionResources.approvals;
///
/// 기능 플래그에 따라 실제 화면 또는 비활성 안내 화면을 보여준다.
class ApprovalPage extends StatelessWidget {
const ApprovalPage({super.key});
const ApprovalPage({super.key, this.routeUri});
final Uri? routeUri;
@override
Widget build(BuildContext context) {
@@ -59,13 +61,15 @@ class ApprovalPage extends StatelessWidget {
);
}
return const _ApprovalEnabledPage();
return _ApprovalEnabledPage(routeUri: routeUri);
}
}
/// 결재 기능이 활성화되었을 때 사용되는 실제 페이지 위젯.
class _ApprovalEnabledPage extends StatefulWidget {
const _ApprovalEnabledPage();
const _ApprovalEnabledPage({this.routeUri});
final Uri? routeUri;
@override
State<_ApprovalEnabledPage> createState() => _ApprovalEnabledPageState();
@@ -76,14 +80,17 @@ class _ApprovalEnabledPageState extends State<_ApprovalEnabledPage> {
final TextEditingController _transactionController = TextEditingController();
final TextEditingController _requesterController = TextEditingController();
final FocusNode _transactionFocus = FocusNode();
final GlobalKey _detailSectionKey = GlobalKey();
InventoryEmployeeSuggestion? _selectedRequester;
final intl.DateFormat _dateTimeFormat = intl.DateFormat('yyyy-MM-dd HH:mm');
String? _lastError;
int? _selectedTemplateId;
String? _pendingRouteSelection;
@override
void initState() {
super.initState();
_pendingRouteSelection = _parseRouteSelection(widget.routeUri);
_controller = ApprovalController(
approvalRepository: GetIt.I<ApprovalRepository>(),
templateRepository: GetIt.I<ApprovalTemplateRepository>(),
@@ -98,9 +105,24 @@ class _ApprovalEnabledPageState extends State<_ApprovalEnabledPage> {
_controller.loadStatusLookups(),
]);
await _controller.fetch();
_applyRouteSelectionIfNeeded(
_controller.result?.items ?? const <Approval>[],
);
});
}
@override
void didUpdateWidget(covariant _ApprovalEnabledPage oldWidget) {
super.didUpdateWidget(oldWidget);
if (oldWidget.routeUri != widget.routeUri) {
_pendingRouteSelection = _parseRouteSelection(widget.routeUri);
final currentResult = _controller.result;
if (currentResult != null) {
_applyRouteSelectionIfNeeded(currentResult.items);
}
}
}
void _handleControllerUpdate() {
final error = _controller.errorMessage;
if (error != null && error != _lastError && mounted) {
@@ -110,6 +132,57 @@ class _ApprovalEnabledPageState extends State<_ApprovalEnabledPage> {
}
}
/// 라우트 쿼리에서 선택된 결재번호를 읽어온다.
String? _parseRouteSelection(Uri? routeUri) {
final value = routeUri?.queryParameters['selected']?.trim();
if (value == null || value.isEmpty) {
return null;
}
return value;
}
/// 최초 로딩 시 라우트에서 전달된 결재번호가 있으면 자동으로 상세를 연다.
void _applyRouteSelectionIfNeeded(List<Approval> approvals) {
final target = _pendingRouteSelection;
if (target == null) {
return;
}
for (final approval in approvals) {
if (approval.approvalNo == target && approval.id != null) {
WidgetsBinding.instance.addPostFrameCallback((_) {
if (!mounted) return;
_selectApproval(approval.id!);
});
break;
}
}
_pendingRouteSelection = null;
}
Future<void> _selectApproval(int id, {bool reveal = true}) async {
await _controller.selectApproval(id);
if (!mounted) {
return;
}
if (!reveal) {
return;
}
await _revealDetailSection();
}
Future<void> _revealDetailSection() async {
final detailContext = _detailSectionKey.currentContext;
if (detailContext == null) {
return;
}
await Scrollable.ensureVisible(
detailContext,
duration: const Duration(milliseconds: 300),
alignment: 0.05,
curve: Curves.easeInOut,
);
}
@override
void dispose() {
_controller.removeListener(_handleControllerUpdate);
@@ -130,6 +203,9 @@ class _ApprovalEnabledPageState extends State<_ApprovalEnabledPage> {
builder: (context, _) {
final result = _controller.result;
final approvals = result?.items ?? const <Approval>[];
if (result != null) {
_applyRouteSelectionIfNeeded(approvals);
}
final selectedApproval = _controller.selected;
final totalCount = result?.total ?? 0;
final currentPage = result?.page ?? 1;
@@ -279,8 +355,8 @@ class _ApprovalEnabledPageState extends State<_ApprovalEnabledPage> {
size: ShadButtonSize.sm,
onPressed:
_controller.isLoadingList || currentPage <= 1
? null
: () => _controller.fetch(page: 1),
? null
: () => _controller.fetch(page: 1),
child: const Text('처음'),
),
const SizedBox(width: 8),
@@ -304,9 +380,10 @@ class _ApprovalEnabledPageState extends State<_ApprovalEnabledPage> {
ShadButton.outline(
size: ShadButtonSize.sm,
onPressed:
_controller.isLoadingList || currentPage >= totalPages
? null
: () => _controller.fetch(page: totalPages),
_controller.isLoadingList ||
currentPage >= totalPages
? null
: () => _controller.fetch(page: totalPages),
child: const Text('마지막'),
),
],
@@ -332,13 +409,14 @@ class _ApprovalEnabledPageState extends State<_ApprovalEnabledPage> {
onView: (approval) {
final id = approval.id;
if (id != null) {
_controller.selectApproval(id);
_selectApproval(id);
}
},
),
),
const SizedBox(height: 24),
_DetailSection(
key: _detailSectionKey,
approval: selectedApproval,
isLoading: _controller.isLoadingDetail,
isLoadingActions: isLoadingActions,
@@ -358,7 +436,7 @@ class _ApprovalEnabledPageState extends State<_ApprovalEnabledPage> {
onRefresh: () {
final id = selectedApproval?.id;
if (id != null) {
_controller.selectApproval(id);
_selectApproval(id, reveal: false);
}
},
onClose: selectedApproval == null
@@ -411,9 +489,9 @@ class _ApprovalEnabledPageState extends State<_ApprovalEnabledPage> {
onPressed: isSubmitting
? null
: () => Navigator.of(
context,
rootNavigator: true,
).pop(false),
context,
rootNavigator: true,
).pop(false),
child: const Text('취소'),
),
ShadButton(
@@ -678,10 +756,7 @@ class _ApprovalEnabledPageState extends State<_ApprovalEnabledPage> {
if (created == true && mounted) {
final number = createdApprovalNo ?? '-';
SuperportToast.success(
context,
'결재를 생성했습니다. ($number)',
);
SuperportToast.success(context, '결재를 생성했습니다. ($number)');
}
}
@@ -798,10 +873,8 @@ class _ApprovalEnabledPageState extends State<_ApprovalEnabledPage> {
constraints: const BoxConstraints(maxWidth: 420),
actions: [
ShadButton.ghost(
onPressed: () => Navigator.of(
dialogContext,
rootNavigator: true,
).pop(),
onPressed: () =>
Navigator.of(dialogContext, rootNavigator: true).pop(),
child: const Text('취소'),
),
ShadButton(
@@ -811,10 +884,7 @@ class _ApprovalEnabledPageState extends State<_ApprovalEnabledPage> {
setState(() => errorText = '비고를 입력하세요.');
return;
}
Navigator.of(
dialogContext,
rootNavigator: true,
).pop(
Navigator.of(dialogContext, rootNavigator: true).pop(
_StepActionDialogResult(note: note.isEmpty ? null : note),
);
},
@@ -1049,6 +1119,7 @@ class _ApprovalTable extends StatelessWidget {
/// 선택 상태와 로딩 여부에 따라 안내 문구 또는 상세 정보를 노출한다.
class _DetailSection extends StatelessWidget {
const _DetailSection({
super.key,
required this.approval,
required this.isLoading,
required this.isLoadingActions,