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:
156
lib/features/approvals/data/dtos/approval_audit_dto.dart
Normal file
156
lib/features/approvals/data/dtos/approval_audit_dto.dart
Normal file
@@ -0,0 +1,156 @@
|
||||
import 'package:superport_v2/core/common/utils/json_utils.dart';
|
||||
|
||||
import '../../domain/entities/approval.dart';
|
||||
import 'approval_step_dto.dart';
|
||||
|
||||
/// 결재 감사 로그(Audit) DTO.
|
||||
class ApprovalAuditDto {
|
||||
ApprovalAuditDto({
|
||||
this.id,
|
||||
required this.action,
|
||||
this.fromStatus,
|
||||
required this.toStatus,
|
||||
required this.actor,
|
||||
required this.actionAt,
|
||||
this.note,
|
||||
this.actionCode,
|
||||
this.payload,
|
||||
});
|
||||
|
||||
final int? id;
|
||||
final ApprovalActionDto action;
|
||||
final ApprovalStatusDto? fromStatus;
|
||||
final ApprovalStatusDto toStatus;
|
||||
final ApprovalApproverDto actor;
|
||||
final DateTime actionAt;
|
||||
final String? note;
|
||||
final String? actionCode;
|
||||
final Map<String, dynamic>? payload;
|
||||
|
||||
factory ApprovalAuditDto.fromJson(Map<String, dynamic> json) {
|
||||
final actionMap = {
|
||||
...?_asMap(json['action']),
|
||||
...?_asMap(json['approval_action']),
|
||||
};
|
||||
final fallbackActionId = json['approval_action_id'] ?? json['action_id'];
|
||||
if (fallbackActionId != null) {
|
||||
actionMap.putIfAbsent('id', () => fallbackActionId);
|
||||
}
|
||||
final fallbackActionName = _firstNonEmpty(<String?>[
|
||||
_readString(json, 'action_name'),
|
||||
_readString(json, 'approval_action_name'),
|
||||
]);
|
||||
if (fallbackActionName != null) {
|
||||
actionMap.putIfAbsent('name', () => fallbackActionName);
|
||||
}
|
||||
final rootActionCode = _firstNonEmpty(<String?>[
|
||||
_readString(json, 'action_code'),
|
||||
_readString(json, 'approval_action_code'),
|
||||
]);
|
||||
if (rootActionCode != null) {
|
||||
actionMap.putIfAbsent('code', () => rootActionCode);
|
||||
actionMap.putIfAbsent('action_code', () => rootActionCode);
|
||||
}
|
||||
final actionDto = ApprovalActionDto.fromJson(actionMap);
|
||||
final fromStatusMap = _asMap(json['from_status']);
|
||||
final toStatusMap = _asMap(json['to_status']) ?? const <String, dynamic>{};
|
||||
final actorMap =
|
||||
_asMap(json['actor']) ??
|
||||
_asMap(json['approver']) ??
|
||||
const <String, dynamic>{};
|
||||
final resolvedActionCode = rootActionCode ?? actionDto.code;
|
||||
|
||||
return ApprovalAuditDto(
|
||||
id: JsonUtils.readInt(json, 'id'),
|
||||
action: actionDto,
|
||||
fromStatus: fromStatusMap == null
|
||||
? null
|
||||
: ApprovalStatusDto.fromJson(fromStatusMap),
|
||||
toStatus: ApprovalStatusDto.fromJson(toStatusMap),
|
||||
actor: ApprovalApproverDto.fromJson(actorMap),
|
||||
actionAt: _parseDate(json['action_at']) ?? DateTime.now(),
|
||||
note: _readString(json, 'note'),
|
||||
actionCode: resolvedActionCode,
|
||||
payload: _asMap(
|
||||
json['payload'],
|
||||
)?.map((key, value) => MapEntry(key, value)),
|
||||
);
|
||||
}
|
||||
|
||||
ApprovalHistory toEntity() => ApprovalHistory(
|
||||
id: id,
|
||||
action: action.toEntity(),
|
||||
fromStatus: fromStatus?.toEntity(),
|
||||
toStatus: toStatus.toEntity(),
|
||||
approver: actor.toEntity(),
|
||||
actionAt: actionAt,
|
||||
note: note,
|
||||
actionCode: actionCode,
|
||||
payload: payload,
|
||||
);
|
||||
}
|
||||
|
||||
/// 결재 감사 로그 액션 DTO.
|
||||
class ApprovalActionDto {
|
||||
ApprovalActionDto({required this.id, required this.name, this.code});
|
||||
|
||||
final int id;
|
||||
final String name;
|
||||
final String? code;
|
||||
|
||||
factory ApprovalActionDto.fromJson(Map<String, dynamic> json) {
|
||||
if (json['action'] is Map<String, dynamic>) {
|
||||
return ApprovalActionDto.fromJson(json['action'] as Map<String, dynamic>);
|
||||
}
|
||||
final id = JsonUtils.readInt(json, 'id', fallback: 0);
|
||||
final name = _firstNonEmpty(<String?>[
|
||||
_readString(json, 'name'),
|
||||
_readString(json, 'action_name'),
|
||||
]);
|
||||
if (name == null) {
|
||||
throw const FormatException('결재 감사 로그 액션 이름이 누락되었습니다.');
|
||||
}
|
||||
final code = _firstNonEmpty(<String?>[
|
||||
_readString(json, 'code'),
|
||||
_readString(json, 'action_code'),
|
||||
]);
|
||||
return ApprovalActionDto(id: id, name: name, code: code);
|
||||
}
|
||||
|
||||
ApprovalAction toEntity() => ApprovalAction(id: id, name: name, code: code);
|
||||
}
|
||||
|
||||
Map<String, dynamic>? _asMap(dynamic value) =>
|
||||
value is Map<String, dynamic> ? value : null;
|
||||
|
||||
String? _readString(
|
||||
Map<String, dynamic>? source,
|
||||
String key, {
|
||||
String? fallback,
|
||||
}) {
|
||||
if (source == null) return fallback;
|
||||
final value = source[key];
|
||||
if (value is String) return value;
|
||||
if (value == null) return fallback;
|
||||
return value.toString();
|
||||
}
|
||||
|
||||
DateTime? _parseDate(Object? value) {
|
||||
if (value == null) return null;
|
||||
if (value is DateTime) return value;
|
||||
if (value is String) return DateTime.tryParse(value);
|
||||
return null;
|
||||
}
|
||||
|
||||
String? _firstNonEmpty(Iterable<String?> values) {
|
||||
for (final value in values) {
|
||||
if (value == null) {
|
||||
continue;
|
||||
}
|
||||
final trimmed = value.trim();
|
||||
if (trimmed.isNotEmpty) {
|
||||
return trimmed;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
268
lib/features/approvals/data/dtos/approval_draft_dto.dart
Normal file
268
lib/features/approvals/data/dtos/approval_draft_dto.dart
Normal file
@@ -0,0 +1,268 @@
|
||||
import 'package:superport_v2/core/common/models/paginated_result.dart';
|
||||
import 'package:superport_v2/core/common/utils/json_utils.dart';
|
||||
|
||||
import '../../domain/entities/approval_draft.dart';
|
||||
|
||||
/// 결재 초안 단계 DTO.
|
||||
class ApprovalDraftStepDto {
|
||||
ApprovalDraftStepDto({
|
||||
required this.stepOrder,
|
||||
required this.approverId,
|
||||
this.approverRole,
|
||||
this.note,
|
||||
this.isOptional = false,
|
||||
});
|
||||
|
||||
final int stepOrder;
|
||||
final int approverId;
|
||||
final String? approverRole;
|
||||
final String? note;
|
||||
final bool isOptional;
|
||||
|
||||
factory ApprovalDraftStepDto.fromJson(Map<String, dynamic> json) {
|
||||
return ApprovalDraftStepDto(
|
||||
stepOrder: JsonUtils.readInt(json, 'step_order', fallback: 0),
|
||||
approverId: JsonUtils.readInt(json, 'approver_id', fallback: 0),
|
||||
approverRole: _readString(json['approver_role']),
|
||||
note: _readString(json['note']),
|
||||
isOptional: json['is_optional'] is bool
|
||||
? json['is_optional'] as bool
|
||||
: false,
|
||||
);
|
||||
}
|
||||
|
||||
ApprovalDraftStep toEntity() => ApprovalDraftStep(
|
||||
stepOrder: stepOrder,
|
||||
approverId: approverId,
|
||||
approverRole: approverRole,
|
||||
note: note,
|
||||
isOptional: isOptional,
|
||||
);
|
||||
}
|
||||
|
||||
/// 결재 초안 페이로드 DTO.
|
||||
class ApprovalDraftPayloadDto {
|
||||
ApprovalDraftPayloadDto({
|
||||
this.title,
|
||||
this.summary,
|
||||
this.note,
|
||||
this.templateId,
|
||||
this.metadata,
|
||||
this.steps = const [],
|
||||
});
|
||||
|
||||
final String? title;
|
||||
final String? summary;
|
||||
final String? note;
|
||||
final int? templateId;
|
||||
final Map<String, dynamic>? metadata;
|
||||
final List<ApprovalDraftStepDto> steps;
|
||||
|
||||
factory ApprovalDraftPayloadDto.fromJson(Map<String, dynamic> json) {
|
||||
final steps = (json['steps'] as List<dynamic>? ?? const [])
|
||||
.whereType<Map<String, dynamic>>()
|
||||
.map(ApprovalDraftStepDto.fromJson)
|
||||
.toList(growable: false);
|
||||
final metadata = json['metadata'] is Map<String, dynamic>
|
||||
? Map<String, dynamic>.from(json['metadata'] as Map)
|
||||
: null;
|
||||
return ApprovalDraftPayloadDto(
|
||||
title: _readString(json['title']),
|
||||
summary: _readString(json['summary']),
|
||||
note: _readString(json['note']),
|
||||
templateId: json['template_id'] as int?,
|
||||
metadata: metadata,
|
||||
steps: steps,
|
||||
);
|
||||
}
|
||||
|
||||
ApprovalDraftPayload toEntity() => ApprovalDraftPayload(
|
||||
title: title,
|
||||
summary: summary,
|
||||
note: note,
|
||||
templateId: templateId,
|
||||
metadata: metadata,
|
||||
steps: steps.map((step) => step.toEntity()).toList(growable: false),
|
||||
);
|
||||
}
|
||||
|
||||
/// 결재 초안 요약 DTO.
|
||||
class ApprovalDraftSummaryDto {
|
||||
ApprovalDraftSummaryDto({
|
||||
required this.id,
|
||||
required this.requesterId,
|
||||
required this.status,
|
||||
required this.savedAt,
|
||||
this.requestId,
|
||||
this.transactionId,
|
||||
this.templateId,
|
||||
this.title,
|
||||
this.summary,
|
||||
this.expiresAt,
|
||||
this.sessionKey,
|
||||
this.stepCount = 0,
|
||||
});
|
||||
|
||||
final int id;
|
||||
final int requesterId;
|
||||
final ApprovalDraftStatus status;
|
||||
final DateTime savedAt;
|
||||
final int? requestId;
|
||||
final int? transactionId;
|
||||
final int? templateId;
|
||||
final String? title;
|
||||
final String? summary;
|
||||
final DateTime? expiresAt;
|
||||
final String? sessionKey;
|
||||
final int stepCount;
|
||||
|
||||
factory ApprovalDraftSummaryDto.fromJson(Map<String, dynamic> json) {
|
||||
final savedAtRaw = json['saved_at'];
|
||||
final expiresAtRaw = json['expires_at'];
|
||||
return ApprovalDraftSummaryDto(
|
||||
id: JsonUtils.readInt(json, 'id', fallback: 0),
|
||||
requesterId: JsonUtils.readInt(json, 'requester_id', fallback: 0),
|
||||
status: _parseStatus(_readString(json['status'])),
|
||||
savedAt: _parseDate(savedAtRaw) ?? DateTime.now().toUtc(),
|
||||
requestId: json['request_id'] as int?,
|
||||
transactionId: json['transaction_id'] as int?,
|
||||
templateId: json['template_id'] as int?,
|
||||
title: _readString(json['title']),
|
||||
summary: _readString(json['summary']),
|
||||
expiresAt: _parseDate(expiresAtRaw),
|
||||
sessionKey: _readString(json['session_key']),
|
||||
stepCount: JsonUtils.readInt(json, 'step_count', fallback: 0),
|
||||
);
|
||||
}
|
||||
|
||||
ApprovalDraftSummary toEntity() => ApprovalDraftSummary(
|
||||
id: id,
|
||||
requesterId: requesterId,
|
||||
status: status,
|
||||
savedAt: savedAt,
|
||||
requestId: requestId,
|
||||
transactionId: transactionId,
|
||||
templateId: templateId,
|
||||
title: title,
|
||||
summary: summary,
|
||||
expiresAt: expiresAt,
|
||||
sessionKey: sessionKey,
|
||||
stepCount: stepCount,
|
||||
);
|
||||
}
|
||||
|
||||
/// 결재 초안 상세 DTO.
|
||||
class ApprovalDraftDetailDto {
|
||||
ApprovalDraftDetailDto({
|
||||
required this.id,
|
||||
required this.requesterId,
|
||||
required this.payload,
|
||||
required this.savedAt,
|
||||
this.transactionId,
|
||||
this.templateId,
|
||||
this.expiresAt,
|
||||
this.sessionKey,
|
||||
});
|
||||
|
||||
final int id;
|
||||
final int requesterId;
|
||||
final ApprovalDraftPayloadDto payload;
|
||||
final DateTime savedAt;
|
||||
final int? transactionId;
|
||||
final int? templateId;
|
||||
final DateTime? expiresAt;
|
||||
final String? sessionKey;
|
||||
|
||||
factory ApprovalDraftDetailDto.fromJson(Map<String, dynamic> json) {
|
||||
final payloadMap = json['payload'] is Map<String, dynamic>
|
||||
? json['payload'] as Map<String, dynamic>
|
||||
: const <String, dynamic>{};
|
||||
return ApprovalDraftDetailDto(
|
||||
id: JsonUtils.readInt(json, 'id', fallback: 0),
|
||||
requesterId: JsonUtils.readInt(json, 'requester_id', fallback: 0),
|
||||
payload: ApprovalDraftPayloadDto.fromJson(payloadMap),
|
||||
savedAt: _parseDate(json['saved_at']) ?? DateTime.now().toUtc(),
|
||||
transactionId: json['transaction_id'] as int?,
|
||||
templateId: json['template_id'] as int?,
|
||||
expiresAt: _parseDate(json['expires_at']),
|
||||
sessionKey: _readString(json['session_key']),
|
||||
);
|
||||
}
|
||||
|
||||
ApprovalDraftDetail toEntity() => ApprovalDraftDetail(
|
||||
id: id,
|
||||
requesterId: requesterId,
|
||||
payload: payload.toEntity(),
|
||||
savedAt: savedAt,
|
||||
transactionId: transactionId,
|
||||
templateId: templateId,
|
||||
expiresAt: expiresAt,
|
||||
sessionKey: sessionKey,
|
||||
);
|
||||
}
|
||||
|
||||
class ApprovalDraftDto {
|
||||
ApprovalDraftDto._();
|
||||
|
||||
static PaginatedResult<ApprovalDraftSummary> parsePaginated(
|
||||
Map<String, dynamic>? json,
|
||||
) {
|
||||
final items = JsonUtils.extractList(json)
|
||||
.map(ApprovalDraftSummaryDto.fromJson)
|
||||
.map((dto) => dto.toEntity())
|
||||
.toList(growable: false);
|
||||
return PaginatedResult<ApprovalDraftSummary>(
|
||||
items: items,
|
||||
page: JsonUtils.readInt(json, 'page', fallback: 1),
|
||||
pageSize: JsonUtils.readInt(json, 'page_size', fallback: items.length),
|
||||
total: JsonUtils.readInt(json, 'total', fallback: items.length),
|
||||
);
|
||||
}
|
||||
|
||||
static ApprovalDraftDetail? parseDetail(Map<String, dynamic>? json) {
|
||||
if (json == null || json.isEmpty) {
|
||||
return null;
|
||||
}
|
||||
final map = JsonUtils.extractMap(json);
|
||||
if (map.isEmpty) {
|
||||
return null;
|
||||
}
|
||||
return ApprovalDraftDetailDto.fromJson(map).toEntity();
|
||||
}
|
||||
}
|
||||
|
||||
String? _readString(dynamic value) {
|
||||
if (value == null) {
|
||||
return null;
|
||||
}
|
||||
if (value is String) {
|
||||
final trimmed = value.trim();
|
||||
return trimmed.isEmpty ? null : trimmed;
|
||||
}
|
||||
return value.toString();
|
||||
}
|
||||
|
||||
DateTime? _parseDate(dynamic value) {
|
||||
if (value == null) {
|
||||
return null;
|
||||
}
|
||||
if (value is DateTime) {
|
||||
return value.toUtc();
|
||||
}
|
||||
if (value is String && value.isNotEmpty) {
|
||||
return DateTime.tryParse(value)?.toUtc();
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
ApprovalDraftStatus _parseStatus(String? value) {
|
||||
switch (value) {
|
||||
case 'expired':
|
||||
return ApprovalDraftStatus.expired;
|
||||
case 'archived':
|
||||
return ApprovalDraftStatus.archived;
|
||||
case 'active':
|
||||
default:
|
||||
return ApprovalDraftStatus.active;
|
||||
}
|
||||
}
|
||||
@@ -2,6 +2,8 @@ import 'package:superport_v2/core/common/models/paginated_result.dart';
|
||||
import 'package:superport_v2/core/common/utils/json_utils.dart';
|
||||
|
||||
import '../../domain/entities/approval.dart';
|
||||
import 'approval_audit_dto.dart';
|
||||
import 'approval_step_dto.dart';
|
||||
|
||||
/// 결재 API 응답을 표현하는 DTO.
|
||||
///
|
||||
@@ -11,7 +13,9 @@ class ApprovalDto {
|
||||
ApprovalDto({
|
||||
this.id,
|
||||
required this.approvalNo,
|
||||
this.transactionId,
|
||||
this.transactionNo,
|
||||
this.transactionUpdatedAt,
|
||||
required this.status,
|
||||
this.currentStep,
|
||||
required this.requester,
|
||||
@@ -28,7 +32,9 @@ class ApprovalDto {
|
||||
|
||||
final int? id;
|
||||
final String approvalNo;
|
||||
final int? transactionId;
|
||||
final String? transactionNo;
|
||||
final DateTime? transactionUpdatedAt;
|
||||
final ApprovalStatusDto status;
|
||||
final ApprovalStepDto? currentStep;
|
||||
final ApprovalRequesterDto requester;
|
||||
@@ -38,7 +44,7 @@ class ApprovalDto {
|
||||
final bool isActive;
|
||||
final bool isDeleted;
|
||||
final List<ApprovalStepDto> steps;
|
||||
final List<ApprovalHistoryDto> histories;
|
||||
final List<ApprovalAuditDto> histories;
|
||||
final DateTime? createdAt;
|
||||
final DateTime? updatedAt;
|
||||
|
||||
@@ -51,7 +57,7 @@ class ApprovalDto {
|
||||
approvalEnvelope['status'],
|
||||
approvalEnvelope['approval_status'],
|
||||
]);
|
||||
final requesterMap = _firstNonEmptyMap([
|
||||
final rawRequesterMap = _firstNonEmptyMap([
|
||||
json['requester'],
|
||||
json['requested_by'],
|
||||
approvalEnvelope['requester'],
|
||||
@@ -86,14 +92,29 @@ class ApprovalDto {
|
||||
[json, transactionMap, approvalEnvelope, envelopeTransactionMap],
|
||||
const ['transaction_no', 'transactionNo'],
|
||||
);
|
||||
final transactionId =
|
||||
json['transaction_id'] as int? ??
|
||||
approvalEnvelope['transaction_id'] as int? ??
|
||||
transactionMap['id'] as int? ??
|
||||
envelopeTransactionMap['id'] as int?;
|
||||
final transactionUpdatedAt = _parseDate(
|
||||
transactionMap['updated_at'] ??
|
||||
envelopeTransactionMap['updated_at'] ??
|
||||
json['transaction_updated_at'] ??
|
||||
approvalEnvelope['transaction_updated_at'],
|
||||
);
|
||||
|
||||
return ApprovalDto(
|
||||
id: json['id'] as int? ?? approvalEnvelope['id'] as int?,
|
||||
approvalNo: approvalNo,
|
||||
transactionId: transactionId,
|
||||
transactionNo: transactionNo,
|
||||
transactionUpdatedAt: transactionUpdatedAt,
|
||||
status: ApprovalStatusDto.fromJson(statusMap),
|
||||
currentStep: currentStepDto,
|
||||
requester: ApprovalRequesterDto.fromJson(requesterMap),
|
||||
requester: ApprovalRequesterDto.fromJson(
|
||||
_resolveRequesterMap(json, approvalEnvelope, rawRequesterMap),
|
||||
),
|
||||
requestedAt:
|
||||
_parseDate(
|
||||
json['requested_at'] ?? approvalEnvelope['requested_at'],
|
||||
@@ -113,7 +134,7 @@ class ApprovalDto {
|
||||
false,
|
||||
steps: stepsSource.map(ApprovalStepDto.fromJson).toList(growable: false),
|
||||
histories: historiesSource
|
||||
.map(ApprovalHistoryDto.fromJson)
|
||||
.map(ApprovalAuditDto.fromJson)
|
||||
.toList(growable: false),
|
||||
createdAt: _parseDate(
|
||||
json['created_at'] ?? approvalEnvelope['created_at'],
|
||||
@@ -128,7 +149,9 @@ class ApprovalDto {
|
||||
Approval toEntity() => Approval(
|
||||
id: id,
|
||||
approvalNo: approvalNo,
|
||||
transactionId: transactionId,
|
||||
transactionNo: transactionNo ?? '-',
|
||||
transactionUpdatedAt: transactionUpdatedAt,
|
||||
status: status.toEntity(),
|
||||
currentStep: currentStep?.toEntity(),
|
||||
requester: requester.toEntity(),
|
||||
@@ -159,38 +182,6 @@ class ApprovalDto {
|
||||
}
|
||||
}
|
||||
|
||||
/// 결재 상태(Status) DTO.
|
||||
class ApprovalStatusDto {
|
||||
ApprovalStatusDto({required this.id, required this.name, this.color});
|
||||
|
||||
final int id;
|
||||
final String name;
|
||||
final String? color;
|
||||
|
||||
factory ApprovalStatusDto.fromJson(Map<String, dynamic> json) {
|
||||
if (json['status'] is Map<String, dynamic>) {
|
||||
return ApprovalStatusDto.fromJson(json['status'] as Map<String, dynamic>);
|
||||
}
|
||||
return ApprovalStatusDto(
|
||||
id:
|
||||
json['id'] as int? ??
|
||||
json['status_id'] as int? ??
|
||||
json['approval_status_id'] as int? ??
|
||||
0,
|
||||
name:
|
||||
_readString(json['name']) ??
|
||||
_readString(json['status_name']) ??
|
||||
_readString(json['approval_status_name']) ??
|
||||
_readString(json['status']) ??
|
||||
'-',
|
||||
color: _readString(json['color']),
|
||||
);
|
||||
}
|
||||
|
||||
/// DTO를 [ApprovalStatus]로 변환한다.
|
||||
ApprovalStatus toEntity() => ApprovalStatus(id: id, name: name, color: color);
|
||||
}
|
||||
|
||||
/// 결재 요청자 DTO.
|
||||
class ApprovalRequesterDto {
|
||||
ApprovalRequesterDto({
|
||||
@@ -205,8 +196,11 @@ class ApprovalRequesterDto {
|
||||
|
||||
factory ApprovalRequesterDto.fromJson(Map<String, dynamic> json) {
|
||||
return ApprovalRequesterDto(
|
||||
id: json['id'] as int? ?? json['employee_id'] as int? ?? 0,
|
||||
employeeNo: _readString(json['employee_no']) ?? '-',
|
||||
id: JsonUtils.readInt(json, 'id', fallback: 0),
|
||||
employeeNo:
|
||||
_readString(json['employee_no']) ??
|
||||
_readString(json['employee_id']) ??
|
||||
'-',
|
||||
name:
|
||||
_readString(json['name']) ??
|
||||
_readString(json['employee_name']) ??
|
||||
@@ -219,194 +213,6 @@ class ApprovalRequesterDto {
|
||||
ApprovalRequester(id: id, employeeNo: employeeNo, name: name);
|
||||
}
|
||||
|
||||
/// 결재 승인자 DTO.
|
||||
class ApprovalApproverDto {
|
||||
ApprovalApproverDto({
|
||||
required this.id,
|
||||
required this.employeeNo,
|
||||
required this.name,
|
||||
});
|
||||
|
||||
final int id;
|
||||
final String employeeNo;
|
||||
final String name;
|
||||
|
||||
factory ApprovalApproverDto.fromJson(Map<String, dynamic> json) {
|
||||
return ApprovalApproverDto(
|
||||
id: json['id'] as int? ?? json['approver_id'] as int? ?? 0,
|
||||
employeeNo: _readString(json['employee_no']) ?? '-',
|
||||
name:
|
||||
_readString(json['name']) ??
|
||||
_readString(json['employee_name']) ??
|
||||
'-',
|
||||
);
|
||||
}
|
||||
|
||||
/// DTO를 [ApprovalApprover]로 변환한다.
|
||||
ApprovalApprover toEntity() =>
|
||||
ApprovalApprover(id: id, employeeNo: employeeNo, name: name);
|
||||
}
|
||||
|
||||
/// 결재 단계 DTO.
|
||||
class ApprovalStepDto {
|
||||
ApprovalStepDto({
|
||||
this.id,
|
||||
required this.stepOrder,
|
||||
required this.approver,
|
||||
required this.status,
|
||||
required this.assignedAt,
|
||||
this.decidedAt,
|
||||
this.note,
|
||||
this.isDeleted = false,
|
||||
});
|
||||
|
||||
final int? id;
|
||||
final int stepOrder;
|
||||
final ApprovalApproverDto approver;
|
||||
final ApprovalStatusDto status;
|
||||
final DateTime assignedAt;
|
||||
final DateTime? decidedAt;
|
||||
final String? note;
|
||||
final bool isDeleted;
|
||||
|
||||
factory ApprovalStepDto.fromJson(Map<String, dynamic> json) {
|
||||
return ApprovalStepDto(
|
||||
id: json['id'] as int?,
|
||||
stepOrder: json['step_order'] as int? ?? 0,
|
||||
approver: ApprovalApproverDto.fromJson(
|
||||
(json['approver'] as Map<String, dynamic>? ?? const {}),
|
||||
),
|
||||
status: ApprovalStatusDto.fromJson(
|
||||
(json['status'] as Map<String, dynamic>? ??
|
||||
json['step_status'] as Map<String, dynamic>? ??
|
||||
json['approval_status'] as Map<String, dynamic>? ??
|
||||
const {}),
|
||||
),
|
||||
assignedAt: _parseDate(json['assigned_at']) ?? DateTime.now(),
|
||||
decidedAt: _parseDate(json['decided_at']),
|
||||
note: _readString(json['note']),
|
||||
isDeleted:
|
||||
json['is_deleted'] as bool? ??
|
||||
(json['deleted_at'] != null ||
|
||||
(json['is_active'] is bool && !(json['is_active'] as bool))),
|
||||
);
|
||||
}
|
||||
|
||||
/// DTO를 [ApprovalStep]으로 변환한다.
|
||||
ApprovalStep toEntity() => ApprovalStep(
|
||||
id: id,
|
||||
stepOrder: stepOrder,
|
||||
approver: approver.toEntity(),
|
||||
status: status.toEntity(),
|
||||
assignedAt: assignedAt,
|
||||
decidedAt: decidedAt,
|
||||
note: note,
|
||||
isDeleted: isDeleted,
|
||||
);
|
||||
}
|
||||
|
||||
/// 결재 이력 DTO.
|
||||
class ApprovalHistoryDto {
|
||||
ApprovalHistoryDto({
|
||||
this.id,
|
||||
required this.action,
|
||||
this.fromStatus,
|
||||
required this.toStatus,
|
||||
required this.approver,
|
||||
required this.actionAt,
|
||||
this.note,
|
||||
});
|
||||
|
||||
final int? id;
|
||||
final ApprovalActionDto action;
|
||||
final ApprovalStatusDto? fromStatus;
|
||||
final ApprovalStatusDto toStatus;
|
||||
final ApprovalApproverDto approver;
|
||||
final DateTime actionAt;
|
||||
final String? note;
|
||||
|
||||
factory ApprovalHistoryDto.fromJson(Map<String, dynamic> json) {
|
||||
final actionMap = _firstNonEmptyMap([
|
||||
json['action'],
|
||||
json['approval_action'],
|
||||
json['step_action'],
|
||||
]);
|
||||
final fromStatusMap = _firstNonEmptyMap([
|
||||
json['from_status'],
|
||||
json['fromStatus'],
|
||||
]);
|
||||
final toStatusMap = _firstNonEmptyMap([
|
||||
json['to_status'],
|
||||
json['toStatus'],
|
||||
]);
|
||||
final approverMap = _firstNonEmptyMap([json['approver'], json['employee']]);
|
||||
final fallbackAction = {
|
||||
'id': json['approval_action_id'] ?? json['action_id'],
|
||||
'name':
|
||||
_readString(json['approval_action_name']) ??
|
||||
_readString(json['action_name']) ??
|
||||
_readString(json['action']) ??
|
||||
'-',
|
||||
};
|
||||
|
||||
return ApprovalHistoryDto(
|
||||
id: json['id'] as int?,
|
||||
action: ApprovalActionDto.fromJson(
|
||||
actionMap.isEmpty ? fallbackAction : actionMap,
|
||||
),
|
||||
fromStatus: fromStatusMap.isEmpty
|
||||
? null
|
||||
: ApprovalStatusDto.fromJson(fromStatusMap),
|
||||
toStatus: ApprovalStatusDto.fromJson(toStatusMap),
|
||||
approver: ApprovalApproverDto.fromJson(approverMap),
|
||||
actionAt:
|
||||
_parseDate(json['action_at'] ?? json['actionAt']) ?? DateTime.now(),
|
||||
note: _readString(json['note']),
|
||||
);
|
||||
}
|
||||
|
||||
/// DTO를 [ApprovalHistory]로 변환한다.
|
||||
ApprovalHistory toEntity() => ApprovalHistory(
|
||||
id: id,
|
||||
action: action.toEntity(),
|
||||
fromStatus: fromStatus?.toEntity(),
|
||||
toStatus: toStatus.toEntity(),
|
||||
approver: approver.toEntity(),
|
||||
actionAt: actionAt,
|
||||
note: note,
|
||||
);
|
||||
}
|
||||
|
||||
/// 결재 행위(Action) DTO.
|
||||
class ApprovalActionDto {
|
||||
ApprovalActionDto({required this.id, required this.name});
|
||||
|
||||
final int id;
|
||||
final String name;
|
||||
|
||||
factory ApprovalActionDto.fromJson(Map<String, dynamic> json) {
|
||||
if (json['action'] is Map<String, dynamic>) {
|
||||
return ApprovalActionDto.fromJson(json['action'] as Map<String, dynamic>);
|
||||
}
|
||||
return ApprovalActionDto(
|
||||
id:
|
||||
json['id'] as int? ??
|
||||
json['action_id'] as int? ??
|
||||
json['approval_action_id'] as int? ??
|
||||
0,
|
||||
name:
|
||||
_readString(json['name']) ??
|
||||
_readString(json['action_name']) ??
|
||||
_readString(json['approval_action_name']) ??
|
||||
_readString(json['action']) ??
|
||||
'-',
|
||||
);
|
||||
}
|
||||
|
||||
/// DTO를 [ApprovalAction]으로 변환한다.
|
||||
ApprovalAction toEntity() => ApprovalAction(id: id, name: name);
|
||||
}
|
||||
|
||||
List<Map<String, dynamic>> _asListOfMap(dynamic value) {
|
||||
if (value is List) {
|
||||
return value.whereType<Map<String, dynamic>>().toList(growable: false);
|
||||
@@ -426,6 +232,60 @@ Map<String, dynamic> _firstNonEmptyMap(List<dynamic> candidates) {
|
||||
return const {};
|
||||
}
|
||||
|
||||
Map<String, dynamic> _resolveRequesterMap(
|
||||
Map<String, dynamic> root,
|
||||
Map<String, dynamic> envelope,
|
||||
Map<String, dynamic> candidate,
|
||||
) {
|
||||
if (candidate.isNotEmpty) {
|
||||
return candidate;
|
||||
}
|
||||
final resolved = <String, dynamic>{};
|
||||
final rootRequestedBy = _mapOrEmpty(root['requested_by']);
|
||||
if (rootRequestedBy.isNotEmpty) {
|
||||
resolved.addAll(rootRequestedBy);
|
||||
}
|
||||
final envelopeRequestedBy = _mapOrEmpty(envelope['requested_by']);
|
||||
if (resolved.isEmpty && envelopeRequestedBy.isNotEmpty) {
|
||||
resolved.addAll(envelopeRequestedBy);
|
||||
} else if (envelopeRequestedBy.isNotEmpty) {
|
||||
for (final entry in envelopeRequestedBy.entries) {
|
||||
resolved.putIfAbsent(entry.key, () => entry.value);
|
||||
}
|
||||
}
|
||||
|
||||
final fallbackId = _pickInt(
|
||||
[resolved, rootRequestedBy, envelopeRequestedBy, root, envelope],
|
||||
const ['requester_id', 'requested_by_id', 'id'],
|
||||
);
|
||||
if (fallbackId != null) {
|
||||
resolved['id'] = fallbackId;
|
||||
}
|
||||
|
||||
final fallbackEmployeeNo = _pickString(
|
||||
[resolved, rootRequestedBy, envelopeRequestedBy, root, envelope],
|
||||
const [
|
||||
'employee_no',
|
||||
'employee_id',
|
||||
'requester_employee_no',
|
||||
'requested_by_employee_no',
|
||||
],
|
||||
);
|
||||
if (fallbackEmployeeNo != null) {
|
||||
resolved['employee_no'] = fallbackEmployeeNo;
|
||||
}
|
||||
|
||||
final fallbackName = _pickString(
|
||||
[resolved, rootRequestedBy, envelopeRequestedBy, root, envelope],
|
||||
const ['name', 'employee_name', 'requester_name', 'requested_by_name'],
|
||||
);
|
||||
if (fallbackName != null) {
|
||||
resolved['name'] = fallbackName;
|
||||
}
|
||||
|
||||
return resolved;
|
||||
}
|
||||
|
||||
String? _pickString(List<dynamic> sources, List<String> keys) {
|
||||
for (final source in sources) {
|
||||
if (source is Map<String, dynamic>) {
|
||||
@@ -440,6 +300,29 @@ String? _pickString(List<dynamic> sources, List<String> keys) {
|
||||
return null;
|
||||
}
|
||||
|
||||
int? _pickInt(List<dynamic> sources, List<String> keys) {
|
||||
for (final source in sources) {
|
||||
if (source is Map<String, dynamic>) {
|
||||
for (final key in keys) {
|
||||
final value = source[key];
|
||||
if (value is int) {
|
||||
return value;
|
||||
}
|
||||
if (value is num) {
|
||||
return value.toInt();
|
||||
}
|
||||
if (value is String) {
|
||||
final parsed = int.tryParse(value);
|
||||
if (parsed != null) {
|
||||
return parsed;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
/// 문자열/DateTime 입력을 DateTime으로 변환한다.
|
||||
DateTime? _parseDate(Object? value) {
|
||||
if (value == null) return null;
|
||||
|
||||
@@ -21,8 +21,8 @@ class ApprovalProceedStatusDto {
|
||||
}
|
||||
|
||||
ApprovalProceedStatus toEntity() => ApprovalProceedStatus(
|
||||
approvalId: approvalId,
|
||||
canProceed: canProceed,
|
||||
reason: reason,
|
||||
);
|
||||
approvalId: approvalId,
|
||||
canProceed: canProceed,
|
||||
reason: reason,
|
||||
);
|
||||
}
|
||||
|
||||
254
lib/features/approvals/data/dtos/approval_request_dto.dart
Normal file
254
lib/features/approvals/data/dtos/approval_request_dto.dart
Normal file
@@ -0,0 +1,254 @@
|
||||
import 'package:superport_v2/core/common/utils/json_utils.dart';
|
||||
|
||||
import '../../domain/entities/approval.dart';
|
||||
import 'approval_audit_dto.dart';
|
||||
|
||||
/// 결재 상신(Submit) 요청 DTO.
|
||||
class ApprovalSubmitRequestDto {
|
||||
ApprovalSubmitRequestDto({required this.approval, required this.steps});
|
||||
|
||||
final ApprovalCreatePayloadDto approval;
|
||||
final List<ApprovalStepInputDto> steps;
|
||||
|
||||
Map<String, dynamic> toJson() {
|
||||
return {
|
||||
'approval': approval.toJson(),
|
||||
'steps': steps.map((e) => e.toJson()).toList(growable: false),
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
/// 결재 재상신 요청 DTO.
|
||||
class ApprovalResubmitRequestDto {
|
||||
ApprovalResubmitRequestDto({
|
||||
required this.approvalId,
|
||||
required this.actorId,
|
||||
required this.steps,
|
||||
this.note,
|
||||
this.expectedUpdatedAt,
|
||||
this.transactionExpectedUpdatedAt,
|
||||
});
|
||||
|
||||
final int approvalId;
|
||||
final int actorId;
|
||||
final List<ApprovalStepInputDto> steps;
|
||||
final String? note;
|
||||
final DateTime? expectedUpdatedAt;
|
||||
final DateTime? transactionExpectedUpdatedAt;
|
||||
|
||||
Map<String, dynamic> toJson() {
|
||||
final sanitizedNote = note?.trim();
|
||||
return {
|
||||
'approval_id': approvalId,
|
||||
'actor_id': actorId,
|
||||
'steps': steps.map((e) => e.toJson()).toList(growable: false),
|
||||
if (sanitizedNote != null && sanitizedNote.isNotEmpty)
|
||||
'note': sanitizedNote,
|
||||
if (expectedUpdatedAt != null)
|
||||
'expected_updated_at': expectedUpdatedAt!.toUtc().toIso8601String(),
|
||||
if (transactionExpectedUpdatedAt != null)
|
||||
'transaction_expected_updated_at': transactionExpectedUpdatedAt!
|
||||
.toUtc()
|
||||
.toIso8601String(),
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
/// 결재 승인/반려 요청 DTO.
|
||||
class ApprovalDecisionRequestDto {
|
||||
ApprovalDecisionRequestDto({
|
||||
required this.approvalId,
|
||||
required this.actorId,
|
||||
this.note,
|
||||
this.expectedUpdatedAt,
|
||||
});
|
||||
|
||||
final int approvalId;
|
||||
final int actorId;
|
||||
final String? note;
|
||||
final DateTime? expectedUpdatedAt;
|
||||
|
||||
Map<String, dynamic> toJson() {
|
||||
final sanitizedNote = note?.trim();
|
||||
return {
|
||||
'approval_id': approvalId,
|
||||
'actor_id': actorId,
|
||||
if (sanitizedNote != null && sanitizedNote.isNotEmpty)
|
||||
'note': sanitizedNote,
|
||||
if (expectedUpdatedAt != null)
|
||||
'expected_updated_at': expectedUpdatedAt!.toUtc().toIso8601String(),
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
/// 결재 회수 요청 DTO.
|
||||
class ApprovalRecallRequestDto {
|
||||
ApprovalRecallRequestDto({
|
||||
required this.approvalId,
|
||||
required this.actorId,
|
||||
this.note,
|
||||
this.expectedUpdatedAt,
|
||||
this.transactionExpectedUpdatedAt,
|
||||
});
|
||||
|
||||
final int approvalId;
|
||||
final int actorId;
|
||||
final String? note;
|
||||
final DateTime? expectedUpdatedAt;
|
||||
final DateTime? transactionExpectedUpdatedAt;
|
||||
|
||||
Map<String, dynamic> toJson() {
|
||||
final sanitizedNote = note?.trim();
|
||||
return {
|
||||
'approval_id': approvalId,
|
||||
'actor_id': actorId,
|
||||
if (sanitizedNote != null && sanitizedNote.isNotEmpty)
|
||||
'note': sanitizedNote,
|
||||
if (expectedUpdatedAt != null)
|
||||
'expected_updated_at': expectedUpdatedAt!.toUtc().toIso8601String(),
|
||||
if (transactionExpectedUpdatedAt != null)
|
||||
'transaction_expected_updated_at': transactionExpectedUpdatedAt!
|
||||
.toUtc()
|
||||
.toIso8601String(),
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
/// 결재 본문 생성 DTO.
|
||||
class ApprovalCreatePayloadDto {
|
||||
ApprovalCreatePayloadDto({
|
||||
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,
|
||||
});
|
||||
|
||||
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;
|
||||
|
||||
Map<String, dynamic> toJson() {
|
||||
final sanitizedNote = note?.trim();
|
||||
final sanitizedTitle = title?.trim();
|
||||
final sanitizedSummary = summary?.trim();
|
||||
return {
|
||||
'transaction_id': transactionId,
|
||||
'template_id': templateId,
|
||||
'approval_status_id': statusId,
|
||||
'requested_by_id': requesterId,
|
||||
'final_approver_id': finalApproverId,
|
||||
if (requestedAt != null)
|
||||
'requested_at': requestedAt!.toUtc().toIso8601String(),
|
||||
if (decidedAt != null) 'decided_at': decidedAt!.toUtc().toIso8601String(),
|
||||
if (cancelledAt != null)
|
||||
'cancelled_at': cancelledAt!.toUtc().toIso8601String(),
|
||||
if (lastActionAt != null)
|
||||
'last_action_at': lastActionAt!.toUtc().toIso8601String(),
|
||||
if (sanitizedTitle != null && sanitizedTitle.isNotEmpty)
|
||||
'title': sanitizedTitle,
|
||||
if (sanitizedSummary != null && sanitizedSummary.isNotEmpty)
|
||||
'summary': sanitizedSummary,
|
||||
if (sanitizedNote != null && sanitizedNote.isNotEmpty)
|
||||
'note': sanitizedNote,
|
||||
if (metadata != null && metadata!.isNotEmpty) 'metadata': metadata,
|
||||
};
|
||||
}
|
||||
|
||||
factory ApprovalCreatePayloadDto.fromSubmission(
|
||||
ApprovalSubmissionInput input,
|
||||
) {
|
||||
return ApprovalCreatePayloadDto(
|
||||
transactionId: input.transactionId,
|
||||
templateId: input.templateId,
|
||||
statusId: input.statusId,
|
||||
requesterId: input.requesterId,
|
||||
finalApproverId: input.finalApproverId,
|
||||
requestedAt: input.requestedAt,
|
||||
decidedAt: input.decidedAt,
|
||||
cancelledAt: input.cancelledAt,
|
||||
lastActionAt: input.lastActionAt,
|
||||
title: input.title,
|
||||
summary: input.summary,
|
||||
note: input.note,
|
||||
metadata: input.metadata,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/// 결재 단계 생성 입력 DTO.
|
||||
class ApprovalStepInputDto {
|
||||
ApprovalStepInputDto({
|
||||
required this.stepOrder,
|
||||
required this.approverId,
|
||||
this.note,
|
||||
});
|
||||
|
||||
final int stepOrder;
|
||||
final int approverId;
|
||||
final String? note;
|
||||
|
||||
factory ApprovalStepInputDto.fromDomain(ApprovalStepAssignmentItem item) {
|
||||
return ApprovalStepInputDto(
|
||||
stepOrder: item.stepOrder,
|
||||
approverId: item.approverId,
|
||||
note: item.note,
|
||||
);
|
||||
}
|
||||
|
||||
Map<String, dynamic> toJson() {
|
||||
final sanitizedNote = note?.trim();
|
||||
return {
|
||||
'step_order': stepOrder,
|
||||
'approver_id': approverId,
|
||||
if (sanitizedNote != null && sanitizedNote.isNotEmpty)
|
||||
'note': sanitizedNote,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
/// 감사 로그 리스트 응답 DTO.
|
||||
class ApprovalAuditListDto {
|
||||
ApprovalAuditListDto({
|
||||
required this.items,
|
||||
required this.page,
|
||||
required this.pageSize,
|
||||
required this.total,
|
||||
});
|
||||
|
||||
final List<ApprovalAuditDto> items;
|
||||
final int page;
|
||||
final int pageSize;
|
||||
final int total;
|
||||
|
||||
factory ApprovalAuditListDto.fromJson(Map<String, dynamic>? json) {
|
||||
final rawItems = JsonUtils.extractList(json, keys: const ['items']);
|
||||
final items = rawItems
|
||||
.map((item) => ApprovalAuditDto.fromJson(item))
|
||||
.toList(growable: false);
|
||||
return ApprovalAuditListDto(
|
||||
items: items,
|
||||
page: JsonUtils.readInt(json, 'page', fallback: 1),
|
||||
pageSize: JsonUtils.readInt(json, 'page_size', fallback: items.length),
|
||||
total: JsonUtils.readInt(json, 'total', fallback: items.length),
|
||||
);
|
||||
}
|
||||
}
|
||||
207
lib/features/approvals/data/dtos/approval_step_dto.dart
Normal file
207
lib/features/approvals/data/dtos/approval_step_dto.dart
Normal file
@@ -0,0 +1,207 @@
|
||||
import 'package:superport_v2/core/common/utils/json_utils.dart';
|
||||
|
||||
import '../../domain/entities/approval.dart';
|
||||
|
||||
/// 결재 상태(summary) DTO.
|
||||
class ApprovalStatusDto {
|
||||
ApprovalStatusDto({
|
||||
required this.id,
|
||||
required this.name,
|
||||
this.color,
|
||||
bool? isBlockingNext,
|
||||
bool? isTerminal,
|
||||
}) : isBlockingNext = isBlockingNext ?? true,
|
||||
isTerminal = isTerminal ?? false;
|
||||
|
||||
final int id;
|
||||
final String name;
|
||||
final String? color;
|
||||
final bool isBlockingNext;
|
||||
final bool isTerminal;
|
||||
|
||||
factory ApprovalStatusDto.fromJson(Map<String, dynamic> json) {
|
||||
if (json['status'] is Map<String, dynamic>) {
|
||||
return ApprovalStatusDto.fromJson(json['status'] as Map<String, dynamic>);
|
||||
}
|
||||
final resolvedName =
|
||||
_readString(json, 'name') ??
|
||||
_readString(json, 'status_name') ??
|
||||
_readString(json, 'statusName') ??
|
||||
'-';
|
||||
final rawColor =
|
||||
_readString(json, 'color') ??
|
||||
_readString(json, 'status_color') ??
|
||||
_readString(json, 'statusColor');
|
||||
return ApprovalStatusDto(
|
||||
id: JsonUtils.readInt(json, 'id', fallback: 0),
|
||||
name: resolvedName,
|
||||
color: rawColor,
|
||||
isBlockingNext:
|
||||
_readBool(json, 'is_blocking_next', fallback: true) ??
|
||||
_readBool(json, 'isBlockingNext', fallback: true) ??
|
||||
true,
|
||||
isTerminal:
|
||||
_readBool(json, 'is_terminal', fallback: false) ??
|
||||
_readBool(json, 'isTerminal', fallback: false) ??
|
||||
false,
|
||||
);
|
||||
}
|
||||
|
||||
ApprovalStatus toEntity() => ApprovalStatus(
|
||||
id: id,
|
||||
name: name,
|
||||
color: color,
|
||||
isBlockingNext: isBlockingNext,
|
||||
isTerminal: isTerminal,
|
||||
);
|
||||
}
|
||||
|
||||
/// 결재 사용자 요약 DTO.
|
||||
class ApprovalApproverDto {
|
||||
ApprovalApproverDto({
|
||||
required this.id,
|
||||
required this.employeeNo,
|
||||
required this.name,
|
||||
});
|
||||
|
||||
final int id;
|
||||
final String employeeNo;
|
||||
final String name;
|
||||
|
||||
factory ApprovalApproverDto.fromJson(Map<String, dynamic> json) {
|
||||
final employeeNo =
|
||||
_readString(json, 'employee_no') ??
|
||||
_readString(json, 'employee_id', fallback: '-');
|
||||
return ApprovalApproverDto(
|
||||
id: JsonUtils.readInt(json, 'id', fallback: 0),
|
||||
employeeNo: employeeNo ?? '-',
|
||||
name: _readString(json, 'name', fallback: '-') ?? '-',
|
||||
);
|
||||
}
|
||||
|
||||
ApprovalApprover toEntity() =>
|
||||
ApprovalApprover(id: id, employeeNo: employeeNo, name: name);
|
||||
}
|
||||
|
||||
/// 결재 단계(summary) DTO.
|
||||
class ApprovalStepDto {
|
||||
ApprovalStepDto({
|
||||
this.id,
|
||||
this.requestId,
|
||||
required this.stepOrder,
|
||||
this.templateStepId,
|
||||
this.approverRole,
|
||||
required this.approver,
|
||||
required this.status,
|
||||
this.assignedAt,
|
||||
this.decidedAt,
|
||||
this.actionAt,
|
||||
this.note,
|
||||
this.isDeleted = false,
|
||||
this.isOptional = false,
|
||||
this.escalationMinutes,
|
||||
this.metadata,
|
||||
});
|
||||
|
||||
final int? id;
|
||||
final int? requestId;
|
||||
final int stepOrder;
|
||||
final int? templateStepId;
|
||||
final String? approverRole;
|
||||
final ApprovalApproverDto approver;
|
||||
final ApprovalStatusDto status;
|
||||
final DateTime? assignedAt;
|
||||
final DateTime? decidedAt;
|
||||
final DateTime? actionAt;
|
||||
final String? note;
|
||||
final bool isDeleted;
|
||||
final bool isOptional;
|
||||
final int? escalationMinutes;
|
||||
final Map<String, dynamic>? metadata;
|
||||
|
||||
factory ApprovalStepDto.fromJson(Map<String, dynamic> json) {
|
||||
final statusMap =
|
||||
_asMap(json['status']) ?? _asMap(json['step_status']) ?? const {};
|
||||
final approverMap = _asMap(json['approver']) ?? const {};
|
||||
|
||||
final assignedAt = _parseDate(json['assigned_at']);
|
||||
final decidedAt = _parseDate(json['decided_at']);
|
||||
final actionAt = _parseDate(json['action_at']);
|
||||
|
||||
return ApprovalStepDto(
|
||||
id: JsonUtils.readInt(json, 'id'),
|
||||
requestId: JsonUtils.readInt(json, 'request_id'),
|
||||
stepOrder: JsonUtils.readInt(json, 'step_order', fallback: 0),
|
||||
templateStepId: JsonUtils.readInt(json, 'template_step_id'),
|
||||
approverRole: _readString(json, 'approver_role'),
|
||||
approver: ApprovalApproverDto.fromJson(approverMap),
|
||||
status: ApprovalStatusDto.fromJson(statusMap),
|
||||
assignedAt: assignedAt,
|
||||
decidedAt: decidedAt,
|
||||
actionAt: actionAt,
|
||||
note: _readString(json, 'note'),
|
||||
isDeleted:
|
||||
_readBool(json, 'is_deleted') ??
|
||||
(json['deleted_at'] != null ||
|
||||
(json['is_active'] is bool && !(json['is_active'] as bool))),
|
||||
isOptional: _readBool(json, 'is_optional', fallback: false) ?? false,
|
||||
escalationMinutes: JsonUtils.readInt(json, 'escalation_minutes'),
|
||||
metadata: _asMap(
|
||||
json['metadata'],
|
||||
)?.map((key, value) => MapEntry(key, value)),
|
||||
);
|
||||
}
|
||||
|
||||
ApprovalStep toEntity() => ApprovalStep(
|
||||
id: id,
|
||||
requestId: requestId,
|
||||
stepOrder: stepOrder,
|
||||
templateStepId: templateStepId,
|
||||
approverRole: approverRole,
|
||||
approver: approver.toEntity(),
|
||||
status: status.toEntity(),
|
||||
assignedAt: assignedAt ?? DateTime.now(),
|
||||
decidedAt: decidedAt,
|
||||
actionAt: actionAt,
|
||||
note: note,
|
||||
isDeleted: isDeleted,
|
||||
isOptional: isOptional,
|
||||
escalationMinutes: escalationMinutes,
|
||||
metadata: metadata,
|
||||
);
|
||||
}
|
||||
|
||||
String? _readString(
|
||||
Map<String, dynamic>? source,
|
||||
String key, {
|
||||
String? fallback,
|
||||
}) {
|
||||
if (source == null) return fallback;
|
||||
final value = source[key];
|
||||
if (value is String) return value;
|
||||
if (value == null) return fallback;
|
||||
return value.toString();
|
||||
}
|
||||
|
||||
bool? _readBool(Map<String, dynamic>? source, String key, {bool? fallback}) {
|
||||
if (source == null) return fallback;
|
||||
final value = source[key];
|
||||
if (value is bool) return value;
|
||||
if (value is num) return value != 0;
|
||||
if (value is String) {
|
||||
final normalized = value.trim().toLowerCase();
|
||||
if (normalized.isEmpty) return fallback;
|
||||
return ['1', 'y', 'yes', 'true'].contains(normalized);
|
||||
}
|
||||
return fallback;
|
||||
}
|
||||
|
||||
Map<String, dynamic>? _asMap(dynamic value) =>
|
||||
value is Map<String, dynamic> ? value : null;
|
||||
|
||||
DateTime? _parseDate(Object? value) {
|
||||
if (value == null) return null;
|
||||
if (value is DateTime) return value;
|
||||
if (value is String) return DateTime.tryParse(value);
|
||||
return null;
|
||||
}
|
||||
Reference in New Issue
Block a user