결재 템플릿 단계 적용 구현
- ApprovalTemplate 엔티티·DTO·원격 리포지토리 추가 - ApprovalController에 템플릿 로딩/적용 상태와 assignSteps 호출 연동 - ApprovalPage 단계 탭에 템플릿 선택 UI 및 적용 확인 다이얼로그 구현 - 템플릿 적용 단위 테스트와 IMPLEMENTATION_TASKS 현황 갱신
This commit is contained in:
@@ -2,6 +2,10 @@ import 'package:flutter/material.dart';
|
||||
import 'package:get_it/get_it.dart';
|
||||
import 'package:shadcn_ui/shadcn_ui.dart';
|
||||
|
||||
import 'package:superport_v2/core/constants/app_sections.dart';
|
||||
import 'package:superport_v2/widgets/app_layout.dart';
|
||||
import 'package:superport_v2/widgets/components/filter_bar.dart';
|
||||
|
||||
import '../../../../../core/config/environment.dart';
|
||||
import '../../../../../widgets/spec_page.dart';
|
||||
import '../../../group/domain/entities/group.dart';
|
||||
@@ -143,188 +147,162 @@ class _UserEnabledPageState extends State<_UserEnabledPage> {
|
||||
? false
|
||||
: (result.page * result.pageSize) < result.total;
|
||||
|
||||
return SingleChildScrollView(
|
||||
padding: const EdgeInsets.all(24),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
final showReset = _searchController.text.isNotEmpty ||
|
||||
_controller.groupFilter != null ||
|
||||
_controller.statusFilter != UserStatusFilter.all;
|
||||
|
||||
return AppLayout(
|
||||
title: '사용자(사원) 관리',
|
||||
subtitle: '사번 기반 계정과 그룹, 사용 상태를 관리합니다.',
|
||||
breadcrumbs: const [
|
||||
AppBreadcrumbItem(label: '대시보드', path: dashboardRoutePath),
|
||||
AppBreadcrumbItem(label: '마스터', path: '/masters/users'),
|
||||
AppBreadcrumbItem(label: '사용자'),
|
||||
],
|
||||
actions: [
|
||||
ShadButton(
|
||||
leading: const Icon(LucideIcons.plus, size: 16),
|
||||
onPressed:
|
||||
_controller.isSubmitting ? null : () => _openUserForm(context),
|
||||
child: const Text('신규 등록'),
|
||||
),
|
||||
],
|
||||
toolbar: FilterBar(
|
||||
children: [
|
||||
Row(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Expanded(
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text('사용자(사원) 관리', style: theme.textTheme.h2),
|
||||
const SizedBox(height: 6),
|
||||
Text(
|
||||
'사번 기반 계정과 그룹, 사용 상태를 관리합니다.',
|
||||
style: theme.textTheme.muted,
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 16),
|
||||
ShadButton(
|
||||
onPressed: _controller.isSubmitting
|
||||
? null
|
||||
: () => _openUserForm(context),
|
||||
child: const Text('신규 등록'),
|
||||
),
|
||||
],
|
||||
SizedBox(
|
||||
width: 260,
|
||||
child: ShadInput(
|
||||
controller: _searchController,
|
||||
focusNode: _searchFocus,
|
||||
placeholder: const Text('사번, 성명, 이메일 검색'),
|
||||
leading: const Icon(LucideIcons.search, size: 16),
|
||||
onSubmitted: (_) => _applyFilters(),
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 24),
|
||||
ShadCard(
|
||||
title: Text('검색 및 필터', style: theme.textTheme.h3),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Wrap(
|
||||
spacing: 16,
|
||||
runSpacing: 16,
|
||||
children: [
|
||||
SizedBox(
|
||||
width: 260,
|
||||
child: ShadInput(
|
||||
controller: _searchController,
|
||||
focusNode: _searchFocus,
|
||||
placeholder: const Text('사번, 성명, 이메일 검색'),
|
||||
leading: const Icon(LucideIcons.search, size: 16),
|
||||
onSubmitted: (_) => _applyFilters(),
|
||||
),
|
||||
),
|
||||
SizedBox(
|
||||
width: 220,
|
||||
child: ShadSelect<int?>(
|
||||
key: ValueKey(_controller.groupFilter),
|
||||
initialValue: _controller.groupFilter,
|
||||
placeholder: Text(
|
||||
_groupsLoaded ? '그룹 전체' : '그룹 로딩중...',
|
||||
),
|
||||
selectedOptionBuilder: (context, value) {
|
||||
if (value == null) {
|
||||
return Text(
|
||||
_groupsLoaded ? '그룹 전체' : '그룹 로딩중...',
|
||||
);
|
||||
}
|
||||
final group = _controller.groups.firstWhere(
|
||||
(g) => g.id == value,
|
||||
orElse: () => Group(id: value, groupName: ''),
|
||||
);
|
||||
return Text(group.groupName);
|
||||
},
|
||||
onChanged: _controller.isLoadingGroups
|
||||
? null
|
||||
: (value) {
|
||||
_controller.updateGroupFilter(value);
|
||||
},
|
||||
options: [
|
||||
const ShadOption<int?>(
|
||||
value: null,
|
||||
child: Text('그룹 전체'),
|
||||
),
|
||||
..._controller.groups.map(
|
||||
(group) => ShadOption<int?>(
|
||||
value: group.id,
|
||||
child: Text(group.groupName),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
SizedBox(
|
||||
width: 200,
|
||||
child: ShadSelect<UserStatusFilter>(
|
||||
key: ValueKey(_controller.statusFilter),
|
||||
initialValue: _controller.statusFilter,
|
||||
selectedOptionBuilder: (context, filter) =>
|
||||
Text(_statusLabel(filter)),
|
||||
onChanged: (value) {
|
||||
if (value == null) return;
|
||||
_controller.updateStatusFilter(value);
|
||||
},
|
||||
options: UserStatusFilter.values
|
||||
.map(
|
||||
(filter) => ShadOption(
|
||||
value: filter,
|
||||
child: Text(_statusLabel(filter)),
|
||||
),
|
||||
)
|
||||
.toList(),
|
||||
),
|
||||
),
|
||||
ShadButton.outline(
|
||||
onPressed: _controller.isLoading
|
||||
? null
|
||||
: _applyFilters,
|
||||
child: const Text('검색 적용'),
|
||||
),
|
||||
if (_searchController.text.isNotEmpty ||
|
||||
_controller.groupFilter != null ||
|
||||
_controller.statusFilter != UserStatusFilter.all)
|
||||
ShadButton.ghost(
|
||||
onPressed: _controller.isLoading
|
||||
? null
|
||||
: () {
|
||||
_searchController.clear();
|
||||
_searchFocus.requestFocus();
|
||||
_controller.updateQuery('');
|
||||
_controller.updateGroupFilter(null);
|
||||
_controller.updateStatusFilter(
|
||||
UserStatusFilter.all,
|
||||
);
|
||||
_controller.fetch(page: 1);
|
||||
},
|
||||
child: const Text('초기화'),
|
||||
),
|
||||
],
|
||||
SizedBox(
|
||||
width: 220,
|
||||
child: ShadSelect<int?>(
|
||||
key: ValueKey(_controller.groupFilter),
|
||||
initialValue: _controller.groupFilter,
|
||||
placeholder: Text(
|
||||
_groupsLoaded ? '그룹 전체' : '그룹 로딩중...',
|
||||
),
|
||||
selectedOptionBuilder: (context, value) {
|
||||
if (value == null) {
|
||||
return Text(
|
||||
_groupsLoaded ? '그룹 전체' : '그룹 로딩중...',
|
||||
);
|
||||
}
|
||||
final group = _controller.groups.firstWhere(
|
||||
(g) => g.id == value,
|
||||
orElse: () => Group(id: value, groupName: ''),
|
||||
);
|
||||
return Text(group.groupName);
|
||||
},
|
||||
onChanged: _controller.isLoadingGroups
|
||||
? null
|
||||
: (value) {
|
||||
_controller.updateGroupFilter(value);
|
||||
},
|
||||
options: [
|
||||
const ShadOption<int?>(
|
||||
value: null,
|
||||
child: Text('그룹 전체'),
|
||||
),
|
||||
..._controller.groups.map(
|
||||
(group) => ShadOption<int?>(
|
||||
value: group.id,
|
||||
child: Text(group.groupName),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 24),
|
||||
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('이전'),
|
||||
SizedBox(
|
||||
width: 200,
|
||||
child: ShadSelect<UserStatusFilter>(
|
||||
key: ValueKey(_controller.statusFilter),
|
||||
initialValue: _controller.statusFilter,
|
||||
selectedOptionBuilder: (context, filter) =>
|
||||
Text(_statusLabel(filter)),
|
||||
onChanged: (value) {
|
||||
if (value == null) return;
|
||||
_controller.updateStatusFilter(value);
|
||||
},
|
||||
options: UserStatusFilter.values
|
||||
.map(
|
||||
(filter) => ShadOption(
|
||||
value: filter,
|
||||
child: Text(_statusLabel(filter)),
|
||||
),
|
||||
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()),
|
||||
)
|
||||
: users.isEmpty
|
||||
.toList(),
|
||||
),
|
||||
),
|
||||
ShadButton.outline(
|
||||
onPressed: _controller.isLoading ? null : _applyFilters,
|
||||
child: const Text('검색 적용'),
|
||||
),
|
||||
if (showReset)
|
||||
ShadButton.ghost(
|
||||
onPressed: _controller.isLoading
|
||||
? null
|
||||
: () {
|
||||
_searchController.clear();
|
||||
_searchFocus.requestFocus();
|
||||
_controller.updateQuery('');
|
||||
_controller.updateGroupFilter(null);
|
||||
_controller.updateStatusFilter(
|
||||
UserStatusFilter.all,
|
||||
);
|
||||
_controller.fetch(page: 1);
|
||||
},
|
||||
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()),
|
||||
)
|
||||
: users.isEmpty
|
||||
? Padding(
|
||||
padding: const EdgeInsets.all(32),
|
||||
child: Text(
|
||||
@@ -344,8 +322,6 @@ class _UserEnabledPageState extends State<_UserEnabledPage> {
|
||||
? null
|
||||
: _restoreUser,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
},
|
||||
|
||||
Reference in New Issue
Block a user