464 lines
13 KiB
Dart
464 lines
13 KiB
Dart
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';
|
|
|
|
/// 결재 API 응답을 표현하는 DTO.
|
|
///
|
|
/// - 원본 JSON 형식을 유지하면서 도메인 엔티티 변환을 제공한다.
|
|
/// - 일부 필드는 누락 가능성을 고려하여 기본값을 지정한다.
|
|
class ApprovalDto {
|
|
ApprovalDto({
|
|
this.id,
|
|
required this.approvalNo,
|
|
this.transactionNo,
|
|
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 String? transactionNo;
|
|
final ApprovalStatusDto status;
|
|
final ApprovalStepDto? currentStep;
|
|
final ApprovalRequesterDto requester;
|
|
final DateTime requestedAt;
|
|
final DateTime? decidedAt;
|
|
final String? note;
|
|
final bool isActive;
|
|
final bool isDeleted;
|
|
final List<ApprovalStepDto> steps;
|
|
final List<ApprovalHistoryDto> histories;
|
|
final DateTime? createdAt;
|
|
final DateTime? updatedAt;
|
|
|
|
/// API 응답 JSON을 [ApprovalDto]로 변환한다.
|
|
factory ApprovalDto.fromJson(Map<String, dynamic> json) {
|
|
final approvalEnvelope = _mapOrEmpty(json['approval']);
|
|
final statusMap = _firstNonEmptyMap([
|
|
json['status'],
|
|
json['approval_status'],
|
|
approvalEnvelope['status'],
|
|
approvalEnvelope['approval_status'],
|
|
]);
|
|
final requesterMap = _firstNonEmptyMap([
|
|
json['requester'],
|
|
json['requested_by'],
|
|
approvalEnvelope['requester'],
|
|
approvalEnvelope['requested_by'],
|
|
]);
|
|
final currentStepMap = _firstNonEmptyMap([
|
|
json['current_step'],
|
|
json['currentStep'],
|
|
approvalEnvelope['current_step'],
|
|
]);
|
|
final transactionMap = _mapOrEmpty(json['transaction']);
|
|
final envelopeTransactionMap = _mapOrEmpty(approvalEnvelope['transaction']);
|
|
var stepsSource = _asListOfMap(json['steps']);
|
|
if (stepsSource.isEmpty) {
|
|
stepsSource = _asListOfMap(approvalEnvelope['steps']);
|
|
}
|
|
var historiesSource = _asListOfMap(json['histories']);
|
|
if (historiesSource.isEmpty) {
|
|
historiesSource = _asListOfMap(approvalEnvelope['histories']);
|
|
}
|
|
final currentStepDto = currentStepMap.isEmpty
|
|
? null
|
|
: ApprovalStepDto.fromJson(currentStepMap);
|
|
|
|
final approvalNo =
|
|
_pickString(
|
|
[json, approvalEnvelope],
|
|
const ['approval_no', 'approvalNo'],
|
|
) ??
|
|
'-';
|
|
final transactionNo = _pickString(
|
|
[json, transactionMap, approvalEnvelope, envelopeTransactionMap],
|
|
const ['transaction_no', 'transactionNo'],
|
|
);
|
|
|
|
return ApprovalDto(
|
|
id: json['id'] as int? ?? approvalEnvelope['id'] as int?,
|
|
approvalNo: approvalNo,
|
|
transactionNo: transactionNo,
|
|
status: ApprovalStatusDto.fromJson(statusMap),
|
|
currentStep: currentStepDto,
|
|
requester: ApprovalRequesterDto.fromJson(requesterMap),
|
|
requestedAt:
|
|
_parseDate(
|
|
json['requested_at'] ?? approvalEnvelope['requested_at'],
|
|
) ??
|
|
DateTime.now(),
|
|
decidedAt: _parseDate(
|
|
json['decided_at'] ?? approvalEnvelope['decided_at'],
|
|
),
|
|
note: _readString(json['note']) ?? _readString(approvalEnvelope['note']),
|
|
isActive:
|
|
(json['is_active'] as bool?) ??
|
|
(approvalEnvelope['is_active'] as bool?) ??
|
|
true,
|
|
isDeleted:
|
|
(json['is_deleted'] as bool?) ??
|
|
(approvalEnvelope['is_deleted'] as bool?) ??
|
|
false,
|
|
steps: stepsSource.map(ApprovalStepDto.fromJson).toList(growable: false),
|
|
histories: historiesSource
|
|
.map(ApprovalHistoryDto.fromJson)
|
|
.toList(growable: false),
|
|
createdAt: _parseDate(
|
|
json['created_at'] ?? approvalEnvelope['created_at'],
|
|
),
|
|
updatedAt: _parseDate(
|
|
json['updated_at'] ?? approvalEnvelope['updated_at'],
|
|
),
|
|
);
|
|
}
|
|
|
|
/// DTO를 도메인 [Approval] 엔티티로 변환한다.
|
|
Approval toEntity() => Approval(
|
|
id: id,
|
|
approvalNo: approvalNo,
|
|
transactionNo: transactionNo ?? '-',
|
|
status: status.toEntity(),
|
|
currentStep: currentStep?.toEntity(),
|
|
requester: requester.toEntity(),
|
|
requestedAt: requestedAt,
|
|
decidedAt: decidedAt,
|
|
note: note,
|
|
isActive: isActive,
|
|
isDeleted: isDeleted,
|
|
steps: steps.map((e) => e.toEntity()).toList(),
|
|
histories: histories.map((e) => e.toEntity()).toList(),
|
|
createdAt: createdAt,
|
|
updatedAt: updatedAt,
|
|
);
|
|
|
|
/// 페이징 응답을 파싱해 [PaginatedResult]로 변환한다.
|
|
static PaginatedResult<Approval> parsePaginated(Map<String, dynamic>? json) {
|
|
final rawItems = JsonUtils.extractList(json, keys: const ['items']);
|
|
final items = rawItems
|
|
.map(ApprovalDto.fromJson)
|
|
.map((dto) => dto.toEntity())
|
|
.toList(growable: false);
|
|
return PaginatedResult<Approval>(
|
|
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),
|
|
);
|
|
}
|
|
}
|
|
|
|
/// 결재 상태(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({
|
|
required this.id,
|
|
required this.employeeNo,
|
|
required this.name,
|
|
});
|
|
|
|
final int id;
|
|
final String employeeNo;
|
|
final String name;
|
|
|
|
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']) ?? '-',
|
|
name:
|
|
_readString(json['name']) ??
|
|
_readString(json['employee_name']) ??
|
|
'-',
|
|
);
|
|
}
|
|
|
|
/// DTO를 [ApprovalRequester]로 변환한다.
|
|
ApprovalRequester toEntity() =>
|
|
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);
|
|
}
|
|
return const [];
|
|
}
|
|
|
|
Map<String, dynamic> _mapOrEmpty(dynamic value) =>
|
|
value is Map<String, dynamic> ? value : const {};
|
|
|
|
Map<String, dynamic> _firstNonEmptyMap(List<dynamic> candidates) {
|
|
for (final candidate in candidates) {
|
|
if (candidate is Map<String, dynamic> && candidate.isNotEmpty) {
|
|
return candidate;
|
|
}
|
|
}
|
|
return const {};
|
|
}
|
|
|
|
String? _pickString(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 String && value.isNotEmpty) {
|
|
return value;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
return null;
|
|
}
|
|
|
|
/// 문자열/DateTime 입력을 DateTime으로 변환한다.
|
|
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? _readString(dynamic value) {
|
|
if (value == null) {
|
|
return null;
|
|
}
|
|
if (value is String) {
|
|
final trimmed = value.trim();
|
|
return trimmed.isEmpty ? null : trimmed;
|
|
}
|
|
if (value is num || value is bool) {
|
|
return value.toString();
|
|
}
|
|
return null;
|
|
}
|