결재 및 마스터 모듈을 v4 API 계약에 맞게 조정
This commit is contained in:
@@ -25,24 +25,28 @@ class ApprovalRepositoryRemote implements ApprovalRepository {
|
||||
Future<PaginatedResult<Approval>> list({
|
||||
int page = 1,
|
||||
int pageSize = 20,
|
||||
String? query,
|
||||
String? status,
|
||||
DateTime? from,
|
||||
DateTime? to,
|
||||
int? transactionId,
|
||||
int? approvalStatusId,
|
||||
int? requestedById,
|
||||
bool includeHistories = false,
|
||||
bool includeSteps = false,
|
||||
}) async {
|
||||
final includeParts = <String>[];
|
||||
if (includeSteps) {
|
||||
includeParts.add('steps');
|
||||
}
|
||||
if (includeHistories) {
|
||||
includeParts.add('histories');
|
||||
}
|
||||
final response = await _api.get<Map<String, dynamic>>(
|
||||
_basePath,
|
||||
query: {
|
||||
'page': page,
|
||||
'page_size': pageSize,
|
||||
if (query != null && query.isNotEmpty) 'q': query,
|
||||
if (status != null && status.isNotEmpty) 'status': status,
|
||||
if (from != null) 'from': from.toIso8601String(),
|
||||
if (to != null) 'to': to.toIso8601String(),
|
||||
if (includeHistories) 'include_histories': true,
|
||||
if (includeSteps) 'include_steps': true,
|
||||
if (transactionId != null) 'transaction_id': transactionId,
|
||||
if (approvalStatusId != null) 'approval_status_id': approvalStatusId,
|
||||
if (requestedById != null) 'requested_by_id': requestedById,
|
||||
if (includeParts.isNotEmpty) 'include': includeParts.join(','),
|
||||
},
|
||||
options: Options(responseType: ResponseType.json),
|
||||
);
|
||||
@@ -56,12 +60,16 @@ class ApprovalRepositoryRemote implements ApprovalRepository {
|
||||
bool includeSteps = true,
|
||||
bool includeHistories = true,
|
||||
}) async {
|
||||
final includeParts = <String>[];
|
||||
if (includeSteps) {
|
||||
includeParts.add('steps');
|
||||
}
|
||||
if (includeHistories) {
|
||||
includeParts.add('histories');
|
||||
}
|
||||
final response = await _api.get<Map<String, dynamic>>(
|
||||
'$_basePath/$id',
|
||||
query: {
|
||||
if (includeSteps) 'include_steps': true,
|
||||
if (includeHistories) 'include_histories': true,
|
||||
},
|
||||
query: {if (includeParts.isNotEmpty) 'include': includeParts.join(',')},
|
||||
options: Options(responseType: ResponseType.json),
|
||||
);
|
||||
final data = (response.data?['data'] as Map<String, dynamic>?) ?? {};
|
||||
|
||||
@@ -11,10 +11,9 @@ abstract class ApprovalRepository {
|
||||
Future<PaginatedResult<Approval>> list({
|
||||
int page = 1,
|
||||
int pageSize = 20,
|
||||
String? query,
|
||||
String? status,
|
||||
DateTime? from,
|
||||
DateTime? to,
|
||||
int? transactionId,
|
||||
int? approvalStatusId,
|
||||
int? requestedById,
|
||||
bool includeHistories = false,
|
||||
bool includeSteps = false,
|
||||
});
|
||||
|
||||
@@ -64,10 +64,11 @@ class ApprovalController extends ChangeNotifier {
|
||||
int? _applyingTemplateId;
|
||||
ApprovalProceedStatus? _proceedStatus;
|
||||
String? _errorMessage;
|
||||
String _query = '';
|
||||
ApprovalStatusFilter _statusFilter = ApprovalStatusFilter.all;
|
||||
DateTime? _fromDate;
|
||||
DateTime? _toDate;
|
||||
int? _transactionIdFilter;
|
||||
int? _requestedById;
|
||||
String? _requestedByName;
|
||||
String? _requestedByEmployeeNo;
|
||||
List<ApprovalAction> _actions = const [];
|
||||
List<ApprovalTemplate> _templates = const [];
|
||||
final Map<String, LookupItem> _statusLookup = {};
|
||||
@@ -85,10 +86,11 @@ class ApprovalController extends ChangeNotifier {
|
||||
bool get isPerformingAction => _isPerformingAction;
|
||||
int? get processingStepId => _processingStepId;
|
||||
String? get errorMessage => _errorMessage;
|
||||
String get query => _query;
|
||||
ApprovalStatusFilter get statusFilter => _statusFilter;
|
||||
DateTime? get fromDate => _fromDate;
|
||||
DateTime? get toDate => _toDate;
|
||||
int? get transactionIdFilter => _transactionIdFilter;
|
||||
int? get requestedById => _requestedById;
|
||||
String? get requestedByName => _requestedByName;
|
||||
String? get requestedByEmployeeNo => _requestedByEmployeeNo;
|
||||
List<ApprovalAction> get actionOptions => _actions;
|
||||
bool get hasActionOptions => _actions.isNotEmpty;
|
||||
List<ApprovalTemplate> get templates => _templates;
|
||||
@@ -116,14 +118,13 @@ class ApprovalController extends ChangeNotifier {
|
||||
_errorMessage = null;
|
||||
notifyListeners();
|
||||
try {
|
||||
final statusParam = _statusCodeFor(_statusFilter);
|
||||
final statusId = _statusIdFor(_statusFilter);
|
||||
final response = await _repository.list(
|
||||
page: page,
|
||||
pageSize: _result?.pageSize ?? 20,
|
||||
query: _query.isEmpty ? null : _query,
|
||||
status: statusParam,
|
||||
from: _fromDate,
|
||||
to: _toDate,
|
||||
transactionId: _transactionIdFilter,
|
||||
approvalStatusId: statusId,
|
||||
requestedById: _requestedById,
|
||||
includeSteps: false,
|
||||
includeHistories: false,
|
||||
);
|
||||
@@ -176,18 +177,29 @@ class ApprovalController extends ChangeNotifier {
|
||||
_statusLookup
|
||||
..clear()
|
||||
..addEntries(
|
||||
items.map(
|
||||
(item) => MapEntry(
|
||||
(item.code ?? item.name).toLowerCase(),
|
||||
item,
|
||||
),
|
||||
),
|
||||
items.expand((item) {
|
||||
final keys = <String>{};
|
||||
final code = item.code?.trim();
|
||||
if (code != null && code.isNotEmpty) {
|
||||
keys.add(code.toLowerCase());
|
||||
}
|
||||
final name = item.name.trim();
|
||||
if (name.isNotEmpty) {
|
||||
keys.add(name.toLowerCase());
|
||||
}
|
||||
keys.add(item.id.toString());
|
||||
return keys.map((key) => MapEntry(key, item));
|
||||
}),
|
||||
);
|
||||
for (final entry in _defaultStatusCodes.entries) {
|
||||
final code = entry.value.toLowerCase();
|
||||
final lookup = _statusLookup[code];
|
||||
final defaultCode = entry.value;
|
||||
final normalized = defaultCode.toLowerCase();
|
||||
final lookup = _statusLookup[normalized];
|
||||
if (lookup != null) {
|
||||
_statusCodeAliases[entry.value] = lookup.code?.toLowerCase() ?? code;
|
||||
final alias = lookup.code?.toLowerCase() ?? normalized;
|
||||
_statusCodeAliases[defaultCode] = alias;
|
||||
} else {
|
||||
_statusCodeAliases[defaultCode] = defaultCode;
|
||||
}
|
||||
}
|
||||
notifyListeners();
|
||||
@@ -229,6 +241,15 @@ class ApprovalController extends ChangeNotifier {
|
||||
return _statusCodeAliases[defaultCode] ?? defaultCode;
|
||||
}
|
||||
|
||||
int? _statusIdFor(ApprovalStatusFilter filter) {
|
||||
final code = _statusCodeFor(filter);
|
||||
if (code == null) {
|
||||
return null;
|
||||
}
|
||||
final lookup = _statusLookup[code.toLowerCase()];
|
||||
return lookup?.id;
|
||||
}
|
||||
|
||||
/// 활성화된 결재 템플릿 목록을 조회해 캐싱한다.
|
||||
///
|
||||
/// 템플릿이 비어 있거나 [force]가 `true`이면 API를 다시 호출한다.
|
||||
@@ -325,8 +346,7 @@ class ApprovalController extends ChangeNotifier {
|
||||
final proceedStatus = await _repository.canProceed(approvalId);
|
||||
_proceedStatus = proceedStatus;
|
||||
if (!proceedStatus.canProceed) {
|
||||
_errorMessage = proceedStatus.reason ??
|
||||
'결재 단계가 현재 상태에서 진행될 수 없습니다.';
|
||||
_errorMessage = proceedStatus.reason ?? '결재 단계가 현재 상태에서 진행될 수 없습니다.';
|
||||
return false;
|
||||
}
|
||||
|
||||
@@ -421,31 +441,33 @@ class ApprovalController extends ChangeNotifier {
|
||||
}
|
||||
}
|
||||
|
||||
/// 검색 키워드를 변경하고 UI 갱신을 유도한다.
|
||||
void updateQuery(String value) {
|
||||
_query = value;
|
||||
notifyListeners();
|
||||
}
|
||||
|
||||
/// 상태 필터 값을 변경한다.
|
||||
void updateStatusFilter(ApprovalStatusFilter filter) {
|
||||
_statusFilter = filter;
|
||||
notifyListeners();
|
||||
}
|
||||
|
||||
/// 조회 기간을 설정한다. 두 값 모두 `null`이면 기간 조건을 해제한다.
|
||||
void updateDateRange(DateTime? from, DateTime? to) {
|
||||
_fromDate = from;
|
||||
_toDate = to;
|
||||
/// 트랜잭션 ID 필터를 갱신한다. null이면 조건을 제거한다.
|
||||
void updateTransactionFilter(int? transactionId) {
|
||||
_transactionIdFilter = transactionId;
|
||||
notifyListeners();
|
||||
}
|
||||
|
||||
/// 검색어/상태/기간 등의 필터 조건을 초기화한다.
|
||||
/// 상신자(요청자) 필터를 갱신한다. null 값을 전달하면 조건을 제거한다.
|
||||
void updateRequestedByFilter({int? id, String? name, String? employeeNo}) {
|
||||
_requestedById = id;
|
||||
_requestedByName = name;
|
||||
_requestedByEmployeeNo = employeeNo;
|
||||
notifyListeners();
|
||||
}
|
||||
|
||||
/// 상태/트랜잭션/상신자 필터를 초기값으로 되돌린다.
|
||||
void clearFilters() {
|
||||
_query = '';
|
||||
_statusFilter = ApprovalStatusFilter.all;
|
||||
_fromDate = null;
|
||||
_toDate = null;
|
||||
_transactionIdFilter = null;
|
||||
_requestedById = null;
|
||||
_requestedByName = null;
|
||||
_requestedByEmployeeNo = null;
|
||||
notifyListeners();
|
||||
}
|
||||
|
||||
|
||||
@@ -11,7 +11,6 @@ import '../../../../core/permissions/permission_resources.dart';
|
||||
import '../../../../widgets/app_layout.dart';
|
||||
import '../../../../widgets/components/feedback.dart';
|
||||
import '../../../../widgets/components/filter_bar.dart';
|
||||
import '../../../../widgets/components/superport_date_picker.dart';
|
||||
import '../../../../widgets/components/superport_dialog.dart';
|
||||
import '../../../../widgets/components/superport_table.dart';
|
||||
import '../../../../widgets/components/feature_disabled_placeholder.dart';
|
||||
@@ -20,6 +19,7 @@ import '../../domain/entities/approval_template.dart';
|
||||
import '../../domain/repositories/approval_repository.dart';
|
||||
import '../../domain/repositories/approval_template_repository.dart';
|
||||
import '../../../inventory/lookups/domain/repositories/inventory_lookup_repository.dart';
|
||||
import '../../../inventory/shared/widgets/employee_autocomplete_field.dart';
|
||||
import '../controllers/approval_controller.dart';
|
||||
|
||||
const _approvalsResourcePath = PermissionResources.approvals;
|
||||
@@ -73,11 +73,11 @@ class _ApprovalEnabledPage extends StatefulWidget {
|
||||
|
||||
class _ApprovalEnabledPageState extends State<_ApprovalEnabledPage> {
|
||||
late final ApprovalController _controller;
|
||||
final TextEditingController _searchController = TextEditingController();
|
||||
final FocusNode _searchFocus = FocusNode();
|
||||
final intl.DateFormat _dateFormat = intl.DateFormat('yyyy-MM-dd');
|
||||
final TextEditingController _transactionController = TextEditingController();
|
||||
final TextEditingController _requesterController = TextEditingController();
|
||||
final FocusNode _transactionFocus = FocusNode();
|
||||
InventoryEmployeeSuggestion? _selectedRequester;
|
||||
final intl.DateFormat _dateTimeFormat = intl.DateFormat('yyyy-MM-dd HH:mm');
|
||||
DateTimeRange? _dateRange;
|
||||
String? _lastError;
|
||||
int? _selectedTemplateId;
|
||||
|
||||
@@ -114,8 +114,9 @@ class _ApprovalEnabledPageState extends State<_ApprovalEnabledPage> {
|
||||
void dispose() {
|
||||
_controller.removeListener(_handleControllerUpdate);
|
||||
_controller.dispose();
|
||||
_searchController.dispose();
|
||||
_searchFocus.dispose();
|
||||
_transactionController.dispose();
|
||||
_requesterController.dispose();
|
||||
_transactionFocus.dispose();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
@@ -197,16 +198,39 @@ class _ApprovalEnabledPageState extends State<_ApprovalEnabledPage> {
|
||||
),
|
||||
children: [
|
||||
SizedBox(
|
||||
width: 260,
|
||||
width: 180,
|
||||
child: ShadInput(
|
||||
controller: _searchController,
|
||||
focusNode: _searchFocus,
|
||||
placeholder: const Text('결재번호, 트랜잭션번호, 상신자 검색'),
|
||||
leading: const Icon(lucide.LucideIcons.search, size: 16),
|
||||
controller: _transactionController,
|
||||
focusNode: _transactionFocus,
|
||||
enabled: !_controller.isLoadingList,
|
||||
keyboardType: TextInputType.number,
|
||||
placeholder: const Text('트랜잭션 ID 입력'),
|
||||
leading: const Icon(lucide.LucideIcons.hash, size: 16),
|
||||
onChanged: (_) => setState(() {}),
|
||||
onSubmitted: (_) => _applyFilters(),
|
||||
),
|
||||
),
|
||||
SizedBox(
|
||||
width: 280,
|
||||
child: InventoryEmployeeAutocompleteField(
|
||||
controller: _requesterController,
|
||||
initialSuggestion: _selectedRequester,
|
||||
enabled: !_controller.isLoadingList,
|
||||
onSuggestionSelected: (suggestion) {
|
||||
setState(() => _selectedRequester = suggestion);
|
||||
},
|
||||
onChanged: () {
|
||||
setState(() {
|
||||
final selectedLabel = _selectedRequester == null
|
||||
? ''
|
||||
: '${_selectedRequester!.name} (${_selectedRequester!.employeeNo})';
|
||||
if (_requesterController.text.trim() != selectedLabel) {
|
||||
_selectedRequester = null;
|
||||
}
|
||||
});
|
||||
},
|
||||
),
|
||||
),
|
||||
SizedBox(
|
||||
width: 200,
|
||||
child: ShadSelect<ApprovalStatusFilter>(
|
||||
@@ -229,37 +253,6 @@ class _ApprovalEnabledPageState extends State<_ApprovalEnabledPage> {
|
||||
.toList(),
|
||||
),
|
||||
),
|
||||
SizedBox(
|
||||
width: 220,
|
||||
child: SuperportDateRangePickerButton(
|
||||
value: _dateRange,
|
||||
dateFormat: _dateFormat,
|
||||
enabled: !_controller.isLoadingList,
|
||||
firstDate: DateTime(DateTime.now().year - 5),
|
||||
lastDate: DateTime(DateTime.now().year + 1),
|
||||
initialDateRange:
|
||||
_dateRange ??
|
||||
DateTimeRange(
|
||||
start: DateTime.now().subtract(const Duration(days: 7)),
|
||||
end: DateTime.now(),
|
||||
),
|
||||
onChanged: (range) {
|
||||
if (range == null) return;
|
||||
setState(() => _dateRange = range);
|
||||
_controller.updateDateRange(range.start, range.end);
|
||||
_controller.fetch(page: 1);
|
||||
},
|
||||
),
|
||||
),
|
||||
if (_dateRange != null)
|
||||
ShadButton.ghost(
|
||||
onPressed: () {
|
||||
setState(() => _dateRange = null);
|
||||
_controller.updateDateRange(null, null);
|
||||
_controller.fetch(page: 1);
|
||||
},
|
||||
child: const Text('기간 초기화'),
|
||||
),
|
||||
],
|
||||
),
|
||||
child: Column(
|
||||
@@ -476,25 +469,48 @@ class _ApprovalEnabledPageState extends State<_ApprovalEnabledPage> {
|
||||
}
|
||||
|
||||
void _applyFilters() {
|
||||
_controller.updateQuery(_searchController.text.trim());
|
||||
if (_dateRange != null) {
|
||||
_controller.updateDateRange(_dateRange!.start, _dateRange!.end);
|
||||
final transactionText = _transactionController.text.trim();
|
||||
if (transactionText.isNotEmpty) {
|
||||
final transactionId = int.tryParse(transactionText);
|
||||
if (transactionId == null) {
|
||||
SuperportToast.error(context, '트랜잭션 ID는 숫자만 입력해야 합니다.');
|
||||
return;
|
||||
}
|
||||
_controller.updateTransactionFilter(transactionId);
|
||||
} else {
|
||||
_controller.updateTransactionFilter(null);
|
||||
}
|
||||
|
||||
final requester = _selectedRequester;
|
||||
if (requester != null) {
|
||||
_controller.updateRequestedByFilter(
|
||||
id: requester.id,
|
||||
name: requester.name,
|
||||
employeeNo: requester.employeeNo,
|
||||
);
|
||||
} else if (_requesterController.text.trim().isEmpty) {
|
||||
_controller.updateRequestedByFilter(id: null);
|
||||
}
|
||||
|
||||
_controller.fetch(page: 1);
|
||||
}
|
||||
|
||||
void _resetFilters() {
|
||||
_searchController.clear();
|
||||
_searchFocus.requestFocus();
|
||||
_dateRange = null;
|
||||
_transactionController.clear();
|
||||
_requesterController.clear();
|
||||
setState(() => _selectedRequester = null);
|
||||
_transactionFocus.requestFocus();
|
||||
_controller.clearFilters();
|
||||
_controller.fetch(page: 1);
|
||||
}
|
||||
|
||||
bool _hasFilters() {
|
||||
return _searchController.text.isNotEmpty ||
|
||||
_controller.statusFilter != ApprovalStatusFilter.all ||
|
||||
_dateRange != null;
|
||||
return _transactionController.text.trim().isNotEmpty ||
|
||||
_requesterController.text.trim().isNotEmpty ||
|
||||
_selectedRequester != null ||
|
||||
_controller.transactionIdFilter != null ||
|
||||
_controller.requestedById != null ||
|
||||
_controller.statusFilter != ApprovalStatusFilter.all;
|
||||
}
|
||||
|
||||
Future<void> _handleStepAction(
|
||||
|
||||
Reference in New Issue
Block a user