결재 템플릿 단계 적용 구현

- ApprovalTemplate 엔티티·DTO·원격 리포지토리 추가
- ApprovalController에 템플릿 로딩/적용 상태와 assignSteps 호출 연동
- ApprovalPage 단계 탭에 템플릿 선택 UI 및 적용 확인 다이얼로그 구현
- 템플릿 적용 단위 테스트와 IMPLEMENTATION_TASKS 현황 갱신
This commit is contained in:
JiWoong Sul
2025-09-25 00:21:12 +09:00
parent b6e50464d2
commit c3010965ad
63 changed files with 10179 additions and 1436 deletions

View File

@@ -0,0 +1,292 @@
import 'package:superport_v2/core/common/models/paginated_result.dart';
import '../../domain/entities/approval.dart';
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;
factory ApprovalDto.fromJson(Map<String, dynamic> json) {
return ApprovalDto(
id: json['id'] as int?,
approvalNo: json['approval_no'] as String,
transactionNo: json['transaction'] is Map<String, dynamic>
? (json['transaction']['transaction_no'] as String?)
: json['transaction_no'] as String?,
status: ApprovalStatusDto.fromJson(
(json['status'] as Map<String, dynamic>? ?? const {}),
),
currentStep: json['current_step'] is Map<String, dynamic>
? ApprovalStepDto.fromJson(
json['current_step'] as Map<String, dynamic>,
)
: null,
requester: ApprovalRequesterDto.fromJson(
(json['requester'] as Map<String, dynamic>? ?? const {}),
),
requestedAt: _parseDate(json['requested_at']) ?? DateTime.now(),
decidedAt: _parseDate(json['decided_at']),
note: json['note'] as String?,
isActive: (json['is_active'] as bool?) ?? true,
isDeleted: (json['is_deleted'] as bool?) ?? false,
steps: (json['steps'] as List<dynamic>? ?? [])
.whereType<Map<String, dynamic>>()
.map(ApprovalStepDto.fromJson)
.toList(),
histories: (json['histories'] as List<dynamic>? ?? [])
.whereType<Map<String, dynamic>>()
.map(ApprovalHistoryDto.fromJson)
.toList(),
createdAt: _parseDate(json['created_at']),
updatedAt: _parseDate(json['updated_at']),
);
}
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,
);
static PaginatedResult<Approval> parsePaginated(Map<String, dynamic>? json) {
final items = (json?['items'] as List<dynamic>? ?? [])
.whereType<Map<String, dynamic>>()
.map(ApprovalDto.fromJson)
.map((dto) => dto.toEntity())
.toList();
return PaginatedResult<Approval>(
items: items,
page: json?['page'] as int? ?? 1,
pageSize: json?['page_size'] as int? ?? items.length,
total: json?['total'] as int? ?? items.length,
);
}
}
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) {
return ApprovalStatusDto(
id: json['id'] as int? ?? json['status_id'] as int? ?? 0,
name: json['name'] as String? ?? json['status_name'] as String? ?? '-',
color: json['color'] as String?,
);
}
ApprovalStatus toEntity() => ApprovalStatus(id: id, name: name, color: color);
}
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: json['employee_no'] as String? ?? '-',
name: json['name'] as String? ?? json['employee_name'] as String? ?? '-',
);
}
ApprovalRequester toEntity() =>
ApprovalRequester(id: id, employeeNo: employeeNo, name: name);
}
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: json['employee_no'] as String? ?? '-',
name: json['name'] as String? ?? json['employee_name'] as String? ?? '-',
);
}
ApprovalApprover toEntity() =>
ApprovalApprover(id: id, employeeNo: employeeNo, name: name);
}
class ApprovalStepDto {
ApprovalStepDto({
this.id,
required this.stepOrder,
required this.approver,
required this.status,
required this.assignedAt,
this.decidedAt,
this.note,
});
final int? id;
final int stepOrder;
final ApprovalApproverDto approver;
final ApprovalStatusDto status;
final DateTime assignedAt;
final DateTime? decidedAt;
final String? note;
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>? ?? const {}),
),
assignedAt: _parseDate(json['assigned_at']) ?? DateTime.now(),
decidedAt: _parseDate(json['decided_at']),
note: json['note'] as String?,
);
}
ApprovalStep toEntity() => ApprovalStep(
id: id,
stepOrder: stepOrder,
approver: approver.toEntity(),
status: status.toEntity(),
assignedAt: assignedAt,
decidedAt: decidedAt,
note: note,
);
}
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) {
return ApprovalHistoryDto(
id: json['id'] as int?,
action: ApprovalActionDto.fromJson(
(json['action'] as Map<String, dynamic>? ?? const {}),
),
fromStatus: json['from_status'] is Map<String, dynamic>
? ApprovalStatusDto.fromJson(
json['from_status'] as Map<String, dynamic>,
)
: null,
toStatus: ApprovalStatusDto.fromJson(
(json['to_status'] as Map<String, dynamic>? ?? const {}),
),
approver: ApprovalApproverDto.fromJson(
(json['approver'] as Map<String, dynamic>? ?? const {}),
),
actionAt: _parseDate(json['action_at']) ?? DateTime.now(),
note: json['note'] as String?,
);
}
ApprovalHistory toEntity() => ApprovalHistory(
id: id,
action: action.toEntity(),
fromStatus: fromStatus?.toEntity(),
toStatus: toStatus.toEntity(),
approver: approver.toEntity(),
actionAt: actionAt,
note: note,
);
}
class ApprovalActionDto {
ApprovalActionDto({required this.id, required this.name});
final int id;
final String name;
factory ApprovalActionDto.fromJson(Map<String, dynamic> json) {
return ApprovalActionDto(
id: json['id'] as int? ?? json['action_id'] as int? ?? 0,
name: json['name'] as String? ?? json['action_name'] as String? ?? '-',
);
}
ApprovalAction toEntity() => ApprovalAction(id: id, name: name);
}
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;
}

View File

@@ -0,0 +1,149 @@
import '../../domain/entities/approval_template.dart';
class ApprovalTemplateDto {
ApprovalTemplateDto({
required this.id,
required this.code,
required this.name,
this.description,
required this.isActive,
this.createdBy,
this.createdAt,
this.updatedAt,
this.steps = const [],
});
final int id;
final String code;
final String name;
final String? description;
final bool isActive;
final ApprovalTemplateAuthorDto? createdBy;
final DateTime? createdAt;
final DateTime? updatedAt;
final List<ApprovalTemplateStepDto> steps;
factory ApprovalTemplateDto.fromJson(Map<String, dynamic> json) {
return ApprovalTemplateDto(
id: json['id'] as int? ?? 0,
code: json['template_code'] as String? ?? json['code'] as String? ?? '-',
name: json['template_name'] as String? ?? json['name'] as String? ?? '-',
description: json['description'] as String?,
isActive: (json['is_active'] as bool?) ?? true,
createdBy: json['created_by'] is Map<String, dynamic>
? ApprovalTemplateAuthorDto.fromJson(
json['created_by'] as Map<String, dynamic>,
)
: null,
createdAt: _parseDate(json['created_at']),
updatedAt: _parseDate(json['updated_at']),
steps: (json['steps'] as List<dynamic>? ?? [])
.whereType<Map<String, dynamic>>()
.map(ApprovalTemplateStepDto.fromJson)
.toList(),
);
}
ApprovalTemplate toEntity({bool includeSteps = true}) {
return ApprovalTemplate(
id: id,
code: code,
name: name,
description: description,
isActive: isActive,
createdBy: createdBy?.toEntity(),
createdAt: createdAt,
updatedAt: updatedAt,
steps: includeSteps ? steps.map((e) => e.toEntity()).toList() : const [],
);
}
}
class ApprovalTemplateAuthorDto {
ApprovalTemplateAuthorDto({
required this.id,
required this.employeeNo,
required this.name,
});
final int id;
final String employeeNo;
final String name;
factory ApprovalTemplateAuthorDto.fromJson(Map<String, dynamic> json) {
return ApprovalTemplateAuthorDto(
id: json['id'] as int? ?? json['employee_id'] as int? ?? 0,
employeeNo: json['employee_no'] as String? ?? '-',
name: json['employee_name'] as String? ?? json['name'] as String? ?? '-',
);
}
ApprovalTemplateAuthor toEntity() {
return ApprovalTemplateAuthor(id: id, employeeNo: employeeNo, name: name);
}
}
class ApprovalTemplateStepDto {
ApprovalTemplateStepDto({
this.id,
required this.stepOrder,
required this.approver,
this.note,
});
final int? id;
final int stepOrder;
final ApprovalTemplateApproverDto approver;
final String? note;
factory ApprovalTemplateStepDto.fromJson(Map<String, dynamic> json) {
return ApprovalTemplateStepDto(
id: json['id'] as int?,
stepOrder: json['step_order'] as int? ?? 0,
approver: ApprovalTemplateApproverDto.fromJson(
(json['approver'] as Map<String, dynamic>? ?? const {}),
),
note: json['note'] as String?,
);
}
ApprovalTemplateStep toEntity() {
return ApprovalTemplateStep(
id: id,
stepOrder: stepOrder,
approver: approver.toEntity(),
note: note,
);
}
}
class ApprovalTemplateApproverDto {
ApprovalTemplateApproverDto({
required this.id,
required this.employeeNo,
required this.name,
});
final int id;
final String employeeNo;
final String name;
factory ApprovalTemplateApproverDto.fromJson(Map<String, dynamic> json) {
return ApprovalTemplateApproverDto(
id: json['id'] as int? ?? json['approver_id'] as int? ?? 0,
employeeNo: json['employee_no'] as String? ?? '-',
name: json['employee_name'] as String? ?? json['name'] as String? ?? '-',
);
}
ApprovalTemplateApprover toEntity() {
return ApprovalTemplateApprover(id: id, employeeNo: employeeNo, name: name);
}
}
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;
}