결재 비활성 안내 개선 및 테이블 기능 보강

This commit is contained in:
JiWoong Sul
2025-09-29 15:49:06 +09:00
parent fef7108479
commit 98724762ec
18 changed files with 1134 additions and 297 deletions

View File

@@ -19,6 +19,7 @@ class ApprovalHistoryController extends ChangeNotifier {
DateTime? _from;
DateTime? _to;
String? _errorMessage;
int _pageSize = 20;
PaginatedResult<ApprovalHistoryRecord>? get result => _result;
bool get isLoading => _isLoading;
@@ -27,6 +28,7 @@ class ApprovalHistoryController extends ChangeNotifier {
DateTime? get from => _from;
DateTime? get to => _to;
String? get errorMessage => _errorMessage;
int get pageSize => _result?.pageSize ?? _pageSize;
Future<void> fetch({int page = 1}) async {
_isLoading = true;
@@ -42,13 +44,14 @@ class ApprovalHistoryController extends ChangeNotifier {
final response = await _repository.list(
page: page,
pageSize: _result?.pageSize ?? 20,
pageSize: _pageSize,
query: _query.trim().isEmpty ? null : _query.trim(),
action: action,
from: _from,
to: _to,
);
_result = response;
_pageSize = response.pageSize;
} catch (e) {
_errorMessage = e.toString();
} finally {
@@ -86,6 +89,14 @@ class ApprovalHistoryController extends ChangeNotifier {
notifyListeners();
}
void updatePageSize(int value) {
if (value <= 0) {
return;
}
_pageSize = value;
notifyListeners();
}
bool get hasActiveFilters =>
_query.trim().isNotEmpty ||
_actionFilter != ApprovalHistoryActionFilter.all ||

View File

@@ -10,7 +10,7 @@ import '../../../../../widgets/app_layout.dart';
import '../../../../../widgets/components/filter_bar.dart';
import '../../../../../widgets/components/superport_date_picker.dart';
import '../../../../../widgets/components/superport_table.dart';
import '../../../../../widgets/spec_page.dart';
import '../../../../../widgets/components/feature_disabled_placeholder.dart';
import '../../domain/entities/approval_history_record.dart';
import '../../domain/repositories/approval_history_repository.dart';
import '../controllers/approval_history_controller.dart';
@@ -22,41 +22,28 @@ class ApprovalHistoryPage extends StatelessWidget {
Widget build(BuildContext context) {
final enabled = Environment.flag('FEATURE_APPROVALS_ENABLED');
if (!enabled) {
return const SpecPage(
return AppLayout(
title: '결재 이력 조회',
summary: '결재 단계별 변경 이력을 조회합니다.',
sections: [
SpecSection(
title: '조회 테이블',
description: '수정 없이 이력 리스트만 제공.',
table: SpecTable(
columns: [
'번호',
'결재ID',
'단계순서',
'승인자',
'행위',
'변경전상태',
'변경후상태',
'작업일시',
'비고',
],
rows: [
[
'1',
'APP-20240301-001',
'1',
'최관리',
'승인',
'승인대기',
'승인완료',
'2024-03-01 10:30',
'-',
],
],
subtitle: '결재 단계별 변경 내역을 기간·행위·결재번호 기준으로 확인합니다.',
breadcrumbs: const [
AppBreadcrumbItem(label: '대시보드', path: dashboardRoutePath),
AppBreadcrumbItem(label: '결재', path: '/approvals/history'),
AppBreadcrumbItem(label: '결재 이력'),
],
actions: [
Tooltip(
message: '다운로드는 API 연동 이후 제공됩니다.',
child: ShadButton(
onPressed: null,
leading: const Icon(lucide.LucideIcons.download, size: 16),
child: const Text('엑셀 다운로드'),
),
),
],
child: const FeatureDisabledPlaceholder(
title: '결재 이력 기능 준비 중',
description: '결재 이력 API가 공개되면 검색과 엑셀 다운로드 기능이 활성화됩니다.',
),
);
}
@@ -80,6 +67,10 @@ class _ApprovalHistoryEnabledPageState
final DateFormat _dateTimeFormat = DateFormat('yyyy-MM-dd HH:mm');
DateTimeRange? _dateRange;
String? _lastError;
static const _pageSizeOptions = [10, 20, 50];
int? _sortColumnIndex;
bool _sortAscending = true;
static const _sortableColumns = {0, 1, 2, 3, 4, 5, 6, 7};
@override
void initState() {
@@ -125,9 +116,7 @@ class _ApprovalHistoryEnabledPageState
final totalPages = result == null || result.pageSize == 0
? 1
: (result.total / result.pageSize).ceil().clamp(1, 9999);
final hasNext = result == null
? false
: (result.page * result.pageSize) < result.total;
final sortedHistories = _applySorting(histories);
return AppLayout(
title: '결재 이력 조회',
@@ -231,34 +220,6 @@ class _ApprovalHistoryEnabledPageState
Text('$totalCount건', style: theme.textTheme.muted),
],
),
footer: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Text(
'페이지 $currentPage / $totalPages',
style: theme.textTheme.small,
),
Row(
children: [
ShadButton.outline(
size: ShadButtonSize.sm,
onPressed: _controller.isLoading || currentPage <= 1
? null
: () => _controller.fetch(page: currentPage - 1),
child: const Text('이전'),
),
const SizedBox(width: 8),
ShadButton.outline(
size: ShadButtonSize.sm,
onPressed: _controller.isLoading || !hasNext
? null
: () => _controller.fetch(page: currentPage + 1),
child: const Text('다음'),
),
],
),
],
),
child: _controller.isLoading
? const Padding(
padding: EdgeInsets.all(48),
@@ -273,9 +234,30 @@ class _ApprovalHistoryEnabledPageState
),
)
: _ApprovalHistoryTable(
histories: histories,
histories: sortedHistories,
dateFormat: _dateTimeFormat,
query: _controller.query,
pagination: SuperportTablePagination(
currentPage: currentPage,
totalPages: totalPages,
totalItems: totalCount,
pageSize: _controller.pageSize,
pageSizeOptions: _pageSizeOptions,
),
onPageChange: (page) => _controller.fetch(page: page),
onPageSizeChange: (size) {
_controller.updatePageSize(size);
_controller.fetch(page: 1);
},
isLoading: _controller.isLoading,
sortableColumns: _sortableColumns,
sortState: _sortColumnIndex == null
? null
: SuperportTableSortState(
columnIndex: _sortColumnIndex!,
ascending: _sortAscending,
),
onSortChanged: _handleSortChange,
),
),
);
@@ -305,6 +287,58 @@ class _ApprovalHistoryEnabledPageState
_controller.fetch(page: 1);
}
void _handleSortChange(int columnIndex, bool ascending) {
setState(() {
_sortColumnIndex = columnIndex;
_sortAscending = ascending;
});
}
List<ApprovalHistoryRecord> _applySorting(List<ApprovalHistoryRecord> items) {
final columnIndex = _sortColumnIndex;
if (columnIndex == null) {
return items;
}
final sorted = List<ApprovalHistoryRecord>.from(items);
sorted.sort((a, b) {
int compare;
switch (columnIndex) {
case 0:
compare = a.id.compareTo(b.id);
break;
case 1:
compare = a.approvalNo.compareTo(b.approvalNo);
break;
case 2:
final left = a.stepOrder ?? 0;
final right = b.stepOrder ?? 0;
compare = left.compareTo(right);
break;
case 3:
compare = a.approver.name.compareTo(b.approver.name);
break;
case 4:
compare = a.action.name.compareTo(b.action.name);
break;
case 5:
compare = (a.fromStatus?.name ?? '').compareTo(
b.fromStatus?.name ?? '',
);
break;
case 6:
compare = a.toStatus.name.compareTo(b.toStatus.name);
break;
case 7:
compare = a.actionAt.compareTo(b.actionAt);
break;
default:
compare = 0;
}
return _sortAscending ? compare : -compare;
});
return sorted;
}
String _actionLabel(ApprovalHistoryActionFilter filter) {
switch (filter) {
case ApprovalHistoryActionFilter.all:
@@ -324,11 +358,25 @@ class _ApprovalHistoryTable extends StatelessWidget {
required this.histories,
required this.dateFormat,
required this.query,
required this.pagination,
required this.onPageChange,
required this.onPageSizeChange,
required this.isLoading,
required this.sortableColumns,
required this.sortState,
required this.onSortChanged,
});
final List<ApprovalHistoryRecord> histories;
final DateFormat dateFormat;
final String query;
final SuperportTablePagination pagination;
final ValueChanged<int> onPageChange;
final ValueChanged<int> onPageSizeChange;
final bool isLoading;
final Set<int> sortableColumns;
final SuperportTableSortState? sortState;
final void Function(int columnIndex, bool ascending) onSortChanged;
@override
Widget build(BuildContext context) {
@@ -402,6 +450,13 @@ class _ApprovalHistoryTable extends StatelessWidget {
return const FixedTableSpanExtent(110);
}
},
pagination: pagination,
onPageChange: onPageChange,
onPageSizeChange: onPageSizeChange,
isLoading: isLoading,
sortableColumns: sortableColumns,
sortState: sortState,
onSortChanged: onSortChanged,
);
}
}