fix(inventory): 상세 편집 플로우 안정화

- inbound/outbound/rental controller에 fetchTransactionDetail을 추가해 상세 동기화를 지원

- 각 페이지 초기화 시 결재 초안 로딩 권한을 PermissionScope에서 확인하도록 수정

- 상세 패널의 수정 버튼이 모달과 연동되도록 흐름을 정리하고 생성/수정 후 상세 데이터를 재조회

- 기존 결재 메모 필드는 등록 이후 수정 불가하도록 UI와 입력 상태를 비활성화

- 신규 상세-수정 위젯 테스트와 리포지토리 스텁 fetchDetail 구현을 추가

- flutter analyze, flutter test를 실행해 회귀를 점검
This commit is contained in:
JiWoong Sul
2025-11-11 16:28:49 +09:00
parent 04c6bc9a2e
commit d603fd5c17
10 changed files with 354 additions and 38 deletions

View File

@@ -233,6 +233,24 @@ class OutboundController extends ChangeNotifier {
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) {
final useCase = _saveDraftUseCase;
if (useCase == null) {

View File

@@ -165,7 +165,7 @@ class _OutboundPageState extends State<OutboundPage> {
Future.microtask(() async {
await controller.loadStatusOptions();
final requester = _resolveCurrentWriter();
if (requester != null) {
if (requester != null && _canRestoreApprovalDrafts) {
await controller.loadApprovalDraftFromServer(requesterId: requester.id);
}
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 {
final getIt = GetIt.I;
if (!getIt.isRegistered<CustomerRepository>()) {
@@ -982,7 +993,7 @@ class _OutboundPageState extends State<OutboundPage> {
}
List<Widget> _buildDetailActions(OutboundRecord record) {
final isProcessing = _isProcessing(record.id) || _isLoading;
final isProcessing = _isProcessing(record.id);
final actions = <Widget>[];
if (_canSubmit(record)) {
@@ -1028,12 +1039,7 @@ class _OutboundPageState extends State<OutboundPage> {
actions.add(
ShadButton.outline(
leading: const Icon(lucide.LucideIcons.pencil, size: 16),
onPressed: isProcessing
? null
: () {
Navigator.of(context).maybePop();
_handleEdit(record);
},
onPressed: isProcessing ? null : () => _openEditFromDetail(record),
child: const Text('수정'),
),
);
@@ -1041,6 +1047,17 @@ class _OutboundPageState extends State<OutboundPage> {
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 {
final record = await _showOutboundFormDialog();
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);
if (updated != null) {
_selectRecord(updated, openDetail: true);
} else if (reopenOnCancel) {
_selectRecord(record, openDetail: true);
}
}
@@ -1821,6 +1843,21 @@ class _OutboundPageState extends State<OutboundPage> {
SuperportToast.error(context, '출고 컨트롤러를 찾을 수 없습니다.');
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 =
_transactionTypeLookup ?? controller.transactionType;
@@ -1912,7 +1949,6 @@ class _OutboundPageState extends State<OutboundPage> {
),
refreshAfter: false,
);
result = updated;
final StockTransaction? currentTransaction = initialRecord.raw;
final currentLines =
currentTransaction?.lines ?? const <StockTransactionLine>[];
@@ -1937,6 +1973,8 @@ class _OutboundPageState extends State<OutboundPage> {
);
}
await controller.refresh();
final refreshed = await resolveUpdatedRecord(transactionId);
result = refreshed ?? updated;
updateSaving(false);
if (!mounted) {
return;
@@ -2006,7 +2044,8 @@ class _OutboundPageState extends State<OutboundPage> {
approval: approvalInput,
),
);
result = created;
final refreshed = await resolveUpdatedRecord(created.id);
result = refreshed ?? created;
updateSaving(false);
if (!mounted) {
return;
@@ -2227,9 +2266,14 @@ class _OutboundPageState extends State<OutboundPage> {
width: 500,
child: SuperportFormField(
label: '결재 메모',
caption: initial != null
? '등록된 결재 메모는 수정할 수 없습니다.'
: null,
child: ShadInput(
controller: approvalNoteController,
maxLines: 2,
readOnly: initial != null,
enabled: initial == null,
),
),
),