fix(inventory): 상세 편집 플로우 안정화
- inbound/outbound/rental controller에 fetchTransactionDetail을 추가해 상세 동기화를 지원 - 각 페이지 초기화 시 결재 초안 로딩 권한을 PermissionScope에서 확인하도록 수정 - 상세 패널의 수정 버튼이 모달과 연동되도록 흐름을 정리하고 생성/수정 후 상세 데이터를 재조회 - 기존 결재 메모 필드는 등록 이후 수정 불가하도록 UI와 입력 상태를 비활성화 - 신규 상세-수정 위젯 테스트와 리포지토리 스텁 fetchDetail 구현을 추가 - flutter analyze, flutter test를 실행해 회귀를 점검
This commit is contained in:
@@ -253,6 +253,24 @@ class InboundController extends ChangeNotifier {
|
|||||||
await fetchTransactions(filter: target);
|
await fetchTransactions(filter: target);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// 단일 입고 트랜잭션 상세 정보를 조회한다.
|
||||||
|
Future<InboundRecord?> fetchTransactionDetail(
|
||||||
|
int id, {
|
||||||
|
List<String> include = const ['lines', 'customers', 'approval'],
|
||||||
|
}) async {
|
||||||
|
try {
|
||||||
|
final transaction = await _transactionRepository.fetchDetail(
|
||||||
|
id,
|
||||||
|
include: include,
|
||||||
|
);
|
||||||
|
return InboundRecord.fromTransaction(transaction);
|
||||||
|
} catch (error, stackTrace) {
|
||||||
|
debugPrint('[InboundController] 상세 조회 실패(id=$id): $error');
|
||||||
|
debugPrintStack(stackTrace: stackTrace);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
void _persistApprovalDraft(StockTransactionApprovalInput approval) {
|
void _persistApprovalDraft(StockTransactionApprovalInput approval) {
|
||||||
final useCase = _saveDraftUseCase;
|
final useCase = _saveDraftUseCase;
|
||||||
if (useCase == null) {
|
if (useCase == null) {
|
||||||
|
|||||||
@@ -158,7 +158,7 @@ class _InboundPageState extends State<InboundPage> {
|
|||||||
await controller.loadStatusOptions();
|
await controller.loadStatusOptions();
|
||||||
await controller.loadApprovalStatuses();
|
await controller.loadApprovalStatuses();
|
||||||
final requester = _resolveCurrentWriter();
|
final requester = _resolveCurrentWriter();
|
||||||
if (requester != null) {
|
if (requester != null && _canRestoreApprovalDrafts) {
|
||||||
await controller.loadApprovalDraftFromServer(requesterId: requester.id);
|
await controller.loadApprovalDraftFromServer(requesterId: requester.id);
|
||||||
}
|
}
|
||||||
final hasType = await controller.resolveTransactionType();
|
final hasType = await controller.resolveTransactionType();
|
||||||
@@ -171,6 +171,17 @@ class _InboundPageState extends State<InboundPage> {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
bool get _canRestoreApprovalDrafts {
|
||||||
|
final getIt = GetIt.I;
|
||||||
|
if (!getIt.isRegistered<PermissionManager>()) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
return getIt<PermissionManager>().can(
|
||||||
|
PermissionResources.approvals,
|
||||||
|
PermissionAction.view,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
void _handleControllerChanged() {
|
void _handleControllerChanged() {
|
||||||
if (!mounted) {
|
if (!mounted) {
|
||||||
return;
|
return;
|
||||||
@@ -909,7 +920,7 @@ class _InboundPageState extends State<InboundPage> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
List<Widget> _buildDetailActions(InboundRecord record) {
|
List<Widget> _buildDetailActions(InboundRecord record) {
|
||||||
final isProcessing = _isProcessing(record.id) || _isLoading;
|
final isProcessing = _isProcessing(record.id);
|
||||||
final actions = <Widget>[];
|
final actions = <Widget>[];
|
||||||
|
|
||||||
if (_canSubmit(record)) {
|
if (_canSubmit(record)) {
|
||||||
@@ -1729,6 +1740,21 @@ class _InboundPageState extends State<InboundPage> {
|
|||||||
SuperportToast.error(context, '입고 컨트롤러를 찾을 수 없습니다.');
|
SuperportToast.error(context, '입고 컨트롤러를 찾을 수 없습니다.');
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
Future<InboundRecord?> resolveUpdatedRecord(int? id) async {
|
||||||
|
if (id == null) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
final detail = await controller.fetchTransactionDetail(id);
|
||||||
|
if (detail != null) {
|
||||||
|
return detail;
|
||||||
|
}
|
||||||
|
for (final record in controller.records) {
|
||||||
|
if (record.id == id) {
|
||||||
|
return record;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
final transactionTypeLookup =
|
final transactionTypeLookup =
|
||||||
_transactionTypeLookup ?? controller.transactionType;
|
_transactionTypeLookup ?? controller.transactionType;
|
||||||
@@ -1814,7 +1840,6 @@ class _InboundPageState extends State<InboundPage> {
|
|||||||
),
|
),
|
||||||
refreshAfter: false,
|
refreshAfter: false,
|
||||||
);
|
);
|
||||||
result = updated;
|
|
||||||
final currentLines =
|
final currentLines =
|
||||||
initialRecord.raw?.lines ?? const <StockTransactionLine>[];
|
initialRecord.raw?.lines ?? const <StockTransactionLine>[];
|
||||||
final currentCustomers = initialRecord.customers;
|
final currentCustomers = initialRecord.customers;
|
||||||
@@ -1850,6 +1875,8 @@ class _InboundPageState extends State<InboundPage> {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
await controller.refresh();
|
await controller.refresh();
|
||||||
|
final refreshed = await resolveUpdatedRecord(transactionId);
|
||||||
|
result = refreshed ?? updated;
|
||||||
updateSaving(false);
|
updateSaving(false);
|
||||||
if (!mounted) {
|
if (!mounted) {
|
||||||
return;
|
return;
|
||||||
@@ -1932,7 +1959,8 @@ class _InboundPageState extends State<InboundPage> {
|
|||||||
return true;
|
return true;
|
||||||
}());
|
}());
|
||||||
final created = await controller.createTransaction(createInput);
|
final created = await controller.createTransaction(createInput);
|
||||||
result = created;
|
final refreshed = await resolveUpdatedRecord(created.id);
|
||||||
|
result = refreshed ?? created;
|
||||||
updateSaving(false);
|
updateSaving(false);
|
||||||
if (!mounted) {
|
if (!mounted) {
|
||||||
return;
|
return;
|
||||||
@@ -2157,9 +2185,14 @@ class _InboundPageState extends State<InboundPage> {
|
|||||||
width: 500,
|
width: 500,
|
||||||
child: SuperportFormField(
|
child: SuperportFormField(
|
||||||
label: '결재 메모',
|
label: '결재 메모',
|
||||||
|
caption: initial != null
|
||||||
|
? '등록된 결재 메모는 수정할 수 없습니다.'
|
||||||
|
: null,
|
||||||
child: ShadInput(
|
child: ShadInput(
|
||||||
controller: approvalNoteController,
|
controller: approvalNoteController,
|
||||||
maxLines: 2,
|
maxLines: 2,
|
||||||
|
readOnly: initial != null,
|
||||||
|
enabled: initial == null,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
|||||||
@@ -233,6 +233,24 @@ class OutboundController extends ChangeNotifier {
|
|||||||
await fetchTransactions(filter: target);
|
await fetchTransactions(filter: target);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// 단일 출고 트랜잭션 상세를 조회한다.
|
||||||
|
Future<OutboundRecord?> fetchTransactionDetail(
|
||||||
|
int id, {
|
||||||
|
List<String> include = const ['lines', 'customers', 'approval'],
|
||||||
|
}) async {
|
||||||
|
try {
|
||||||
|
final transaction = await _transactionRepository.fetchDetail(
|
||||||
|
id,
|
||||||
|
include: include,
|
||||||
|
);
|
||||||
|
return OutboundRecord.fromTransaction(transaction);
|
||||||
|
} catch (error, stackTrace) {
|
||||||
|
debugPrint('[OutboundController] 상세 조회 실패(id=$id): $error');
|
||||||
|
debugPrintStack(stackTrace: stackTrace);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
void _persistApprovalDraft(StockTransactionApprovalInput approval) {
|
void _persistApprovalDraft(StockTransactionApprovalInput approval) {
|
||||||
final useCase = _saveDraftUseCase;
|
final useCase = _saveDraftUseCase;
|
||||||
if (useCase == null) {
|
if (useCase == null) {
|
||||||
|
|||||||
@@ -165,7 +165,7 @@ class _OutboundPageState extends State<OutboundPage> {
|
|||||||
Future.microtask(() async {
|
Future.microtask(() async {
|
||||||
await controller.loadStatusOptions();
|
await controller.loadStatusOptions();
|
||||||
final requester = _resolveCurrentWriter();
|
final requester = _resolveCurrentWriter();
|
||||||
if (requester != null) {
|
if (requester != null && _canRestoreApprovalDrafts) {
|
||||||
await controller.loadApprovalDraftFromServer(requesterId: requester.id);
|
await controller.loadApprovalDraftFromServer(requesterId: requester.id);
|
||||||
}
|
}
|
||||||
final hasType = await controller.resolveTransactionType();
|
final hasType = await controller.resolveTransactionType();
|
||||||
@@ -178,6 +178,17 @@ class _OutboundPageState extends State<OutboundPage> {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
bool get _canRestoreApprovalDrafts {
|
||||||
|
final getIt = GetIt.I;
|
||||||
|
if (!getIt.isRegistered<PermissionManager>()) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
return getIt<PermissionManager>().can(
|
||||||
|
PermissionResources.approvals,
|
||||||
|
PermissionAction.view,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
Future<void> _loadCustomerOptions() async {
|
Future<void> _loadCustomerOptions() async {
|
||||||
final getIt = GetIt.I;
|
final getIt = GetIt.I;
|
||||||
if (!getIt.isRegistered<CustomerRepository>()) {
|
if (!getIt.isRegistered<CustomerRepository>()) {
|
||||||
@@ -982,7 +993,7 @@ class _OutboundPageState extends State<OutboundPage> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
List<Widget> _buildDetailActions(OutboundRecord record) {
|
List<Widget> _buildDetailActions(OutboundRecord record) {
|
||||||
final isProcessing = _isProcessing(record.id) || _isLoading;
|
final isProcessing = _isProcessing(record.id);
|
||||||
final actions = <Widget>[];
|
final actions = <Widget>[];
|
||||||
|
|
||||||
if (_canSubmit(record)) {
|
if (_canSubmit(record)) {
|
||||||
@@ -1028,12 +1039,7 @@ class _OutboundPageState extends State<OutboundPage> {
|
|||||||
actions.add(
|
actions.add(
|
||||||
ShadButton.outline(
|
ShadButton.outline(
|
||||||
leading: const Icon(lucide.LucideIcons.pencil, size: 16),
|
leading: const Icon(lucide.LucideIcons.pencil, size: 16),
|
||||||
onPressed: isProcessing
|
onPressed: isProcessing ? null : () => _openEditFromDetail(record),
|
||||||
? null
|
|
||||||
: () {
|
|
||||||
Navigator.of(context).maybePop();
|
|
||||||
_handleEdit(record);
|
|
||||||
},
|
|
||||||
child: const Text('수정'),
|
child: const Text('수정'),
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
@@ -1041,6 +1047,17 @@ class _OutboundPageState extends State<OutboundPage> {
|
|||||||
return actions;
|
return actions;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void _openEditFromDetail(OutboundRecord record) {
|
||||||
|
final navigator = Navigator.of(context, rootNavigator: true);
|
||||||
|
navigator.pop();
|
||||||
|
WidgetsBinding.instance.addPostFrameCallback((_) async {
|
||||||
|
if (!mounted) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
await _handleEdit(record, reopenOnCancel: true);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
Future<void> _handleCreate() async {
|
Future<void> _handleCreate() async {
|
||||||
final record = await _showOutboundFormDialog();
|
final record = await _showOutboundFormDialog();
|
||||||
if (record != null) {
|
if (record != null) {
|
||||||
@@ -1048,10 +1065,15 @@ class _OutboundPageState extends State<OutboundPage> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<void> _handleEdit(OutboundRecord record) async {
|
Future<void> _handleEdit(
|
||||||
|
OutboundRecord record, {
|
||||||
|
bool reopenOnCancel = false,
|
||||||
|
}) async {
|
||||||
final updated = await _showOutboundFormDialog(initial: record);
|
final updated = await _showOutboundFormDialog(initial: record);
|
||||||
if (updated != null) {
|
if (updated != null) {
|
||||||
_selectRecord(updated, openDetail: true);
|
_selectRecord(updated, openDetail: true);
|
||||||
|
} else if (reopenOnCancel) {
|
||||||
|
_selectRecord(record, openDetail: true);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1821,6 +1843,21 @@ class _OutboundPageState extends State<OutboundPage> {
|
|||||||
SuperportToast.error(context, '출고 컨트롤러를 찾을 수 없습니다.');
|
SuperportToast.error(context, '출고 컨트롤러를 찾을 수 없습니다.');
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
Future<OutboundRecord?> resolveUpdatedRecord(int? id) async {
|
||||||
|
if (id == null) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
final detail = await controller.fetchTransactionDetail(id);
|
||||||
|
if (detail != null) {
|
||||||
|
return detail;
|
||||||
|
}
|
||||||
|
for (final record in controller.records) {
|
||||||
|
if (record.id == id) {
|
||||||
|
return record;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
final transactionTypeLookup =
|
final transactionTypeLookup =
|
||||||
_transactionTypeLookup ?? controller.transactionType;
|
_transactionTypeLookup ?? controller.transactionType;
|
||||||
@@ -1912,7 +1949,6 @@ class _OutboundPageState extends State<OutboundPage> {
|
|||||||
),
|
),
|
||||||
refreshAfter: false,
|
refreshAfter: false,
|
||||||
);
|
);
|
||||||
result = updated;
|
|
||||||
final StockTransaction? currentTransaction = initialRecord.raw;
|
final StockTransaction? currentTransaction = initialRecord.raw;
|
||||||
final currentLines =
|
final currentLines =
|
||||||
currentTransaction?.lines ?? const <StockTransactionLine>[];
|
currentTransaction?.lines ?? const <StockTransactionLine>[];
|
||||||
@@ -1937,6 +1973,8 @@ class _OutboundPageState extends State<OutboundPage> {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
await controller.refresh();
|
await controller.refresh();
|
||||||
|
final refreshed = await resolveUpdatedRecord(transactionId);
|
||||||
|
result = refreshed ?? updated;
|
||||||
updateSaving(false);
|
updateSaving(false);
|
||||||
if (!mounted) {
|
if (!mounted) {
|
||||||
return;
|
return;
|
||||||
@@ -2006,7 +2044,8 @@ class _OutboundPageState extends State<OutboundPage> {
|
|||||||
approval: approvalInput,
|
approval: approvalInput,
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
result = created;
|
final refreshed = await resolveUpdatedRecord(created.id);
|
||||||
|
result = refreshed ?? created;
|
||||||
updateSaving(false);
|
updateSaving(false);
|
||||||
if (!mounted) {
|
if (!mounted) {
|
||||||
return;
|
return;
|
||||||
@@ -2227,9 +2266,14 @@ class _OutboundPageState extends State<OutboundPage> {
|
|||||||
width: 500,
|
width: 500,
|
||||||
child: SuperportFormField(
|
child: SuperportFormField(
|
||||||
label: '결재 메모',
|
label: '결재 메모',
|
||||||
|
caption: initial != null
|
||||||
|
? '등록된 결재 메모는 수정할 수 없습니다.'
|
||||||
|
: null,
|
||||||
child: ShadInput(
|
child: ShadInput(
|
||||||
controller: approvalNoteController,
|
controller: approvalNoteController,
|
||||||
maxLines: 2,
|
maxLines: 2,
|
||||||
|
readOnly: initial != null,
|
||||||
|
enabled: initial == null,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
|||||||
@@ -262,6 +262,24 @@ class RentalController extends ChangeNotifier {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// 단일 대여/반납 트랜잭션 상세를 조회한다.
|
||||||
|
Future<RentalRecord?> fetchTransactionDetail(
|
||||||
|
int id, {
|
||||||
|
List<String> include = const ['lines', 'customers', 'approval'],
|
||||||
|
}) async {
|
||||||
|
try {
|
||||||
|
final transaction = await _transactionRepository.fetchDetail(
|
||||||
|
id,
|
||||||
|
include: include,
|
||||||
|
);
|
||||||
|
return RentalRecord.fromTransaction(transaction);
|
||||||
|
} catch (error, stackTrace) {
|
||||||
|
debugPrint('[RentalController] 상세 조회 실패(id=$id): $error');
|
||||||
|
debugPrintStack(stackTrace: stackTrace);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
void _persistApprovalDraft(StockTransactionApprovalInput approval) {
|
void _persistApprovalDraft(StockTransactionApprovalInput approval) {
|
||||||
final useCase = _saveDraftUseCase;
|
final useCase = _saveDraftUseCase;
|
||||||
if (useCase == null) {
|
if (useCase == null) {
|
||||||
|
|||||||
@@ -162,7 +162,7 @@ class _RentalPageState extends State<RentalPage> {
|
|||||||
Future.microtask(() async {
|
Future.microtask(() async {
|
||||||
await controller.loadStatusOptions();
|
await controller.loadStatusOptions();
|
||||||
final requester = _resolveCurrentWriter();
|
final requester = _resolveCurrentWriter();
|
||||||
if (requester != null) {
|
if (requester != null && _canRestoreApprovalDrafts) {
|
||||||
await controller.loadApprovalDraftFromServer(requesterId: requester.id);
|
await controller.loadApprovalDraftFromServer(requesterId: requester.id);
|
||||||
}
|
}
|
||||||
final hasTypes = await controller.resolveTransactionTypes();
|
final hasTypes = await controller.resolveTransactionTypes();
|
||||||
@@ -175,6 +175,17 @@ class _RentalPageState extends State<RentalPage> {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
bool get _canRestoreApprovalDrafts {
|
||||||
|
final getIt = GetIt.I;
|
||||||
|
if (!getIt.isRegistered<PermissionManager>()) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
return getIt<PermissionManager>().can(
|
||||||
|
PermissionResources.approvals,
|
||||||
|
PermissionAction.view,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
void _handleControllerChanged() {
|
void _handleControllerChanged() {
|
||||||
if (!mounted) {
|
if (!mounted) {
|
||||||
return;
|
return;
|
||||||
@@ -948,7 +959,7 @@ class _RentalPageState extends State<RentalPage> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
List<Widget> _buildDetailActions(RentalRecord record) {
|
List<Widget> _buildDetailActions(RentalRecord record) {
|
||||||
final isProcessing = _isProcessing(record.id) || _isLoading;
|
final isProcessing = _isProcessing(record.id);
|
||||||
final actions = <Widget>[];
|
final actions = <Widget>[];
|
||||||
|
|
||||||
if (_canSubmit(record)) {
|
if (_canSubmit(record)) {
|
||||||
@@ -994,12 +1005,7 @@ class _RentalPageState extends State<RentalPage> {
|
|||||||
actions.add(
|
actions.add(
|
||||||
ShadButton.outline(
|
ShadButton.outline(
|
||||||
leading: const Icon(lucide.LucideIcons.pencil, size: 16),
|
leading: const Icon(lucide.LucideIcons.pencil, size: 16),
|
||||||
onPressed: isProcessing
|
onPressed: isProcessing ? null : () => _openEditFromDetail(record),
|
||||||
? null
|
|
||||||
: () {
|
|
||||||
Navigator.of(context).maybePop();
|
|
||||||
_handleEdit(record);
|
|
||||||
},
|
|
||||||
child: const Text('수정'),
|
child: const Text('수정'),
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
@@ -1007,6 +1013,17 @@ class _RentalPageState extends State<RentalPage> {
|
|||||||
return actions;
|
return actions;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void _openEditFromDetail(RentalRecord record) {
|
||||||
|
final navigator = Navigator.of(context, rootNavigator: true);
|
||||||
|
navigator.pop();
|
||||||
|
WidgetsBinding.instance.addPostFrameCallback((_) async {
|
||||||
|
if (!mounted) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
await _handleEdit(record, reopenOnCancel: true);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
Future<void> _handleCreate() async {
|
Future<void> _handleCreate() async {
|
||||||
final record = await _showRentalFormDialog();
|
final record = await _showRentalFormDialog();
|
||||||
if (record != null) {
|
if (record != null) {
|
||||||
@@ -1014,10 +1031,15 @@ class _RentalPageState extends State<RentalPage> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<void> _handleEdit(RentalRecord record) async {
|
Future<void> _handleEdit(
|
||||||
|
RentalRecord record, {
|
||||||
|
bool reopenOnCancel = false,
|
||||||
|
}) async {
|
||||||
final updated = await _showRentalFormDialog(initial: record);
|
final updated = await _showRentalFormDialog(initial: record);
|
||||||
if (updated != null) {
|
if (updated != null) {
|
||||||
_selectRecord(updated, openDetail: true);
|
_selectRecord(updated, openDetail: true);
|
||||||
|
} else if (reopenOnCancel) {
|
||||||
|
_selectRecord(record, openDetail: true);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1805,6 +1827,22 @@ class _RentalPageState extends State<RentalPage> {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Future<RentalRecord?> resolveUpdatedRecord(int? id) async {
|
||||||
|
if (id == null) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
final detail = await controller.fetchTransactionDetail(id);
|
||||||
|
if (detail != null) {
|
||||||
|
return detail;
|
||||||
|
}
|
||||||
|
for (final record in controller.records) {
|
||||||
|
if (record.id == id) {
|
||||||
|
return record;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
final selectedLookup =
|
final selectedLookup =
|
||||||
rentalTypeValue.value == RentalTableSpec.rentalTypes.last
|
rentalTypeValue.value == RentalTableSpec.rentalTypes.last
|
||||||
? (_returnTransactionType ?? controller.returnTransactionType)
|
? (_returnTransactionType ?? controller.returnTransactionType)
|
||||||
@@ -1898,7 +1936,6 @@ class _RentalPageState extends State<RentalPage> {
|
|||||||
),
|
),
|
||||||
refreshAfter: false,
|
refreshAfter: false,
|
||||||
);
|
);
|
||||||
result = updated;
|
|
||||||
final currentLines =
|
final currentLines =
|
||||||
initialRecord.raw?.lines ?? const <StockTransactionLine>[];
|
initialRecord.raw?.lines ?? const <StockTransactionLine>[];
|
||||||
final currentCustomers =
|
final currentCustomers =
|
||||||
@@ -1922,6 +1959,8 @@ class _RentalPageState extends State<RentalPage> {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
await controller.refresh();
|
await controller.refresh();
|
||||||
|
final refreshed = await resolveUpdatedRecord(transactionId);
|
||||||
|
result = refreshed ?? updated;
|
||||||
updateSaving(false);
|
updateSaving(false);
|
||||||
if (!mounted) {
|
if (!mounted) {
|
||||||
return;
|
return;
|
||||||
@@ -1993,7 +2032,8 @@ class _RentalPageState extends State<RentalPage> {
|
|||||||
approval: approvalInput,
|
approval: approvalInput,
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
result = created;
|
final refreshed = await resolveUpdatedRecord(created.id);
|
||||||
|
result = refreshed ?? created;
|
||||||
updateSaving(false);
|
updateSaving(false);
|
||||||
if (!mounted) {
|
if (!mounted) {
|
||||||
return;
|
return;
|
||||||
@@ -2329,9 +2369,26 @@ class _RentalPageState extends State<RentalPage> {
|
|||||||
width: 500,
|
width: 500,
|
||||||
child: _FormFieldLabel(
|
child: _FormFieldLabel(
|
||||||
label: '결재 메모',
|
label: '결재 메모',
|
||||||
child: ShadInput(
|
child: Column(
|
||||||
controller: approvalNoteController,
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
maxLines: 2,
|
children: [
|
||||||
|
ShadInput(
|
||||||
|
controller: approvalNoteController,
|
||||||
|
maxLines: 2,
|
||||||
|
readOnly: initial != null,
|
||||||
|
enabled: initial == null,
|
||||||
|
),
|
||||||
|
if (initial != null)
|
||||||
|
Padding(
|
||||||
|
padding: const EdgeInsets.only(top: 6),
|
||||||
|
child: Text(
|
||||||
|
'등록된 결재 메모는 수정할 수 없습니다.',
|
||||||
|
style: theme.textTheme.small.copyWith(
|
||||||
|
color: theme.colorScheme.mutedForeground,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
|||||||
@@ -207,4 +207,46 @@ void main() {
|
|||||||
expect(find.textContaining('자동완성에서 선택'), findsOneWidget);
|
expect(find.textContaining('자동완성에서 선택'), findsOneWidget);
|
||||||
expect(find.textContaining('창고를 선택'), findsOneWidget);
|
expect(find.textContaining('창고를 선택'), findsOneWidget);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
testWidgets('입고 상세의 수정 버튼은 수정 모달을 연다', (tester) async {
|
||||||
|
final view = tester.view;
|
||||||
|
view.physicalSize = const Size(1280, 900);
|
||||||
|
view.devicePixelRatio = 1.0;
|
||||||
|
addTearDown(() {
|
||||||
|
view.resetPhysicalSize();
|
||||||
|
view.resetDevicePixelRatio();
|
||||||
|
});
|
||||||
|
|
||||||
|
await tester.pumpWidget(
|
||||||
|
MaterialApp(
|
||||||
|
home: ScaffoldMessenger(
|
||||||
|
child: PermissionScope(
|
||||||
|
manager: PermissionManager(),
|
||||||
|
child: ShadTheme(
|
||||||
|
data: SuperportShadTheme.light(),
|
||||||
|
child: Scaffold(
|
||||||
|
body: InboundPage(routeUri: Uri.parse('/inventory/inbound')),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
await tester.pumpAndSettle();
|
||||||
|
|
||||||
|
await tester.tap(find.text('TX-20240301-001').first);
|
||||||
|
await tester.pumpAndSettle();
|
||||||
|
|
||||||
|
expect(find.text('입고 상세'), findsOneWidget);
|
||||||
|
|
||||||
|
final editButton = find.widgetWithText(ShadButton, '수정').last;
|
||||||
|
await tester.ensureVisible(editButton);
|
||||||
|
await tester.tap(editButton);
|
||||||
|
await tester.pump();
|
||||||
|
|
||||||
|
await tester.pumpAndSettle(const Duration(milliseconds: 500));
|
||||||
|
|
||||||
|
expect(find.text('입고 수정'), findsOneWidget);
|
||||||
|
expect(find.widgetWithText(ShadButton, '저장'), findsWidgets);
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -66,4 +66,45 @@ void main() {
|
|||||||
expect(find.textContaining('자동완성에서 선택'), findsOneWidget);
|
expect(find.textContaining('자동완성에서 선택'), findsOneWidget);
|
||||||
expect(find.text('창고를 선택하세요.'), findsOneWidget);
|
expect(find.text('창고를 선택하세요.'), findsOneWidget);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
testWidgets('출고 상세의 수정 버튼은 수정 모달을 연다', (tester) async {
|
||||||
|
final view = tester.view;
|
||||||
|
view.physicalSize = const Size(1280, 900);
|
||||||
|
view.devicePixelRatio = 1.0;
|
||||||
|
addTearDown(() {
|
||||||
|
view.resetPhysicalSize();
|
||||||
|
view.resetDevicePixelRatio();
|
||||||
|
});
|
||||||
|
|
||||||
|
await tester.pumpWidget(
|
||||||
|
MaterialApp(
|
||||||
|
home: ScaffoldMessenger(
|
||||||
|
child: PermissionScope(
|
||||||
|
manager: PermissionManager(),
|
||||||
|
child: ShadTheme(
|
||||||
|
data: SuperportShadTheme.light(),
|
||||||
|
child: Scaffold(
|
||||||
|
body: OutboundPage(routeUri: Uri.parse('/inventory/outbound')),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
await tester.pumpAndSettle();
|
||||||
|
|
||||||
|
await tester.tap(find.text('TX-20240302-010').first);
|
||||||
|
await tester.pumpAndSettle();
|
||||||
|
|
||||||
|
expect(find.text('출고 상세'), findsOneWidget);
|
||||||
|
|
||||||
|
final editButton = find.widgetWithText(ShadButton, '수정').last;
|
||||||
|
await tester.ensureVisible(editButton);
|
||||||
|
await tester.tap(editButton);
|
||||||
|
await tester.pump();
|
||||||
|
await tester.pumpAndSettle(const Duration(milliseconds: 500));
|
||||||
|
|
||||||
|
expect(find.text('출고 수정'), findsOneWidget);
|
||||||
|
expect(find.widgetWithText(ShadButton, '저장'), findsWidgets);
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -66,4 +66,45 @@ void main() {
|
|||||||
expect(find.textContaining('자동완성에서 선택'), findsOneWidget);
|
expect(find.textContaining('자동완성에서 선택'), findsOneWidget);
|
||||||
expect(find.text('최소 1개의 고객사를 선택하세요.'), findsOneWidget);
|
expect(find.text('최소 1개의 고객사를 선택하세요.'), findsOneWidget);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
testWidgets('대여 상세의 수정 버튼은 수정 모달을 연다', (tester) async {
|
||||||
|
final view = tester.view;
|
||||||
|
view.physicalSize = const Size(1280, 900);
|
||||||
|
view.devicePixelRatio = 1.0;
|
||||||
|
addTearDown(() {
|
||||||
|
view.resetPhysicalSize();
|
||||||
|
view.resetDevicePixelRatio();
|
||||||
|
});
|
||||||
|
|
||||||
|
await tester.pumpWidget(
|
||||||
|
MaterialApp(
|
||||||
|
home: ScaffoldMessenger(
|
||||||
|
child: PermissionScope(
|
||||||
|
manager: PermissionManager(),
|
||||||
|
child: ShadTheme(
|
||||||
|
data: SuperportShadTheme.light(),
|
||||||
|
child: Scaffold(
|
||||||
|
body: RentalPage(routeUri: Uri.parse('/inventory/rental')),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
await tester.pumpAndSettle();
|
||||||
|
|
||||||
|
await tester.tap(find.text('TX-20240305-030').first);
|
||||||
|
await tester.pumpAndSettle();
|
||||||
|
|
||||||
|
expect(find.text('대여 상세'), findsOneWidget);
|
||||||
|
|
||||||
|
final editButton = find.widgetWithText(ShadButton, '수정').last;
|
||||||
|
await tester.ensureVisible(editButton);
|
||||||
|
await tester.tap(editButton);
|
||||||
|
await tester.pump();
|
||||||
|
await tester.pumpAndSettle(const Duration(milliseconds: 500));
|
||||||
|
|
||||||
|
expect(find.text('대여 수정'), findsOneWidget);
|
||||||
|
expect(find.widgetWithText(ShadButton, '저장'), findsWidgets);
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -240,6 +240,15 @@ class _StubStockTransactionRepository implements StockTransactionRepository {
|
|||||||
}) : _transactions = transactions;
|
}) : _transactions = transactions;
|
||||||
|
|
||||||
final List<StockTransaction> _transactions;
|
final List<StockTransaction> _transactions;
|
||||||
|
StockTransaction _findTransaction(int id) {
|
||||||
|
final index = _transactions.indexWhere(
|
||||||
|
(transaction) => transaction.id == id,
|
||||||
|
);
|
||||||
|
if (index == -1) {
|
||||||
|
throw StateError('Transaction $id not found');
|
||||||
|
}
|
||||||
|
return _transactions[index];
|
||||||
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Future<PaginatedResult<StockTransaction>> list({
|
Future<PaginatedResult<StockTransaction>> list({
|
||||||
@@ -302,8 +311,8 @@ class _StubStockTransactionRepository implements StockTransactionRepository {
|
|||||||
Future<StockTransaction> fetchDetail(
|
Future<StockTransaction> fetchDetail(
|
||||||
int id, {
|
int id, {
|
||||||
List<String> include = const ['lines', 'customers', 'approval'],
|
List<String> include = const ['lines', 'customers', 'approval'],
|
||||||
}) {
|
}) async {
|
||||||
throw UnimplementedError();
|
return _findTransaction(id);
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
@@ -359,14 +368,9 @@ class _StubStockTransactionRepository implements StockTransactionRepository {
|
|||||||
int id,
|
int id,
|
||||||
StockTransaction Function(StockTransaction transaction) transform,
|
StockTransaction Function(StockTransaction transaction) transform,
|
||||||
) async {
|
) async {
|
||||||
final index = _transactions.indexWhere(
|
final current = _findTransaction(id);
|
||||||
(transaction) => transaction.id == id,
|
|
||||||
);
|
|
||||||
if (index == -1) {
|
|
||||||
throw StateError('Transaction $id not found');
|
|
||||||
}
|
|
||||||
final current = _transactions[index];
|
|
||||||
final updated = transform(current);
|
final updated = transform(current);
|
||||||
|
final index = _transactions.indexOf(current);
|
||||||
_transactions[index] = updated;
|
_transactions[index] = updated;
|
||||||
return updated;
|
return updated;
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user