결재 단계 UI 보완 및 분석 통과
This commit is contained in:
@@ -513,8 +513,8 @@ class _ApprovalEnabledPageState extends State<_ApprovalEnabledPage> {
|
|||||||
const SizedBox(height: 8),
|
const SizedBox(height: 8),
|
||||||
ShadTextarea(
|
ShadTextarea(
|
||||||
controller: noteController,
|
controller: noteController,
|
||||||
minLines: 3,
|
minHeight: 120,
|
||||||
maxLines: 5,
|
maxHeight: 220,
|
||||||
),
|
),
|
||||||
if (requireNote)
|
if (requireNote)
|
||||||
Padding(
|
Padding(
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
import '../../../../core/common/models/paginated_result.dart';
|
import 'package:superport_v2/core/common/models/paginated_result.dart';
|
||||||
import '../../../domain/entities/approval.dart';
|
import 'package:superport_v2/features/approvals/data/dtos/approval_dto.dart';
|
||||||
import '../../data/dtos/approval_dto.dart';
|
import 'package:superport_v2/features/approvals/domain/entities/approval.dart';
|
||||||
|
|
||||||
import '../../domain/entities/approval_step_record.dart';
|
import '../../domain/entities/approval_step_record.dart';
|
||||||
|
|
||||||
class ApprovalStepRecordDto {
|
class ApprovalStepRecordDto {
|
||||||
|
|||||||
@@ -1,9 +1,9 @@
|
|||||||
import 'package:dio/dio.dart';
|
import 'package:dio/dio.dart';
|
||||||
|
|
||||||
import '../../../../core/common/models/paginated_result.dart';
|
import 'package:superport_v2/core/common/models/paginated_result.dart';
|
||||||
import '../../../../core/network/api_client.dart';
|
import 'package:superport_v2/core/network/api_client.dart';
|
||||||
import '../../domain/entities/approval_step_record.dart';
|
import 'package:superport_v2/features/approvals/step/domain/entities/approval_step_record.dart';
|
||||||
import '../../domain/repositories/approval_step_repository.dart';
|
import 'package:superport_v2/features/approvals/step/domain/repositories/approval_step_repository.dart';
|
||||||
import '../dtos/approval_step_record_dto.dart';
|
import '../dtos/approval_step_record_dto.dart';
|
||||||
|
|
||||||
class ApprovalStepRepositoryRemote implements ApprovalStepRepository {
|
class ApprovalStepRepositoryRemote implements ApprovalStepRepository {
|
||||||
|
|||||||
@@ -1,6 +1,5 @@
|
|||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:get_it/get_it.dart';
|
import 'package:get_it/get_it.dart';
|
||||||
import 'package:intl/intl.dart';
|
|
||||||
import 'package:lucide_icons_flutter/lucide_icons.dart' as lucide;
|
import 'package:lucide_icons_flutter/lucide_icons.dart' as lucide;
|
||||||
import 'package:shadcn_ui/shadcn_ui.dart';
|
import 'package:shadcn_ui/shadcn_ui.dart';
|
||||||
|
|
||||||
@@ -321,6 +320,9 @@ class _ApprovalStepEnabledPageState extends State<_ApprovalStepEnabledPage> {
|
|||||||
child: Align(
|
child: Align(
|
||||||
alignment: Alignment.centerRight,
|
alignment: Alignment.centerRight,
|
||||||
child: ShadButton.outline(
|
child: ShadButton.outline(
|
||||||
|
key: ValueKey(
|
||||||
|
'step_detail_${step.id ?? record.approvalId}_${step.stepOrder}',
|
||||||
|
),
|
||||||
size: ShadButtonSize.sm,
|
size: ShadButtonSize.sm,
|
||||||
onPressed: step.id == null
|
onPressed: step.id == null
|
||||||
? null
|
? null
|
||||||
@@ -382,9 +384,7 @@ class _ApprovalStepEnabledPageState extends State<_ApprovalStepEnabledPage> {
|
|||||||
options.add(const _StatusOption(id: -1, name: '전체'));
|
options.add(const _StatusOption(id: -1, name: '전체'));
|
||||||
for (final record in records) {
|
for (final record in records) {
|
||||||
final status = record.step.status;
|
final status = record.step.status;
|
||||||
if (status.id != null) {
|
options.add(_StatusOption(id: status.id, name: status.name));
|
||||||
options.add(_StatusOption(id: status.id!, name: status.name));
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
return options.toList()..sort((a, b) => a.id.compareTo(b.id));
|
return options.toList()..sort((a, b) => a.id.compareTo(b.id));
|
||||||
}
|
}
|
||||||
@@ -444,6 +444,15 @@ class _ApprovalStepEnabledPageState extends State<_ApprovalStepEnabledPage> {
|
|||||||
'결재번호 ${detail.approvalNo}',
|
'결재번호 ${detail.approvalNo}',
|
||||||
style: theme.textTheme.muted,
|
style: theme.textTheme.muted,
|
||||||
),
|
),
|
||||||
|
footer: Row(
|
||||||
|
mainAxisAlignment: MainAxisAlignment.end,
|
||||||
|
children: [
|
||||||
|
ShadButton.ghost(
|
||||||
|
onPressed: () => Navigator.of(dialogContext).pop(),
|
||||||
|
child: const Text('닫기'),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
child: Padding(
|
child: Padding(
|
||||||
padding: const EdgeInsets.symmetric(vertical: 16),
|
padding: const EdgeInsets.symmetric(vertical: 16),
|
||||||
child: Column(
|
child: Column(
|
||||||
@@ -485,15 +494,6 @@ class _ApprovalStepEnabledPageState extends State<_ApprovalStepEnabledPage> {
|
|||||||
],
|
],
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
footer: Row(
|
|
||||||
mainAxisAlignment: MainAxisAlignment.end,
|
|
||||||
children: [
|
|
||||||
ShadButton.ghost(
|
|
||||||
onPressed: () => Navigator.of(dialogContext).pop(),
|
|
||||||
child: const Text('닫기'),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -1,6 +1,5 @@
|
|||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:get_it/get_it.dart';
|
import 'package:get_it/get_it.dart';
|
||||||
import 'package:intl/intl.dart';
|
|
||||||
import 'package:lucide_icons_flutter/lucide_icons.dart' as lucide;
|
import 'package:lucide_icons_flutter/lucide_icons.dart' as lucide;
|
||||||
import 'package:shadcn_ui/shadcn_ui.dart';
|
import 'package:shadcn_ui/shadcn_ui.dart';
|
||||||
|
|
||||||
@@ -442,6 +441,7 @@ class _ApprovalTemplateEnabledPageState
|
|||||||
|
|
||||||
Future<bool?> _openTemplateForm({ApprovalTemplate? template}) async {
|
Future<bool?> _openTemplateForm({ApprovalTemplate? template}) async {
|
||||||
final isEdit = template != null;
|
final isEdit = template != null;
|
||||||
|
final existingTemplate = template;
|
||||||
final codeController = TextEditingController(text: template?.code ?? '');
|
final codeController = TextEditingController(text: template?.code ?? '');
|
||||||
final nameController = TextEditingController(text: template?.name ?? '');
|
final nameController = TextEditingController(text: template?.name ?? '');
|
||||||
final descriptionController = TextEditingController(
|
final descriptionController = TextEditingController(
|
||||||
@@ -587,77 +587,76 @@ class _ApprovalTemplateEnabledPageState
|
|||||||
),
|
),
|
||||||
actions: [
|
actions: [
|
||||||
ShadButton.ghost(
|
ShadButton.ghost(
|
||||||
onPressed: isSaving ? null : () => Navigator.of(context).pop(false),
|
onPressed: () {
|
||||||
|
if (isSaving) return;
|
||||||
|
Navigator.of(context).pop(false);
|
||||||
|
},
|
||||||
child: const Text('취소'),
|
child: const Text('취소'),
|
||||||
),
|
),
|
||||||
ShadButton(
|
ShadButton(
|
||||||
onPressed: isSaving
|
onPressed: () async {
|
||||||
? null
|
if (isSaving) return;
|
||||||
: () async {
|
final codeValue = codeController.text.trim();
|
||||||
final codeValue = codeController.text.trim();
|
final nameValue = nameController.text.trim();
|
||||||
final nameValue = nameController.text.trim();
|
if (!isEdit && codeValue.isEmpty) {
|
||||||
if (!isEdit && codeValue.isEmpty) {
|
modalSetState?.call(() => errorText = '템플릿 코드를 입력하세요.');
|
||||||
modalSetState?.call(() => errorText = '템플릿 코드를 입력하세요.');
|
return;
|
||||||
return;
|
}
|
||||||
}
|
if (nameValue.isEmpty) {
|
||||||
if (nameValue.isEmpty) {
|
modalSetState?.call(() => errorText = '템플릿명을 입력하세요.');
|
||||||
modalSetState?.call(() => errorText = '템플릿명을 입력하세요.');
|
return;
|
||||||
return;
|
}
|
||||||
}
|
final validation = _validateSteps(steps);
|
||||||
final validation = _validateSteps(steps);
|
if (validation != null) {
|
||||||
if (validation != null) {
|
modalSetState?.call(() => errorText = validation);
|
||||||
modalSetState?.call(() => errorText = validation);
|
return;
|
||||||
return;
|
}
|
||||||
}
|
modalSetState?.call(() => errorText = null);
|
||||||
modalSetState?.call(() => errorText = null);
|
final stepInputs = steps
|
||||||
final stepInputs = steps
|
.map(
|
||||||
.map(
|
(field) => ApprovalTemplateStepInput(
|
||||||
(field) => ApprovalTemplateStepInput(
|
id: field.id,
|
||||||
id: field.id,
|
stepOrder: int.parse(field.orderController.text.trim()),
|
||||||
stepOrder: int.parse(
|
approverId: int.parse(field.approverController.text.trim()),
|
||||||
field.orderController.text.trim(),
|
note: field.noteController.text.trim().isEmpty
|
||||||
),
|
|
||||||
approverId: int.parse(
|
|
||||||
field.approverController.text.trim(),
|
|
||||||
),
|
|
||||||
note: field.noteController.text.trim().isEmpty
|
|
||||||
? null
|
|
||||||
: field.noteController.text.trim(),
|
|
||||||
),
|
|
||||||
)
|
|
||||||
.toList();
|
|
||||||
final input = ApprovalTemplateInput(
|
|
||||||
code: isEdit ? template!.code : codeValue,
|
|
||||||
name: nameValue,
|
|
||||||
description: descriptionController.text.trim().isEmpty
|
|
||||||
? null
|
? null
|
||||||
: descriptionController.text.trim(),
|
: field.noteController.text.trim(),
|
||||||
note: noteController.text.trim().isEmpty
|
),
|
||||||
? null
|
|
||||||
: noteController.text.trim(),
|
|
||||||
isActive: statusNotifier.value,
|
|
||||||
);
|
|
||||||
modalSetState?.call(() => isSaving = true);
|
|
||||||
final success = isEdit
|
|
||||||
? await _controller.update(
|
|
||||||
template!.id,
|
|
||||||
input,
|
|
||||||
stepInputs,
|
|
||||||
)
|
|
||||||
: await _controller.create(input, stepInputs);
|
|
||||||
if (success != null && mounted) {
|
|
||||||
Navigator.of(context).pop(true);
|
|
||||||
} else {
|
|
||||||
modalSetState?.call(() => isSaving = false);
|
|
||||||
}
|
|
||||||
},
|
|
||||||
child: isSaving
|
|
||||||
? const SizedBox(
|
|
||||||
width: 16,
|
|
||||||
height: 16,
|
|
||||||
child: CircularProgressIndicator(strokeWidth: 2),
|
|
||||||
)
|
)
|
||||||
: Text(isEdit ? '수정 완료' : '생성 완료'),
|
.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);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
child: Text(isEdit ? '수정 완료' : '생성 완료'),
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
);
|
);
|
||||||
@@ -780,7 +779,9 @@ class _StepEditorRow extends StatelessWidget {
|
|||||||
margin: const EdgeInsets.symmetric(vertical: 6),
|
margin: const EdgeInsets.symmetric(vertical: 6),
|
||||||
padding: const EdgeInsets.all(12),
|
padding: const EdgeInsets.all(12),
|
||||||
decoration: BoxDecoration(
|
decoration: BoxDecoration(
|
||||||
border: Border.all(color: theme.colorScheme.border.withOpacity(0.6)),
|
border: Border.all(
|
||||||
|
color: theme.colorScheme.border.withValues(alpha: 0.6),
|
||||||
|
),
|
||||||
borderRadius: BorderRadius.circular(12),
|
borderRadius: BorderRadius.circular(12),
|
||||||
),
|
),
|
||||||
child: Column(
|
child: Column(
|
||||||
|
|||||||
@@ -4,7 +4,6 @@ import 'package:flutter_test/flutter_test.dart';
|
|||||||
import 'package:get_it/get_it.dart';
|
import 'package:get_it/get_it.dart';
|
||||||
import 'package:mocktail/mocktail.dart';
|
import 'package:mocktail/mocktail.dart';
|
||||||
import 'package:shadcn_ui/shadcn_ui.dart';
|
import 'package:shadcn_ui/shadcn_ui.dart';
|
||||||
import 'package:two_dimensional_scrollables/two_dimensional_scrollables.dart';
|
|
||||||
|
|
||||||
import 'package:superport_v2/core/common/models/paginated_result.dart';
|
import 'package:superport_v2/core/common/models/paginated_result.dart';
|
||||||
import 'package:superport_v2/features/approvals/domain/entities/approval.dart';
|
import 'package:superport_v2/features/approvals/domain/entities/approval.dart';
|
||||||
@@ -95,14 +94,9 @@ void main() {
|
|||||||
expect(find.text('APP-2024-0001'), findsOneWidget);
|
expect(find.text('APP-2024-0001'), findsOneWidget);
|
||||||
expect(find.text('최승인'), findsOneWidget);
|
expect(find.text('최승인'), findsOneWidget);
|
||||||
|
|
||||||
await tester.dragUntilVisible(
|
final detailButtonFinder = find.byKey(const ValueKey('step_detail_501_1'));
|
||||||
find.widgetWithText(ShadButton, '상세'),
|
final detailButton = tester.widget<ShadButton>(detailButtonFinder);
|
||||||
find.byType(TwoDimensionalScrollable),
|
detailButton.onPressed?.call();
|
||||||
const Offset(-200, 0),
|
|
||||||
);
|
|
||||||
await tester.pumpAndSettle();
|
|
||||||
|
|
||||||
await tester.tap(find.widgetWithText(ShadButton, '상세').first);
|
|
||||||
await tester.pump();
|
await tester.pump();
|
||||||
await tester.pumpAndSettle();
|
await tester.pumpAndSettle();
|
||||||
|
|
||||||
|
|||||||
@@ -65,7 +65,7 @@ void main() {
|
|||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
ApprovalTemplate _template({bool isActive = true}) {
|
ApprovalTemplate buildTemplate({bool isActive = true}) {
|
||||||
return ApprovalTemplate(
|
return ApprovalTemplate(
|
||||||
id: 10,
|
id: 10,
|
||||||
code: 'AP_INBOUND',
|
code: 'AP_INBOUND',
|
||||||
@@ -101,7 +101,7 @@ void main() {
|
|||||||
),
|
),
|
||||||
).thenAnswer(
|
).thenAnswer(
|
||||||
(_) async => PaginatedResult<ApprovalTemplate>(
|
(_) async => PaginatedResult<ApprovalTemplate>(
|
||||||
items: [_template()],
|
items: [buildTemplate()],
|
||||||
page: 1,
|
page: 1,
|
||||||
pageSize: 20,
|
pageSize: 20,
|
||||||
total: 1,
|
total: 1,
|
||||||
@@ -140,7 +140,7 @@ void main() {
|
|||||||
|
|
||||||
when(
|
when(
|
||||||
() => repository.create(any(), steps: any(named: 'steps')),
|
() => repository.create(any(), steps: any(named: 'steps')),
|
||||||
).thenAnswer((_) async => _template());
|
).thenAnswer((_) async => buildTemplate());
|
||||||
|
|
||||||
await tester.pumpWidget(_buildApp(const ApprovalTemplatePage()));
|
await tester.pumpWidget(_buildApp(const ApprovalTemplatePage()));
|
||||||
await tester.pump();
|
await tester.pump();
|
||||||
@@ -192,7 +192,7 @@ void main() {
|
|||||||
});
|
});
|
||||||
|
|
||||||
testWidgets('수정 플로우에서 fetchDetail 후 update를 호출한다', (tester) async {
|
testWidgets('수정 플로우에서 fetchDetail 후 update를 호출한다', (tester) async {
|
||||||
final activeTemplate = _template();
|
final activeTemplate = buildTemplate();
|
||||||
|
|
||||||
when(
|
when(
|
||||||
() => repository.list(
|
() => repository.list(
|
||||||
|
|||||||
Reference in New Issue
Block a user