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 'dart:math' as math;
|
||||||
|
|
||||||
|
import 'package:flutter/foundation.dart';
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:get_it/get_it.dart';
|
||||||
import 'package:go_router/go_router.dart';
|
import 'package:go_router/go_router.dart';
|
||||||
import 'package:shadcn_ui/shadcn_ui.dart';
|
import 'package:shadcn_ui/shadcn_ui.dart';
|
||||||
|
|
||||||
import '../../../../core/constants/app_sections.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 로그인 화면. 간단한 유효성 검증 후 대시보드로 이동한다.
|
/// Superport 로그인 화면. 간단한 유효성 검증 후 대시보드로 이동한다.
|
||||||
class LoginPage extends StatefulWidget {
|
class LoginPage extends StatefulWidget {
|
||||||
@@ -58,6 +67,24 @@ class _LoginPageState extends State<LoginPage> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (!mounted) return;
|
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);
|
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/material.dart';
|
||||||
|
import 'package:flutter/services.dart';
|
||||||
import 'package:get_it/get_it.dart';
|
import 'package:get_it/get_it.dart';
|
||||||
import 'package:intl/intl.dart' as intl;
|
import 'package:intl/intl.dart' as intl;
|
||||||
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';
|
||||||
|
import 'package:url_launcher/url_launcher.dart';
|
||||||
|
|
||||||
import 'package:superport_v2/core/constants/app_sections.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/entities/warehouse.dart';
|
||||||
import 'package:superport_v2/features/masters/warehouse/domain/repositories/warehouse_repository.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/app_layout.dart';
|
||||||
import 'package:superport_v2/widgets/components/empty_state.dart';
|
import 'package:superport_v2/widgets/components/empty_state.dart';
|
||||||
import 'package:superport_v2/widgets/components/feedback.dart';
|
import 'package:superport_v2/widgets/components/feedback.dart';
|
||||||
import 'package:superport_v2/widgets/components/filter_bar.dart';
|
import 'package:superport_v2/widgets/components/filter_bar.dart';
|
||||||
import 'package:superport_v2/widgets/components/superport_date_picker.dart';
|
import 'package:superport_v2/widgets/components/superport_date_picker.dart';
|
||||||
|
import 'package:superport_v2/core/network/failure.dart';
|
||||||
|
|
||||||
/// 보고서 다운로드 화면 루트 위젯.
|
/// 보고서 다운로드 화면 루트 위젯.
|
||||||
class ReportingPage extends StatefulWidget {
|
class ReportingPage extends StatefulWidget {
|
||||||
@@ -24,7 +33,37 @@ class ReportingPage extends StatefulWidget {
|
|||||||
/// 보고서 페이지 UI 상태와 필터 조건을 관리하는 상태 클래스.
|
/// 보고서 페이지 UI 상태와 필터 조건을 관리하는 상태 클래스.
|
||||||
class _ReportingPageState extends State<ReportingPage> {
|
class _ReportingPageState extends State<ReportingPage> {
|
||||||
late final WarehouseRepository _warehouseRepository;
|
late final WarehouseRepository _warehouseRepository;
|
||||||
|
ReportingRepository? _reportingRepository;
|
||||||
|
InventoryLookupRepository? _lookupRepository;
|
||||||
final intl.DateFormat _dateFormat = intl.DateFormat('yyyy.MM.dd');
|
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? _appliedDateRange;
|
||||||
DateTimeRange? _pendingDateRange;
|
DateTimeRange? _pendingDateRange;
|
||||||
@@ -41,10 +80,28 @@ class _ReportingPageState extends State<ReportingPage> {
|
|||||||
bool _isLoadingWarehouses = false;
|
bool _isLoadingWarehouses = false;
|
||||||
String? _warehouseError;
|
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
|
@override
|
||||||
void initState() {
|
void initState() {
|
||||||
super.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();
|
_loadWarehouses();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -85,12 +142,17 @@ class _ReportingPageState extends State<ReportingPage> {
|
|||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
if (mounted) {
|
if (mounted) {
|
||||||
|
final message = _failureMessage(
|
||||||
|
error,
|
||||||
|
'창고 목록을 불러오지 못했습니다. 잠시 후 다시 시도하세요.',
|
||||||
|
);
|
||||||
setState(() {
|
setState(() {
|
||||||
_warehouseError = '창고 목록을 불러오지 못했습니다. 잠시 후 다시 시도하세요.';
|
_warehouseError = message;
|
||||||
_warehouseOptions = const [WarehouseFilterOption.all];
|
_warehouseOptions = const [WarehouseFilterOption.all];
|
||||||
_appliedWarehouse = WarehouseFilterOption.all;
|
_appliedWarehouse = WarehouseFilterOption.all;
|
||||||
_pendingWarehouse = WarehouseFilterOption.all;
|
_pendingWarehouse = WarehouseFilterOption.all;
|
||||||
});
|
});
|
||||||
|
SuperportToast.error(context, message);
|
||||||
}
|
}
|
||||||
} finally {
|
} finally {
|
||||||
if (mounted) {
|
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() {
|
void _resetFilters() {
|
||||||
setState(() {
|
setState(() {
|
||||||
@@ -166,6 +282,69 @@ class _ReportingPageState extends State<ReportingPage> {
|
|||||||
return options.first;
|
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) {
|
String _dateRangeLabel(DateTimeRange? range) {
|
||||||
if (range == null) {
|
if (range == null) {
|
||||||
return '기간 선택';
|
return '기간 선택';
|
||||||
@@ -175,8 +354,159 @@ class _ReportingPageState extends State<ReportingPage> {
|
|||||||
|
|
||||||
String _formatDate(DateTime value) => _dateFormat.format(value);
|
String _formatDate(DateTime value) => _dateFormat.format(value);
|
||||||
|
|
||||||
void _handleExport(ReportExportFormat format) {
|
Future<void> _handleExport(ReportExportFormat format) async {
|
||||||
SuperportToast.info(context, '${format.label} 다운로드 연동은 준비 중입니다.');
|
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
|
@override
|
||||||
@@ -241,13 +571,26 @@ class _ReportingPageState extends State<ReportingPage> {
|
|||||||
child: ShadSelect<ReportTypeFilter>(
|
child: ShadSelect<ReportTypeFilter>(
|
||||||
key: ValueKey(_pendingType),
|
key: ValueKey(_pendingType),
|
||||||
initialValue: _pendingType,
|
initialValue: _pendingType,
|
||||||
selectedOptionBuilder: (_, value) => Text(value.label),
|
selectedOptionBuilder: (context, value) {
|
||||||
onChanged: (value) {
|
final theme = ShadTheme.of(context);
|
||||||
if (value == null) return;
|
if (_lookupError != null) {
|
||||||
setState(() {
|
return Text(
|
||||||
_pendingType = value;
|
_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: [
|
options: [
|
||||||
for (final type in ReportTypeFilter.values)
|
for (final type in ReportTypeFilter.values)
|
||||||
ShadOption(value: type, child: Text(type.label)),
|
ShadOption(value: type, child: Text(type.label)),
|
||||||
@@ -279,13 +622,26 @@ class _ReportingPageState extends State<ReportingPage> {
|
|||||||
child: ShadSelect<ReportStatusFilter>(
|
child: ShadSelect<ReportStatusFilter>(
|
||||||
key: ValueKey(_pendingStatus),
|
key: ValueKey(_pendingStatus),
|
||||||
initialValue: _pendingStatus,
|
initialValue: _pendingStatus,
|
||||||
selectedOptionBuilder: (_, value) => Text(value.label),
|
selectedOptionBuilder: (context, value) {
|
||||||
onChanged: (value) {
|
final theme = ShadTheme.of(context);
|
||||||
if (value == null) return;
|
if (_lookupError != null) {
|
||||||
setState(() {
|
return Text(
|
||||||
_pendingStatus = value;
|
_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: [
|
options: [
|
||||||
for (final status in ReportStatusFilter.values)
|
for (final status in ReportStatusFilter.values)
|
||||||
ShadOption(value: status, child: Text(status.label)),
|
ShadOption(value: status, child: Text(status.label)),
|
||||||
@@ -373,15 +729,18 @@ class _ReportingPageState extends State<ReportingPage> {
|
|||||||
ShadCard(
|
ShadCard(
|
||||||
title: Text('보고서 미리보기', style: theme.textTheme.h3),
|
title: Text('보고서 미리보기', style: theme.textTheme.h3),
|
||||||
description: Text(
|
description: Text(
|
||||||
'조건을 적용하면 다운로드 진행 상태와 결과 테이블이 이 영역에 표시됩니다.',
|
'다운로드 상태와 결과 요약을 확인하세요.',
|
||||||
style: theme.textTheme.muted,
|
style: theme.textTheme.muted,
|
||||||
),
|
),
|
||||||
child: SizedBox(
|
child: AnimatedSize(
|
||||||
height: 240,
|
duration: const Duration(milliseconds: 200),
|
||||||
child: SuperportEmptyState(
|
curve: Curves.easeOut,
|
||||||
icon: lucide.LucideIcons.chartBar,
|
child: ConstrainedBox(
|
||||||
title: '미리보기 데이터가 없습니다.',
|
constraints: const BoxConstraints(minHeight: 200),
|
||||||
description: '필터를 적용하거나 보고서를 다운로드하면 이 영역에 요약이 표시됩니다.',
|
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 {
|
class WarehouseFilterOption {
|
||||||
const WarehouseFilterOption({this.id, required this.label});
|
const WarehouseFilterOption({this.id, required this.label});
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,246 @@
|
|||||||
|
import 'package:flutter_test/flutter_test.dart';
|
||||||
|
import 'package:mocktail/mocktail.dart';
|
||||||
|
|
||||||
|
import 'package:superport_v2/core/common/models/paginated_result.dart';
|
||||||
|
import 'package:superport_v2/core/network/api_error.dart';
|
||||||
|
import 'package:superport_v2/core/network/failure.dart';
|
||||||
|
import 'package:superport_v2/features/inventory/inbound/presentation/controllers/inbound_controller.dart';
|
||||||
|
import 'package:superport_v2/features/inventory/transactions/domain/entities/stock_transaction.dart';
|
||||||
|
import 'package:superport_v2/features/inventory/transactions/domain/entities/stock_transaction_input.dart';
|
||||||
|
import 'package:superport_v2/features/inventory/transactions/domain/repositories/stock_transaction_repository.dart';
|
||||||
|
import 'package:superport_v2/features/inventory/lookups/domain/repositories/inventory_lookup_repository.dart';
|
||||||
|
|
||||||
|
class _MockStockTransactionRepository extends Mock
|
||||||
|
implements StockTransactionRepository {}
|
||||||
|
|
||||||
|
class _MockInventoryLookupRepository extends Mock
|
||||||
|
implements InventoryLookupRepository {}
|
||||||
|
|
||||||
|
class _MockTransactionLineRepository extends Mock
|
||||||
|
implements TransactionLineRepository {}
|
||||||
|
|
||||||
|
class _FakeStockTransactionCreateInput extends Fake
|
||||||
|
implements StockTransactionCreateInput {}
|
||||||
|
|
||||||
|
class _FakeStockTransactionUpdateInput extends Fake
|
||||||
|
implements StockTransactionUpdateInput {}
|
||||||
|
|
||||||
|
class _FakeStockTransactionListFilter extends Fake
|
||||||
|
implements StockTransactionListFilter {}
|
||||||
|
|
||||||
|
void main() {
|
||||||
|
group('InboundController', () {
|
||||||
|
late StockTransactionRepository transactionRepository;
|
||||||
|
late InventoryLookupRepository lookupRepository;
|
||||||
|
late TransactionLineRepository lineRepository;
|
||||||
|
late InboundController controller;
|
||||||
|
|
||||||
|
setUpAll(() {
|
||||||
|
registerFallbackValue(_FakeStockTransactionCreateInput());
|
||||||
|
registerFallbackValue(_FakeStockTransactionUpdateInput());
|
||||||
|
registerFallbackValue(_FakeStockTransactionListFilter());
|
||||||
|
});
|
||||||
|
|
||||||
|
setUp(() {
|
||||||
|
transactionRepository = _MockStockTransactionRepository();
|
||||||
|
lookupRepository = _MockInventoryLookupRepository();
|
||||||
|
lineRepository = _MockTransactionLineRepository();
|
||||||
|
controller = InboundController(
|
||||||
|
transactionRepository: transactionRepository,
|
||||||
|
lineRepository: lineRepository,
|
||||||
|
lookupRepository: lookupRepository,
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('createTransaction은 레코드를 추가하고 결과를 반환한다', () async {
|
||||||
|
final transaction = _buildTransaction();
|
||||||
|
when(
|
||||||
|
() => transactionRepository.create(any()),
|
||||||
|
).thenAnswer((_) async => transaction);
|
||||||
|
|
||||||
|
final input = StockTransactionCreateInput(
|
||||||
|
transactionTypeId: 1,
|
||||||
|
transactionStatusId: 2,
|
||||||
|
warehouseId: 3,
|
||||||
|
transactionDate: DateTime(2024, 3, 1),
|
||||||
|
createdById: 9,
|
||||||
|
);
|
||||||
|
|
||||||
|
final record = await controller.createTransaction(
|
||||||
|
input,
|
||||||
|
refreshAfter: false,
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(record.id, equals(transaction.id));
|
||||||
|
expect(controller.records.length, equals(1));
|
||||||
|
expect(controller.records.first.id, equals(transaction.id));
|
||||||
|
verify(() => transactionRepository.create(any())).called(1);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('updateTransaction은 로컬 레코드를 갱신한다', () async {
|
||||||
|
final original = _buildTransaction();
|
||||||
|
final updated = _buildTransaction(statusName: '승인완료');
|
||||||
|
when(
|
||||||
|
() => transactionRepository.create(any()),
|
||||||
|
).thenAnswer((_) async => original);
|
||||||
|
when(
|
||||||
|
() => transactionRepository.update(any(), any()),
|
||||||
|
).thenAnswer((_) async => updated);
|
||||||
|
|
||||||
|
await controller.createTransaction(
|
||||||
|
StockTransactionCreateInput(
|
||||||
|
transactionTypeId: 1,
|
||||||
|
transactionStatusId: 2,
|
||||||
|
warehouseId: 3,
|
||||||
|
transactionDate: DateTime(2024, 3, 1),
|
||||||
|
createdById: 9,
|
||||||
|
),
|
||||||
|
refreshAfter: false,
|
||||||
|
);
|
||||||
|
|
||||||
|
final future = controller.updateTransaction(
|
||||||
|
original.id!,
|
||||||
|
StockTransactionUpdateInput(transactionStatusId: 3),
|
||||||
|
refreshAfter: false,
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(controller.processingTransactionIds.contains(original.id), isTrue);
|
||||||
|
|
||||||
|
final record = await future;
|
||||||
|
|
||||||
|
expect(record.status, equals('승인완료'));
|
||||||
|
expect(controller.records.first.status, equals('승인완료'));
|
||||||
|
expect(
|
||||||
|
controller.processingTransactionIds.contains(original.id),
|
||||||
|
isFalse,
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('deleteTransaction은 레코드를 제거한다', () async {
|
||||||
|
final transaction = _buildTransaction();
|
||||||
|
when(
|
||||||
|
() => transactionRepository.create(any()),
|
||||||
|
).thenAnswer((_) async => transaction);
|
||||||
|
when(
|
||||||
|
() => transactionRepository.delete(any()),
|
||||||
|
).thenAnswer((_) async => Future<void>.value());
|
||||||
|
|
||||||
|
await controller.createTransaction(
|
||||||
|
StockTransactionCreateInput(
|
||||||
|
transactionTypeId: 1,
|
||||||
|
transactionStatusId: 2,
|
||||||
|
warehouseId: 3,
|
||||||
|
transactionDate: DateTime(2024, 3, 1),
|
||||||
|
createdById: 9,
|
||||||
|
),
|
||||||
|
refreshAfter: false,
|
||||||
|
);
|
||||||
|
|
||||||
|
final future = controller.deleteTransaction(
|
||||||
|
transaction.id!,
|
||||||
|
refreshAfter: false,
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(
|
||||||
|
controller.processingTransactionIds.contains(transaction.id),
|
||||||
|
isTrue,
|
||||||
|
);
|
||||||
|
|
||||||
|
await future;
|
||||||
|
|
||||||
|
expect(controller.records, isEmpty);
|
||||||
|
expect(
|
||||||
|
controller.processingTransactionIds.contains(transaction.id),
|
||||||
|
isFalse,
|
||||||
|
);
|
||||||
|
verify(() => transactionRepository.delete(transaction.id!)).called(1);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('submitTransaction은 refreshAfter가 true일 때 목록을 다시 불러온다', () async {
|
||||||
|
final filter = StockTransactionListFilter(transactionTypeId: 1);
|
||||||
|
final initial = _buildTransaction();
|
||||||
|
when(
|
||||||
|
() => transactionRepository.list(filter: any(named: 'filter')),
|
||||||
|
).thenAnswer(
|
||||||
|
(_) async => PaginatedResult<StockTransaction>(
|
||||||
|
items: [initial],
|
||||||
|
page: 1,
|
||||||
|
pageSize: 20,
|
||||||
|
total: 1,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
await controller.fetchTransactions(filter: filter);
|
||||||
|
|
||||||
|
final updated = _buildTransaction(statusName: '승인대기');
|
||||||
|
when(
|
||||||
|
() => transactionRepository.submit(any()),
|
||||||
|
).thenAnswer((_) async => updated);
|
||||||
|
when(
|
||||||
|
() => transactionRepository.list(filter: any(named: 'filter')),
|
||||||
|
).thenAnswer(
|
||||||
|
(_) async => PaginatedResult<StockTransaction>(
|
||||||
|
items: [updated],
|
||||||
|
page: 1,
|
||||||
|
pageSize: 20,
|
||||||
|
total: 1,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
|
final result = await controller.submitTransaction(initial.id!);
|
||||||
|
|
||||||
|
expect(result.status, equals('승인대기'));
|
||||||
|
expect(controller.records.first.status, equals('승인대기'));
|
||||||
|
verify(() => transactionRepository.submit(initial.id!)).called(1);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('fetchTransactions 실패 시 Failure 메시지를 노출한다', () async {
|
||||||
|
final exception = ApiException(
|
||||||
|
code: ApiErrorCode.unprocessableEntity,
|
||||||
|
message: '입고 목록을 불러오지 못했습니다.',
|
||||||
|
details: {
|
||||||
|
'errors': {
|
||||||
|
'transaction_date': ['처리일자를 선택해 주세요.'],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
when(
|
||||||
|
() => transactionRepository.list(filter: any(named: 'filter')),
|
||||||
|
).thenThrow(exception);
|
||||||
|
|
||||||
|
await controller.fetchTransactions(
|
||||||
|
filter: StockTransactionListFilter(transactionTypeId: 1),
|
||||||
|
);
|
||||||
|
|
||||||
|
final failure = Failure.from(exception);
|
||||||
|
expect(controller.errorMessage, equals(failure.describe()));
|
||||||
|
expect(controller.records, isEmpty);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
StockTransaction _buildTransaction({int id = 100, String statusName = '작성중'}) {
|
||||||
|
return StockTransaction(
|
||||||
|
id: id,
|
||||||
|
transactionNo: 'TX-$id',
|
||||||
|
transactionDate: DateTime(2024, 3, 1),
|
||||||
|
type: StockTransactionType(id: 10, name: '입고'),
|
||||||
|
status: StockTransactionStatus(id: 11, name: statusName),
|
||||||
|
warehouse: StockTransactionWarehouse(id: 1, code: 'WH', name: '서울 물류'),
|
||||||
|
createdBy: StockTransactionEmployee(
|
||||||
|
id: 1,
|
||||||
|
employeeNo: 'EMP-1',
|
||||||
|
name: '관리자',
|
||||||
|
),
|
||||||
|
lines: [
|
||||||
|
StockTransactionLine(
|
||||||
|
id: 1,
|
||||||
|
lineNo: 1,
|
||||||
|
product: StockTransactionProduct(id: 1, code: 'P-1', name: '테스트 상품'),
|
||||||
|
quantity: 5,
|
||||||
|
unitPrice: 1000.0,
|
||||||
|
),
|
||||||
|
],
|
||||||
|
customers: const [],
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -3,11 +3,19 @@ import 'package:flutter/material.dart';
|
|||||||
import 'package:get_it/get_it.dart';
|
import 'package:get_it/get_it.dart';
|
||||||
import 'package:shadcn_ui/shadcn_ui.dart';
|
import 'package:shadcn_ui/shadcn_ui.dart';
|
||||||
|
|
||||||
import 'package:superport_v2/core/common/models/paginated_result.dart';
|
|
||||||
import 'package:superport_v2/core/config/environment.dart';
|
import 'package:superport_v2/core/config/environment.dart';
|
||||||
|
import 'package:superport_v2/core/common/models/paginated_result.dart';
|
||||||
|
import 'package:superport_v2/core/network/api_error.dart';
|
||||||
import 'package:superport_v2/features/masters/warehouse/domain/entities/warehouse.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/masters/warehouse/domain/repositories/warehouse_repository.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/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/features/reporting/presentation/pages/reporting_page.dart';
|
import 'package:superport_v2/features/reporting/presentation/pages/reporting_page.dart';
|
||||||
|
import 'package:superport_v2/widgets/components/empty_state.dart';
|
||||||
|
|
||||||
import '../../helpers/test_app.dart';
|
import '../../helpers/test_app.dart';
|
||||||
|
|
||||||
@@ -25,6 +33,10 @@ void main() {
|
|||||||
testWidgets('보고서 화면은 창고 목록 재시도 흐름을 제공한다', (tester) async {
|
testWidgets('보고서 화면은 창고 목록 재시도 흐름을 제공한다', (tester) async {
|
||||||
final repo = _FlakyWarehouseRepository();
|
final repo = _FlakyWarehouseRepository();
|
||||||
GetIt.I.registerSingleton<WarehouseRepository>(repo);
|
GetIt.I.registerSingleton<WarehouseRepository>(repo);
|
||||||
|
GetIt.I.registerSingleton<InventoryLookupRepository>(
|
||||||
|
_StubLookupRepository(),
|
||||||
|
);
|
||||||
|
GetIt.I.registerSingleton<ReportingRepository>(_FakeReportingRepository());
|
||||||
|
|
||||||
final view = tester.view;
|
final view = tester.view;
|
||||||
view.physicalSize = const Size(1280, 800);
|
view.physicalSize = const Size(1280, 800);
|
||||||
@@ -38,13 +50,16 @@ void main() {
|
|||||||
await tester.pumpAndSettle();
|
await tester.pumpAndSettle();
|
||||||
|
|
||||||
expect(repo.attempts, 1);
|
expect(repo.attempts, 1);
|
||||||
expect(find.text('창고 목록을 불러오지 못했습니다. 잠시 후 다시 시도하세요.'), findsOneWidget);
|
expect(
|
||||||
|
find.text('창고 목록을 불러오지 못했습니다. 잠시 후 다시 시도하세요.'),
|
||||||
|
findsWidgets,
|
||||||
|
);
|
||||||
|
|
||||||
await tester.tap(find.widgetWithText(ShadButton, '재시도'));
|
await tester.tap(find.widgetWithText(ShadButton, '재시도'));
|
||||||
await tester.pumpAndSettle();
|
await tester.pumpAndSettle();
|
||||||
|
await tester.pump(const Duration(seconds: 4));
|
||||||
|
|
||||||
expect(repo.attempts, 2);
|
expect(repo.attempts, 2);
|
||||||
expect(find.text('창고 목록을 불러오지 못했습니다. 잠시 후 다시 시도하세요.'), findsNothing);
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -57,10 +72,14 @@ class _FlakyWarehouseRepository implements WarehouseRepository {
|
|||||||
int pageSize = 20,
|
int pageSize = 20,
|
||||||
String? query,
|
String? query,
|
||||||
bool? isActive,
|
bool? isActive,
|
||||||
|
bool includeZipcode = true,
|
||||||
}) async {
|
}) async {
|
||||||
attempts += 1;
|
attempts += 1;
|
||||||
if (attempts == 1) {
|
if (attempts == 1) {
|
||||||
throw Exception('network down');
|
throw const ApiException(
|
||||||
|
code: ApiErrorCode.network,
|
||||||
|
message: '창고 목록을 불러오지 못했습니다. 잠시 후 다시 시도하세요.',
|
||||||
|
);
|
||||||
}
|
}
|
||||||
return PaginatedResult<Warehouse>(
|
return PaginatedResult<Warehouse>(
|
||||||
items: [
|
items: [
|
||||||
@@ -98,3 +117,77 @@ class _FlakyWarehouseRepository implements WarehouseRepository {
|
|||||||
throw UnimplementedError();
|
throw UnimplementedError();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
class _StubLookupRepository implements InventoryLookupRepository {
|
||||||
|
@override
|
||||||
|
Future<List<LookupItem>> fetchTransactionTypes({
|
||||||
|
bool activeOnly = true,
|
||||||
|
}) async {
|
||||||
|
return [
|
||||||
|
LookupItem(id: 1, name: '입고'),
|
||||||
|
LookupItem(id: 2, name: '출고'),
|
||||||
|
LookupItem(id: 3, name: '대여'),
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Future<List<LookupItem>> fetchTransactionStatuses({
|
||||||
|
bool activeOnly = true,
|
||||||
|
}) async {
|
||||||
|
return [
|
||||||
|
LookupItem(id: 11, name: '작성중'),
|
||||||
|
LookupItem(id: 12, name: '완료'),
|
||||||
|
LookupItem(id: 13, name: '취소'),
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Future<List<LookupItem>> fetchApprovalStatuses({
|
||||||
|
bool activeOnly = true,
|
||||||
|
}) async {
|
||||||
|
return [
|
||||||
|
LookupItem(id: 21, name: '진행중'),
|
||||||
|
LookupItem(id: 22, name: '완료'),
|
||||||
|
LookupItem(id: 23, name: '취소'),
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Future<List<LookupItem>> fetchApprovalActions({
|
||||||
|
bool activeOnly = true,
|
||||||
|
}) async {
|
||||||
|
return const [];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class _FakeReportingRepository implements ReportingRepository {
|
||||||
|
ReportExportRequest? lastRequest;
|
||||||
|
ReportExportFormat? lastFormat;
|
||||||
|
|
||||||
|
@override
|
||||||
|
Future<ReportDownloadResult> exportApprovals(
|
||||||
|
ReportExportRequest request,
|
||||||
|
) async {
|
||||||
|
lastRequest = request;
|
||||||
|
lastFormat = request.format;
|
||||||
|
return ReportDownloadResult(
|
||||||
|
downloadUrl: Uri.parse('https://example.com/approvals.pdf'),
|
||||||
|
filename: 'approvals.pdf',
|
||||||
|
mimeType: 'application/pdf',
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Future<ReportDownloadResult> exportTransactions(
|
||||||
|
ReportExportRequest request,
|
||||||
|
) async {
|
||||||
|
lastRequest = request;
|
||||||
|
lastFormat = request.format;
|
||||||
|
return ReportDownloadResult(
|
||||||
|
downloadUrl: Uri.parse('https://example.com/transactions.xlsx'),
|
||||||
|
filename: 'transactions.xlsx',
|
||||||
|
mimeType:
|
||||||
|
'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet',
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
937
test/helpers/inventory_test_stubs.dart
Normal file
937
test/helpers/inventory_test_stubs.dart
Normal file
@@ -0,0 +1,937 @@
|
|||||||
|
import 'package:get_it/get_it.dart';
|
||||||
|
import 'package:superport_v2/core/network/api_error.dart';
|
||||||
|
import 'package:superport_v2/core/common/models/paginated_result.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/inventory/transactions/domain/entities/stock_transaction.dart';
|
||||||
|
import 'package:superport_v2/features/inventory/transactions/domain/entities/stock_transaction_input.dart';
|
||||||
|
import 'package:superport_v2/features/inventory/transactions/domain/repositories/stock_transaction_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';
|
||||||
|
|
||||||
|
const int _inboundTypeId = 100;
|
||||||
|
const int _outboundTypeId = 200;
|
||||||
|
const int _rentalRentTypeId = 300;
|
||||||
|
const int _rentalReturnTypeId = 301;
|
||||||
|
|
||||||
|
const int _statusDraftId = 10;
|
||||||
|
const int _statusPendingId = 11;
|
||||||
|
const int _statusCompleteId = 12;
|
||||||
|
const int _statusOutboundWaitId = 21;
|
||||||
|
const int _statusOutboundDoneId = 22;
|
||||||
|
const int _statusRentalRentingId = 31;
|
||||||
|
const int _statusRentalReturnWaitId = 32;
|
||||||
|
const int _statusRentalFinishedId = 33;
|
||||||
|
|
||||||
|
StockTransactionListFilter? lastTransactionListFilter;
|
||||||
|
|
||||||
|
class InventoryTestStubConfig {
|
||||||
|
const InventoryTestStubConfig({this.submitFailure});
|
||||||
|
|
||||||
|
final ApiException? submitFailure;
|
||||||
|
}
|
||||||
|
|
||||||
|
InventoryTestStubConfig _stubConfig = const InventoryTestStubConfig();
|
||||||
|
|
||||||
|
void registerInventoryTestStubs([
|
||||||
|
InventoryTestStubConfig config = const InventoryTestStubConfig(),
|
||||||
|
]) {
|
||||||
|
_stubConfig = config;
|
||||||
|
lastTransactionListFilter = null;
|
||||||
|
final lookup = _StubInventoryLookupRepository(
|
||||||
|
transactionTypes: [
|
||||||
|
LookupItem(id: _inboundTypeId, name: '입고', code: 'INBOUND'),
|
||||||
|
LookupItem(id: _outboundTypeId, name: '출고', code: 'OUTBOUND'),
|
||||||
|
LookupItem(id: _rentalRentTypeId, name: '대여', code: 'RENT'),
|
||||||
|
LookupItem(id: _rentalReturnTypeId, name: '반납', code: 'RETURN'),
|
||||||
|
],
|
||||||
|
statuses: [
|
||||||
|
LookupItem(id: _statusDraftId, name: '작성중'),
|
||||||
|
LookupItem(id: _statusPendingId, name: '승인대기'),
|
||||||
|
LookupItem(id: _statusCompleteId, name: '승인완료'),
|
||||||
|
LookupItem(id: _statusOutboundWaitId, name: '출고대기'),
|
||||||
|
LookupItem(id: _statusOutboundDoneId, name: '출고완료'),
|
||||||
|
LookupItem(id: _statusRentalRentingId, name: '대여중'),
|
||||||
|
LookupItem(id: _statusRentalReturnWaitId, name: '반납대기'),
|
||||||
|
LookupItem(id: _statusRentalFinishedId, name: '완료'),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
|
||||||
|
final transactions = _buildTransactions();
|
||||||
|
final repository = _StubStockTransactionRepository(
|
||||||
|
transactions: transactions,
|
||||||
|
);
|
||||||
|
final lineRepository = _StubTransactionLineRepository(
|
||||||
|
transactions: transactions,
|
||||||
|
);
|
||||||
|
final customerRepository = _StubTransactionCustomerRepository(
|
||||||
|
transactions: transactions,
|
||||||
|
);
|
||||||
|
final warehouses = [
|
||||||
|
Warehouse(id: 1, warehouseCode: 'WH-001', warehouseName: '서울 1창고'),
|
||||||
|
Warehouse(id: 2, warehouseCode: 'WH-002', warehouseName: '부산 센터'),
|
||||||
|
Warehouse(id: 3, warehouseCode: 'WH-003', warehouseName: '대전 물류'),
|
||||||
|
];
|
||||||
|
final warehouseRepository = _StubWarehouseRepository(warehouses: warehouses);
|
||||||
|
|
||||||
|
final getIt = GetIt.I;
|
||||||
|
if (getIt.isRegistered<InventoryLookupRepository>()) {
|
||||||
|
getIt.unregister<InventoryLookupRepository>();
|
||||||
|
}
|
||||||
|
if (getIt.isRegistered<StockTransactionRepository>()) {
|
||||||
|
getIt.unregister<StockTransactionRepository>();
|
||||||
|
}
|
||||||
|
if (getIt.isRegistered<TransactionLineRepository>()) {
|
||||||
|
getIt.unregister<TransactionLineRepository>();
|
||||||
|
}
|
||||||
|
if (getIt.isRegistered<TransactionCustomerRepository>()) {
|
||||||
|
getIt.unregister<TransactionCustomerRepository>();
|
||||||
|
}
|
||||||
|
if (getIt.isRegistered<WarehouseRepository>()) {
|
||||||
|
getIt.unregister<WarehouseRepository>();
|
||||||
|
}
|
||||||
|
getIt.registerSingleton<InventoryLookupRepository>(lookup);
|
||||||
|
getIt.registerSingleton<StockTransactionRepository>(repository);
|
||||||
|
getIt.registerSingleton<TransactionLineRepository>(lineRepository);
|
||||||
|
getIt.registerSingleton<TransactionCustomerRepository>(customerRepository);
|
||||||
|
getIt.registerSingleton<WarehouseRepository>(warehouseRepository);
|
||||||
|
}
|
||||||
|
|
||||||
|
class _StubInventoryLookupRepository implements InventoryLookupRepository {
|
||||||
|
_StubInventoryLookupRepository({
|
||||||
|
required List<LookupItem> transactionTypes,
|
||||||
|
required List<LookupItem> statuses,
|
||||||
|
}) : _transactionTypes = transactionTypes,
|
||||||
|
_statuses = statuses;
|
||||||
|
|
||||||
|
final List<LookupItem> _transactionTypes;
|
||||||
|
final List<LookupItem> _statuses;
|
||||||
|
|
||||||
|
@override
|
||||||
|
Future<List<LookupItem>> fetchTransactionTypes({
|
||||||
|
bool activeOnly = true,
|
||||||
|
}) async {
|
||||||
|
return _transactionTypes;
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Future<List<LookupItem>> fetchTransactionStatuses({
|
||||||
|
bool activeOnly = true,
|
||||||
|
}) async {
|
||||||
|
return _statuses;
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Future<List<LookupItem>> fetchApprovalStatuses({
|
||||||
|
bool activeOnly = true,
|
||||||
|
}) async {
|
||||||
|
return const [];
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Future<List<LookupItem>> fetchApprovalActions({
|
||||||
|
bool activeOnly = true,
|
||||||
|
}) async {
|
||||||
|
return const [];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class _StubStockTransactionRepository implements StockTransactionRepository {
|
||||||
|
_StubStockTransactionRepository({
|
||||||
|
required List<StockTransaction> transactions,
|
||||||
|
}) : _transactions = transactions;
|
||||||
|
|
||||||
|
final List<StockTransaction> _transactions;
|
||||||
|
|
||||||
|
@override
|
||||||
|
Future<PaginatedResult<StockTransaction>> list({
|
||||||
|
StockTransactionListFilter? filter,
|
||||||
|
}) async {
|
||||||
|
final resolved = filter ?? StockTransactionListFilter();
|
||||||
|
lastTransactionListFilter = resolved;
|
||||||
|
final filtered = _transactions.where((transaction) {
|
||||||
|
if (resolved.transactionTypeId != null &&
|
||||||
|
transaction.type.id != resolved.transactionTypeId) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
if (resolved.transactionStatusId != null &&
|
||||||
|
transaction.status.id != resolved.transactionStatusId) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
if (resolved.from != null &&
|
||||||
|
transaction.transactionDate.isBefore(resolved.from!)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
if (resolved.to != null &&
|
||||||
|
transaction.transactionDate.isAfter(resolved.to!)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
if (resolved.query != null) {
|
||||||
|
final query = resolved.query!.toLowerCase();
|
||||||
|
final matchesQuery =
|
||||||
|
transaction.transactionNo.toLowerCase().contains(query) ||
|
||||||
|
transaction.createdBy.name.toLowerCase().contains(query) ||
|
||||||
|
transaction.lines.any(
|
||||||
|
(line) => line.product.name.toLowerCase().contains(query),
|
||||||
|
) ||
|
||||||
|
transaction.customers.any(
|
||||||
|
(customer) =>
|
||||||
|
customer.customer.name.toLowerCase().contains(query),
|
||||||
|
);
|
||||||
|
if (!matchesQuery) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}).toList();
|
||||||
|
|
||||||
|
final page = resolved.page;
|
||||||
|
final pageSize = resolved.pageSize;
|
||||||
|
final startIndex = (page - 1) * pageSize;
|
||||||
|
final paged = startIndex >= filtered.length
|
||||||
|
? const <StockTransaction>[]
|
||||||
|
: filtered.skip(startIndex).take(pageSize).toList();
|
||||||
|
|
||||||
|
return PaginatedResult<StockTransaction>(
|
||||||
|
items: paged,
|
||||||
|
page: page,
|
||||||
|
pageSize: pageSize,
|
||||||
|
total: filtered.length,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Future<StockTransaction> fetchDetail(
|
||||||
|
int id, {
|
||||||
|
List<String> include = const ['lines', 'customers', 'approval'],
|
||||||
|
}) {
|
||||||
|
throw UnimplementedError();
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Future<StockTransaction> create(StockTransactionCreateInput input) {
|
||||||
|
throw UnimplementedError();
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Future<StockTransaction> update(int id, StockTransactionUpdateInput input) {
|
||||||
|
throw UnimplementedError();
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Future<void> delete(int id) {
|
||||||
|
throw UnimplementedError();
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Future<StockTransaction> restore(int id) {
|
||||||
|
throw UnimplementedError();
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Future<StockTransaction> submit(int id) async {
|
||||||
|
final failure = _stubConfig.submitFailure;
|
||||||
|
if (failure != null) {
|
||||||
|
throw failure;
|
||||||
|
}
|
||||||
|
return _mutateTransaction(id, _applySubmitStatus);
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Future<StockTransaction> complete(int id) async {
|
||||||
|
return _mutateTransaction(id, _applyCompleteStatus);
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Future<StockTransaction> approve(int id) async {
|
||||||
|
return _mutateTransaction(id, (transaction) => transaction);
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Future<StockTransaction> reject(int id) async {
|
||||||
|
return _mutateTransaction(id, (transaction) => transaction);
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Future<StockTransaction> cancel(int id) async {
|
||||||
|
return _mutateTransaction(id, (transaction) => transaction);
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<StockTransaction> _mutateTransaction(
|
||||||
|
int id,
|
||||||
|
StockTransaction Function(StockTransaction transaction) transform,
|
||||||
|
) async {
|
||||||
|
final index = _transactions.indexWhere(
|
||||||
|
(transaction) => transaction.id == id,
|
||||||
|
);
|
||||||
|
if (index == -1) {
|
||||||
|
throw StateError('Transaction $id not found');
|
||||||
|
}
|
||||||
|
final current = _transactions[index];
|
||||||
|
final updated = transform(current);
|
||||||
|
_transactions[index] = updated;
|
||||||
|
return updated;
|
||||||
|
}
|
||||||
|
|
||||||
|
StockTransaction _applySubmitStatus(StockTransaction transaction) {
|
||||||
|
final nextStatus = _nextSubmitStatus(transaction);
|
||||||
|
if (nextStatus == null) {
|
||||||
|
return transaction;
|
||||||
|
}
|
||||||
|
return transaction.copyWith(status: nextStatus);
|
||||||
|
}
|
||||||
|
|
||||||
|
StockTransaction _applyCompleteStatus(StockTransaction transaction) {
|
||||||
|
final nextStatus = _nextCompleteStatus(transaction);
|
||||||
|
if (nextStatus == null) {
|
||||||
|
return transaction;
|
||||||
|
}
|
||||||
|
return transaction.copyWith(status: nextStatus);
|
||||||
|
}
|
||||||
|
|
||||||
|
StockTransactionStatus? _nextSubmitStatus(StockTransaction transaction) {
|
||||||
|
final status = transaction.status.name;
|
||||||
|
if (status.contains('작성중')) {
|
||||||
|
if (transaction.type.name.contains('입고')) {
|
||||||
|
return StockTransactionStatus(id: _statusPendingId, name: '승인대기');
|
||||||
|
}
|
||||||
|
if (transaction.type.name.contains('출고')) {
|
||||||
|
return StockTransactionStatus(id: _statusOutboundWaitId, name: '출고대기');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (status.contains('대여중')) {
|
||||||
|
return StockTransactionStatus(
|
||||||
|
id: _statusRentalReturnWaitId,
|
||||||
|
name: '반납대기',
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
StockTransactionStatus? _nextCompleteStatus(StockTransaction transaction) {
|
||||||
|
final status = transaction.status.name;
|
||||||
|
if (status.contains('승인대기')) {
|
||||||
|
return StockTransactionStatus(id: _statusCompleteId, name: '승인완료');
|
||||||
|
}
|
||||||
|
if (status.contains('출고대기')) {
|
||||||
|
return StockTransactionStatus(id: _statusOutboundDoneId, name: '출고완료');
|
||||||
|
}
|
||||||
|
if (status.contains('반납대기')) {
|
||||||
|
return StockTransactionStatus(id: _statusRentalFinishedId, name: '완료');
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class _StubTransactionLineRepository implements TransactionLineRepository {
|
||||||
|
_StubTransactionLineRepository({required List<StockTransaction> transactions})
|
||||||
|
: _transactions = transactions;
|
||||||
|
|
||||||
|
final List<StockTransaction> _transactions;
|
||||||
|
|
||||||
|
List<StockTransactionLine> _linesFor(int transactionId) {
|
||||||
|
final transaction = _transactions.firstWhere(
|
||||||
|
(item) => item.id == transactionId,
|
||||||
|
orElse: () => throw StateError('Transaction $transactionId not found'),
|
||||||
|
);
|
||||||
|
return transaction.lines;
|
||||||
|
}
|
||||||
|
|
||||||
|
StockTransactionLine _findLine(int lineId) {
|
||||||
|
for (final transaction in _transactions) {
|
||||||
|
for (final line in transaction.lines) {
|
||||||
|
if (line.id == lineId) {
|
||||||
|
return line;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
throw StateError('Line $lineId not found');
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Future<List<StockTransactionLine>> addLines(
|
||||||
|
int transactionId,
|
||||||
|
List<TransactionLineCreateInput> lines,
|
||||||
|
) async {
|
||||||
|
return _linesFor(transactionId);
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Future<void> deleteLine(int lineId) async {}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Future<List<StockTransactionLine>> updateLines(
|
||||||
|
int transactionId,
|
||||||
|
List<TransactionLineUpdateInput> lines,
|
||||||
|
) async {
|
||||||
|
return _linesFor(transactionId);
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Future<StockTransactionLine> restoreLine(int lineId) async {
|
||||||
|
return _findLine(lineId);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class _StubTransactionCustomerRepository
|
||||||
|
implements TransactionCustomerRepository {
|
||||||
|
_StubTransactionCustomerRepository({
|
||||||
|
required List<StockTransaction> transactions,
|
||||||
|
}) : _transactions = transactions;
|
||||||
|
|
||||||
|
final List<StockTransaction> _transactions;
|
||||||
|
|
||||||
|
List<StockTransactionCustomer> _customersFor(int transactionId) {
|
||||||
|
final transaction = _transactions.firstWhere(
|
||||||
|
(item) => item.id == transactionId,
|
||||||
|
orElse: () => throw StateError('Transaction $transactionId not found'),
|
||||||
|
);
|
||||||
|
return transaction.customers;
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Future<List<StockTransactionCustomer>> addCustomers(
|
||||||
|
int transactionId,
|
||||||
|
List<TransactionCustomerCreateInput> customers,
|
||||||
|
) async {
|
||||||
|
return _customersFor(transactionId);
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Future<void> deleteCustomer(int customerLinkId) async {}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Future<List<StockTransactionCustomer>> updateCustomers(
|
||||||
|
int transactionId,
|
||||||
|
List<TransactionCustomerUpdateInput> customers,
|
||||||
|
) async {
|
||||||
|
return _customersFor(transactionId);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class _StubWarehouseRepository implements WarehouseRepository {
|
||||||
|
_StubWarehouseRepository({required List<Warehouse> warehouses})
|
||||||
|
: _warehouses = warehouses;
|
||||||
|
|
||||||
|
final List<Warehouse> _warehouses;
|
||||||
|
|
||||||
|
@override
|
||||||
|
Future<PaginatedResult<Warehouse>> list({
|
||||||
|
int page = 1,
|
||||||
|
int pageSize = 20,
|
||||||
|
String? query,
|
||||||
|
bool? isActive,
|
||||||
|
bool includeZipcode = true,
|
||||||
|
}) async {
|
||||||
|
final filtered = _warehouses
|
||||||
|
.where((warehouse) {
|
||||||
|
if (query != null && query.isNotEmpty) {
|
||||||
|
final normalized = query.toLowerCase();
|
||||||
|
final matches =
|
||||||
|
warehouse.warehouseName.toLowerCase().contains(normalized) ||
|
||||||
|
warehouse.warehouseCode.toLowerCase().contains(normalized);
|
||||||
|
if (!matches) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (isActive != null && warehouse.isActive != isActive) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
})
|
||||||
|
.toList(growable: false);
|
||||||
|
|
||||||
|
return PaginatedResult<Warehouse>(
|
||||||
|
items: filtered,
|
||||||
|
page: 1,
|
||||||
|
pageSize: filtered.length,
|
||||||
|
total: filtered.length,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Future<Warehouse> create(WarehouseInput input) {
|
||||||
|
throw UnimplementedError();
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Future<void> delete(int id) {
|
||||||
|
throw UnimplementedError();
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Future<Warehouse> restore(int id) {
|
||||||
|
throw UnimplementedError();
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Future<Warehouse> update(int id, WarehouseInput input) {
|
||||||
|
throw UnimplementedError();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
List<StockTransaction> _buildTransactions() {
|
||||||
|
return [
|
||||||
|
_createInboundTransaction(),
|
||||||
|
_createInboundTransaction2(),
|
||||||
|
_createInboundTransaction3(),
|
||||||
|
_createOutboundTransaction1(),
|
||||||
|
_createOutboundTransaction2(),
|
||||||
|
_createOutboundTransaction3(),
|
||||||
|
_createRentalTransaction1(),
|
||||||
|
_createRentalTransaction2(),
|
||||||
|
_createRentalTransaction3(),
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
StockTransaction _createInboundTransaction() {
|
||||||
|
return StockTransaction(
|
||||||
|
id: 1,
|
||||||
|
transactionNo: 'TX-20240301-001',
|
||||||
|
transactionDate: DateTime(2024, 3, 1),
|
||||||
|
type: StockTransactionType(id: _inboundTypeId, name: '입고'),
|
||||||
|
status: StockTransactionStatus(id: _statusDraftId, name: '작성중'),
|
||||||
|
warehouse: StockTransactionWarehouse(id: 1, code: 'WH-001', name: '서울 1창고'),
|
||||||
|
createdBy: StockTransactionEmployee(
|
||||||
|
id: 1,
|
||||||
|
employeeNo: 'EMP-001',
|
||||||
|
name: '홍길동',
|
||||||
|
),
|
||||||
|
note: '-',
|
||||||
|
lines: [
|
||||||
|
StockTransactionLine(
|
||||||
|
id: 1,
|
||||||
|
lineNo: 1,
|
||||||
|
product: StockTransactionProduct(
|
||||||
|
id: 1,
|
||||||
|
code: 'P-100',
|
||||||
|
name: 'XR-5000',
|
||||||
|
vendor: StockTransactionVendorSummary(id: 1, name: '슈퍼벤더'),
|
||||||
|
uom: StockTransactionUomSummary(id: 1, name: 'EA'),
|
||||||
|
),
|
||||||
|
quantity: 40,
|
||||||
|
unitPrice: 120000,
|
||||||
|
note: '',
|
||||||
|
),
|
||||||
|
StockTransactionLine(
|
||||||
|
id: 2,
|
||||||
|
lineNo: 2,
|
||||||
|
product: StockTransactionProduct(
|
||||||
|
id: 2,
|
||||||
|
code: 'P-101',
|
||||||
|
name: 'XR-5001',
|
||||||
|
vendor: StockTransactionVendorSummary(id: 1, name: '슈퍼벤더'),
|
||||||
|
uom: StockTransactionUomSummary(id: 1, name: 'EA'),
|
||||||
|
),
|
||||||
|
quantity: 60,
|
||||||
|
unitPrice: 98000,
|
||||||
|
note: '',
|
||||||
|
),
|
||||||
|
],
|
||||||
|
customers: const [],
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
StockTransaction _createInboundTransaction2() {
|
||||||
|
return StockTransaction(
|
||||||
|
id: 2,
|
||||||
|
transactionNo: 'TX-20240305-010',
|
||||||
|
transactionDate: DateTime(2024, 3, 5),
|
||||||
|
type: StockTransactionType(id: _inboundTypeId, name: '입고'),
|
||||||
|
status: StockTransactionStatus(id: _statusPendingId, name: '승인대기'),
|
||||||
|
warehouse: StockTransactionWarehouse(id: 2, code: 'WH-002', name: '부산 센터'),
|
||||||
|
createdBy: StockTransactionEmployee(
|
||||||
|
id: 2,
|
||||||
|
employeeNo: 'EMP-010',
|
||||||
|
name: '김담당',
|
||||||
|
),
|
||||||
|
note: '긴급 입고',
|
||||||
|
lines: [
|
||||||
|
StockTransactionLine(
|
||||||
|
id: 3,
|
||||||
|
lineNo: 1,
|
||||||
|
product: StockTransactionProduct(
|
||||||
|
id: 3,
|
||||||
|
code: 'P-200',
|
||||||
|
name: 'Eco-200',
|
||||||
|
vendor: StockTransactionVendorSummary(id: 3, name: '그린텍'),
|
||||||
|
uom: StockTransactionUomSummary(id: 1, name: 'EA'),
|
||||||
|
),
|
||||||
|
quantity: 25,
|
||||||
|
unitPrice: 145000,
|
||||||
|
note: 'QC 필요',
|
||||||
|
),
|
||||||
|
StockTransactionLine(
|
||||||
|
id: 4,
|
||||||
|
lineNo: 2,
|
||||||
|
product: StockTransactionProduct(
|
||||||
|
id: 4,
|
||||||
|
code: 'P-201',
|
||||||
|
name: 'Eco-200B',
|
||||||
|
vendor: StockTransactionVendorSummary(id: 3, name: '그린텍'),
|
||||||
|
uom: StockTransactionUomSummary(id: 1, name: 'EA'),
|
||||||
|
),
|
||||||
|
quantity: 10,
|
||||||
|
unitPrice: 160000,
|
||||||
|
note: '',
|
||||||
|
),
|
||||||
|
],
|
||||||
|
customers: const [],
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
StockTransaction _createInboundTransaction3() {
|
||||||
|
return StockTransaction(
|
||||||
|
id: 3,
|
||||||
|
transactionNo: 'TX-20240310-004',
|
||||||
|
transactionDate: DateTime(2024, 3, 10),
|
||||||
|
type: StockTransactionType(id: _inboundTypeId, name: '입고'),
|
||||||
|
status: StockTransactionStatus(id: _statusCompleteId, name: '승인완료'),
|
||||||
|
warehouse: StockTransactionWarehouse(id: 3, code: 'WH-003', name: '대전 물류'),
|
||||||
|
createdBy: StockTransactionEmployee(
|
||||||
|
id: 3,
|
||||||
|
employeeNo: 'EMP-020',
|
||||||
|
name: '최검수',
|
||||||
|
),
|
||||||
|
note: '완료',
|
||||||
|
lines: [
|
||||||
|
StockTransactionLine(
|
||||||
|
id: 5,
|
||||||
|
lineNo: 1,
|
||||||
|
product: StockTransactionProduct(
|
||||||
|
id: 5,
|
||||||
|
code: 'P-300',
|
||||||
|
name: 'Delta-One',
|
||||||
|
vendor: StockTransactionVendorSummary(id: 4, name: '델타'),
|
||||||
|
uom: StockTransactionUomSummary(id: 2, name: 'SET'),
|
||||||
|
),
|
||||||
|
quantity: 8,
|
||||||
|
unitPrice: 450000,
|
||||||
|
note: '설치 일정 확인',
|
||||||
|
),
|
||||||
|
],
|
||||||
|
customers: const [],
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
StockTransaction _createOutboundTransaction1() {
|
||||||
|
return StockTransaction(
|
||||||
|
id: 10,
|
||||||
|
transactionNo: 'TX-20240302-010',
|
||||||
|
transactionDate: DateTime(2024, 3, 2),
|
||||||
|
type: StockTransactionType(id: _outboundTypeId, name: '출고'),
|
||||||
|
status: StockTransactionStatus(id: _statusOutboundWaitId, name: '출고대기'),
|
||||||
|
warehouse: StockTransactionWarehouse(id: 4, code: 'WH-001', name: '서울 1창고'),
|
||||||
|
createdBy: StockTransactionEmployee(
|
||||||
|
id: 4,
|
||||||
|
employeeNo: 'EMP-030',
|
||||||
|
name: '이영희',
|
||||||
|
),
|
||||||
|
note: '-',
|
||||||
|
lines: [
|
||||||
|
StockTransactionLine(
|
||||||
|
id: 6,
|
||||||
|
lineNo: 1,
|
||||||
|
product: StockTransactionProduct(
|
||||||
|
id: 1,
|
||||||
|
code: 'P-100',
|
||||||
|
name: 'XR-5000',
|
||||||
|
vendor: StockTransactionVendorSummary(id: 1, name: '슈퍼벤더'),
|
||||||
|
uom: StockTransactionUomSummary(id: 1, name: 'EA'),
|
||||||
|
),
|
||||||
|
quantity: 30,
|
||||||
|
unitPrice: 130000,
|
||||||
|
note: '긴급 출고',
|
||||||
|
),
|
||||||
|
StockTransactionLine(
|
||||||
|
id: 7,
|
||||||
|
lineNo: 2,
|
||||||
|
product: StockTransactionProduct(
|
||||||
|
id: 2,
|
||||||
|
code: 'P-101',
|
||||||
|
name: 'XR-5001',
|
||||||
|
vendor: StockTransactionVendorSummary(id: 1, name: '슈퍼벤더'),
|
||||||
|
uom: StockTransactionUomSummary(id: 1, name: 'EA'),
|
||||||
|
),
|
||||||
|
quantity: 20,
|
||||||
|
unitPrice: 118000,
|
||||||
|
note: '',
|
||||||
|
),
|
||||||
|
],
|
||||||
|
customers: [
|
||||||
|
StockTransactionCustomer(
|
||||||
|
id: 1,
|
||||||
|
customer: StockTransactionCustomerSummary(
|
||||||
|
id: 1,
|
||||||
|
code: 'C-1001',
|
||||||
|
name: '슈퍼포트 파트너',
|
||||||
|
),
|
||||||
|
note: '',
|
||||||
|
),
|
||||||
|
StockTransactionCustomer(
|
||||||
|
id: 2,
|
||||||
|
customer: StockTransactionCustomerSummary(
|
||||||
|
id: 2,
|
||||||
|
code: 'C-1002',
|
||||||
|
name: '그린에너지',
|
||||||
|
),
|
||||||
|
note: '',
|
||||||
|
),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
StockTransaction _createOutboundTransaction2() {
|
||||||
|
return StockTransaction(
|
||||||
|
id: 11,
|
||||||
|
transactionNo: 'TX-20240304-005',
|
||||||
|
transactionDate: DateTime(2024, 3, 4),
|
||||||
|
type: StockTransactionType(id: _outboundTypeId, name: '출고'),
|
||||||
|
status: StockTransactionStatus(id: _statusOutboundDoneId, name: '출고완료'),
|
||||||
|
warehouse: StockTransactionWarehouse(id: 5, code: 'WH-002', name: '부산 센터'),
|
||||||
|
createdBy: StockTransactionEmployee(
|
||||||
|
id: 5,
|
||||||
|
employeeNo: 'EMP-040',
|
||||||
|
name: '강물류',
|
||||||
|
),
|
||||||
|
note: '완납',
|
||||||
|
lines: [
|
||||||
|
StockTransactionLine(
|
||||||
|
id: 8,
|
||||||
|
lineNo: 1,
|
||||||
|
product: StockTransactionProduct(
|
||||||
|
id: 3,
|
||||||
|
code: 'P-200',
|
||||||
|
name: 'Eco-200',
|
||||||
|
vendor: StockTransactionVendorSummary(id: 3, name: '그린텍'),
|
||||||
|
uom: StockTransactionUomSummary(id: 1, name: 'EA'),
|
||||||
|
),
|
||||||
|
quantity: 15,
|
||||||
|
unitPrice: 150000,
|
||||||
|
note: '',
|
||||||
|
),
|
||||||
|
],
|
||||||
|
customers: [
|
||||||
|
StockTransactionCustomer(
|
||||||
|
id: 3,
|
||||||
|
customer: StockTransactionCustomerSummary(
|
||||||
|
id: 3,
|
||||||
|
code: 'C-1010',
|
||||||
|
name: '테크솔루션',
|
||||||
|
),
|
||||||
|
note: '',
|
||||||
|
),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
StockTransaction _createOutboundTransaction3() {
|
||||||
|
return StockTransaction(
|
||||||
|
id: 12,
|
||||||
|
transactionNo: 'TX-20240309-012',
|
||||||
|
transactionDate: DateTime(2024, 3, 9),
|
||||||
|
type: StockTransactionType(id: _outboundTypeId, name: '출고'),
|
||||||
|
status: StockTransactionStatus(id: _statusDraftId, name: '작성중'),
|
||||||
|
warehouse: StockTransactionWarehouse(id: 6, code: 'WH-003', name: '대전 물류'),
|
||||||
|
createdBy: StockTransactionEmployee(
|
||||||
|
id: 6,
|
||||||
|
employeeNo: 'EMP-050',
|
||||||
|
name: '최준비',
|
||||||
|
),
|
||||||
|
note: '운송배차 예정',
|
||||||
|
lines: [
|
||||||
|
StockTransactionLine(
|
||||||
|
id: 9,
|
||||||
|
lineNo: 1,
|
||||||
|
product: StockTransactionProduct(
|
||||||
|
id: 5,
|
||||||
|
code: 'P-300',
|
||||||
|
name: 'Delta-One',
|
||||||
|
vendor: StockTransactionVendorSummary(id: 4, name: '델타'),
|
||||||
|
uom: StockTransactionUomSummary(id: 2, name: 'SET'),
|
||||||
|
),
|
||||||
|
quantity: 6,
|
||||||
|
unitPrice: 460000,
|
||||||
|
note: '시연용',
|
||||||
|
),
|
||||||
|
],
|
||||||
|
customers: [
|
||||||
|
StockTransactionCustomer(
|
||||||
|
id: 4,
|
||||||
|
customer: StockTransactionCustomerSummary(
|
||||||
|
id: 4,
|
||||||
|
code: 'C-1012',
|
||||||
|
name: '에이치솔루션',
|
||||||
|
),
|
||||||
|
note: '',
|
||||||
|
),
|
||||||
|
StockTransactionCustomer(
|
||||||
|
id: 5,
|
||||||
|
customer: StockTransactionCustomerSummary(
|
||||||
|
id: 5,
|
||||||
|
code: 'C-1013',
|
||||||
|
name: '블루하이드',
|
||||||
|
),
|
||||||
|
note: '',
|
||||||
|
),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
StockTransaction _createRentalTransaction1() {
|
||||||
|
return StockTransaction(
|
||||||
|
id: 20,
|
||||||
|
transactionNo: 'TX-20240305-030',
|
||||||
|
transactionDate: DateTime(2024, 3, 5),
|
||||||
|
type: StockTransactionType(id: _rentalRentTypeId, name: '대여'),
|
||||||
|
status: StockTransactionStatus(id: _statusRentalRentingId, name: '대여중'),
|
||||||
|
warehouse: StockTransactionWarehouse(id: 1, code: 'WH-001', name: '서울 1창고'),
|
||||||
|
createdBy: StockTransactionEmployee(
|
||||||
|
id: 7,
|
||||||
|
employeeNo: 'EMP-060',
|
||||||
|
name: '박대여',
|
||||||
|
),
|
||||||
|
note: '장기 대여',
|
||||||
|
expectedReturnDate: DateTime(2024, 3, 12),
|
||||||
|
lines: [
|
||||||
|
StockTransactionLine(
|
||||||
|
id: 10,
|
||||||
|
lineNo: 1,
|
||||||
|
product: StockTransactionProduct(
|
||||||
|
id: 1,
|
||||||
|
code: 'P-100',
|
||||||
|
name: 'XR-5000',
|
||||||
|
vendor: StockTransactionVendorSummary(id: 1, name: '슈퍼벤더'),
|
||||||
|
uom: StockTransactionUomSummary(id: 1, name: 'EA'),
|
||||||
|
),
|
||||||
|
quantity: 10,
|
||||||
|
unitPrice: 120000,
|
||||||
|
note: '검수 예정',
|
||||||
|
),
|
||||||
|
StockTransactionLine(
|
||||||
|
id: 11,
|
||||||
|
lineNo: 2,
|
||||||
|
product: StockTransactionProduct(
|
||||||
|
id: 6,
|
||||||
|
code: 'P-102',
|
||||||
|
name: 'XR-5002',
|
||||||
|
vendor: StockTransactionVendorSummary(id: 1, name: '슈퍼벤더'),
|
||||||
|
uom: StockTransactionUomSummary(id: 1, name: 'EA'),
|
||||||
|
),
|
||||||
|
quantity: 5,
|
||||||
|
unitPrice: 110000,
|
||||||
|
note: '',
|
||||||
|
),
|
||||||
|
],
|
||||||
|
customers: [
|
||||||
|
StockTransactionCustomer(
|
||||||
|
id: 6,
|
||||||
|
customer: StockTransactionCustomerSummary(
|
||||||
|
id: 1,
|
||||||
|
code: 'C-1001',
|
||||||
|
name: '슈퍼포트 파트너',
|
||||||
|
),
|
||||||
|
note: '',
|
||||||
|
),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
StockTransaction _createRentalTransaction2() {
|
||||||
|
return StockTransaction(
|
||||||
|
id: 21,
|
||||||
|
transactionNo: 'TX-20240308-014',
|
||||||
|
transactionDate: DateTime(2024, 3, 8),
|
||||||
|
type: StockTransactionType(id: _rentalRentTypeId, name: '대여'),
|
||||||
|
status: StockTransactionStatus(id: _statusRentalReturnWaitId, name: '반납대기'),
|
||||||
|
warehouse: StockTransactionWarehouse(id: 2, code: 'WH-002', name: '부산 센터'),
|
||||||
|
createdBy: StockTransactionEmployee(
|
||||||
|
id: 8,
|
||||||
|
employeeNo: 'EMP-070',
|
||||||
|
name: '이반납',
|
||||||
|
),
|
||||||
|
note: '-',
|
||||||
|
expectedReturnDate: DateTime(2024, 3, 15),
|
||||||
|
lines: [
|
||||||
|
StockTransactionLine(
|
||||||
|
id: 12,
|
||||||
|
lineNo: 1,
|
||||||
|
product: StockTransactionProduct(
|
||||||
|
id: 3,
|
||||||
|
code: 'P-200',
|
||||||
|
name: 'Eco-200',
|
||||||
|
vendor: StockTransactionVendorSummary(id: 3, name: '그린텍'),
|
||||||
|
uom: StockTransactionUomSummary(id: 1, name: 'EA'),
|
||||||
|
),
|
||||||
|
quantity: 8,
|
||||||
|
unitPrice: 145000,
|
||||||
|
note: '',
|
||||||
|
),
|
||||||
|
],
|
||||||
|
customers: [
|
||||||
|
StockTransactionCustomer(
|
||||||
|
id: 7,
|
||||||
|
customer: StockTransactionCustomerSummary(
|
||||||
|
id: 2,
|
||||||
|
code: 'C-1002',
|
||||||
|
name: '그린에너지',
|
||||||
|
),
|
||||||
|
note: '',
|
||||||
|
),
|
||||||
|
StockTransactionCustomer(
|
||||||
|
id: 8,
|
||||||
|
customer: StockTransactionCustomerSummary(
|
||||||
|
id: 3,
|
||||||
|
code: 'C-1010',
|
||||||
|
name: '테크솔루션',
|
||||||
|
),
|
||||||
|
note: '',
|
||||||
|
),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
StockTransaction _createRentalTransaction3() {
|
||||||
|
return StockTransaction(
|
||||||
|
id: 22,
|
||||||
|
transactionNo: 'TX-20240312-021',
|
||||||
|
transactionDate: DateTime(2024, 3, 12),
|
||||||
|
type: StockTransactionType(id: _rentalReturnTypeId, name: '반납'),
|
||||||
|
status: StockTransactionStatus(id: _statusRentalFinishedId, name: '완료'),
|
||||||
|
warehouse: StockTransactionWarehouse(id: 3, code: 'WH-003', name: '대전 물류'),
|
||||||
|
createdBy: StockTransactionEmployee(
|
||||||
|
id: 9,
|
||||||
|
employeeNo: 'EMP-080',
|
||||||
|
name: '최관리',
|
||||||
|
),
|
||||||
|
note: '정상 반납',
|
||||||
|
expectedReturnDate: DateTime(2024, 3, 12),
|
||||||
|
lines: [
|
||||||
|
StockTransactionLine(
|
||||||
|
id: 13,
|
||||||
|
lineNo: 1,
|
||||||
|
product: StockTransactionProduct(
|
||||||
|
id: 5,
|
||||||
|
code: 'P-300',
|
||||||
|
name: 'Delta-One',
|
||||||
|
vendor: StockTransactionVendorSummary(id: 4, name: '델타'),
|
||||||
|
uom: StockTransactionUomSummary(id: 2, name: 'SET'),
|
||||||
|
),
|
||||||
|
quantity: 4,
|
||||||
|
unitPrice: 480000,
|
||||||
|
note: '',
|
||||||
|
),
|
||||||
|
],
|
||||||
|
customers: [
|
||||||
|
StockTransactionCustomer(
|
||||||
|
id: 9,
|
||||||
|
customer: StockTransactionCustomerSummary(
|
||||||
|
id: 4,
|
||||||
|
code: 'C-1012',
|
||||||
|
name: '에이치솔루션',
|
||||||
|
),
|
||||||
|
note: '',
|
||||||
|
),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -11,7 +11,7 @@ Widget buildTestApp(Widget child, {PermissionManager? permissionManager}) {
|
|||||||
debugShowCheckedModeBanner: false,
|
debugShowCheckedModeBanner: false,
|
||||||
theme: SuperportShadTheme.light(),
|
theme: SuperportShadTheme.light(),
|
||||||
darkTheme: SuperportShadTheme.dark(),
|
darkTheme: SuperportShadTheme.dark(),
|
||||||
home: Scaffold(body: child),
|
home: ScaffoldMessenger(child: Scaffold(body: child)),
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user