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. /// /// - 원본 JSON 형식을 유지하면서 도메인 엔티티 변환을 제공한다. /// - 일부 필드는 누락 가능성을 고려하여 기본값을 지정한다. class ApprovalDto { ApprovalDto({ this.id, required this.approvalNo, this.transactionId, 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 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 rawRequesterMap = _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'], ); 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( _resolveRequesterMap(json, approvalEnvelope, rawRequesterMap), ), 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(ApprovalAuditDto.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, transactionId: transactionId, transactionNo: transactionNo ?? '-', transactionUpdatedAt: transactionUpdatedAt, 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), ); } } /// 결재 요청자 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: JsonUtils.readInt(json, 'id', fallback: 0), employeeNo: _readString(json['employee_no']) ?? _readString(json['employee_id']) ?? '-', name: _readString(json['name']) ?? _readString(json['employee_name']) ?? '-', ); } /// DTO를 [ApprovalRequester]로 변환한다. ApprovalRequester toEntity() => ApprovalRequester(id: id, employeeNo: employeeNo, 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 {}; } Map _resolveRequesterMap( Map root, Map envelope, Map candidate, ) { if (candidate.isNotEmpty) { return candidate; } final resolved = {}; 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 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; } int? _pickInt(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 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; 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; }