결재 단계 편집 다이얼로그 구현
This commit is contained in:
@@ -0,0 +1,114 @@
|
||||
import 'package:superport_v2/core/common/models/paginated_result.dart';
|
||||
import 'package:superport_v2/features/approvals/data/dtos/approval_dto.dart';
|
||||
import 'package:superport_v2/features/approvals/domain/entities/approval.dart';
|
||||
|
||||
import '../../domain/entities/approval_history_record.dart';
|
||||
|
||||
class ApprovalHistoryRecordDto {
|
||||
ApprovalHistoryRecordDto({
|
||||
required this.id,
|
||||
required this.approvalId,
|
||||
required this.approvalNo,
|
||||
this.stepOrder,
|
||||
required this.action,
|
||||
this.fromStatus,
|
||||
required this.toStatus,
|
||||
required this.approver,
|
||||
required this.actionAt,
|
||||
this.note,
|
||||
});
|
||||
|
||||
final int id;
|
||||
final int approvalId;
|
||||
final String approvalNo;
|
||||
final int? stepOrder;
|
||||
final ApprovalAction action;
|
||||
final ApprovalStatus? fromStatus;
|
||||
final ApprovalStatus toStatus;
|
||||
final ApprovalApprover approver;
|
||||
final DateTime actionAt;
|
||||
final String? note;
|
||||
|
||||
factory ApprovalHistoryRecordDto.fromJson(Map<String, dynamic> json) {
|
||||
final approvalData = json['approval'] as Map<String, dynamic>?;
|
||||
final id = json['id'] as int? ?? 0;
|
||||
final approvalId =
|
||||
json['approval_id'] as int? ?? approvalData?['id'] as int? ?? 0;
|
||||
final approvalNo =
|
||||
json['approval_no'] as String? ??
|
||||
approvalData?['approval_no'] as String? ??
|
||||
approvalData?['approvalNo'] as String? ??
|
||||
'-';
|
||||
final stepOrder =
|
||||
json['step_order'] as int? ??
|
||||
(json['step'] as Map<String, dynamic>?)?['step_order'] as int?;
|
||||
final action = ApprovalActionDto.fromJson(
|
||||
json['action'] as Map<String, dynamic>? ?? const {},
|
||||
).toEntity();
|
||||
final fromStatus = json['from_status'] is Map<String, dynamic>
|
||||
? ApprovalStatusDto.fromJson(
|
||||
json['from_status'] as Map<String, dynamic>,
|
||||
).toEntity()
|
||||
: null;
|
||||
final toStatus = ApprovalStatusDto.fromJson(
|
||||
json['to_status'] as Map<String, dynamic>? ?? const {},
|
||||
).toEntity();
|
||||
final approver = ApprovalApproverDto.fromJson(
|
||||
json['approver'] as Map<String, dynamic>? ?? const {},
|
||||
).toEntity();
|
||||
final actionAt = _parseDate(json['action_at']) ?? DateTime.now();
|
||||
final note = json['note'] as String?;
|
||||
|
||||
return ApprovalHistoryRecordDto(
|
||||
id: id,
|
||||
approvalId: approvalId,
|
||||
approvalNo: approvalNo,
|
||||
stepOrder: stepOrder,
|
||||
action: action,
|
||||
fromStatus: fromStatus,
|
||||
toStatus: toStatus,
|
||||
approver: approver,
|
||||
actionAt: actionAt,
|
||||
note: note,
|
||||
);
|
||||
}
|
||||
|
||||
ApprovalHistoryRecord toEntity() {
|
||||
return ApprovalHistoryRecord(
|
||||
id: id,
|
||||
approvalId: approvalId,
|
||||
approvalNo: approvalNo,
|
||||
stepOrder: stepOrder,
|
||||
action: action,
|
||||
fromStatus: fromStatus,
|
||||
toStatus: toStatus,
|
||||
approver: approver,
|
||||
actionAt: actionAt,
|
||||
note: note,
|
||||
);
|
||||
}
|
||||
|
||||
static PaginatedResult<ApprovalHistoryRecord> parsePaginated(
|
||||
Map<String, dynamic>? json,
|
||||
) {
|
||||
final items = (json?['items'] as List<dynamic>? ?? [])
|
||||
.whereType<Map<String, dynamic>>()
|
||||
.map(ApprovalHistoryRecordDto.fromJson)
|
||||
.map((dto) => dto.toEntity())
|
||||
.toList();
|
||||
|
||||
return PaginatedResult<ApprovalHistoryRecord>(
|
||||
items: items,
|
||||
page: json?['page'] as int? ?? 1,
|
||||
pageSize: json?['page_size'] as int? ?? items.length,
|
||||
total: json?['total'] as int? ?? items.length,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
@@ -0,0 +1,41 @@
|
||||
import 'package:dio/dio.dart';
|
||||
import 'package:superport_v2/core/common/models/paginated_result.dart';
|
||||
import 'package:superport_v2/core/network/api_client.dart';
|
||||
|
||||
import '../../domain/entities/approval_history_record.dart';
|
||||
import '../../domain/repositories/approval_history_repository.dart';
|
||||
import '../dtos/approval_history_record_dto.dart';
|
||||
|
||||
class ApprovalHistoryRepositoryRemote implements ApprovalHistoryRepository {
|
||||
ApprovalHistoryRepositoryRemote({required ApiClient apiClient})
|
||||
: _api = apiClient;
|
||||
|
||||
final ApiClient _api;
|
||||
|
||||
static const _basePath = '/approval-histories';
|
||||
|
||||
@override
|
||||
Future<PaginatedResult<ApprovalHistoryRecord>> list({
|
||||
int page = 1,
|
||||
int pageSize = 20,
|
||||
String? query,
|
||||
String? action,
|
||||
DateTime? from,
|
||||
DateTime? to,
|
||||
}) async {
|
||||
final response = await _api.get<Map<String, dynamic>>(
|
||||
_basePath,
|
||||
query: {
|
||||
'page': page,
|
||||
'page_size': pageSize,
|
||||
if (query != null && query.isNotEmpty) 'q': query,
|
||||
if (action != null && action.isNotEmpty) 'action': action,
|
||||
if (from != null) 'from': from.toIso8601String(),
|
||||
if (to != null) 'to': to.toIso8601String(),
|
||||
},
|
||||
options: Options(responseType: ResponseType.json),
|
||||
);
|
||||
|
||||
return ApprovalHistoryRecordDto.parsePaginated(response.data);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,56 @@
|
||||
import '../../../domain/entities/approval.dart';
|
||||
|
||||
/// 결재 이력 레코드
|
||||
///
|
||||
/// - 결재별 단계 변경 로그를 목록 화면에서 표시하기 위한 모델이다.
|
||||
class ApprovalHistoryRecord {
|
||||
ApprovalHistoryRecord({
|
||||
required this.id,
|
||||
required this.approvalId,
|
||||
required this.approvalNo,
|
||||
this.stepOrder,
|
||||
required this.action,
|
||||
this.fromStatus,
|
||||
required this.toStatus,
|
||||
required this.approver,
|
||||
required this.actionAt,
|
||||
this.note,
|
||||
});
|
||||
|
||||
final int id;
|
||||
final int approvalId;
|
||||
final String approvalNo;
|
||||
final int? stepOrder;
|
||||
final ApprovalAction action;
|
||||
final ApprovalStatus? fromStatus;
|
||||
final ApprovalStatus toStatus;
|
||||
final ApprovalApprover approver;
|
||||
final DateTime actionAt;
|
||||
final String? note;
|
||||
|
||||
ApprovalHistoryRecord copyWith({
|
||||
int? id,
|
||||
int? approvalId,
|
||||
String? approvalNo,
|
||||
int? stepOrder,
|
||||
ApprovalAction? action,
|
||||
ApprovalStatus? fromStatus,
|
||||
ApprovalStatus? toStatus,
|
||||
ApprovalApprover? approver,
|
||||
DateTime? actionAt,
|
||||
String? note,
|
||||
}) {
|
||||
return ApprovalHistoryRecord(
|
||||
id: id ?? this.id,
|
||||
approvalId: approvalId ?? this.approvalId,
|
||||
approvalNo: approvalNo ?? this.approvalNo,
|
||||
stepOrder: stepOrder ?? this.stepOrder,
|
||||
action: action ?? this.action,
|
||||
fromStatus: fromStatus ?? this.fromStatus,
|
||||
toStatus: toStatus ?? this.toStatus,
|
||||
approver: approver ?? this.approver,
|
||||
actionAt: actionAt ?? this.actionAt,
|
||||
note: note ?? this.note,
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,14 @@
|
||||
import 'package:superport_v2/core/common/models/paginated_result.dart';
|
||||
|
||||
import '../entities/approval_history_record.dart';
|
||||
|
||||
abstract class ApprovalHistoryRepository {
|
||||
Future<PaginatedResult<ApprovalHistoryRecord>> list({
|
||||
int page = 1,
|
||||
int pageSize = 20,
|
||||
String? query,
|
||||
String? action,
|
||||
DateTime? from,
|
||||
DateTime? to,
|
||||
});
|
||||
}
|
||||
@@ -0,0 +1,94 @@
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:superport_v2/core/common/models/paginated_result.dart';
|
||||
|
||||
import '../../domain/entities/approval_history_record.dart';
|
||||
import '../../domain/repositories/approval_history_repository.dart';
|
||||
|
||||
enum ApprovalHistoryActionFilter { all, approve, reject, comment }
|
||||
|
||||
class ApprovalHistoryController extends ChangeNotifier {
|
||||
ApprovalHistoryController({required ApprovalHistoryRepository repository})
|
||||
: _repository = repository;
|
||||
|
||||
final ApprovalHistoryRepository _repository;
|
||||
|
||||
PaginatedResult<ApprovalHistoryRecord>? _result;
|
||||
bool _isLoading = false;
|
||||
String _query = '';
|
||||
ApprovalHistoryActionFilter _actionFilter = ApprovalHistoryActionFilter.all;
|
||||
DateTime? _from;
|
||||
DateTime? _to;
|
||||
String? _errorMessage;
|
||||
|
||||
PaginatedResult<ApprovalHistoryRecord>? get result => _result;
|
||||
bool get isLoading => _isLoading;
|
||||
String get query => _query;
|
||||
ApprovalHistoryActionFilter get actionFilter => _actionFilter;
|
||||
DateTime? get from => _from;
|
||||
DateTime? get to => _to;
|
||||
String? get errorMessage => _errorMessage;
|
||||
|
||||
Future<void> fetch({int page = 1}) async {
|
||||
_isLoading = true;
|
||||
_errorMessage = null;
|
||||
notifyListeners();
|
||||
try {
|
||||
final action = switch (_actionFilter) {
|
||||
ApprovalHistoryActionFilter.all => null,
|
||||
ApprovalHistoryActionFilter.approve => 'approve',
|
||||
ApprovalHistoryActionFilter.reject => 'reject',
|
||||
ApprovalHistoryActionFilter.comment => 'comment',
|
||||
};
|
||||
|
||||
final response = await _repository.list(
|
||||
page: page,
|
||||
pageSize: _result?.pageSize ?? 20,
|
||||
query: _query.trim().isEmpty ? null : _query.trim(),
|
||||
action: action,
|
||||
from: _from,
|
||||
to: _to,
|
||||
);
|
||||
_result = response;
|
||||
} catch (e) {
|
||||
_errorMessage = e.toString();
|
||||
} finally {
|
||||
_isLoading = false;
|
||||
notifyListeners();
|
||||
}
|
||||
}
|
||||
|
||||
void updateQuery(String value) {
|
||||
_query = value;
|
||||
notifyListeners();
|
||||
}
|
||||
|
||||
void updateActionFilter(ApprovalHistoryActionFilter filter) {
|
||||
_actionFilter = filter;
|
||||
notifyListeners();
|
||||
}
|
||||
|
||||
void updateDateRange(DateTime? from, DateTime? to) {
|
||||
_from = from;
|
||||
_to = to;
|
||||
notifyListeners();
|
||||
}
|
||||
|
||||
void clearFilters() {
|
||||
_query = '';
|
||||
_actionFilter = ApprovalHistoryActionFilter.all;
|
||||
_from = null;
|
||||
_to = null;
|
||||
notifyListeners();
|
||||
}
|
||||
|
||||
void clearError() {
|
||||
_errorMessage = null;
|
||||
notifyListeners();
|
||||
}
|
||||
|
||||
bool get hasActiveFilters =>
|
||||
_query.trim().isNotEmpty ||
|
||||
_actionFilter != ApprovalHistoryActionFilter.all ||
|
||||
_from != null ||
|
||||
_to != null;
|
||||
}
|
||||
@@ -1,11 +1,16 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:get_it/get_it.dart';
|
||||
import 'package:lucide_icons_flutter/lucide_icons.dart' as lucide;
|
||||
import 'package:shadcn_ui/shadcn_ui.dart';
|
||||
|
||||
import '../../../../../core/config/environment.dart';
|
||||
import '../../../../../core/constants/app_sections.dart';
|
||||
import '../../../../../widgets/app_layout.dart';
|
||||
import '../../../../../widgets/components/coming_soon_card.dart';
|
||||
import '../../../../../widgets/components/filter_bar.dart';
|
||||
import '../../../../../widgets/spec_page.dart';
|
||||
import '../../domain/entities/approval_history_record.dart';
|
||||
import '../../domain/repositories/approval_history_repository.dart';
|
||||
import '../controllers/approval_history_controller.dart';
|
||||
|
||||
class ApprovalHistoryPage extends StatelessWidget {
|
||||
const ApprovalHistoryPage({super.key});
|
||||
@@ -25,7 +30,7 @@ class ApprovalHistoryPage extends StatelessWidget {
|
||||
columns: [
|
||||
'번호',
|
||||
'결재ID',
|
||||
'단계ID',
|
||||
'단계순서',
|
||||
'승인자',
|
||||
'행위',
|
||||
'변경전상태',
|
||||
@@ -37,7 +42,7 @@ class ApprovalHistoryPage extends StatelessWidget {
|
||||
[
|
||||
'1',
|
||||
'APP-20240301-001',
|
||||
'STEP-1',
|
||||
'1',
|
||||
'최관리',
|
||||
'승인',
|
||||
'승인대기',
|
||||
@@ -52,22 +57,366 @@ class ApprovalHistoryPage extends StatelessWidget {
|
||||
);
|
||||
}
|
||||
|
||||
return AppLayout(
|
||||
title: '결재 이력 조회',
|
||||
subtitle: '결재 단계별 변경 기록을 확인할 수 있도록 준비 중입니다.',
|
||||
breadcrumbs: const [
|
||||
AppBreadcrumbItem(label: '대시보드', path: dashboardRoutePath),
|
||||
AppBreadcrumbItem(label: '결재', path: '/approvals/history'),
|
||||
AppBreadcrumbItem(label: '결재 이력'),
|
||||
],
|
||||
toolbar: FilterBar(
|
||||
children: const [Text('이력 검색 조건은 API 사양 확정 후 제공될 예정입니다.')],
|
||||
),
|
||||
child: const ComingSoonCard(
|
||||
title: '결재 이력 화면 구현 준비 중',
|
||||
description: '결재 단계 로그 API와 연동해 조건 검색 및 엑셀 내보내기를 제공할 예정입니다.',
|
||||
items: ['결재번호/승인자/행위 유형별 필터', '기간·상태 조건 조합 검색', '다운로드(Excel/PDF) 기능'],
|
||||
),
|
||||
return const _ApprovalHistoryEnabledPage();
|
||||
}
|
||||
}
|
||||
|
||||
class _ApprovalHistoryEnabledPage extends StatefulWidget {
|
||||
const _ApprovalHistoryEnabledPage();
|
||||
|
||||
@override
|
||||
State<_ApprovalHistoryEnabledPage> createState() =>
|
||||
_ApprovalHistoryEnabledPageState();
|
||||
}
|
||||
|
||||
class _ApprovalHistoryEnabledPageState
|
||||
extends State<_ApprovalHistoryEnabledPage> {
|
||||
late final ApprovalHistoryController _controller;
|
||||
final TextEditingController _searchController = TextEditingController();
|
||||
final FocusNode _searchFocus = FocusNode();
|
||||
final DateFormat _dateTimeFormat = DateFormat('yyyy-MM-dd HH:mm');
|
||||
DateTimeRange? _dateRange;
|
||||
String? _lastError;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
_controller = ApprovalHistoryController(
|
||||
repository: GetIt.I<ApprovalHistoryRepository>(),
|
||||
)..addListener(_handleUpdate);
|
||||
WidgetsBinding.instance.addPostFrameCallback((_) async {
|
||||
await _controller.fetch();
|
||||
});
|
||||
}
|
||||
|
||||
void _handleUpdate() {
|
||||
final error = _controller.errorMessage;
|
||||
if (error != null && error != _lastError && mounted) {
|
||||
_lastError = error;
|
||||
ScaffoldMessenger.of(
|
||||
context,
|
||||
).showSnackBar(SnackBar(content: Text(error)));
|
||||
_controller.clearError();
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
_controller.removeListener(_handleUpdate);
|
||||
_controller.dispose();
|
||||
_searchController.dispose();
|
||||
_searchFocus.dispose();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return AnimatedBuilder(
|
||||
animation: _controller,
|
||||
builder: (context, _) {
|
||||
final theme = ShadTheme.of(context);
|
||||
final result = _controller.result;
|
||||
final histories = result?.items ?? const <ApprovalHistoryRecord>[];
|
||||
final totalCount = result?.total ?? 0;
|
||||
final currentPage = result?.page ?? 1;
|
||||
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;
|
||||
|
||||
return AppLayout(
|
||||
title: '결재 이력 조회',
|
||||
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('엑셀 다운로드'),
|
||||
),
|
||||
),
|
||||
],
|
||||
toolbar: FilterBar(
|
||||
children: [
|
||||
SizedBox(
|
||||
width: 240,
|
||||
child: ShadInput(
|
||||
controller: _searchController,
|
||||
focusNode: _searchFocus,
|
||||
placeholder: const Text('결재번호, 승인자 검색'),
|
||||
leading: const Icon(lucide.LucideIcons.search, size: 16),
|
||||
onSubmitted: (_) => _applyFilters(),
|
||||
),
|
||||
),
|
||||
SizedBox(
|
||||
width: 200,
|
||||
child: ShadSelect<ApprovalHistoryActionFilter>(
|
||||
key: ValueKey(_controller.actionFilter),
|
||||
initialValue: _controller.actionFilter,
|
||||
selectedOptionBuilder: (context, value) =>
|
||||
Text(_actionLabel(value)),
|
||||
onChanged: (value) {
|
||||
if (value == null) return;
|
||||
_controller.updateActionFilter(value);
|
||||
_controller.fetch(page: 1);
|
||||
},
|
||||
options: ApprovalHistoryActionFilter.values
|
||||
.map(
|
||||
(filter) => ShadOption(
|
||||
value: filter,
|
||||
child: Text(_actionLabel(filter)),
|
||||
),
|
||||
)
|
||||
.toList(),
|
||||
),
|
||||
),
|
||||
SizedBox(
|
||||
width: 220,
|
||||
child: ShadButton.outline(
|
||||
onPressed: _pickDateRange,
|
||||
child: Row(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
const Icon(lucide.LucideIcons.calendar, size: 16),
|
||||
const SizedBox(width: 8),
|
||||
Text(
|
||||
_dateRange == null
|
||||
? '기간 선택'
|
||||
: '${_formatDate(_dateRange!.start)} ~ ${_formatDate(_dateRange!.end)}',
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
if (_dateRange != null)
|
||||
ShadButton.ghost(
|
||||
onPressed: _controller.isLoading ? null : _clearDateRange,
|
||||
child: const Text('기간 초기화'),
|
||||
),
|
||||
ShadButton.outline(
|
||||
onPressed: _controller.isLoading ? null : _applyFilters,
|
||||
child: const Text('검색 적용'),
|
||||
),
|
||||
ShadButton.ghost(
|
||||
onPressed:
|
||||
_controller.isLoading || !_controller.hasActiveFilters
|
||||
? null
|
||||
: _resetFilters,
|
||||
child: const Text('필터 초기화'),
|
||||
),
|
||||
],
|
||||
),
|
||||
child: ShadCard(
|
||||
title: Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
Text('이력 목록', style: theme.textTheme.h3),
|
||||
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),
|
||||
child: Center(child: CircularProgressIndicator()),
|
||||
)
|
||||
: histories.isEmpty
|
||||
? Padding(
|
||||
padding: const EdgeInsets.all(32),
|
||||
child: Text(
|
||||
'조건에 맞는 결재 이력이 없습니다.',
|
||||
style: theme.textTheme.muted,
|
||||
),
|
||||
)
|
||||
: _ApprovalHistoryTable(
|
||||
histories: histories,
|
||||
dateFormat: _dateTimeFormat,
|
||||
query: _controller.query,
|
||||
),
|
||||
),
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
void _applyFilters() {
|
||||
_controller.updateQuery(_searchController.text.trim());
|
||||
if (_dateRange != null) {
|
||||
_controller.updateDateRange(_dateRange!.start, _dateRange!.end);
|
||||
}
|
||||
_controller.fetch(page: 1);
|
||||
}
|
||||
|
||||
Future<void> _pickDateRange() async {
|
||||
final now = DateTime.now();
|
||||
final initial =
|
||||
_dateRange ??
|
||||
DateTimeRange(
|
||||
start: DateTime(now.year, now.month, now.day - 7),
|
||||
end: now,
|
||||
);
|
||||
final range = await showDateRangePicker(
|
||||
context: context,
|
||||
initialDateRange: initial,
|
||||
firstDate: DateTime(now.year - 5),
|
||||
lastDate: DateTime(now.year + 1),
|
||||
);
|
||||
if (range != null) {
|
||||
setState(() => _dateRange = range);
|
||||
_controller.updateDateRange(range.start, range.end);
|
||||
_controller.fetch(page: 1);
|
||||
}
|
||||
}
|
||||
|
||||
void _clearDateRange() {
|
||||
setState(() => _dateRange = null);
|
||||
_controller.updateDateRange(null, null);
|
||||
_controller.fetch(page: 1);
|
||||
}
|
||||
|
||||
void _resetFilters() {
|
||||
_searchController.clear();
|
||||
_searchFocus.requestFocus();
|
||||
_dateRange = null;
|
||||
_controller.clearFilters();
|
||||
_controller.fetch(page: 1);
|
||||
}
|
||||
|
||||
String _formatDate(DateTime date) {
|
||||
return DateFormat('yyyy-MM-dd').format(date.toLocal());
|
||||
}
|
||||
|
||||
String _actionLabel(ApprovalHistoryActionFilter filter) {
|
||||
switch (filter) {
|
||||
case ApprovalHistoryActionFilter.all:
|
||||
return '전체 행위';
|
||||
case ApprovalHistoryActionFilter.approve:
|
||||
return '승인';
|
||||
case ApprovalHistoryActionFilter.reject:
|
||||
return '반려';
|
||||
case ApprovalHistoryActionFilter.comment:
|
||||
return '코멘트';
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
class _ApprovalHistoryTable extends StatelessWidget {
|
||||
const _ApprovalHistoryTable({
|
||||
required this.histories,
|
||||
required this.dateFormat,
|
||||
required this.query,
|
||||
});
|
||||
|
||||
final List<ApprovalHistoryRecord> histories;
|
||||
final DateFormat dateFormat;
|
||||
final String query;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final normalizedQuery = query.trim().toLowerCase();
|
||||
final header = [
|
||||
'ID',
|
||||
'결재번호',
|
||||
'단계순서',
|
||||
'승인자',
|
||||
'행위',
|
||||
'변경전 상태',
|
||||
'변경후 상태',
|
||||
'작업일시',
|
||||
'비고',
|
||||
].map((label) => ShadTableCell.header(child: Text(label))).toList();
|
||||
|
||||
final rows = histories.map((history) {
|
||||
final isHighlighted =
|
||||
normalizedQuery.isNotEmpty &&
|
||||
history.approvalNo.toLowerCase().contains(normalizedQuery);
|
||||
return [
|
||||
ShadTableCell(child: Text(history.id.toString())),
|
||||
ShadTableCell(
|
||||
child: Text(
|
||||
history.approvalNo,
|
||||
style: isHighlighted
|
||||
? ShadTheme.of(
|
||||
context,
|
||||
).textTheme.small.copyWith(fontWeight: FontWeight.w600)
|
||||
: null,
|
||||
),
|
||||
),
|
||||
ShadTableCell(
|
||||
child: Text(
|
||||
history.stepOrder == null ? '-' : history.stepOrder.toString(),
|
||||
),
|
||||
),
|
||||
ShadTableCell(child: Text(history.approver.name)),
|
||||
ShadTableCell(child: Text(history.action.name)),
|
||||
ShadTableCell(child: Text(history.fromStatus?.name ?? '-')),
|
||||
ShadTableCell(child: Text(history.toStatus.name)),
|
||||
ShadTableCell(
|
||||
child: Text(dateFormat.format(history.actionAt.toLocal())),
|
||||
),
|
||||
ShadTableCell(
|
||||
child: Text(
|
||||
history.note?.trim().isEmpty ?? true ? '-' : history.note!,
|
||||
),
|
||||
),
|
||||
];
|
||||
}).toList();
|
||||
|
||||
return ShadTable.list(
|
||||
header: header,
|
||||
children: rows,
|
||||
columnSpanExtent: (index) {
|
||||
switch (index) {
|
||||
case 1:
|
||||
return const FixedTableSpanExtent(180);
|
||||
case 2:
|
||||
case 4:
|
||||
return const FixedTableSpanExtent(120);
|
||||
case 5:
|
||||
case 6:
|
||||
return const FixedTableSpanExtent(150);
|
||||
case 7:
|
||||
return const FixedTableSpanExtent(180);
|
||||
default:
|
||||
return const FixedTableSpanExtent(110);
|
||||
}
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user