전역 구조 리팩터링 및 테스트 확장
This commit is contained in:
@@ -211,19 +211,13 @@ class ApprovalStepActionInput {
|
||||
|
||||
/// 결재 단계를 일괄 등록/재배치하기 위한 입력 모델
|
||||
class ApprovalStepAssignmentInput {
|
||||
ApprovalStepAssignmentInput({
|
||||
required this.approvalId,
|
||||
required this.steps,
|
||||
});
|
||||
ApprovalStepAssignmentInput({required this.approvalId, required this.steps});
|
||||
|
||||
final int approvalId;
|
||||
final List<ApprovalStepAssignmentItem> steps;
|
||||
|
||||
Map<String, dynamic> toPayload() {
|
||||
return {
|
||||
'id': approvalId,
|
||||
'steps': steps.map((e) => e.toJson()).toList(),
|
||||
};
|
||||
return {'id': approvalId, 'steps': steps.map((e) => e.toJson()).toList()};
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:get_it/get_it.dart';
|
||||
import 'package:intl/intl.dart' as intl;
|
||||
import 'package:lucide_icons_flutter/lucide_icons.dart' as lucide;
|
||||
import 'package:shadcn_ui/shadcn_ui.dart';
|
||||
|
||||
@@ -7,6 +8,8 @@ import '../../../../../core/config/environment.dart';
|
||||
import '../../../../../core/constants/app_sections.dart';
|
||||
import '../../../../../widgets/app_layout.dart';
|
||||
import '../../../../../widgets/components/filter_bar.dart';
|
||||
import '../../../../../widgets/components/superport_date_picker.dart';
|
||||
import '../../../../../widgets/components/superport_table.dart';
|
||||
import '../../../../../widgets/spec_page.dart';
|
||||
import '../../domain/entities/approval_history_record.dart';
|
||||
import '../../domain/repositories/approval_history_repository.dart';
|
||||
@@ -145,6 +148,19 @@ class _ApprovalHistoryEnabledPageState
|
||||
),
|
||||
],
|
||||
toolbar: FilterBar(
|
||||
actions: [
|
||||
ShadButton.outline(
|
||||
onPressed: _controller.isLoading ? null : _applyFilters,
|
||||
child: const Text('검색 적용'),
|
||||
),
|
||||
ShadButton.ghost(
|
||||
onPressed:
|
||||
_controller.isLoading || !_controller.hasActiveFilters
|
||||
? null
|
||||
: _resetFilters,
|
||||
child: const Text('필터 초기화'),
|
||||
),
|
||||
],
|
||||
children: [
|
||||
SizedBox(
|
||||
width: 240,
|
||||
@@ -180,21 +196,24 @@ class _ApprovalHistoryEnabledPageState
|
||||
),
|
||||
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)}',
|
||||
child: SuperportDateRangePickerButton(
|
||||
value: _dateRange,
|
||||
dateFormat: intl.DateFormat('yyyy-MM-dd'),
|
||||
enabled: !_controller.isLoading,
|
||||
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)
|
||||
@@ -202,17 +221,6 @@ class _ApprovalHistoryEnabledPageState
|
||||
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(
|
||||
@@ -283,27 +291,6 @@ class _ApprovalHistoryEnabledPageState
|
||||
_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);
|
||||
@@ -318,10 +305,6 @@ class _ApprovalHistoryEnabledPageState
|
||||
_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:
|
||||
@@ -349,58 +332,60 @@ class _ApprovalHistoryTable extends StatelessWidget {
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final theme = ShadTheme.of(context);
|
||||
final normalizedQuery = query.trim().toLowerCase();
|
||||
final header = [
|
||||
'ID',
|
||||
'결재번호',
|
||||
'단계순서',
|
||||
'승인자',
|
||||
'행위',
|
||||
'변경전 상태',
|
||||
'변경후 상태',
|
||||
'작업일시',
|
||||
'비고',
|
||||
].map((label) => ShadTableCell.header(child: Text(label))).toList();
|
||||
|
||||
final columns = const [
|
||||
Text('ID'),
|
||||
Text('결재번호'),
|
||||
Text('단계순서'),
|
||||
Text('승인자'),
|
||||
Text('행위'),
|
||||
Text('변경전 상태'),
|
||||
Text('변경후 상태'),
|
||||
Text('작업일시'),
|
||||
Text('비고'),
|
||||
];
|
||||
|
||||
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!,
|
||||
),
|
||||
final highlightStyle = theme.textTheme.small.copyWith(
|
||||
fontWeight: FontWeight.w600,
|
||||
color: theme.colorScheme.foreground,
|
||||
);
|
||||
final noteText = history.note?.trim();
|
||||
final noteContent = noteText?.isNotEmpty == true ? noteText : null;
|
||||
final subLabelStyle = theme.textTheme.muted.copyWith(
|
||||
fontSize: (theme.textTheme.muted.fontSize ?? 14) - 1,
|
||||
);
|
||||
|
||||
return <Widget>[
|
||||
Text(history.id.toString()),
|
||||
Text(history.approvalNo, style: isHighlighted ? highlightStyle : null),
|
||||
Text(history.stepOrder == null ? '-' : history.stepOrder.toString()),
|
||||
Text(history.approver.name),
|
||||
Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
Text(history.action.name),
|
||||
if (noteContent != null) Text(noteContent, style: subLabelStyle),
|
||||
],
|
||||
),
|
||||
Text(history.fromStatus?.name ?? '-'),
|
||||
Text(history.toStatus.name),
|
||||
Text(dateFormat.format(history.actionAt.toLocal())),
|
||||
Text(noteContent ?? '-'),
|
||||
];
|
||||
}).toList();
|
||||
|
||||
return ShadTable.list(
|
||||
header: header,
|
||||
children: rows,
|
||||
return SuperportTable(
|
||||
columns: columns,
|
||||
rows: rows,
|
||||
rowHeight: 64,
|
||||
maxHeight: 520,
|
||||
columnSpanExtent: (index) {
|
||||
switch (index) {
|
||||
case 1:
|
||||
|
||||
@@ -7,6 +7,7 @@ import '../../../../../core/config/environment.dart';
|
||||
import '../../../../../core/constants/app_sections.dart';
|
||||
import '../../../../../widgets/app_layout.dart';
|
||||
import '../../../../../widgets/components/filter_bar.dart';
|
||||
import '../../../../../widgets/components/superport_dialog.dart';
|
||||
import '../../../../../widgets/spec_page.dart';
|
||||
import '../controllers/approval_step_controller.dart';
|
||||
import '../../domain/entities/approval_step_input.dart';
|
||||
@@ -528,73 +529,50 @@ class _ApprovalStepEnabledPageState extends State<_ApprovalStepEnabledPage> {
|
||||
if (!mounted) return;
|
||||
Navigator.of(context, rootNavigator: true).pop();
|
||||
if (detail == null) return;
|
||||
await showDialog<void>(
|
||||
final step = detail.step;
|
||||
await SuperportDialog.show<void>(
|
||||
context: context,
|
||||
builder: (dialogContext) {
|
||||
final step = detail.step;
|
||||
final theme = ShadTheme.of(dialogContext);
|
||||
return Dialog(
|
||||
insetPadding: const EdgeInsets.all(24),
|
||||
clipBehavior: Clip.antiAlias,
|
||||
child: ShadCard(
|
||||
title: Text('결재 단계 상세', style: theme.textTheme.h3),
|
||||
description: Text(
|
||||
'결재번호 ${detail.approvalNo}',
|
||||
style: theme.textTheme.muted,
|
||||
dialog: SuperportDialog(
|
||||
title: '결재 단계 상세',
|
||||
description: '결재번호 ${detail.approvalNo}',
|
||||
constraints: const BoxConstraints(maxWidth: 560),
|
||||
contentPadding: const EdgeInsets.symmetric(
|
||||
horizontal: 20,
|
||||
vertical: 18,
|
||||
),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
_DetailRow(label: '단계 순서', value: '${step.stepOrder}'),
|
||||
_DetailRow(label: '승인자', value: step.approver.name),
|
||||
_DetailRow(label: '상태', value: step.status.name),
|
||||
_DetailRow(label: '배정일시', value: _formatDate(step.assignedAt)),
|
||||
_DetailRow(
|
||||
label: '결정일시',
|
||||
value: step.decidedAt == null
|
||||
? '-'
|
||||
: _formatDate(step.decidedAt!),
|
||||
),
|
||||
footer: Row(
|
||||
mainAxisAlignment: MainAxisAlignment.end,
|
||||
children: [
|
||||
ShadButton.ghost(
|
||||
onPressed: () => Navigator.of(dialogContext).pop(),
|
||||
child: const Text('닫기'),
|
||||
),
|
||||
],
|
||||
_DetailRow(label: '템플릿', value: detail.templateName ?? '-'),
|
||||
_DetailRow(label: '트랜잭션번호', value: detail.transactionNo ?? '-'),
|
||||
const SizedBox(height: 12),
|
||||
Text(
|
||||
'비고',
|
||||
style: ShadTheme.of(
|
||||
context,
|
||||
).textTheme.small.copyWith(fontWeight: FontWeight.w600),
|
||||
),
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.symmetric(vertical: 16),
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
_DetailRow(label: '단계 순서', value: '${step.stepOrder}'),
|
||||
_DetailRow(label: '승인자', value: step.approver.name),
|
||||
_DetailRow(label: '상태', value: step.status.name),
|
||||
_DetailRow(
|
||||
label: '배정일시',
|
||||
value: _formatDate(step.assignedAt),
|
||||
),
|
||||
_DetailRow(
|
||||
label: '결정일시',
|
||||
value: step.decidedAt == null
|
||||
? '-'
|
||||
: _formatDate(step.decidedAt!),
|
||||
),
|
||||
_DetailRow(label: '템플릿', value: detail.templateName ?? '-'),
|
||||
_DetailRow(
|
||||
label: '트랜잭션번호',
|
||||
value: detail.transactionNo ?? '-',
|
||||
),
|
||||
const SizedBox(height: 12),
|
||||
Text(
|
||||
'비고',
|
||||
style: theme.textTheme.small.copyWith(
|
||||
fontWeight: FontWeight.w600,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
ShadTextarea(
|
||||
initialValue: step.note ?? '',
|
||||
readOnly: true,
|
||||
minHeight: 80,
|
||||
maxHeight: 200,
|
||||
),
|
||||
],
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
ShadTextarea(
|
||||
initialValue: step.note ?? '',
|
||||
readOnly: true,
|
||||
minHeight: 80,
|
||||
maxHeight: 200,
|
||||
),
|
||||
),
|
||||
);
|
||||
},
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
@@ -724,102 +702,93 @@ class _StepFormDialogState extends State<_StepFormDialog> {
|
||||
final theme = ShadTheme.of(context);
|
||||
final materialTheme = Theme.of(context);
|
||||
|
||||
return Dialog(
|
||||
insetPadding: const EdgeInsets.all(24),
|
||||
clipBehavior: Clip.antiAlias,
|
||||
child: ShadCard(
|
||||
title: Text(widget.title, style: theme.textTheme.h3),
|
||||
footer: Row(
|
||||
mainAxisAlignment: MainAxisAlignment.end,
|
||||
return SuperportDialog(
|
||||
title: widget.title,
|
||||
constraints: const BoxConstraints(maxWidth: 560),
|
||||
primaryAction: ShadButton(
|
||||
key: const ValueKey('step_form_submit'),
|
||||
onPressed: _handleSubmit,
|
||||
child: Text(widget.submitLabel),
|
||||
),
|
||||
secondaryAction: ShadButton.ghost(
|
||||
onPressed: () => Navigator.of(context).pop(),
|
||||
child: const Text('취소'),
|
||||
),
|
||||
contentPadding: const EdgeInsets.symmetric(horizontal: 20, vertical: 16),
|
||||
child: SingleChildScrollView(
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
ShadButton.ghost(
|
||||
onPressed: () => Navigator.of(context).pop(),
|
||||
child: const Text('취소'),
|
||||
),
|
||||
const SizedBox(width: 12),
|
||||
ShadButton(
|
||||
key: const ValueKey('step_form_submit'),
|
||||
onPressed: _handleSubmit,
|
||||
child: Text(widget.submitLabel),
|
||||
),
|
||||
],
|
||||
),
|
||||
child: SingleChildScrollView(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 20, vertical: 16),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
if (!widget.isEditing)
|
||||
_FormFieldBlock(
|
||||
label: '결재 ID',
|
||||
errorText: _errors['approvalId'],
|
||||
child: ShadInput(
|
||||
key: const ValueKey('step_form_approval_id'),
|
||||
controller: _approvalIdController,
|
||||
onChanged: (_) => _clearError('approvalId'),
|
||||
),
|
||||
)
|
||||
else ...[
|
||||
_FormFieldBlock(
|
||||
label: '결재 ID',
|
||||
child: ShadInput(
|
||||
controller: _approvalIdController,
|
||||
readOnly: true,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
_FormFieldBlock(
|
||||
label: '결재번호',
|
||||
child: ShadInput(
|
||||
controller: _approvalNoController,
|
||||
readOnly: true,
|
||||
),
|
||||
),
|
||||
],
|
||||
if (!widget.isEditing) const SizedBox(height: 16),
|
||||
if (!widget.isEditing)
|
||||
_FormFieldBlock(
|
||||
label: '단계 순서',
|
||||
errorText: _errors['stepOrder'],
|
||||
label: '결재 ID',
|
||||
errorText: _errors['approvalId'],
|
||||
child: ShadInput(
|
||||
key: const ValueKey('step_form_step_order'),
|
||||
controller: _stepOrderController,
|
||||
onChanged: (_) => _clearError('stepOrder'),
|
||||
key: const ValueKey('step_form_approval_id'),
|
||||
controller: _approvalIdController,
|
||||
onChanged: (_) => _clearError('approvalId'),
|
||||
),
|
||||
)
|
||||
else ...[
|
||||
_FormFieldBlock(
|
||||
label: '결재 ID',
|
||||
child: ShadInput(
|
||||
controller: _approvalIdController,
|
||||
readOnly: true,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
_FormFieldBlock(
|
||||
label: '승인자 ID',
|
||||
errorText: _errors['approverId'],
|
||||
label: '결재번호',
|
||||
child: ShadInput(
|
||||
key: const ValueKey('step_form_approver_id'),
|
||||
controller: _approverIdController,
|
||||
onChanged: (_) => _clearError('approverId'),
|
||||
controller: _approvalNoController,
|
||||
readOnly: true,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
_FormFieldBlock(
|
||||
label: '비고',
|
||||
helperText: '필요 시 단계에 대한 참고 내용을 남길 수 있습니다.',
|
||||
child: ShadTextarea(
|
||||
key: const ValueKey('step_form_note'),
|
||||
controller: _noteController,
|
||||
minHeight: 100,
|
||||
maxHeight: 200,
|
||||
),
|
||||
),
|
||||
if (_errors['form'] != null)
|
||||
Padding(
|
||||
padding: const EdgeInsets.only(top: 12),
|
||||
child: Text(
|
||||
_errors['form']!,
|
||||
style: theme.textTheme.small.copyWith(
|
||||
color: materialTheme.colorScheme.error,
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
if (!widget.isEditing) const SizedBox(height: 16),
|
||||
_FormFieldBlock(
|
||||
label: '단계 순서',
|
||||
errorText: _errors['stepOrder'],
|
||||
child: ShadInput(
|
||||
key: const ValueKey('step_form_step_order'),
|
||||
controller: _stepOrderController,
|
||||
onChanged: (_) => _clearError('stepOrder'),
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
_FormFieldBlock(
|
||||
label: '승인자 ID',
|
||||
errorText: _errors['approverId'],
|
||||
child: ShadInput(
|
||||
key: const ValueKey('step_form_approver_id'),
|
||||
controller: _approverIdController,
|
||||
onChanged: (_) => _clearError('approverId'),
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
_FormFieldBlock(
|
||||
label: '비고',
|
||||
helperText: '필요 시 단계에 대한 참고 내용을 남길 수 있습니다.',
|
||||
child: ShadTextarea(
|
||||
key: const ValueKey('step_form_note'),
|
||||
controller: _noteController,
|
||||
minHeight: 100,
|
||||
maxHeight: 200,
|
||||
),
|
||||
),
|
||||
if (_errors['form'] != null)
|
||||
Padding(
|
||||
padding: const EdgeInsets.only(top: 12),
|
||||
child: Text(
|
||||
_errors['form']!,
|
||||
style: theme.textTheme.small.copyWith(
|
||||
color: materialTheme.colorScheme.error,
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
|
||||
@@ -7,6 +7,7 @@ import '../../../../../core/config/environment.dart';
|
||||
import '../../../../../core/constants/app_sections.dart';
|
||||
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 '../../../domain/entities/approval_template.dart';
|
||||
@@ -151,6 +152,18 @@ class _ApprovalTemplateEnabledPageState
|
||||
),
|
||||
],
|
||||
toolbar: FilterBar(
|
||||
actions: [
|
||||
ShadButton.outline(
|
||||
onPressed: _controller.isLoading ? null : _applyFilters,
|
||||
child: const Text('검색 적용'),
|
||||
),
|
||||
ShadButton.ghost(
|
||||
onPressed: !_controller.isLoading && showReset
|
||||
? _resetFilters
|
||||
: null,
|
||||
child: const Text('필터 초기화'),
|
||||
),
|
||||
],
|
||||
children: [
|
||||
SizedBox(
|
||||
width: 260,
|
||||
@@ -183,16 +196,6 @@ class _ApprovalTemplateEnabledPageState
|
||||
.toList(),
|
||||
),
|
||||
),
|
||||
ShadButton.outline(
|
||||
onPressed: _controller.isLoading ? null : _applyFilters,
|
||||
child: const Text('검색 적용'),
|
||||
),
|
||||
ShadButton.ghost(
|
||||
onPressed: !_controller.isLoading && showReset
|
||||
? _resetFilters
|
||||
: null,
|
||||
child: const Text('필터 초기화'),
|
||||
),
|
||||
],
|
||||
),
|
||||
child: ShadCard(
|
||||
@@ -213,97 +216,95 @@ class _ApprovalTemplateEnabledPageState
|
||||
crossAxisAlignment: CrossAxisAlignment.stretch,
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
SizedBox(
|
||||
height: 480,
|
||||
child: ShadTable.list(
|
||||
header:
|
||||
['ID', '템플릿코드', '템플릿명', '설명', '사용', '변경일시', '동작']
|
||||
.map(
|
||||
(e) => ShadTableCell.header(child: Text(e)),
|
||||
)
|
||||
.toList(),
|
||||
columnSpanExtent: (index) {
|
||||
switch (index) {
|
||||
case 2:
|
||||
return const FixedTableSpanExtent(220);
|
||||
case 3:
|
||||
return const FixedTableSpanExtent(260);
|
||||
case 4:
|
||||
return const FixedTableSpanExtent(100);
|
||||
case 5:
|
||||
return const FixedTableSpanExtent(180);
|
||||
case 6:
|
||||
return const FixedTableSpanExtent(160);
|
||||
default:
|
||||
return const FixedTableSpanExtent(140);
|
||||
}
|
||||
},
|
||||
children: templates.map((template) {
|
||||
return [
|
||||
ShadTableCell(child: Text('${template.id}')),
|
||||
ShadTableCell(child: Text(template.code)),
|
||||
ShadTableCell(child: Text(template.name)),
|
||||
ShadTableCell(
|
||||
child: Text(
|
||||
template.description?.isNotEmpty == true
|
||||
? template.description!
|
||||
: '-',
|
||||
),
|
||||
SuperportTable.fromCells(
|
||||
header: const [
|
||||
ShadTableCell.header(child: Text('ID')),
|
||||
ShadTableCell.header(child: Text('템플릿코드')),
|
||||
ShadTableCell.header(child: Text('템플릿명')),
|
||||
ShadTableCell.header(child: Text('설명')),
|
||||
ShadTableCell.header(child: Text('사용')),
|
||||
ShadTableCell.header(child: Text('변경일시')),
|
||||
ShadTableCell.header(child: Text('동작')),
|
||||
],
|
||||
rows: templates.map((template) {
|
||||
return [
|
||||
ShadTableCell(child: Text('${template.id}')),
|
||||
ShadTableCell(child: Text(template.code)),
|
||||
ShadTableCell(child: Text(template.name)),
|
||||
ShadTableCell(
|
||||
child: Text(
|
||||
template.description?.isNotEmpty == true
|
||||
? template.description!
|
||||
: '-',
|
||||
),
|
||||
ShadTableCell(
|
||||
child: template.isActive
|
||||
? const ShadBadge(child: Text('사용'))
|
||||
: const ShadBadge.outline(
|
||||
child: Text('미사용'),
|
||||
),
|
||||
ShadTableCell(
|
||||
child: template.isActive
|
||||
? const ShadBadge(child: Text('사용'))
|
||||
: const ShadBadge.outline(child: Text('미사용')),
|
||||
),
|
||||
ShadTableCell(
|
||||
child: Text(
|
||||
template.updatedAt == null
|
||||
? '-'
|
||||
: _dateFormat.format(
|
||||
template.updatedAt!.toLocal(),
|
||||
),
|
||||
),
|
||||
ShadTableCell(
|
||||
child: Text(
|
||||
template.updatedAt == null
|
||||
? '-'
|
||||
: _dateFormat.format(
|
||||
template.updatedAt!.toLocal(),
|
||||
),
|
||||
),
|
||||
),
|
||||
ShadTableCell(
|
||||
child: Row(
|
||||
mainAxisAlignment: MainAxisAlignment.end,
|
||||
children: [
|
||||
ShadButton.ghost(
|
||||
key: ValueKey(
|
||||
'template_edit_${template.id}',
|
||||
),
|
||||
size: ShadButtonSize.sm,
|
||||
onPressed: _controller.isSubmitting
|
||||
? null
|
||||
: () => _openEditTemplate(template),
|
||||
child: const Text('수정'),
|
||||
),
|
||||
ShadTableCell(
|
||||
alignment: Alignment.centerRight,
|
||||
child: Wrap(
|
||||
spacing: 8,
|
||||
children: [
|
||||
ShadButton.ghost(
|
||||
key: ValueKey(
|
||||
'template_edit_${template.id}',
|
||||
),
|
||||
const SizedBox(width: 8),
|
||||
template.isActive
|
||||
? ShadButton.outline(
|
||||
size: ShadButtonSize.sm,
|
||||
onPressed: _controller.isSubmitting
|
||||
? null
|
||||
: () =>
|
||||
_confirmDelete(template),
|
||||
child: const Text('삭제'),
|
||||
)
|
||||
: ShadButton.outline(
|
||||
size: ShadButtonSize.sm,
|
||||
onPressed: _controller.isSubmitting
|
||||
? null
|
||||
: () =>
|
||||
_confirmRestore(template),
|
||||
child: const Text('복구'),
|
||||
),
|
||||
],
|
||||
),
|
||||
size: ShadButtonSize.sm,
|
||||
onPressed: _controller.isSubmitting
|
||||
? null
|
||||
: () => _openEditTemplate(template),
|
||||
child: const Text('수정'),
|
||||
),
|
||||
template.isActive
|
||||
? ShadButton.outline(
|
||||
size: ShadButtonSize.sm,
|
||||
onPressed: _controller.isSubmitting
|
||||
? null
|
||||
: () => _confirmDelete(template),
|
||||
child: const Text('삭제'),
|
||||
)
|
||||
: ShadButton.outline(
|
||||
size: ShadButtonSize.sm,
|
||||
onPressed: _controller.isSubmitting
|
||||
? null
|
||||
: () => _confirmRestore(template),
|
||||
child: const Text('복구'),
|
||||
),
|
||||
],
|
||||
),
|
||||
];
|
||||
}).toList(),
|
||||
),
|
||||
),
|
||||
];
|
||||
}).toList(),
|
||||
rowHeight: 56,
|
||||
maxHeight: 480,
|
||||
columnSpanExtent: (index) {
|
||||
switch (index) {
|
||||
case 2:
|
||||
return const FixedTableSpanExtent(220);
|
||||
case 3:
|
||||
return const FixedTableSpanExtent(260);
|
||||
case 4:
|
||||
return const FixedTableSpanExtent(100);
|
||||
case 5:
|
||||
return const FixedTableSpanExtent(180);
|
||||
case 6:
|
||||
return const FixedTableSpanExtent(160);
|
||||
default:
|
||||
return const FixedTableSpanExtent(140);
|
||||
}
|
||||
},
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
Row(
|
||||
@@ -382,26 +383,23 @@ class _ApprovalTemplateEnabledPageState
|
||||
}
|
||||
|
||||
Future<void> _confirmDelete(ApprovalTemplate template) async {
|
||||
final confirmed = await showDialog<bool>(
|
||||
final confirmed = await SuperportDialog.show<bool>(
|
||||
context: context,
|
||||
builder: (dialogContext) {
|
||||
return AlertDialog(
|
||||
title: const Text('템플릿 삭제'),
|
||||
content: Text(
|
||||
dialog: SuperportDialog(
|
||||
title: '템플릿 삭제',
|
||||
description:
|
||||
'"${template.name}" 템플릿을 삭제하시겠습니까?\n삭제 시 템플릿은 미사용 상태로 전환됩니다.',
|
||||
actions: [
|
||||
ShadButton.ghost(
|
||||
onPressed: () => Navigator.of(context).pop(false),
|
||||
child: const Text('취소'),
|
||||
),
|
||||
actions: [
|
||||
TextButton(
|
||||
onPressed: () => Navigator.of(dialogContext).pop(false),
|
||||
child: const Text('취소'),
|
||||
),
|
||||
FilledButton.tonal(
|
||||
onPressed: () => Navigator.of(dialogContext).pop(true),
|
||||
child: const Text('삭제'),
|
||||
),
|
||||
],
|
||||
);
|
||||
},
|
||||
ShadButton(
|
||||
onPressed: () => Navigator.of(context).pop(true),
|
||||
child: const Text('삭제'),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
if (confirmed != true) return;
|
||||
final ok = await _controller.delete(template.id);
|
||||
@@ -412,24 +410,22 @@ class _ApprovalTemplateEnabledPageState
|
||||
}
|
||||
|
||||
Future<void> _confirmRestore(ApprovalTemplate template) async {
|
||||
final confirmed = await showDialog<bool>(
|
||||
final confirmed = await SuperportDialog.show<bool>(
|
||||
context: context,
|
||||
builder: (dialogContext) {
|
||||
return AlertDialog(
|
||||
title: const Text('템플릿 복구'),
|
||||
content: Text('"${template.name}" 템플릿을 복구하시겠습니까?'),
|
||||
actions: [
|
||||
TextButton(
|
||||
onPressed: () => Navigator.of(dialogContext).pop(false),
|
||||
child: const Text('취소'),
|
||||
),
|
||||
FilledButton(
|
||||
onPressed: () => Navigator.of(dialogContext).pop(true),
|
||||
child: const Text('복구'),
|
||||
),
|
||||
],
|
||||
);
|
||||
},
|
||||
dialog: SuperportDialog(
|
||||
title: '템플릿 복구',
|
||||
description: '"${template.name}" 템플릿을 복구하시겠습니까?',
|
||||
actions: [
|
||||
ShadButton.ghost(
|
||||
onPressed: () => Navigator.of(context).pop(false),
|
||||
child: const Text('취소'),
|
||||
),
|
||||
ShadButton(
|
||||
onPressed: () => Navigator.of(context).pop(true),
|
||||
child: const Text('복구'),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
if (confirmed != true) return;
|
||||
final restored = await _controller.restore(template.id);
|
||||
@@ -454,10 +450,74 @@ class _ApprovalTemplateEnabledPageState
|
||||
String? errorText;
|
||||
StateSetter? modalSetState;
|
||||
|
||||
Future<void> handleSubmit() async {
|
||||
if (isSaving) return;
|
||||
final codeValue = codeController.text.trim();
|
||||
final nameValue = nameController.text.trim();
|
||||
if (!isEdit && codeValue.isEmpty) {
|
||||
modalSetState?.call(() => errorText = '템플릿 코드를 입력하세요.');
|
||||
return;
|
||||
}
|
||||
if (nameValue.isEmpty) {
|
||||
modalSetState?.call(() => errorText = '템플릿명을 입력하세요.');
|
||||
return;
|
||||
}
|
||||
final validation = _validateSteps(steps);
|
||||
if (validation != null) {
|
||||
modalSetState?.call(() => errorText = validation);
|
||||
return;
|
||||
}
|
||||
modalSetState?.call(() => errorText = null);
|
||||
final stepInputs = steps
|
||||
.map(
|
||||
(field) => ApprovalTemplateStepInput(
|
||||
id: field.id,
|
||||
stepOrder: int.parse(field.orderController.text.trim()),
|
||||
approverId: int.parse(field.approverController.text.trim()),
|
||||
note: field.noteController.text.trim().isEmpty
|
||||
? null
|
||||
: field.noteController.text.trim(),
|
||||
),
|
||||
)
|
||||
.toList();
|
||||
final input = ApprovalTemplateInput(
|
||||
code: isEdit ? existingTemplate?.code : codeValue,
|
||||
name: nameValue,
|
||||
description: descriptionController.text.trim().isEmpty
|
||||
? null
|
||||
: descriptionController.text.trim(),
|
||||
note: noteController.text.trim().isEmpty
|
||||
? null
|
||||
: noteController.text.trim(),
|
||||
isActive: statusNotifier.value,
|
||||
);
|
||||
if (isEdit && existingTemplate == null) {
|
||||
modalSetState?.call(() => errorText = '템플릿 정보를 불러오지 못했습니다.');
|
||||
modalSetState?.call(() => isSaving = false);
|
||||
return;
|
||||
}
|
||||
|
||||
modalSetState?.call(() => isSaving = true);
|
||||
|
||||
final success = isEdit && existingTemplate != null
|
||||
? await _controller.update(
|
||||
existingTemplate.id,
|
||||
input,
|
||||
stepInputs,
|
||||
)
|
||||
: await _controller.create(input, stepInputs);
|
||||
if (success != null && mounted) {
|
||||
Navigator.of(context).pop(true);
|
||||
} else {
|
||||
modalSetState?.call(() => isSaving = false);
|
||||
}
|
||||
}
|
||||
|
||||
final result = await showSuperportDialog<bool>(
|
||||
context: context,
|
||||
title: isEdit ? '템플릿 수정' : '템플릿 생성',
|
||||
barrierDismissible: !isSaving,
|
||||
onSubmit: handleSubmit,
|
||||
body: StatefulBuilder(
|
||||
builder: (dialogContext, setModalState) {
|
||||
modalSetState = setModalState;
|
||||
@@ -594,68 +654,7 @@ class _ApprovalTemplateEnabledPageState
|
||||
child: const Text('취소'),
|
||||
),
|
||||
ShadButton(
|
||||
onPressed: () async {
|
||||
if (isSaving) return;
|
||||
final codeValue = codeController.text.trim();
|
||||
final nameValue = nameController.text.trim();
|
||||
if (!isEdit && codeValue.isEmpty) {
|
||||
modalSetState?.call(() => errorText = '템플릿 코드를 입력하세요.');
|
||||
return;
|
||||
}
|
||||
if (nameValue.isEmpty) {
|
||||
modalSetState?.call(() => errorText = '템플릿명을 입력하세요.');
|
||||
return;
|
||||
}
|
||||
final validation = _validateSteps(steps);
|
||||
if (validation != null) {
|
||||
modalSetState?.call(() => errorText = validation);
|
||||
return;
|
||||
}
|
||||
modalSetState?.call(() => errorText = null);
|
||||
final stepInputs = steps
|
||||
.map(
|
||||
(field) => ApprovalTemplateStepInput(
|
||||
id: field.id,
|
||||
stepOrder: int.parse(field.orderController.text.trim()),
|
||||
approverId: int.parse(field.approverController.text.trim()),
|
||||
note: field.noteController.text.trim().isEmpty
|
||||
? null
|
||||
: field.noteController.text.trim(),
|
||||
),
|
||||
)
|
||||
.toList();
|
||||
final input = ApprovalTemplateInput(
|
||||
code: isEdit ? existingTemplate?.code : codeValue,
|
||||
name: nameValue,
|
||||
description: descriptionController.text.trim().isEmpty
|
||||
? null
|
||||
: descriptionController.text.trim(),
|
||||
note: noteController.text.trim().isEmpty
|
||||
? null
|
||||
: noteController.text.trim(),
|
||||
isActive: statusNotifier.value,
|
||||
);
|
||||
if (isEdit && existingTemplate == null) {
|
||||
modalSetState?.call(() => errorText = '템플릿 정보를 불러오지 못했습니다.');
|
||||
modalSetState?.call(() => isSaving = false);
|
||||
return;
|
||||
}
|
||||
|
||||
modalSetState?.call(() => isSaving = true);
|
||||
|
||||
final success = isEdit && existingTemplate != null
|
||||
? await _controller.update(
|
||||
existingTemplate.id,
|
||||
input,
|
||||
stepInputs,
|
||||
)
|
||||
: await _controller.create(input, stepInputs);
|
||||
if (success != null && mounted) {
|
||||
Navigator.of(context).pop(true);
|
||||
} else {
|
||||
modalSetState?.call(() => isSaving = false);
|
||||
}
|
||||
},
|
||||
onPressed: handleSubmit,
|
||||
child: Text(isEdit ? '수정 완료' : '생성 완료'),
|
||||
),
|
||||
],
|
||||
|
||||
Reference in New Issue
Block a user