- 환경/라우터 모듈에 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
450 lines
11 KiB
Dart
450 lines
11 KiB
Dart
/// 결재(Approval) 엔티티
|
|
///
|
|
/// - 결재 기본 정보와 현재 단계, 라인(단계/이력) 데이터를 포함한다.
|
|
/// - presentation/data 레이어 구현에 의존하지 않는다.
|
|
class Approval {
|
|
Approval({
|
|
this.id,
|
|
required this.approvalNo,
|
|
this.transactionId,
|
|
required this.transactionNo,
|
|
this.transactionUpdatedAt,
|
|
required this.status,
|
|
this.currentStep,
|
|
required this.requester,
|
|
required this.requestedAt,
|
|
this.decidedAt,
|
|
this.note,
|
|
this.isActive = true,
|
|
this.isDeleted = false,
|
|
this.steps = const [],
|
|
this.histories = const [],
|
|
this.createdAt,
|
|
this.updatedAt,
|
|
});
|
|
|
|
final int? id;
|
|
final String approvalNo;
|
|
final int? transactionId;
|
|
final String transactionNo;
|
|
final DateTime? transactionUpdatedAt;
|
|
final ApprovalStatus status;
|
|
final ApprovalStep? currentStep;
|
|
final ApprovalRequester requester;
|
|
final DateTime requestedAt;
|
|
final DateTime? decidedAt;
|
|
final String? note;
|
|
final bool isActive;
|
|
final bool isDeleted;
|
|
final List<ApprovalStep> steps;
|
|
final List<ApprovalHistory> histories;
|
|
final DateTime? createdAt;
|
|
final DateTime? updatedAt;
|
|
|
|
Approval copyWith({
|
|
int? id,
|
|
String? approvalNo,
|
|
int? transactionId,
|
|
String? transactionNo,
|
|
DateTime? transactionUpdatedAt,
|
|
ApprovalStatus? status,
|
|
ApprovalStep? currentStep,
|
|
ApprovalRequester? requester,
|
|
DateTime? requestedAt,
|
|
DateTime? decidedAt,
|
|
String? note,
|
|
bool? isActive,
|
|
bool? isDeleted,
|
|
List<ApprovalStep>? steps,
|
|
List<ApprovalHistory>? histories,
|
|
DateTime? createdAt,
|
|
DateTime? updatedAt,
|
|
}) {
|
|
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,
|
|
requestedAt: requestedAt ?? this.requestedAt,
|
|
decidedAt: decidedAt ?? this.decidedAt,
|
|
note: note ?? this.note,
|
|
isActive: isActive ?? this.isActive,
|
|
isDeleted: isDeleted ?? this.isDeleted,
|
|
steps: steps ?? this.steps,
|
|
histories: histories ?? this.histories,
|
|
createdAt: createdAt ?? this.createdAt,
|
|
updatedAt: updatedAt ?? this.updatedAt,
|
|
);
|
|
}
|
|
}
|
|
|
|
class ApprovalStatus {
|
|
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 {
|
|
ApprovalRequester({
|
|
required this.id,
|
|
required this.employeeNo,
|
|
required this.name,
|
|
});
|
|
|
|
final int id;
|
|
final String employeeNo;
|
|
final String name;
|
|
}
|
|
|
|
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,
|
|
);
|
|
}
|
|
}
|
|
|
|
class ApprovalApprover {
|
|
ApprovalApprover({
|
|
required this.id,
|
|
required this.employeeNo,
|
|
required this.name,
|
|
});
|
|
|
|
final int id;
|
|
final String employeeNo;
|
|
final String name;
|
|
}
|
|
|
|
class ApprovalHistory {
|
|
ApprovalHistory({
|
|
this.id,
|
|
required this.action,
|
|
this.fromStatus,
|
|
required this.toStatus,
|
|
required this.approver,
|
|
required this.actionAt,
|
|
this.note,
|
|
this.actionCode,
|
|
this.payload,
|
|
});
|
|
|
|
final int? id;
|
|
final ApprovalAction action;
|
|
final ApprovalStatus? fromStatus;
|
|
final ApprovalStatus toStatus;
|
|
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, this.code});
|
|
|
|
final int id;
|
|
final String name;
|
|
final String? code;
|
|
}
|
|
|
|
/// 결재 단계에서 수행 가능한 행위 타입
|
|
///
|
|
/// - API `approval_actions` 테이블의 대표 코드와 매핑된다.
|
|
/// - UI에서는 이 타입을 기반으로 표시 라벨과 권한을 제어한다.
|
|
enum ApprovalStepActionType { approve, reject, comment }
|
|
|
|
extension ApprovalStepActionTypeX on ApprovalStepActionType {
|
|
/// API 호출 시 사용되는 행위 코드
|
|
String get code {
|
|
switch (this) {
|
|
case ApprovalStepActionType.approve:
|
|
return 'approve';
|
|
case ApprovalStepActionType.reject:
|
|
return 'reject';
|
|
case ApprovalStepActionType.comment:
|
|
return 'comment';
|
|
}
|
|
}
|
|
}
|
|
|
|
/// 결재 생성 입력 모델
|
|
/// 결재 신규 생성 입력 모델
|
|
///
|
|
/// - 트랜잭션, 결재번호, 상태, 상신자 정보를 백엔드 계약에 맞춰 전달한다.
|
|
class ApprovalCreateInput {
|
|
ApprovalCreateInput({
|
|
required this.transactionId,
|
|
required this.approvalStatusId,
|
|
required this.requestedById,
|
|
this.note,
|
|
});
|
|
|
|
final int transactionId;
|
|
final int approvalStatusId;
|
|
final int requestedById;
|
|
final String? note;
|
|
|
|
Map<String, dynamic> toPayload() {
|
|
final trimmedNote = note?.trim();
|
|
return {
|
|
'transaction_id': transactionId,
|
|
'approval_status_id': approvalStatusId,
|
|
'requested_by_id': requestedById,
|
|
if (trimmedNote != null && trimmedNote.isNotEmpty) 'note': trimmedNote,
|
|
};
|
|
}
|
|
}
|
|
|
|
/// 결재 기본 정보 수정 입력 모델
|
|
///
|
|
/// - 상태/비고 변경 시 결재 식별자를 포함해 패치를 수행한다.
|
|
class ApprovalUpdateInput {
|
|
ApprovalUpdateInput({required this.id, this.approvalStatusId, this.note});
|
|
|
|
final int id;
|
|
final int? approvalStatusId;
|
|
final String? note;
|
|
|
|
Map<String, dynamic> toPayload() {
|
|
final trimmedNote = note?.trim();
|
|
return {
|
|
'id': id,
|
|
if (approvalStatusId != null) 'approval_status_id': approvalStatusId,
|
|
if (trimmedNote != null && trimmedNote.isNotEmpty) 'note': trimmedNote,
|
|
};
|
|
}
|
|
}
|
|
|
|
/// 결재 단계 행위 입력 모델
|
|
///
|
|
/// - `POST /approval-steps/{id}/actions` 요청 바디를 구성한다.
|
|
/// - `note`는 비고가 있을 때만 포함한다.
|
|
class ApprovalStepActionInput {
|
|
ApprovalStepActionInput({
|
|
required this.stepId,
|
|
required this.actionId,
|
|
this.note,
|
|
});
|
|
|
|
final int stepId;
|
|
final int actionId;
|
|
final String? note;
|
|
|
|
Map<String, dynamic> toPayload() {
|
|
return {
|
|
'id': stepId,
|
|
'approval_action_id': actionId,
|
|
if (note != null && note!.trim().isNotEmpty) 'note': note,
|
|
};
|
|
}
|
|
}
|
|
|
|
/// 결재 단계를 일괄 등록/재배치하기 위한 입력 모델
|
|
class ApprovalStepAssignmentInput {
|
|
ApprovalStepAssignmentInput({required this.approvalId, required this.steps});
|
|
|
|
final int approvalId;
|
|
final List<ApprovalStepAssignmentItem> steps;
|
|
|
|
Map<String, dynamic> toPayload() {
|
|
return {'id': approvalId, 'steps': steps.map((e) => e.toJson()).toList()};
|
|
}
|
|
}
|
|
|
|
class ApprovalStepAssignmentItem {
|
|
ApprovalStepAssignmentItem({
|
|
required this.stepOrder,
|
|
required this.approverId,
|
|
this.note,
|
|
});
|
|
|
|
final int stepOrder;
|
|
final int approverId;
|
|
final String? note;
|
|
|
|
Map<String, dynamic> toJson() {
|
|
return {
|
|
'step_order': stepOrder,
|
|
'approver_id': approverId,
|
|
if (note != null && note!.trim().isNotEmpty) 'note': note,
|
|
};
|
|
}
|
|
}
|
|
|
|
/// 결재 상신 입력 모델.
|
|
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;
|
|
}
|