feat(frontend): 승인 템플릿 API 통합 및 디버그 로그인 확장
- docs 폴더 문서를 최신 API 계약으로 갱신하고 가이드를 다듬었다\n- approvals data/presentation 레이어를 API v4 스펙에 맞춰 리팩터링했다\n- approver 자동완성 위젯을 신규 공유 레포지토리에 맞춰 교체하고 UX를 보강했다\n- inventory/rental 페이지 테이블 초기화 시 승인 기준 연동을 정비했다\n- 로그인 페이지 디버그 버튼을 tera/exa 계정으로 분리해 QA 로그인을 단순화했다\n- get_it 등록과 테스트 케이스를 신규 공유 리포지토리에 맞춰 업데이트했다
This commit is contained in:
@@ -0,0 +1,53 @@
|
||||
import 'package:superport_v2/core/common/utils/json_utils.dart';
|
||||
|
||||
import '../../domain/entities/approval_approver_candidate.dart';
|
||||
|
||||
/// 승인자 후보 응답을 파싱하는 DTO.
|
||||
class ApprovalApproverCandidateDto {
|
||||
ApprovalApproverCandidateDto({
|
||||
required this.id,
|
||||
required this.employeeNo,
|
||||
required this.name,
|
||||
this.team,
|
||||
this.email,
|
||||
this.phone,
|
||||
});
|
||||
|
||||
final int id;
|
||||
final String employeeNo;
|
||||
final String name;
|
||||
final String? team;
|
||||
final String? email;
|
||||
final String? phone;
|
||||
|
||||
/// JSON 응답에서 DTO를 생성한다.
|
||||
factory ApprovalApproverCandidateDto.fromJson(Map<String, dynamic> json) {
|
||||
final group = json['group'] is Map<String, dynamic>
|
||||
? json['group'] as Map<String, dynamic>
|
||||
: null;
|
||||
return ApprovalApproverCandidateDto(
|
||||
id: json['id'] as int? ?? JsonUtils.readInt(json, 'user_id', fallback: 0),
|
||||
employeeNo: json['employee_id'] as String? ??
|
||||
json['employee_no'] as String? ??
|
||||
'-',
|
||||
name: json['name'] as String? ??
|
||||
json['employee_name'] as String? ??
|
||||
'-',
|
||||
team: group?['group_name'] as String? ?? json['team'] as String?,
|
||||
email: json['email'] as String?,
|
||||
phone: json['phone'] as String? ?? json['mobile_no'] as String?,
|
||||
);
|
||||
}
|
||||
|
||||
/// DTO를 도메인 엔티티로 변환한다.
|
||||
ApprovalApproverCandidate toEntity() {
|
||||
return ApprovalApproverCandidate(
|
||||
id: id,
|
||||
employeeNo: employeeNo,
|
||||
name: name,
|
||||
team: team,
|
||||
email: email,
|
||||
phone: phone,
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,84 @@
|
||||
import 'package:dio/dio.dart';
|
||||
|
||||
import '../../../../../core/network/api_client.dart';
|
||||
import '../../../../../core/network/api_routes.dart';
|
||||
import '../../domain/entities/approval_approver_candidate.dart';
|
||||
import '../../domain/repositories/approval_approver_repository.dart';
|
||||
import '../dtos/approval_approver_candidate_dto.dart';
|
||||
|
||||
/// 승인자 자동완성용 원격 저장소 구현체.
|
||||
class ApprovalApproverRepositoryRemote implements ApprovalApproverRepository {
|
||||
ApprovalApproverRepositoryRemote({required ApiClient apiClient})
|
||||
: _api = apiClient;
|
||||
|
||||
final ApiClient _api;
|
||||
|
||||
static const _basePath = '${ApiRoutes.apiV1}/users';
|
||||
|
||||
@override
|
||||
Future<List<ApprovalApproverCandidate>> search({
|
||||
required String keyword,
|
||||
int limit = 20,
|
||||
}) async {
|
||||
final trimmed = keyword.trim();
|
||||
if (trimmed.isEmpty) {
|
||||
return const [];
|
||||
}
|
||||
|
||||
final response = await _api.get<Map<String, dynamic>>(
|
||||
_basePath,
|
||||
query: _buildQuery(limit: limit, keyword: trimmed),
|
||||
options: Options(responseType: ResponseType.json),
|
||||
);
|
||||
|
||||
return _mapCandidates(response.data);
|
||||
}
|
||||
|
||||
@override
|
||||
Future<ApprovalApproverCandidate?> fetchById(int id) async {
|
||||
final response = await _api.get<Map<String, dynamic>>(
|
||||
'$_basePath/$id',
|
||||
query: ApiClient.buildQuery(include: const ['group']),
|
||||
options: Options(responseType: ResponseType.json),
|
||||
);
|
||||
|
||||
final payload = _api.unwrapAsMap(response);
|
||||
if (payload.isEmpty) {
|
||||
return null;
|
||||
}
|
||||
return ApprovalApproverCandidateDto.fromJson(payload).toEntity();
|
||||
}
|
||||
|
||||
@override
|
||||
Future<List<ApprovalApproverCandidate>> listInitial({int limit = 20}) async {
|
||||
final response = await _api.get<Map<String, dynamic>>(
|
||||
_basePath,
|
||||
query: _buildQuery(limit: limit),
|
||||
options: Options(responseType: ResponseType.json),
|
||||
);
|
||||
|
||||
return _mapCandidates(response.data);
|
||||
}
|
||||
|
||||
Map<String, dynamic> _buildQuery({required int limit, String? keyword}) {
|
||||
return ApiClient.buildQuery(
|
||||
page: 1,
|
||||
pageSize: limit,
|
||||
q: keyword,
|
||||
sort: 'name',
|
||||
order: 'asc',
|
||||
include: const ['group'],
|
||||
filters: const {'is_active': true},
|
||||
);
|
||||
}
|
||||
|
||||
List<ApprovalApproverCandidate> _mapCandidates(
|
||||
Map<String, dynamic>? payload,
|
||||
) {
|
||||
return (payload?['items'] as List<dynamic>? ?? const [])
|
||||
.whereType<Map<String, dynamic>>()
|
||||
.map(ApprovalApproverCandidateDto.fromJson)
|
||||
.map((dto) => dto.toEntity())
|
||||
.toList(growable: false);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user