결재 비활성 안내 개선 및 테이블 기능 보강
This commit is contained in:
@@ -21,6 +21,7 @@ class ApprovalTemplateController extends ChangeNotifier {
|
||||
String _query = '';
|
||||
ApprovalTemplateStatusFilter _statusFilter = ApprovalTemplateStatusFilter.all;
|
||||
String? _errorMessage;
|
||||
int _pageSize = 20;
|
||||
|
||||
PaginatedResult<ApprovalTemplate>? get result => _result;
|
||||
bool get isLoading => _isLoading;
|
||||
@@ -28,6 +29,7 @@ class ApprovalTemplateController extends ChangeNotifier {
|
||||
String get query => _query;
|
||||
ApprovalTemplateStatusFilter get statusFilter => _statusFilter;
|
||||
String? get errorMessage => _errorMessage;
|
||||
int get pageSize => _result?.pageSize ?? _pageSize;
|
||||
|
||||
Future<void> fetch({int page = 1}) async {
|
||||
_isLoading = true;
|
||||
@@ -42,11 +44,12 @@ class ApprovalTemplateController extends ChangeNotifier {
|
||||
};
|
||||
final response = await _repository.list(
|
||||
page: page,
|
||||
pageSize: _result?.pageSize ?? 20,
|
||||
pageSize: _pageSize,
|
||||
query: sanitizedQuery.isEmpty ? null : sanitizedQuery,
|
||||
isActive: isActive,
|
||||
);
|
||||
_result = response;
|
||||
_pageSize = response.pageSize;
|
||||
} catch (e) {
|
||||
_errorMessage = e.toString();
|
||||
} finally {
|
||||
@@ -160,6 +163,14 @@ class ApprovalTemplateController extends ChangeNotifier {
|
||||
notifyListeners();
|
||||
}
|
||||
|
||||
void updatePageSize(int value) {
|
||||
if (value <= 0) {
|
||||
return;
|
||||
}
|
||||
_pageSize = value;
|
||||
notifyListeners();
|
||||
}
|
||||
|
||||
void _setSubmitting(bool value) {
|
||||
_isSubmitting = value;
|
||||
notifyListeners();
|
||||
|
||||
@@ -9,7 +9,8 @@ import '../../../../../widgets/app_layout.dart';
|
||||
import '../../../../../widgets/components/filter_bar.dart';
|
||||
import '../../../../../widgets/components/superport_table.dart';
|
||||
import '../../../../../widgets/components/superport_dialog.dart';
|
||||
import '../../../../../widgets/spec_page.dart';
|
||||
import '../../../../../widgets/components/feature_disabled_placeholder.dart';
|
||||
import '../../../shared/widgets/approver_autocomplete_field.dart';
|
||||
import '../../../domain/entities/approval_template.dart';
|
||||
import '../../../domain/repositories/approval_template_repository.dart';
|
||||
import '../controllers/approval_template_controller.dart';
|
||||
@@ -21,43 +22,28 @@ class ApprovalTemplatePage 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: '입력 폼',
|
||||
items: [
|
||||
'템플릿코드 [Text]',
|
||||
'템플릿명 [Text]',
|
||||
'설명 [Text]',
|
||||
'사용여부 [Switch]',
|
||||
'비고 [Textarea]',
|
||||
'단계 추가: 순서 [Number], 승인자ID [Number]',
|
||||
],
|
||||
),
|
||||
SpecSection(
|
||||
title: '수정 폼',
|
||||
items: ['템플릿코드 [ReadOnly]', '작성자 [ReadOnly]'],
|
||||
),
|
||||
SpecSection(
|
||||
title: '테이블 리스트',
|
||||
description: '1행 예시',
|
||||
table: SpecTable(
|
||||
columns: ['번호', '템플릿코드', '템플릿명', '설명', '사용여부', '변경일시'],
|
||||
rows: [
|
||||
[
|
||||
'1',
|
||||
'AP_INBOUND',
|
||||
'입고 결재 기본',
|
||||
'입고 2단계',
|
||||
'Y',
|
||||
'2024-04-01 09:00',
|
||||
],
|
||||
],
|
||||
subtitle: '반복되는 결재 단계를 템플릿으로 저장하고 재사용합니다.',
|
||||
breadcrumbs: const [
|
||||
AppBreadcrumbItem(label: '대시보드', path: dashboardRoutePath),
|
||||
AppBreadcrumbItem(label: '결재', path: '/approvals/templates'),
|
||||
AppBreadcrumbItem(label: '결재 템플릿'),
|
||||
],
|
||||
actions: [
|
||||
Tooltip(
|
||||
message: '템플릿 생성은 서버 연동 후 지원됩니다.',
|
||||
child: ShadButton(
|
||||
onPressed: null,
|
||||
leading: const Icon(lucide.LucideIcons.plus, size: 16),
|
||||
child: const Text('템플릿 생성'),
|
||||
),
|
||||
),
|
||||
],
|
||||
child: const FeatureDisabledPlaceholder(
|
||||
title: '결재 템플릿 기능 준비 중',
|
||||
description: '백엔드 템플릿 API가 연동되면 템플릿 생성과 단계 구성 기능이 활성화됩니다.',
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
@@ -80,6 +66,7 @@ class _ApprovalTemplateEnabledPageState
|
||||
final FocusNode _searchFocus = FocusNode();
|
||||
final DateFormat _dateFormat = DateFormat('yyyy-MM-dd HH:mm');
|
||||
String? _lastError;
|
||||
static const _pageSizeOptions = [10, 20, 50];
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
@@ -126,10 +113,6 @@ class _ApprovalTemplateEnabledPageState
|
||||
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 showReset =
|
||||
_searchController.text.trim().isNotEmpty ||
|
||||
_controller.statusFilter != ApprovalTemplateStatusFilter.all;
|
||||
@@ -305,42 +288,19 @@ class _ApprovalTemplateEnabledPageState
|
||||
return const FixedTableSpanExtent(140);
|
||||
}
|
||||
},
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
Text('총 $totalCount건', 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('다음'),
|
||||
),
|
||||
const SizedBox(width: 12),
|
||||
Text(
|
||||
'페이지 $currentPage / $totalPages',
|
||||
style: theme.textTheme.small,
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
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,
|
||||
),
|
||||
],
|
||||
),
|
||||
@@ -500,11 +460,7 @@ class _ApprovalTemplateEnabledPageState
|
||||
modalSetState?.call(() => isSaving = true);
|
||||
|
||||
final success = isEdit && existingTemplate != null
|
||||
? await _controller.update(
|
||||
existingTemplate.id,
|
||||
input,
|
||||
stepInputs,
|
||||
)
|
||||
? await _controller.update(existingTemplate.id, input, stepInputs)
|
||||
: await _controller.create(input, stepInputs);
|
||||
if (success != null && mounted) {
|
||||
Navigator.of(context).pop(true);
|
||||
@@ -517,7 +473,7 @@ class _ApprovalTemplateEnabledPageState
|
||||
context: context,
|
||||
title: isEdit ? '템플릿 수정' : '템플릿 생성',
|
||||
barrierDismissible: !isSaving,
|
||||
onSubmit: handleSubmit,
|
||||
onSubmit: handleSubmit,
|
||||
body: StatefulBuilder(
|
||||
builder: (dialogContext, setModalState) {
|
||||
modalSetState = setModalState;
|
||||
@@ -593,6 +549,7 @@ class _ApprovalTemplateEnabledPageState
|
||||
field: steps[index],
|
||||
index: index,
|
||||
isEdit: isEdit,
|
||||
isDisabled: isSaving,
|
||||
onRemove: steps.length <= 1 || isSaving
|
||||
? null
|
||||
: () {
|
||||
@@ -763,12 +720,14 @@ class _StepEditorRow extends StatelessWidget {
|
||||
required this.field,
|
||||
required this.index,
|
||||
required this.isEdit,
|
||||
required this.isDisabled,
|
||||
required this.onRemove,
|
||||
});
|
||||
|
||||
final _TemplateStepField field;
|
||||
final int index;
|
||||
final bool isEdit;
|
||||
final bool isDisabled;
|
||||
final VoidCallback? onRemove;
|
||||
|
||||
@override
|
||||
@@ -793,14 +752,18 @@ class _StepEditorRow extends StatelessWidget {
|
||||
controller: field.orderController,
|
||||
keyboardType: TextInputType.number,
|
||||
placeholder: const Text('단계 순서'),
|
||||
enabled: !isDisabled,
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 12),
|
||||
Expanded(
|
||||
child: ShadInput(
|
||||
controller: field.approverController,
|
||||
keyboardType: TextInputType.number,
|
||||
placeholder: const Text('승인자 ID'),
|
||||
child: IgnorePointer(
|
||||
ignoring: isDisabled,
|
||||
child: ApprovalApproverAutocompleteField(
|
||||
idController: field.approverController,
|
||||
hintText: '승인자 검색',
|
||||
onSelected: (_) {},
|
||||
),
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 12),
|
||||
@@ -813,11 +776,14 @@ class _StepEditorRow extends StatelessWidget {
|
||||
],
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
ShadTextarea(
|
||||
controller: field.noteController,
|
||||
minHeight: 60,
|
||||
maxHeight: 160,
|
||||
placeholder: const Text('비고 (선택)'),
|
||||
IgnorePointer(
|
||||
ignoring: isDisabled,
|
||||
child: ShadTextarea(
|
||||
controller: field.noteController,
|
||||
minHeight: 60,
|
||||
maxHeight: 160,
|
||||
placeholder: const Text('비고 (선택)'),
|
||||
),
|
||||
),
|
||||
if (isEdit && field.id != null)
|
||||
Padding(
|
||||
|
||||
Reference in New Issue
Block a user