feat(approvals): Approval Flow v2 프런트엔드 전면 개편

- 환경/라우터 모듈에 approval_flow_v2 토글을 추가하고 FeatureFlags 초기화를 연결 (.env*, lib/core/**)
- ApiClient 빌더·ApiRoutes 확장과 ApprovalRepositoryRemote 리팩터링으로 include·액션 시그니처를 정합화
- ApprovalFlow·ApprovalDraft 엔티티/레포/유즈케이스를 도입해 서버 초안과 단계 액션(승인·회수·재상신)을 지원
- Approval 컨트롤러·히스토리·템플릿 페이지와 공유 위젯을 재작성해 감사 로그·회수 UX·템플릿 CRUD를 반영
- Inbound/Outbound/Rental 컨트롤러·페이지에 결재 섹션을 삽입하고 대시보드 pending 카드 요약을 갱신
- SuperportDialog·FormField 등 공통 위젯을 보강하고 승인 위젯 가이드를 추가해 UI 가이드를 정리
- 결재/재고 테스트 픽스처와 단위·위젯·통합 테스트를 확장하고 flutter_test_config로 스테이징 호스트를 허용
- Approval Flow 레포트/플랜 문서를 업데이트하고 ApprovalFlow_System_Integration_and_ChangePlan.md를 추가
- 실행: flutter analyze, flutter test
This commit is contained in:
JiWoong Sul
2025-10-31 01:05:39 +09:00
parent 259b056072
commit d76f765814
133 changed files with 13878 additions and 947 deletions

View File

@@ -6,7 +6,9 @@ class Approval {
Approval({
this.id,
required this.approvalNo,
this.transactionId,
required this.transactionNo,
this.transactionUpdatedAt,
required this.status,
this.currentStep,
required this.requester,
@@ -23,7 +25,9 @@ class Approval {
final int? id;
final String approvalNo;
final int? transactionId;
final String transactionNo;
final DateTime? transactionUpdatedAt;
final ApprovalStatus status;
final ApprovalStep? currentStep;
final ApprovalRequester requester;
@@ -40,7 +44,9 @@ class Approval {
Approval copyWith({
int? id,
String? approvalNo,
int? transactionId,
String? transactionNo,
DateTime? transactionUpdatedAt,
ApprovalStatus? status,
ApprovalStep? currentStep,
ApprovalRequester? requester,
@@ -57,7 +63,9 @@ class Approval {
return Approval(
id: id ?? this.id,
approvalNo: approvalNo ?? this.approvalNo,
transactionId: transactionId ?? this.transactionId,
transactionNo: transactionNo ?? this.transactionNo,
transactionUpdatedAt: transactionUpdatedAt ?? this.transactionUpdatedAt,
status: status ?? this.status,
currentStep: currentStep ?? this.currentStep,
requester: requester ?? this.requester,
@@ -75,11 +83,35 @@ class Approval {
}
class ApprovalStatus {
ApprovalStatus({required this.id, required this.name, this.color});
ApprovalStatus({
required this.id,
required this.name,
this.color,
this.isBlockingNext = true,
this.isTerminal = false,
});
final int id;
final String name;
final String? color;
final bool isBlockingNext;
final bool isTerminal;
ApprovalStatus copyWith({
int? id,
String? name,
String? color,
bool? isBlockingNext,
bool? isTerminal,
}) {
return ApprovalStatus(
id: id ?? this.id,
name: name ?? this.name,
color: color ?? this.color,
isBlockingNext: isBlockingNext ?? this.isBlockingNext,
isTerminal: isTerminal ?? this.isTerminal,
);
}
}
class ApprovalRequester {
@@ -97,43 +129,71 @@ class ApprovalRequester {
class ApprovalStep {
ApprovalStep({
this.id,
this.requestId,
required this.stepOrder,
this.templateStepId,
this.approverRole,
required this.approver,
required this.status,
required this.assignedAt,
this.decidedAt,
this.note,
this.isDeleted = false,
this.actionAt,
this.isOptional = false,
this.escalationMinutes,
this.metadata,
});
final int? id;
final int? requestId;
final int stepOrder;
final int? templateStepId;
final String? approverRole;
final ApprovalApprover approver;
final ApprovalStatus status;
final DateTime assignedAt;
final DateTime? decidedAt;
final String? note;
final bool isDeleted;
final DateTime? actionAt;
final bool isOptional;
final int? escalationMinutes;
final Map<String, dynamic>? metadata;
ApprovalStep copyWith({
int? id,
int? requestId,
int? stepOrder,
int? templateStepId,
String? approverRole,
ApprovalApprover? approver,
ApprovalStatus? status,
DateTime? assignedAt,
DateTime? decidedAt,
String? note,
bool? isDeleted,
DateTime? actionAt,
bool? isOptional,
int? escalationMinutes,
Map<String, dynamic>? metadata,
}) {
return ApprovalStep(
id: id ?? this.id,
requestId: requestId ?? this.requestId,
stepOrder: stepOrder ?? this.stepOrder,
templateStepId: templateStepId ?? this.templateStepId,
approverRole: approverRole ?? this.approverRole,
approver: approver ?? this.approver,
status: status ?? this.status,
assignedAt: assignedAt ?? this.assignedAt,
decidedAt: decidedAt ?? this.decidedAt,
note: note ?? this.note,
isDeleted: isDeleted ?? this.isDeleted,
actionAt: actionAt ?? this.actionAt,
isOptional: isOptional ?? this.isOptional,
escalationMinutes: escalationMinutes ?? this.escalationMinutes,
metadata: metadata ?? this.metadata,
);
}
}
@@ -159,6 +219,8 @@ class ApprovalHistory {
required this.approver,
required this.actionAt,
this.note,
this.actionCode,
this.payload,
});
final int? id;
@@ -168,13 +230,16 @@ class ApprovalHistory {
final ApprovalApprover approver;
final DateTime actionAt;
final String? note;
final String? actionCode;
final Map<String, dynamic>? payload;
}
class ApprovalAction {
ApprovalAction({required this.id, required this.name});
ApprovalAction({required this.id, required this.name, this.code});
final int id;
final String name;
final String? code;
}
/// 결재 단계에서 수행 가능한 행위 타입
@@ -300,3 +365,85 @@ class ApprovalStepAssignmentItem {
};
}
}
/// 결재 상신 입력 모델.
class ApprovalSubmissionInput {
ApprovalSubmissionInput({
this.transactionId,
this.templateId,
required this.statusId,
required this.requesterId,
this.finalApproverId,
this.requestedAt,
this.decidedAt,
this.cancelledAt,
this.lastActionAt,
this.title,
this.summary,
this.note,
this.metadata,
this.steps = const [],
});
final int? transactionId;
final int? templateId;
final int statusId;
final int requesterId;
final int? finalApproverId;
final DateTime? requestedAt;
final DateTime? decidedAt;
final DateTime? cancelledAt;
final DateTime? lastActionAt;
final String? title;
final String? summary;
final String? note;
final Map<String, dynamic>? metadata;
final List<ApprovalStepAssignmentItem> steps;
}
/// 결재 승인/반려 입력 모델.
class ApprovalDecisionInput {
ApprovalDecisionInput({
required this.approvalId,
required this.actorId,
this.note,
this.expectedUpdatedAt,
});
final int approvalId;
final int actorId;
final String? note;
final DateTime? expectedUpdatedAt;
}
/// 결재 회수 입력 모델.
class ApprovalRecallInput extends ApprovalDecisionInput {
ApprovalRecallInput({
required super.approvalId,
required super.actorId,
super.note,
super.expectedUpdatedAt,
this.transactionExpectedUpdatedAt,
});
final DateTime? transactionExpectedUpdatedAt;
}
/// 결재 재상신 입력 모델.
class ApprovalResubmissionInput {
ApprovalResubmissionInput({
required this.approvalId,
required this.actorId,
required this.submission,
this.note,
this.expectedUpdatedAt,
this.transactionExpectedUpdatedAt,
});
final int approvalId;
final int actorId;
final ApprovalSubmissionInput submission;
final String? note;
final DateTime? expectedUpdatedAt;
final DateTime? transactionExpectedUpdatedAt;
}