feat(frontend): 승인 템플릿 API 통합 및 디버그 로그인 확장
- docs 폴더 문서를 최신 API 계약으로 갱신하고 가이드를 다듬었다\n- approvals data/presentation 레이어를 API v4 스펙에 맞춰 리팩터링했다\n- approver 자동완성 위젯을 신규 공유 레포지토리에 맞춰 교체하고 UX를 보강했다\n- inventory/rental 페이지 테이블 초기화 시 승인 기준 연동을 정비했다\n- 로그인 페이지 디버그 버튼을 tera/exa 계정으로 분리해 QA 로그인을 단순화했다\n- get_it 등록과 테스트 케이스를 신규 공유 리포지토리에 맞춰 업데이트했다
This commit is contained in:
@@ -1,5 +1,9 @@
|
||||
import 'dart:async';
|
||||
|
||||
import 'package:get_it/get_it.dart';
|
||||
|
||||
import '../../../domain/entities/approval.dart';
|
||||
import '../../../shared/approver_catalog.dart';
|
||||
import '../../../shared/domain/repositories/approval_approver_repository.dart';
|
||||
import '../controllers/approval_request_controller.dart';
|
||||
import '../../../../inventory/transactions/domain/entities/stock_transaction_input.dart';
|
||||
|
||||
@@ -10,12 +14,13 @@ class ApprovalFormInitializer {
|
||||
ApprovalFormInitializer._();
|
||||
|
||||
/// 결재 구성 컨트롤러에 기본값을 주입한다.
|
||||
static void populate({
|
||||
static Future<void> populate({
|
||||
required ApprovalRequestController controller,
|
||||
Approval? existingApproval,
|
||||
StockTransactionApprovalInput? draft,
|
||||
ApprovalRequestParticipant? defaultRequester,
|
||||
}) {
|
||||
ApprovalApproverRepository? repository,
|
||||
}) async {
|
||||
if (existingApproval != null) {
|
||||
_applyExistingApproval(controller, existingApproval);
|
||||
return;
|
||||
@@ -24,7 +29,11 @@ class ApprovalFormInitializer {
|
||||
controller.setRequester(defaultRequester);
|
||||
}
|
||||
if (draft != null) {
|
||||
_applyDraft(controller, draft);
|
||||
await _applyDraft(
|
||||
controller,
|
||||
draft,
|
||||
repository ?? _resolveRepository(),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -57,40 +66,62 @@ class ApprovalFormInitializer {
|
||||
}
|
||||
}
|
||||
|
||||
static void _applyDraft(
|
||||
static Future<void> _applyDraft(
|
||||
ApprovalRequestController controller,
|
||||
StockTransactionApprovalInput draft,
|
||||
) {
|
||||
final requesterCatalog = ApprovalApproverCatalog.byId(draft.requestedById);
|
||||
if (requesterCatalog != null) {
|
||||
controller.setRequester(
|
||||
ApprovalRequestParticipant(
|
||||
id: requesterCatalog.id,
|
||||
name: requesterCatalog.name,
|
||||
employeeNo: requesterCatalog.employeeNo,
|
||||
),
|
||||
);
|
||||
ApprovalApproverRepository? repository,
|
||||
) async {
|
||||
final repo = repository;
|
||||
if (repo == null) {
|
||||
return;
|
||||
}
|
||||
final steps = draft.steps
|
||||
.map((step) {
|
||||
final catalog = ApprovalApproverCatalog.byId(step.approverId);
|
||||
if (catalog == null) {
|
||||
|
||||
final requester = await _fetchParticipant(repo, draft.requestedById);
|
||||
if (requester != null) {
|
||||
controller.setRequester(requester);
|
||||
}
|
||||
|
||||
final futures = draft.steps
|
||||
.map((step) async {
|
||||
final participant = await _fetchParticipant(repo, step.approverId);
|
||||
if (participant == null) {
|
||||
return null;
|
||||
}
|
||||
return ApprovalRequestStep(
|
||||
stepOrder: step.stepOrder,
|
||||
approver: ApprovalRequestParticipant(
|
||||
id: catalog.id,
|
||||
name: catalog.name,
|
||||
employeeNo: catalog.employeeNo,
|
||||
),
|
||||
approver: participant,
|
||||
note: step.note,
|
||||
);
|
||||
})
|
||||
.whereType<ApprovalRequestStep>()
|
||||
.toList(growable: false);
|
||||
|
||||
final resolvedSteps = await Future.wait(futures);
|
||||
final steps = resolvedSteps.whereType<ApprovalRequestStep>().toList();
|
||||
if (steps.isNotEmpty) {
|
||||
controller.applyTemplateSteps(steps);
|
||||
}
|
||||
}
|
||||
|
||||
static Future<ApprovalRequestParticipant?> _fetchParticipant(
|
||||
ApprovalApproverRepository repository,
|
||||
int id,
|
||||
) async {
|
||||
final candidate = await repository.fetchById(id);
|
||||
if (candidate == null) {
|
||||
return null;
|
||||
}
|
||||
return ApprovalRequestParticipant(
|
||||
id: candidate.id,
|
||||
name: candidate.name,
|
||||
employeeNo: candidate.employeeNo,
|
||||
);
|
||||
}
|
||||
|
||||
static ApprovalApproverRepository? _resolveRepository() {
|
||||
final getIt = GetIt.I;
|
||||
if (!getIt.isRegistered<ApprovalApproverRepository>()) {
|
||||
return null;
|
||||
}
|
||||
return getIt<ApprovalApproverRepository>();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4,7 +4,7 @@ import 'package:shadcn_ui/shadcn_ui.dart';
|
||||
|
||||
import '../../../../../widgets/components/feedback.dart';
|
||||
import '../../../../../widgets/components/superport_dialog.dart';
|
||||
import '../../../shared/approver_catalog.dart';
|
||||
import '../../../shared/domain/entities/approval_approver_candidate.dart';
|
||||
import '../../../shared/widgets/approver_autocomplete_field.dart';
|
||||
import '../controllers/approval_request_controller.dart';
|
||||
import 'approval_step_row.dart';
|
||||
@@ -435,7 +435,7 @@ class _ConfiguratorDialogBodyState extends State<_ConfiguratorDialogBody> {
|
||||
}
|
||||
|
||||
Future<void> _openAddStepDialog() async {
|
||||
ApprovalApproverCatalogItem? selected;
|
||||
ApprovalApproverCandidate? selected;
|
||||
final idController = TextEditingController();
|
||||
|
||||
final result = await SuperportDialog.show<bool>(
|
||||
@@ -483,13 +483,19 @@ class _ConfiguratorDialogBodyState extends State<_ConfiguratorDialogBody> {
|
||||
return;
|
||||
}
|
||||
|
||||
final participant = _resolveParticipant(selected, idController.text.trim());
|
||||
if (participant == null) {
|
||||
final selection = selected;
|
||||
if (selection == null) {
|
||||
SuperportToast.warning(context, '유효한 승인자를 선택해주세요.');
|
||||
idController.dispose();
|
||||
return;
|
||||
}
|
||||
|
||||
final participant = ApprovalRequestParticipant(
|
||||
id: selection.id,
|
||||
name: selection.name,
|
||||
employeeNo: selection.employeeNo,
|
||||
);
|
||||
|
||||
final added = widget.controller.addStep(approver: participant);
|
||||
if (!added) {
|
||||
final message = widget.controller.errorMessage ?? '결재 단계를 추가하지 못했습니다.';
|
||||
@@ -503,31 +509,6 @@ class _ConfiguratorDialogBodyState extends State<_ConfiguratorDialogBody> {
|
||||
idController.dispose();
|
||||
}
|
||||
|
||||
ApprovalRequestParticipant? _resolveParticipant(
|
||||
ApprovalApproverCatalogItem? selected,
|
||||
String manualInput,
|
||||
) {
|
||||
if (selected != null) {
|
||||
return ApprovalRequestParticipant(
|
||||
id: selected.id,
|
||||
name: selected.name,
|
||||
employeeNo: selected.employeeNo,
|
||||
);
|
||||
}
|
||||
final manualId = int.tryParse(manualInput);
|
||||
if (manualId == null) {
|
||||
return null;
|
||||
}
|
||||
final match = ApprovalApproverCatalog.byId(manualId);
|
||||
if (match == null) {
|
||||
return null;
|
||||
}
|
||||
return ApprovalRequestParticipant(
|
||||
id: match.id,
|
||||
name: match.name,
|
||||
employeeNo: match.employeeNo,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class _InfoBadge extends StatelessWidget {
|
||||
|
||||
@@ -3,7 +3,7 @@ import 'package:lucide_icons_flutter/lucide_icons.dart' as lucide;
|
||||
import 'package:shadcn_ui/shadcn_ui.dart';
|
||||
|
||||
import '../../../../../widgets/components/feedback.dart';
|
||||
import '../../../shared/approver_catalog.dart';
|
||||
import '../../../shared/domain/entities/approval_approver_candidate.dart';
|
||||
import '../../../shared/widgets/approver_autocomplete_field.dart';
|
||||
import '../controllers/approval_request_controller.dart';
|
||||
|
||||
@@ -96,37 +96,23 @@ class _ApprovalStepRowState extends State<ApprovalStepRow> {
|
||||
|
||||
Future<void> _handleApproverSelected(
|
||||
BuildContext context,
|
||||
ApprovalApproverCatalogItem? item,
|
||||
ApprovalApproverCandidate? candidate,
|
||||
) async {
|
||||
if (widget.readOnly) {
|
||||
return;
|
||||
}
|
||||
ApprovalRequestParticipant? nextParticipant;
|
||||
|
||||
if (item != null) {
|
||||
if (candidate != null) {
|
||||
nextParticipant = ApprovalRequestParticipant(
|
||||
id: item.id,
|
||||
name: item.name,
|
||||
employeeNo: item.employeeNo,
|
||||
id: candidate.id,
|
||||
name: candidate.name,
|
||||
employeeNo: candidate.employeeNo,
|
||||
);
|
||||
} else {
|
||||
final manualId = int.tryParse(_approverIdController.text.trim());
|
||||
if (manualId == null) {
|
||||
SuperportToast.warning(context, '승인자를 다시 선택해주세요.');
|
||||
_restorePreviousApprover();
|
||||
return;
|
||||
}
|
||||
final catalogMatch = ApprovalApproverCatalog.byId(manualId);
|
||||
if (catalogMatch == null) {
|
||||
SuperportToast.warning(context, '등록되지 않은 승인자입니다.');
|
||||
_restorePreviousApprover();
|
||||
return;
|
||||
}
|
||||
nextParticipant = ApprovalRequestParticipant(
|
||||
id: catalogMatch.id,
|
||||
name: catalogMatch.name,
|
||||
employeeNo: catalogMatch.employeeNo,
|
||||
);
|
||||
SuperportToast.warning(context, '승인자를 다시 선택해주세요.');
|
||||
_restorePreviousApprover();
|
||||
return;
|
||||
}
|
||||
|
||||
final updated = widget.controller.updateStep(
|
||||
|
||||
Reference in New Issue
Block a user