feat(dashboard): 결재 역할 뱃지와 문서 정합성 반영

- doc/frontend_backend_alignment_report.md에 roles 필터 적용 배경과 UI 반영 사항을 기록

- lib/features/dashboard/data/dashboard_summary_dto.dart에서 roles 파싱과 enum 매핑 로직, 문자열 리스트 util 추가

- lib/features/dashboard/domain/entities/dashboard_pending_approval.dart에 역할 enum과 도메인 필드 추가

- lib/features/dashboard/presentation/pages/dashboard_page.dart에서 결재 카드 헤더/설명 수정 및 역할 뱃지 렌더링 지원

- test/features/dashboard/data/dashboard_summary_dto_test.dart 신규 작성해 DTO→도메인 매핑과 무시 케이스를 검증

- test/features/masters/user/presentation/pages/user_page_test.dart에서 사용되지 않는 PermissionManager import 제거
This commit is contained in:
JiWoong Sul
2025-11-13 17:30:27 +09:00
parent 753f76e952
commit 80f3df770d
6 changed files with 154 additions and 5 deletions

View File

@@ -141,6 +141,7 @@ class DashboardApprovalDto {
required this.title,
required this.stepSummary,
this.requestedAt,
this.roles = const [],
});
final int? approvalId;
@@ -148,6 +149,7 @@ class DashboardApprovalDto {
final String title;
final String stepSummary;
final DateTime? requestedAt;
final List<String> roles;
factory DashboardApprovalDto.fromJson(Map<String, dynamic> json) {
num? rawId = _readNum(json, 'approval_id');
@@ -176,16 +178,22 @@ class DashboardApprovalDto {
title: _readString(json, 'title') ?? '',
stepSummary: _readString(json, 'step_summary') ?? '',
requestedAt: _parseDate(json['requested_at']),
roles: _readStringList(json, 'roles'),
);
}
DashboardPendingApproval toEntity() {
final parsedRoles = roles
.map(DashboardApprovalRole.fromName)
.whereType<DashboardApprovalRole>()
.toList(growable: false);
return DashboardPendingApproval(
approvalId: approvalId,
approvalNo: approvalNo,
title: title,
stepSummary: stepSummary,
requestedAt: requestedAt,
roles: parsedRoles,
);
}
}
@@ -233,3 +241,25 @@ double? _readDouble(Map<String, dynamic>? source, String key) {
}
return value.toDouble();
}
List<String> _readStringList(Map<String, dynamic>? source, String key) {
if (source == null) {
return const [];
}
final value = source[key];
if (value is List) {
return value
.whereType<String>()
.map((text) => text.trim())
.where((text) => text.isNotEmpty)
.toList(growable: false);
}
if (value is String) {
final normalized = value.trim();
if (normalized.isEmpty) {
return const [];
}
return [normalized];
}
return const [];
}

View File

@@ -1,3 +1,42 @@
/// 대시보드 결재 항목에서 사용자의 역할을 구분한다.
enum DashboardApprovalRole {
assigned(key: 'assigned', label: '배정됨'),
requester(key: 'requester', label: '상신자', aliases: ['requested']),
completed(key: 'completed', label: '기결재자');
const DashboardApprovalRole({
required this.key,
required this.label,
this.aliases = const [],
});
/// 백엔드가 내려주는 원시 값.
final String key;
/// UI에 노출할 한국어 라벨.
final String label;
/// 과거/대체 표기를 허용하기 위한 값.
final List<String> aliases;
/// 문자열에서 역할 정보를 복원한다.
static DashboardApprovalRole? fromName(String? raw) {
if (raw == null) {
return null;
}
final normalized = raw.trim().toLowerCase();
if (normalized.isEmpty) {
return null;
}
for (final role in values) {
if (role.key == normalized || role.aliases.contains(normalized)) {
return role;
}
}
return null;
}
}
/// 결재 대기 요약 정보.
class DashboardPendingApproval {
const DashboardPendingApproval({
@@ -6,6 +45,7 @@ class DashboardPendingApproval {
required this.title,
required this.stepSummary,
this.requestedAt,
this.roles = const [],
});
/// 결재 고유 식별자(ID). 상세 조회가 필요할 때 사용된다.
@@ -22,4 +62,7 @@ class DashboardPendingApproval {
/// 상신 일시
final DateTime? requestedAt;
/// 로그인 사용자가 해당 결재에서 가진 역할 집합.
final List<DashboardApprovalRole> roles;
}

View File

@@ -392,9 +392,9 @@ class _PendingApprovalCard extends StatelessWidget {
final theme = ShadTheme.of(context);
if (approvals.isEmpty) {
return ShadCard(
title: Text('결재 대기', style: theme.textTheme.h3),
title: Text('결재 현황', style: theme.textTheme.h3),
description: Text(
'최종 승인 대기 전표는 기본 목록에 노출되지 않습니다.',
'배정됨/상신자/기결재자 역할만 표시하며 최종 상태 건은 자동 제외됩니다.',
style: theme.textTheme.muted,
),
child: const SuperportEmptyState(
@@ -408,9 +408,9 @@ class _PendingApprovalCard extends StatelessWidget {
final dateFormat = intl.DateFormat('yyyy-MM-dd HH:mm');
return ShadCard(
title: Text('결재 대기', style: theme.textTheme.h3),
title: Text('결재 현황', style: theme.textTheme.h3),
description: Text(
'최종 승인 대기 전표를 한곳에서 확인하고 처리할 수 있습니다.',
'내 역할(배정됨/상신자/기결재자)에 해당하는 전표만 모아 보여주고 최종 상태는 제외됩니다.',
style: theme.textTheme.muted,
),
child: Column(
@@ -537,6 +537,12 @@ class _PendingApprovalListTile extends StatelessWidget {
? null
: _formatElapsedKorean(now.difference(requestedAt));
final chips = <Widget>[ShadBadge(child: Text(approval.approvalNo))];
final roleBadges = approval.roles
.map(_buildRoleBadge)
.toList(growable: false);
if (roleBadges.isNotEmpty) {
chips.addAll(roleBadges);
}
if (summary.stage != null && summary.stage!.isNotEmpty) {
chips.add(ShadBadge.outline(child: Text(summary.stage!)));
}
@@ -603,6 +609,17 @@ class _PendingApprovalListTile extends StatelessWidget {
),
);
}
Widget _buildRoleBadge(DashboardApprovalRole role) {
switch (role) {
case DashboardApprovalRole.assigned:
return ShadBadge(child: Text(role.label));
case DashboardApprovalRole.requester:
return ShadBadge.outline(child: Text(role.label));
case DashboardApprovalRole.completed:
return ShadBadge.outline(child: Text(role.label));
}
}
}
class _PendingApprovalSummary {