전역 구조 리팩터링 및 테스트 확장
This commit is contained in:
@@ -1,5 +1,7 @@
|
||||
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';
|
||||
|
||||
import 'package:superport_v2/core/constants/app_sections.dart';
|
||||
@@ -7,7 +9,9 @@ import 'package:superport_v2/features/masters/warehouse/domain/entities/warehous
|
||||
import 'package:superport_v2/features/masters/warehouse/domain/repositories/warehouse_repository.dart';
|
||||
import 'package:superport_v2/widgets/app_layout.dart';
|
||||
import 'package:superport_v2/widgets/components/empty_state.dart';
|
||||
import 'package:superport_v2/widgets/components/feedback.dart';
|
||||
import 'package:superport_v2/widgets/components/filter_bar.dart';
|
||||
import 'package:superport_v2/widgets/components/superport_date_picker.dart';
|
||||
|
||||
class ReportingPage extends StatefulWidget {
|
||||
const ReportingPage({super.key});
|
||||
@@ -18,12 +22,16 @@ class ReportingPage extends StatefulWidget {
|
||||
|
||||
class _ReportingPageState extends State<ReportingPage> {
|
||||
late final WarehouseRepository _warehouseRepository;
|
||||
final DateFormat _dateFormat = DateFormat('yyyy.MM.dd');
|
||||
final intl.DateFormat _dateFormat = intl.DateFormat('yyyy.MM.dd');
|
||||
|
||||
DateTimeRange? _dateRange;
|
||||
ReportTypeFilter _selectedType = ReportTypeFilter.all;
|
||||
ReportStatusFilter _selectedStatus = ReportStatusFilter.all;
|
||||
WarehouseFilterOption _selectedWarehouse = WarehouseFilterOption.all;
|
||||
DateTimeRange? _appliedDateRange;
|
||||
DateTimeRange? _pendingDateRange;
|
||||
ReportTypeFilter _appliedType = ReportTypeFilter.all;
|
||||
ReportTypeFilter _pendingType = ReportTypeFilter.all;
|
||||
ReportStatusFilter _appliedStatus = ReportStatusFilter.all;
|
||||
ReportStatusFilter _pendingStatus = ReportStatusFilter.all;
|
||||
WarehouseFilterOption _appliedWarehouse = WarehouseFilterOption.all;
|
||||
WarehouseFilterOption _pendingWarehouse = WarehouseFilterOption.all;
|
||||
|
||||
List<WarehouseFilterOption> _warehouseOptions = const [
|
||||
WarehouseFilterOption.all,
|
||||
@@ -63,14 +71,14 @@ class _ReportingPageState extends State<ReportingPage> {
|
||||
if (mounted) {
|
||||
setState(() {
|
||||
_warehouseOptions = options;
|
||||
WarehouseFilterOption nextSelected = WarehouseFilterOption.all;
|
||||
for (final option in options) {
|
||||
if (option == _selectedWarehouse) {
|
||||
nextSelected = option;
|
||||
break;
|
||||
}
|
||||
}
|
||||
_selectedWarehouse = nextSelected;
|
||||
_appliedWarehouse = _resolveWarehouseOption(
|
||||
_appliedWarehouse,
|
||||
options,
|
||||
);
|
||||
_pendingWarehouse = _resolveWarehouseOption(
|
||||
_pendingWarehouse,
|
||||
options,
|
||||
);
|
||||
});
|
||||
}
|
||||
} catch (error) {
|
||||
@@ -78,7 +86,8 @@ class _ReportingPageState extends State<ReportingPage> {
|
||||
setState(() {
|
||||
_warehouseError = '창고 목록을 불러오지 못했습니다. 잠시 후 다시 시도하세요.';
|
||||
_warehouseOptions = const [WarehouseFilterOption.all];
|
||||
_selectedWarehouse = WarehouseFilterOption.all;
|
||||
_appliedWarehouse = WarehouseFilterOption.all;
|
||||
_pendingWarehouse = WarehouseFilterOption.all;
|
||||
});
|
||||
}
|
||||
} finally {
|
||||
@@ -90,57 +99,70 @@ class _ReportingPageState extends State<ReportingPage> {
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> _pickDateRange() async {
|
||||
final now = DateTime.now();
|
||||
final initialRange =
|
||||
_dateRange ??
|
||||
DateTimeRange(
|
||||
start: DateTime(
|
||||
now.year,
|
||||
now.month,
|
||||
now.day,
|
||||
).subtract(const Duration(days: 6)),
|
||||
end: DateTime(now.year, now.month, now.day),
|
||||
);
|
||||
final picked = await showDateRangePicker(
|
||||
context: context,
|
||||
initialDateRange: initialRange,
|
||||
firstDate: DateTime(now.year - 5),
|
||||
lastDate: DateTime(now.year + 2),
|
||||
helpText: '기간 선택',
|
||||
saveText: '적용',
|
||||
currentDate: now,
|
||||
locale: Localizations.localeOf(context),
|
||||
);
|
||||
if (picked != null && mounted) {
|
||||
setState(() {
|
||||
_dateRange = picked;
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
void _resetFilters() {
|
||||
setState(() {
|
||||
_dateRange = null;
|
||||
_selectedType = ReportTypeFilter.all;
|
||||
_selectedStatus = ReportStatusFilter.all;
|
||||
_selectedWarehouse = WarehouseFilterOption.all;
|
||||
_appliedDateRange = null;
|
||||
_pendingDateRange = null;
|
||||
_appliedType = ReportTypeFilter.all;
|
||||
_pendingType = ReportTypeFilter.all;
|
||||
_appliedStatus = ReportStatusFilter.all;
|
||||
_pendingStatus = ReportStatusFilter.all;
|
||||
_appliedWarehouse = WarehouseFilterOption.all;
|
||||
_pendingWarehouse = WarehouseFilterOption.all;
|
||||
});
|
||||
}
|
||||
|
||||
void _applyFilters() {
|
||||
setState(() {
|
||||
_appliedDateRange = _pendingDateRange;
|
||||
_appliedType = _pendingType;
|
||||
_appliedStatus = _pendingStatus;
|
||||
_appliedWarehouse = _pendingWarehouse;
|
||||
});
|
||||
}
|
||||
|
||||
bool get _canExport {
|
||||
return _dateRange != null && _selectedType != ReportTypeFilter.all;
|
||||
return _appliedDateRange != null && _appliedType != ReportTypeFilter.all;
|
||||
}
|
||||
|
||||
bool get _hasCustomFilters {
|
||||
return _dateRange != null ||
|
||||
_selectedType != ReportTypeFilter.all ||
|
||||
_selectedStatus != ReportStatusFilter.all ||
|
||||
_selectedWarehouse != WarehouseFilterOption.all;
|
||||
return _appliedDateRange != null ||
|
||||
_appliedType != ReportTypeFilter.all ||
|
||||
_appliedStatus != ReportStatusFilter.all ||
|
||||
_appliedWarehouse != WarehouseFilterOption.all;
|
||||
}
|
||||
|
||||
String get _dateRangeLabel {
|
||||
final range = _dateRange;
|
||||
bool get _hasAppliedFilters => _hasCustomFilters;
|
||||
|
||||
bool get _hasDirtyFilters =>
|
||||
!_isSameRange(_pendingDateRange, _appliedDateRange) ||
|
||||
_pendingType != _appliedType ||
|
||||
_pendingStatus != _appliedStatus ||
|
||||
_pendingWarehouse != _appliedWarehouse;
|
||||
|
||||
bool _isSameRange(DateTimeRange? a, DateTimeRange? b) {
|
||||
if (identical(a, b)) {
|
||||
return true;
|
||||
}
|
||||
if (a == null || b == null) {
|
||||
return a == b;
|
||||
}
|
||||
return a.start == b.start && a.end == b.end;
|
||||
}
|
||||
|
||||
WarehouseFilterOption _resolveWarehouseOption(
|
||||
WarehouseFilterOption target,
|
||||
List<WarehouseFilterOption> options,
|
||||
) {
|
||||
for (final option in options) {
|
||||
if (option == target) {
|
||||
return option;
|
||||
}
|
||||
}
|
||||
return options.first;
|
||||
}
|
||||
|
||||
String _dateRangeLabel(DateTimeRange? range) {
|
||||
if (range == null) {
|
||||
return '기간 선택';
|
||||
}
|
||||
@@ -150,11 +172,7 @@ class _ReportingPageState extends State<ReportingPage> {
|
||||
String _formatDate(DateTime value) => _dateFormat.format(value);
|
||||
|
||||
void _handleExport(ReportExportFormat format) {
|
||||
final messenger = ScaffoldMessenger.of(context);
|
||||
messenger.clearSnackBars();
|
||||
messenger.showSnackBar(
|
||||
SnackBar(content: Text('${format.label} 다운로드 연동은 준비 중입니다.')),
|
||||
);
|
||||
SuperportToast.info(context, '${format.label} 다운로드 연동은 준비 중입니다.');
|
||||
}
|
||||
|
||||
@override
|
||||
@@ -174,34 +192,56 @@ class _ReportingPageState extends State<ReportingPage> {
|
||||
onPressed: _canExport
|
||||
? () => _handleExport(ReportExportFormat.xlsx)
|
||||
: null,
|
||||
leading: const Icon(LucideIcons.fileDown, size: 16),
|
||||
leading: const Icon(lucide.LucideIcons.fileDown, size: 16),
|
||||
child: const Text('XLSX 다운로드'),
|
||||
),
|
||||
ShadButton.outline(
|
||||
onPressed: _canExport
|
||||
? () => _handleExport(ReportExportFormat.pdf)
|
||||
: null,
|
||||
leading: const Icon(LucideIcons.fileText, size: 16),
|
||||
leading: const Icon(lucide.LucideIcons.fileText, size: 16),
|
||||
child: const Text('PDF 다운로드'),
|
||||
),
|
||||
],
|
||||
toolbar: FilterBar(
|
||||
actionConfig: FilterBarActionConfig(
|
||||
onApply: _applyFilters,
|
||||
onReset: _resetFilters,
|
||||
hasPendingChanges: _hasDirtyFilters,
|
||||
hasActiveFilters: _hasAppliedFilters,
|
||||
),
|
||||
children: [
|
||||
ShadButton.outline(
|
||||
onPressed: _pickDateRange,
|
||||
leading: const Icon(LucideIcons.calendar, size: 16),
|
||||
child: Text(_dateRangeLabel),
|
||||
SizedBox(
|
||||
width: 220,
|
||||
child: SuperportDateRangePickerButton(
|
||||
value: _pendingDateRange ?? _appliedDateRange,
|
||||
dateFormat: _dateFormat,
|
||||
firstDate: DateTime(DateTime.now().year - 5),
|
||||
lastDate: DateTime(DateTime.now().year + 2),
|
||||
initialDateRange:
|
||||
_pendingDateRange ??
|
||||
_appliedDateRange ??
|
||||
DateTimeRange(
|
||||
start: DateTime.now().subtract(const Duration(days: 6)),
|
||||
end: DateTime.now(),
|
||||
),
|
||||
onChanged: (range) {
|
||||
setState(() {
|
||||
_pendingDateRange = range;
|
||||
});
|
||||
},
|
||||
),
|
||||
),
|
||||
SizedBox(
|
||||
width: 200,
|
||||
child: ShadSelect<ReportTypeFilter>(
|
||||
key: ValueKey(_selectedType),
|
||||
initialValue: _selectedType,
|
||||
key: ValueKey(_pendingType),
|
||||
initialValue: _pendingType,
|
||||
selectedOptionBuilder: (_, value) => Text(value.label),
|
||||
onChanged: (value) {
|
||||
if (value == null) return;
|
||||
setState(() {
|
||||
_selectedType = value;
|
||||
_pendingType = value;
|
||||
});
|
||||
},
|
||||
options: [
|
||||
@@ -214,14 +254,14 @@ class _ReportingPageState extends State<ReportingPage> {
|
||||
width: 220,
|
||||
child: ShadSelect<WarehouseFilterOption>(
|
||||
key: ValueKey(
|
||||
'${_selectedWarehouse.cacheKey}-${_warehouseOptions.length}',
|
||||
'${_pendingWarehouse.cacheKey}-${_warehouseOptions.length}',
|
||||
),
|
||||
initialValue: _selectedWarehouse,
|
||||
initialValue: _pendingWarehouse,
|
||||
selectedOptionBuilder: (_, value) => Text(value.label),
|
||||
onChanged: (value) {
|
||||
if (value == null) return;
|
||||
setState(() {
|
||||
_selectedWarehouse = value;
|
||||
_pendingWarehouse = value;
|
||||
});
|
||||
},
|
||||
options: [
|
||||
@@ -233,13 +273,13 @@ class _ReportingPageState extends State<ReportingPage> {
|
||||
SizedBox(
|
||||
width: 200,
|
||||
child: ShadSelect<ReportStatusFilter>(
|
||||
key: ValueKey(_selectedStatus),
|
||||
initialValue: _selectedStatus,
|
||||
key: ValueKey(_pendingStatus),
|
||||
initialValue: _pendingStatus,
|
||||
selectedOptionBuilder: (_, value) => Text(value.label),
|
||||
onChanged: (value) {
|
||||
if (value == null) return;
|
||||
setState(() {
|
||||
_selectedStatus = value;
|
||||
_pendingStatus = value;
|
||||
});
|
||||
},
|
||||
options: [
|
||||
@@ -248,11 +288,6 @@ class _ReportingPageState extends State<ReportingPage> {
|
||||
],
|
||||
),
|
||||
),
|
||||
ShadButton.ghost(
|
||||
onPressed: _hasCustomFilters ? _resetFilters : null,
|
||||
leading: const Icon(LucideIcons.rotateCcw, size: 16),
|
||||
child: const Text('초기화'),
|
||||
),
|
||||
],
|
||||
),
|
||||
child: Column(
|
||||
@@ -264,7 +299,7 @@ class _ReportingPageState extends State<ReportingPage> {
|
||||
child: Row(
|
||||
children: [
|
||||
Icon(
|
||||
LucideIcons.circleAlert,
|
||||
lucide.LucideIcons.circleAlert,
|
||||
size: 16,
|
||||
color: theme.colorScheme.destructive,
|
||||
),
|
||||
@@ -280,7 +315,7 @@ class _ReportingPageState extends State<ReportingPage> {
|
||||
const SizedBox(width: 8),
|
||||
ShadButton.ghost(
|
||||
onPressed: _isLoadingWarehouses ? null : _loadWarehouses,
|
||||
leading: const Icon(LucideIcons.refreshCw, size: 16),
|
||||
leading: const Icon(lucide.LucideIcons.refreshCw, size: 16),
|
||||
child: const Text('재시도'),
|
||||
),
|
||||
],
|
||||
@@ -290,14 +325,12 @@ class _ReportingPageState extends State<ReportingPage> {
|
||||
Padding(
|
||||
padding: const EdgeInsets.only(bottom: 16),
|
||||
child: Row(
|
||||
children: [
|
||||
const SizedBox(
|
||||
width: 16,
|
||||
height: 16,
|
||||
child: CircularProgressIndicator(strokeWidth: 2),
|
||||
),
|
||||
const SizedBox(width: 8),
|
||||
Text('창고 목록을 불러오는 중입니다...', style: theme.textTheme.small),
|
||||
children: const [
|
||||
SuperportSkeleton(width: 180, height: 20),
|
||||
SizedBox(width: 12),
|
||||
SuperportSkeleton(width: 140, height: 20),
|
||||
SizedBox(width: 12),
|
||||
SuperportSkeleton(width: 120, height: 20),
|
||||
],
|
||||
),
|
||||
),
|
||||
@@ -312,11 +345,13 @@ class _ReportingPageState extends State<ReportingPage> {
|
||||
children: [
|
||||
_SummaryRow(
|
||||
label: '기간',
|
||||
value: _dateRange == null ? '기간을 선택하세요.' : _dateRangeLabel,
|
||||
value: _appliedDateRange == null
|
||||
? '기간을 선택하세요.'
|
||||
: _dateRangeLabel(_appliedDateRange),
|
||||
),
|
||||
_SummaryRow(label: '유형', value: _selectedType.label),
|
||||
_SummaryRow(label: '창고', value: _selectedWarehouse.label),
|
||||
_SummaryRow(label: '상태', value: _selectedStatus.label),
|
||||
_SummaryRow(label: '유형', value: _appliedType.label),
|
||||
_SummaryRow(label: '창고', value: _appliedWarehouse.label),
|
||||
_SummaryRow(label: '상태', value: _appliedStatus.label),
|
||||
if (!_canExport)
|
||||
Padding(
|
||||
padding: const EdgeInsets.only(top: 12),
|
||||
@@ -339,9 +374,10 @@ class _ReportingPageState extends State<ReportingPage> {
|
||||
),
|
||||
child: SizedBox(
|
||||
height: 240,
|
||||
child: EmptyState(
|
||||
icon: LucideIcons.chartBar,
|
||||
message: '필터를 선택하고 다운로드하면 결과 미리보기가 제공됩니다.',
|
||||
child: SuperportEmptyState(
|
||||
icon: lucide.LucideIcons.chartBar,
|
||||
title: '미리보기 데이터가 없습니다.',
|
||||
description: '필터를 적용하거나 보고서를 다운로드하면 이 영역에 요약이 표시됩니다.',
|
||||
),
|
||||
),
|
||||
),
|
||||
|
||||
Reference in New Issue
Block a user