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

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

@@ -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();

View File

@@ -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(