feat: 재고 상태 전이 플래그 적용 및 실패 메시지 정비
This commit is contained in:
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
@@ -1,10 +1,19 @@
|
||||
import 'dart:math' as math;
|
||||
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:get_it/get_it.dart';
|
||||
import 'package:go_router/go_router.dart';
|
||||
import 'package:shadcn_ui/shadcn_ui.dart';
|
||||
|
||||
import '../../../../core/constants/app_sections.dart';
|
||||
import '../../../../core/network/failure.dart';
|
||||
import '../../../../core/permissions/permission_manager.dart';
|
||||
import '../../../../core/permissions/permission_resources.dart';
|
||||
import '../../../masters/group/domain/entities/group.dart';
|
||||
import '../../../masters/group/domain/repositories/group_repository.dart';
|
||||
import '../../../masters/group_permission/application/permission_synchronizer.dart';
|
||||
import '../../../masters/group_permission/domain/repositories/group_permission_repository.dart';
|
||||
|
||||
/// Superport 로그인 화면. 간단한 유효성 검증 후 대시보드로 이동한다.
|
||||
class LoginPage extends StatefulWidget {
|
||||
@@ -58,6 +67,24 @@ class _LoginPageState extends State<LoginPage> {
|
||||
}
|
||||
|
||||
if (!mounted) return;
|
||||
try {
|
||||
await _synchronizePermissions();
|
||||
} catch (error) {
|
||||
if (!mounted) return;
|
||||
final failure = Failure.from(error);
|
||||
final description = failure.describe();
|
||||
final message = description.isEmpty
|
||||
? '권한 정보를 불러오지 못했습니다. 잠시 후 다시 시도하세요.'
|
||||
: description;
|
||||
setState(() {
|
||||
errorMessage = message;
|
||||
isLoading = false;
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
if (!mounted) return;
|
||||
setState(() => isLoading = false);
|
||||
context.go(dashboardRoutePath);
|
||||
}
|
||||
|
||||
@@ -222,6 +249,13 @@ class _LoginPageState extends State<LoginPage> {
|
||||
],
|
||||
),
|
||||
),
|
||||
if (kDebugMode) ...[
|
||||
const SizedBox(height: 12),
|
||||
ShadButton.ghost(
|
||||
onPressed: isLoading ? null : _handleTestLogin,
|
||||
child: const Text('테스트 로그인'),
|
||||
),
|
||||
],
|
||||
],
|
||||
),
|
||||
);
|
||||
@@ -250,4 +284,73 @@ class _LoginPageState extends State<LoginPage> {
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
/// 디버그 모드에서 모든 권한을 부여하고 즉시 대시보드로 이동한다.
|
||||
void _handleTestLogin() {
|
||||
final manager = PermissionScope.of(context);
|
||||
manager.clearServerPermissions();
|
||||
|
||||
final allActions = PermissionAction.values.toSet();
|
||||
final overrides = <String, Set<PermissionAction>>{};
|
||||
final dashboardResource = PermissionResources.normalize(dashboardRoutePath);
|
||||
if (dashboardResource.isNotEmpty) {
|
||||
overrides[dashboardResource] = allActions;
|
||||
}
|
||||
for (final page in allAppPages) {
|
||||
final resource = PermissionResources.normalize(page.path);
|
||||
if (resource.isEmpty) {
|
||||
continue;
|
||||
}
|
||||
overrides[resource] = allActions;
|
||||
}
|
||||
manager.updateOverrides(overrides);
|
||||
|
||||
setState(() {
|
||||
errorMessage = null;
|
||||
isLoading = false;
|
||||
});
|
||||
if (!mounted) {
|
||||
return;
|
||||
}
|
||||
context.go(dashboardRoutePath);
|
||||
}
|
||||
|
||||
Future<void> _synchronizePermissions() async {
|
||||
final manager = PermissionScope.of(context);
|
||||
manager.clearServerPermissions();
|
||||
|
||||
final groupRepository = GetIt.I<GroupRepository>();
|
||||
final defaultGroups = await groupRepository.list(
|
||||
page: 1,
|
||||
pageSize: 1,
|
||||
isDefault: true,
|
||||
);
|
||||
Group? targetGroup = _firstGroupWithId(defaultGroups.items);
|
||||
|
||||
if (targetGroup == null) {
|
||||
final fallbackGroups = await groupRepository.list(page: 1, pageSize: 1);
|
||||
targetGroup = _firstGroupWithId(fallbackGroups.items);
|
||||
}
|
||||
|
||||
if (targetGroup?.id == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
final permissionRepository = GetIt.I<GroupPermissionRepository>();
|
||||
final synchronizer = PermissionSynchronizer(
|
||||
repository: permissionRepository,
|
||||
manager: manager,
|
||||
);
|
||||
final groupId = targetGroup!.id!;
|
||||
await synchronizer.syncForGroup(groupId);
|
||||
}
|
||||
|
||||
Group? _firstGroupWithId(List<Group> groups) {
|
||||
for (final group in groups) {
|
||||
if (group.id != null) {
|
||||
return group;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,17 +1,26 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter/services.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:url_launcher/url_launcher.dart';
|
||||
|
||||
import 'package:superport_v2/core/constants/app_sections.dart';
|
||||
import 'package:superport_v2/features/inventory/lookups/domain/entities/lookup_item.dart';
|
||||
import 'package:superport_v2/features/inventory/lookups/domain/repositories/inventory_lookup_repository.dart';
|
||||
import 'package:superport_v2/features/masters/warehouse/domain/entities/warehouse.dart';
|
||||
import 'package:superport_v2/features/masters/warehouse/domain/repositories/warehouse_repository.dart';
|
||||
import 'package:superport_v2/features/reporting/domain/entities/report_download_result.dart';
|
||||
import 'package:superport_v2/features/reporting/domain/entities/report_export_format.dart';
|
||||
import 'package:superport_v2/features/reporting/domain/entities/report_export_request.dart';
|
||||
import 'package:superport_v2/features/reporting/domain/repositories/reporting_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';
|
||||
import 'package:superport_v2/core/network/failure.dart';
|
||||
|
||||
/// 보고서 다운로드 화면 루트 위젯.
|
||||
class ReportingPage extends StatefulWidget {
|
||||
@@ -24,7 +33,37 @@ class ReportingPage extends StatefulWidget {
|
||||
/// 보고서 페이지 UI 상태와 필터 조건을 관리하는 상태 클래스.
|
||||
class _ReportingPageState extends State<ReportingPage> {
|
||||
late final WarehouseRepository _warehouseRepository;
|
||||
ReportingRepository? _reportingRepository;
|
||||
InventoryLookupRepository? _lookupRepository;
|
||||
final intl.DateFormat _dateFormat = intl.DateFormat('yyyy.MM.dd');
|
||||
final Map<ReportTypeFilter, LookupItem> _transactionTypeLookup = {};
|
||||
final Map<ReportStatusFilter, LookupItem> _transactionStatusLookup = {};
|
||||
final Map<ReportStatusFilter, LookupItem> _approvalStatusLookup = {};
|
||||
bool _isLoadingLookups = false;
|
||||
String? _lookupError;
|
||||
bool _isExporting = false;
|
||||
String? _exportError;
|
||||
ReportDownloadResult? _lastResult;
|
||||
ReportExportFormat? _lastFormat;
|
||||
|
||||
static const Map<ReportTypeFilter, List<String>> _transactionTypeKeywords = {
|
||||
ReportTypeFilter.inbound: ['입고', 'inbound'],
|
||||
ReportTypeFilter.outbound: ['출고', 'outbound'],
|
||||
ReportTypeFilter.rental: ['대여', 'rent', 'rental'],
|
||||
};
|
||||
|
||||
static const Map<ReportStatusFilter, List<String>>
|
||||
_transactionStatusKeywords = {
|
||||
ReportStatusFilter.inProgress: ['작성', '대기', '진행', 'pending'],
|
||||
ReportStatusFilter.completed: ['완료', '승인', 'complete', 'approved'],
|
||||
ReportStatusFilter.cancelled: ['취소', '반려', 'cancel', 'rejected'],
|
||||
};
|
||||
|
||||
static const Map<ReportStatusFilter, List<String>> _approvalStatusKeywords = {
|
||||
ReportStatusFilter.inProgress: ['진행', '대기', 'processing'],
|
||||
ReportStatusFilter.completed: ['완료', '승인', 'approved'],
|
||||
ReportStatusFilter.cancelled: ['취소', '반려', 'cancel'],
|
||||
};
|
||||
|
||||
DateTimeRange? _appliedDateRange;
|
||||
DateTimeRange? _pendingDateRange;
|
||||
@@ -41,10 +80,28 @@ class _ReportingPageState extends State<ReportingPage> {
|
||||
bool _isLoadingWarehouses = false;
|
||||
String? _warehouseError;
|
||||
|
||||
/// 실패 객체에서 사용자 메시지를 추출한다.
|
||||
String _failureMessage(Object error, String fallback) {
|
||||
final failure = Failure.from(error);
|
||||
final description = failure.describe();
|
||||
if (description.isEmpty) {
|
||||
return fallback;
|
||||
}
|
||||
return description;
|
||||
}
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
_warehouseRepository = GetIt.I<WarehouseRepository>();
|
||||
final getIt = GetIt.I;
|
||||
_warehouseRepository = getIt<WarehouseRepository>();
|
||||
if (getIt.isRegistered<ReportingRepository>()) {
|
||||
_reportingRepository = getIt<ReportingRepository>();
|
||||
}
|
||||
if (getIt.isRegistered<InventoryLookupRepository>()) {
|
||||
_lookupRepository = getIt<InventoryLookupRepository>();
|
||||
_loadLookups();
|
||||
}
|
||||
_loadWarehouses();
|
||||
}
|
||||
|
||||
@@ -85,12 +142,17 @@ class _ReportingPageState extends State<ReportingPage> {
|
||||
}
|
||||
} catch (error) {
|
||||
if (mounted) {
|
||||
final message = _failureMessage(
|
||||
error,
|
||||
'창고 목록을 불러오지 못했습니다. 잠시 후 다시 시도하세요.',
|
||||
);
|
||||
setState(() {
|
||||
_warehouseError = '창고 목록을 불러오지 못했습니다. 잠시 후 다시 시도하세요.';
|
||||
_warehouseError = message;
|
||||
_warehouseOptions = const [WarehouseFilterOption.all];
|
||||
_appliedWarehouse = WarehouseFilterOption.all;
|
||||
_pendingWarehouse = WarehouseFilterOption.all;
|
||||
});
|
||||
SuperportToast.error(context, message);
|
||||
}
|
||||
} finally {
|
||||
if (mounted) {
|
||||
@@ -101,6 +163,60 @@ class _ReportingPageState extends State<ReportingPage> {
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> _loadLookups() async {
|
||||
final repository = _lookupRepository;
|
||||
if (repository == null) {
|
||||
return;
|
||||
}
|
||||
setState(() {
|
||||
_isLoadingLookups = true;
|
||||
_lookupError = null;
|
||||
});
|
||||
try {
|
||||
final transactionTypes = await repository.fetchTransactionTypes();
|
||||
final transactionStatuses = await repository.fetchTransactionStatuses();
|
||||
final approvalStatuses = await repository.fetchApprovalStatuses();
|
||||
if (!mounted) {
|
||||
return;
|
||||
}
|
||||
setState(() {
|
||||
_transactionTypeLookup
|
||||
..clear()
|
||||
..addAll(_mapTransactionTypes(transactionTypes));
|
||||
_transactionStatusLookup
|
||||
..clear()
|
||||
..addAll(
|
||||
_mapStatusByKeyword(
|
||||
transactionStatuses,
|
||||
_transactionStatusKeywords,
|
||||
),
|
||||
);
|
||||
_approvalStatusLookup
|
||||
..clear()
|
||||
..addAll(
|
||||
_mapStatusByKeyword(approvalStatuses, _approvalStatusKeywords),
|
||||
);
|
||||
});
|
||||
} catch (error) {
|
||||
if (mounted) {
|
||||
final message = _failureMessage(
|
||||
error,
|
||||
'유형/상태 정보를 불러오지 못했습니다. 잠시 후 다시 시도하세요.',
|
||||
);
|
||||
setState(() {
|
||||
_lookupError = message;
|
||||
});
|
||||
SuperportToast.error(context, message);
|
||||
}
|
||||
} finally {
|
||||
if (mounted) {
|
||||
setState(() {
|
||||
_isLoadingLookups = false;
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// 적용된 필터를 초기 상태로 되돌린다.
|
||||
void _resetFilters() {
|
||||
setState(() {
|
||||
@@ -166,6 +282,69 @@ class _ReportingPageState extends State<ReportingPage> {
|
||||
return options.first;
|
||||
}
|
||||
|
||||
Map<ReportTypeFilter, LookupItem> _mapTransactionTypes(
|
||||
List<LookupItem> items,
|
||||
) {
|
||||
final result = <ReportTypeFilter, LookupItem>{};
|
||||
for (final entry in _transactionTypeKeywords.entries) {
|
||||
final matched = _matchLookup(items, entry.value);
|
||||
if (matched != null) {
|
||||
result[entry.key] = matched;
|
||||
}
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
Map<ReportStatusFilter, LookupItem> _mapStatusByKeyword(
|
||||
List<LookupItem> items,
|
||||
Map<ReportStatusFilter, List<String>> keywords,
|
||||
) {
|
||||
final result = <ReportStatusFilter, LookupItem>{};
|
||||
for (final entry in keywords.entries) {
|
||||
final matched = _matchLookup(items, entry.value);
|
||||
if (matched != null) {
|
||||
result[entry.key] = matched;
|
||||
}
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
LookupItem? _matchLookup(List<LookupItem> items, List<String> keywords) {
|
||||
final normalized = keywords
|
||||
.map((keyword) => keyword.trim().toLowerCase())
|
||||
.where((keyword) => keyword.isNotEmpty)
|
||||
.toList(growable: false);
|
||||
if (normalized.isEmpty) {
|
||||
return null;
|
||||
}
|
||||
for (final item in items) {
|
||||
final name = item.name.trim().toLowerCase();
|
||||
if (normalized.any((keyword) => name.contains(keyword))) {
|
||||
return item;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
int? _resolveTransactionTypeId() {
|
||||
if (_appliedType == ReportTypeFilter.all ||
|
||||
_appliedType == ReportTypeFilter.approval) {
|
||||
return null;
|
||||
}
|
||||
final lookup = _transactionTypeLookup[_appliedType];
|
||||
return lookup?.id;
|
||||
}
|
||||
|
||||
int? _resolveStatusId() {
|
||||
if (_appliedStatus == ReportStatusFilter.all) {
|
||||
return null;
|
||||
}
|
||||
if (_appliedType == ReportTypeFilter.approval) {
|
||||
return _approvalStatusLookup[_appliedStatus]?.id;
|
||||
}
|
||||
return _transactionStatusLookup[_appliedStatus]?.id;
|
||||
}
|
||||
|
||||
String _dateRangeLabel(DateTimeRange? range) {
|
||||
if (range == null) {
|
||||
return '기간 선택';
|
||||
@@ -175,8 +354,159 @@ class _ReportingPageState extends State<ReportingPage> {
|
||||
|
||||
String _formatDate(DateTime value) => _dateFormat.format(value);
|
||||
|
||||
void _handleExport(ReportExportFormat format) {
|
||||
SuperportToast.info(context, '${format.label} 다운로드 연동은 준비 중입니다.');
|
||||
Future<void> _handleExport(ReportExportFormat format) async {
|
||||
if (_isExporting) {
|
||||
return;
|
||||
}
|
||||
final repository = _reportingRepository;
|
||||
final range = _appliedDateRange;
|
||||
if (repository == null) {
|
||||
SuperportToast.error(context, '보고서 다운로드 리포지토리가 초기화되지 않았습니다.');
|
||||
return;
|
||||
}
|
||||
if (range == null) {
|
||||
SuperportToast.error(context, '기간을 먼저 선택하세요.');
|
||||
return;
|
||||
}
|
||||
setState(() {
|
||||
_isExporting = true;
|
||||
_exportError = null;
|
||||
});
|
||||
final request = ReportExportRequest(
|
||||
from: range.start,
|
||||
to: range.end,
|
||||
format: format,
|
||||
transactionTypeId: _resolveTransactionTypeId(),
|
||||
statusId: _resolveStatusId(),
|
||||
warehouseId: _appliedWarehouse.id,
|
||||
);
|
||||
try {
|
||||
final result = _appliedType == ReportTypeFilter.approval
|
||||
? await repository.exportApprovals(request)
|
||||
: await repository.exportTransactions(request);
|
||||
if (!mounted) {
|
||||
return;
|
||||
}
|
||||
setState(() {
|
||||
_lastResult = result;
|
||||
_lastFormat = format;
|
||||
});
|
||||
if (result.hasDownloadUrl) {
|
||||
SuperportToast.success(context, '다운로드 링크가 준비되었습니다.');
|
||||
} else if (result.hasBytes) {
|
||||
SuperportToast.success(context, '보고서 파일이 준비되었습니다. 저장 기능은 추후 제공 예정입니다.');
|
||||
} else {
|
||||
SuperportToast.info(context, '다운로드 결과를 확인했지만 추가 처리 항목이 없습니다.');
|
||||
}
|
||||
} catch (error) {
|
||||
if (!mounted) {
|
||||
return;
|
||||
}
|
||||
final message = _failureMessage(
|
||||
error,
|
||||
'보고서 다운로드에 실패했습니다. 잠시 후 다시 시도하세요.',
|
||||
);
|
||||
setState(() {
|
||||
_exportError = message;
|
||||
});
|
||||
SuperportToast.error(context, message);
|
||||
} finally {
|
||||
if (mounted) {
|
||||
setState(() {
|
||||
_isExporting = false;
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> _launchDownloadUrl(Uri url) async {
|
||||
try {
|
||||
final opened = await launchUrl(url, mode: LaunchMode.externalApplication);
|
||||
if (!opened && mounted) {
|
||||
SuperportToast.error(context, '다운로드 링크를 열 수 없습니다.');
|
||||
}
|
||||
} catch (_) {
|
||||
if (mounted) {
|
||||
SuperportToast.error(context, '다운로드 링크를 열 수 없습니다.');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> _copyDownloadUrl(Uri url) async {
|
||||
await Clipboard.setData(ClipboardData(text: url.toString()));
|
||||
if (mounted) {
|
||||
SuperportToast.success(context, '다운로드 URL을 복사했습니다.');
|
||||
}
|
||||
}
|
||||
|
||||
Widget _buildPreview(ShadThemeData theme) {
|
||||
if (_isExporting) {
|
||||
return const Center(child: CircularProgressIndicator());
|
||||
}
|
||||
if (_exportError != null) {
|
||||
return Center(
|
||||
child: Text(
|
||||
_exportError!,
|
||||
style: theme.textTheme.small.copyWith(
|
||||
color: theme.colorScheme.destructive,
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
final result = _lastResult;
|
||||
if (result == null) {
|
||||
return SuperportEmptyState(
|
||||
icon: lucide.LucideIcons.chartBar,
|
||||
title: '미리보기 데이터가 없습니다.',
|
||||
description: '필터를 적용하거나 보고서를 다운로드하면 이 영역에 요약이 표시됩니다.',
|
||||
);
|
||||
}
|
||||
final rows = <Widget>[
|
||||
if (_lastFormat != null)
|
||||
_SummaryRow(label: '형식', value: _lastFormat!.label),
|
||||
if (result.filename != null)
|
||||
_SummaryRow(label: '파일명', value: result.filename!),
|
||||
if (result.mimeType != null)
|
||||
_SummaryRow(label: 'MIME', value: result.mimeType!),
|
||||
if (result.expiresAt != null)
|
||||
_SummaryRow(
|
||||
label: '만료 시각',
|
||||
value: intl.DateFormat('yyyy-MM-dd HH:mm').format(result.expiresAt!),
|
||||
),
|
||||
if (result.downloadUrl != null)
|
||||
_SummaryRow(label: '다운로드 URL', value: result.downloadUrl!.toString()),
|
||||
if (result.hasBytes && (result.downloadUrl == null))
|
||||
const _SummaryRow(
|
||||
label: '상태',
|
||||
value: '바이너리 응답이 준비되었습니다. 저장 기능은 추후 제공 예정입니다.',
|
||||
),
|
||||
];
|
||||
|
||||
return Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
...rows,
|
||||
const SizedBox(height: 12),
|
||||
Wrap(
|
||||
spacing: 12,
|
||||
runSpacing: 12,
|
||||
children: [
|
||||
if (result.downloadUrl != null)
|
||||
ShadButton(
|
||||
onPressed: () => _launchDownloadUrl(result.downloadUrl!),
|
||||
leading: const Icon(lucide.LucideIcons.externalLink, size: 16),
|
||||
child: const Text('다운로드 열기'),
|
||||
),
|
||||
if (result.downloadUrl != null)
|
||||
ShadButton.outline(
|
||||
onPressed: () => _copyDownloadUrl(result.downloadUrl!),
|
||||
leading: const Icon(lucide.LucideIcons.copy, size: 16),
|
||||
child: const Text('URL 복사'),
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
@@ -241,13 +571,26 @@ class _ReportingPageState extends State<ReportingPage> {
|
||||
child: ShadSelect<ReportTypeFilter>(
|
||||
key: ValueKey(_pendingType),
|
||||
initialValue: _pendingType,
|
||||
selectedOptionBuilder: (_, value) => Text(value.label),
|
||||
onChanged: (value) {
|
||||
if (value == null) return;
|
||||
setState(() {
|
||||
_pendingType = value;
|
||||
});
|
||||
selectedOptionBuilder: (context, value) {
|
||||
final theme = ShadTheme.of(context);
|
||||
if (_lookupError != null) {
|
||||
return Text(
|
||||
_lookupError!,
|
||||
style: theme.textTheme.small.copyWith(
|
||||
color: theme.colorScheme.destructive,
|
||||
),
|
||||
);
|
||||
}
|
||||
return Text(value.label);
|
||||
},
|
||||
onChanged: _isLoadingLookups
|
||||
? null
|
||||
: (value) {
|
||||
if (value == null) return;
|
||||
setState(() {
|
||||
_pendingType = value;
|
||||
});
|
||||
},
|
||||
options: [
|
||||
for (final type in ReportTypeFilter.values)
|
||||
ShadOption(value: type, child: Text(type.label)),
|
||||
@@ -279,13 +622,26 @@ class _ReportingPageState extends State<ReportingPage> {
|
||||
child: ShadSelect<ReportStatusFilter>(
|
||||
key: ValueKey(_pendingStatus),
|
||||
initialValue: _pendingStatus,
|
||||
selectedOptionBuilder: (_, value) => Text(value.label),
|
||||
onChanged: (value) {
|
||||
if (value == null) return;
|
||||
setState(() {
|
||||
_pendingStatus = value;
|
||||
});
|
||||
selectedOptionBuilder: (context, value) {
|
||||
final theme = ShadTheme.of(context);
|
||||
if (_lookupError != null) {
|
||||
return Text(
|
||||
_lookupError!,
|
||||
style: theme.textTheme.small.copyWith(
|
||||
color: theme.colorScheme.destructive,
|
||||
),
|
||||
);
|
||||
}
|
||||
return Text(value.label);
|
||||
},
|
||||
onChanged: _isLoadingLookups
|
||||
? null
|
||||
: (value) {
|
||||
if (value == null) return;
|
||||
setState(() {
|
||||
_pendingStatus = value;
|
||||
});
|
||||
},
|
||||
options: [
|
||||
for (final status in ReportStatusFilter.values)
|
||||
ShadOption(value: status, child: Text(status.label)),
|
||||
@@ -373,15 +729,18 @@ class _ReportingPageState extends State<ReportingPage> {
|
||||
ShadCard(
|
||||
title: Text('보고서 미리보기', style: theme.textTheme.h3),
|
||||
description: Text(
|
||||
'조건을 적용하면 다운로드 진행 상태와 결과 테이블이 이 영역에 표시됩니다.',
|
||||
'다운로드 상태와 결과 요약을 확인하세요.',
|
||||
style: theme.textTheme.muted,
|
||||
),
|
||||
child: SizedBox(
|
||||
height: 240,
|
||||
child: SuperportEmptyState(
|
||||
icon: lucide.LucideIcons.chartBar,
|
||||
title: '미리보기 데이터가 없습니다.',
|
||||
description: '필터를 적용하거나 보고서를 다운로드하면 이 영역에 요약이 표시됩니다.',
|
||||
child: AnimatedSize(
|
||||
duration: const Duration(milliseconds: 200),
|
||||
curve: Curves.easeOut,
|
||||
child: ConstrainedBox(
|
||||
constraints: const BoxConstraints(minHeight: 200),
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.symmetric(vertical: 8),
|
||||
child: _buildPreview(theme),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
@@ -458,15 +817,6 @@ extension ReportStatusFilterX on ReportStatusFilter {
|
||||
}
|
||||
}
|
||||
|
||||
enum ReportExportFormat { xlsx, pdf }
|
||||
|
||||
extension ReportExportFormatX on ReportExportFormat {
|
||||
String get label => switch (this) {
|
||||
ReportExportFormat.xlsx => 'XLSX',
|
||||
ReportExportFormat.pdf => 'PDF',
|
||||
};
|
||||
}
|
||||
|
||||
class WarehouseFilterOption {
|
||||
const WarehouseFilterOption({this.id, required this.label});
|
||||
|
||||
|
||||
Reference in New Issue
Block a user