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 steps; final List histories; final DateTime? createdAt; final DateTime? updatedAt; /// API 응답 JSON을 [ApprovalDto]로 변환한다. factory ApprovalDto.fromJson(Map 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: json['note'] as String? ?? approvalEnvelope['note'] as String?, 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 parsePaginated(Map? json) { final rawItems = JsonUtils.extractList(json, keys: const ['items']); final items = rawItems .map(ApprovalDto.fromJson) .map((dto) => dto.toEntity()) .toList(growable: false); return PaginatedResult( 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 json) { if (json['status'] is Map) { return ApprovalStatusDto.fromJson(json['status'] as Map); } return ApprovalStatusDto( id: json['id'] as int? ?? json['status_id'] as int? ?? json['approval_status_id'] as int? ?? 0, name: json['name'] as String? ?? json['status_name'] as String? ?? json['approval_status_name'] as String? ?? (json['status'] as String?) ?? '-', color: json['color'] as String?, ); } /// 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 json) { return ApprovalRequesterDto( id: json['id'] as int? ?? json['employee_id'] as int? ?? 0, employeeNo: json['employee_no'] as String? ?? '-', name: json['name'] as String? ?? json['employee_name'] as String? ?? '-', ); } /// 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 json) { return ApprovalApproverDto( id: json['id'] as int? ?? json['approver_id'] as int? ?? 0, employeeNo: json['employee_no'] as String? ?? '-', name: json['name'] as String? ?? json['employee_name'] as String? ?? '-', ); } /// 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 json) { return ApprovalStepDto( id: json['id'] as int?, stepOrder: json['step_order'] as int? ?? 0, approver: ApprovalApproverDto.fromJson( (json['approver'] as Map? ?? const {}), ), status: ApprovalStatusDto.fromJson( (json['status'] as Map? ?? json['step_status'] as Map? ?? json['approval_status'] as Map? ?? const {}), ), assignedAt: _parseDate(json['assigned_at']) ?? DateTime.now(), decidedAt: _parseDate(json['decided_at']), note: json['note'] as String?, 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 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': json['approval_action_name'] ?? json['action_name'] ?? (json['action'] as String?) ?? '-', }; 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: json['note'] as String?, ); } /// 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 json) { if (json['action'] is Map) { return ApprovalActionDto.fromJson(json['action'] as Map); } return ApprovalActionDto( id: json['id'] as int? ?? json['action_id'] as int? ?? json['approval_action_id'] as int? ?? 0, name: json['name'] as String? ?? json['action_name'] as String? ?? json['approval_action_name'] as String? ?? (json['action'] as String?) ?? '-', ); } /// DTO를 [ApprovalAction]으로 변환한다. ApprovalAction toEntity() => ApprovalAction(id: id, name: name); } List> _asListOfMap(dynamic value) { if (value is List) { return value.whereType>().toList(growable: false); } return const []; } Map _mapOrEmpty(dynamic value) => value is Map ? value : const {}; Map _firstNonEmptyMap(List candidates) { for (final candidate in candidates) { if (candidate is Map && candidate.isNotEmpty) { return candidate; } } return const {}; } String? _pickString(List sources, List keys) { for (final source in sources) { if (source is Map) { 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; }