번호 자동 부여 대응 및 API 공통 처리 보강

This commit is contained in:
JiWoong Sul
2025-10-23 14:02:31 +09:00
parent 09c31b2503
commit 7e933a2dda
55 changed files with 948 additions and 586 deletions

View File

@@ -11,6 +11,9 @@ class InboundRecord {
required this.processedAt,
required this.warehouse,
this.warehouseId,
this.warehouseCode,
this.warehouseZipcode,
this.warehouseAddress,
required this.status,
this.statusId,
required this.writer,
@@ -32,6 +35,9 @@ class InboundRecord {
processedAt: transaction.transactionDate,
warehouse: transaction.warehouse.name,
warehouseId: transaction.warehouse.id,
warehouseCode: transaction.warehouse.code,
warehouseZipcode: transaction.warehouse.zipcode,
warehouseAddress: transaction.warehouse.addressLine,
status: transaction.status.name,
statusId: transaction.status.id,
writer: transaction.createdBy.name,
@@ -54,6 +60,9 @@ class InboundRecord {
final DateTime processedAt;
final String warehouse;
final int? warehouseId;
final String? warehouseCode;
final String? warehouseZipcode;
final String? warehouseAddress;
final String status;
final int? statusId;
final String writer;

View File

@@ -75,6 +75,10 @@ class _InboundPageState extends State<InboundPage> {
String? _errorMessage;
Set<int> _processingTransactionIds = {};
String? _routeSelectedNumber;
String? _pendingDetailNumber;
bool _suppressNextRouteSelection = false;
late List<String> _statusOptions;
final Map<String, LookupItem> _statusLookup = {};
LookupItem? _transactionTypeLookup;
@@ -184,6 +188,26 @@ class _InboundPageState extends State<InboundPage> {
controller.transactionType ?? _transactionTypeLookup;
_refreshSelection();
});
final detailNumber = _pendingDetailNumber;
if (detailNumber != null) {
_pendingDetailNumber = null;
InboundRecord? matched;
for (final record in _records) {
if (record.transactionNumber == detailNumber) {
matched = record;
break;
}
}
if (matched != null) {
final target = matched;
WidgetsBinding.instance.addPostFrameCallback((_) {
if (!mounted) {
return;
}
_showDetailDialog(target);
});
}
}
}
@override
@@ -456,10 +480,7 @@ class _InboundPageState extends State<InboundPage> {
mobile: (_) => _InboundMobileList(
records: visibleRecords,
selected: _selectedRecord,
onSelect: (record) {
setState(() => _selectedRecord = record);
_showDetailDialog(record);
},
onSelect: _handleRecordTap,
dateFormatter: _dateFormatter,
currencyFormatter: _currencyFormatter,
transitionsEnabled: _transitionsEnabled,
@@ -663,18 +684,37 @@ class _InboundPageState extends State<InboundPage> {
const FixedTableSpanExtent(InboundTableSpec.rowSpanHeight),
onRowTap: (rowIndex) {
final record = records[rowIndex];
setState(() {
_selectedRecord = record;
});
_showDetailDialog(record);
_selectRecord(record, openDetail: true);
},
);
}
void _handleRecordTap(InboundRecord record) {
_selectRecord(record, openDetail: true);
}
List<int> _visibleColumnsFor(DeviceBreakpoint breakpoint) {
return InboundTableSpec.visibleColumns(breakpoint);
}
void _selectRecord(InboundRecord? record, {bool openDetail = false}) {
if (!mounted) return;
setState(() {
_selectedRecord = record;
});
final selectedNumber = record?.transactionNumber;
_routeSelectedNumber = selectedNumber;
_updateRoute(selected: selectedNumber);
if (openDetail && record != null) {
WidgetsBinding.instance.addPostFrameCallback((_) {
if (!mounted) {
return;
}
_showDetailDialog(record);
});
}
}
List<ShadTableCell> _buildTableCells(
InboundRecord record,
List<int> visibleColumns,
@@ -767,18 +807,14 @@ class _InboundPageState extends State<InboundPage> {
Future<void> _handleCreate() async {
final record = await _showInboundFormDialog();
if (record != null) {
setState(() {
_selectedRecord = record;
});
_selectRecord(record, openDetail: true);
}
}
Future<void> _handleEdit(InboundRecord record) async {
final updated = await _showInboundFormDialog(initial: record);
if (updated != null) {
setState(() {
_selectedRecord = updated;
});
_selectRecord(updated, openDetail: true);
}
}
@@ -1082,6 +1118,10 @@ class _InboundPageState extends State<InboundPage> {
? pageSizeParam
: InboundTableSpec.pageSizeOptions.first;
final page = (pageParam != null && pageParam > 0) ? pageParam : 1;
final selectedParam = params['selected']?.trim();
final selectedNumber = selectedParam == null || selectedParam.isEmpty
? null
: selectedParam;
void assign() {
_pendingQuery = query;
@@ -1106,6 +1146,13 @@ class _InboundPageState extends State<InboundPage> {
_sortAscending = resolvedSortAscending;
_pageSize = pageSize;
_currentPage = page;
_routeSelectedNumber = selectedNumber;
if (_suppressNextRouteSelection) {
_pendingDetailNumber = null;
} else {
_pendingDetailNumber = selectedNumber;
}
_suppressNextRouteSelection = false;
_refreshSelection();
}
@@ -1121,9 +1168,7 @@ class _InboundPageState extends State<InboundPage> {
void _goToPage(int page) {
final totalItems = _result?.total ?? _filteredRecords.length;
final totalPages = _calculateTotalPages(totalItems);
final int target = page < 1
? 1
: (page > totalPages ? totalPages : page);
final int target = page < 1 ? 1 : (page > totalPages ? totalPages : page);
if (target == _currentPage) {
return;
}
@@ -1141,6 +1186,16 @@ class _InboundPageState extends State<InboundPage> {
return;
}
final selectedNumber = _routeSelectedNumber;
if (selectedNumber != null) {
for (final record in filtered) {
if (record.transactionNumber == selectedNumber) {
_selectedRecord = record;
return;
}
}
}
final current = _selectedRecord;
if (current != null) {
InboundRecord? matched;
@@ -1162,7 +1217,7 @@ class _InboundPageState extends State<InboundPage> {
_selectedRecord = filtered.first;
}
void _updateRoute({int? page, int? pageSize}) {
void _updateRoute({int? page, int? pageSize, String? selected}) {
if (!mounted) return;
final targetPage = page ?? _currentPage;
final targetPageSize = pageSize ?? _pageSize;
@@ -1208,6 +1263,10 @@ class _InboundPageState extends State<InboundPage> {
if (targetPageSize != InboundTableSpec.pageSizeOptions.first) {
params['page_size'] = targetPageSize.toString();
}
final selectedNumber = selected ?? _selectedRecord?.transactionNumber;
if (selectedNumber != null && selectedNumber.isNotEmpty) {
params['selected'] = selectedNumber;
}
final uri = Uri(
path: widget.routeUri.path,
@@ -1221,6 +1280,7 @@ class _InboundPageState extends State<InboundPage> {
if (router == null) {
return;
}
_suppressNextRouteSelection = true;
router.go(newLocation);
}
@@ -1351,12 +1411,8 @@ class _InboundPageState extends State<InboundPage> {
text: writerLabel(writerSelection),
);
final remarkController = TextEditingController(text: initial?.remark ?? '');
final transactionNumberController = TextEditingController(
text: initial?.transactionNumber ?? '',
);
final approvalNumberController = TextEditingController(
text: initial?.raw?.approval?.approvalNo ?? '',
);
final assignedTransactionNo = initial?.transactionNumber;
final assignedApprovalNo = initial?.raw?.approval?.approvalNo;
final approvalNoteController = TextEditingController();
final transactionTypeValue =
initial?.transactionType ??
@@ -1378,8 +1434,6 @@ class _InboundPageState extends State<InboundPage> {
};
String? writerError;
String? transactionNumberError;
String? approvalNumberError;
String? warehouseError;
String? statusError;
String? headerNotice;
@@ -1408,18 +1462,12 @@ class _InboundPageState extends State<InboundPage> {
writerController: writerController,
writerSelection: writerSelection,
requireWriterSelection: initial == null,
transactionNumberController: transactionNumberController,
transactionNumberRequired: initial == null,
approvalNumberController: approvalNumberController,
approvalNumberRequired: initial == null,
warehouseSelection: warehouseSelection,
statusValue: statusValue.value,
drafts: drafts,
lineErrors: lineErrors,
);
writerError = validationResult.writerError;
transactionNumberError = validationResult.transactionNumberError;
approvalNumberError = validationResult.approvalNumberError;
warehouseError = validationResult.warehouseError;
statusError = validationResult.statusError;
headerNotice = validationResult.headerNotice;
@@ -1463,8 +1511,6 @@ class _InboundPageState extends State<InboundPage> {
final remarkText = remarkController.text.trim();
final remarkValue = remarkText.isEmpty ? null : remarkText;
final transactionNoValue = transactionNumberController.text.trim();
final approvalNoValue = approvalNumberController.text.trim();
final approvalNoteValue = approvalNoteController.text.trim();
final transactionId = initial?.id;
@@ -1529,7 +1575,10 @@ class _InboundPageState extends State<InboundPage> {
if (!mounted) {
return;
}
SuperportToast.success(context, '입고 정보가 수정되었습니다.');
SuperportToast.success(
context,
'입고 정보가 수정되었습니다. (${updated.transactionNumber})',
);
navigator.pop();
return;
}
@@ -1553,7 +1602,6 @@ class _InboundPageState extends State<InboundPage> {
.toList(growable: false);
final created = await controller.createTransaction(
StockTransactionCreateInput(
transactionNo: transactionNoValue,
transactionTypeId: transactionTypeLookup.id,
transactionStatusId: statusItem.id,
warehouseId: warehouseId,
@@ -1562,7 +1610,6 @@ class _InboundPageState extends State<InboundPage> {
note: remarkValue,
lines: createLines,
approval: StockTransactionApprovalInput(
approvalNo: approvalNoValue,
requestedById: createdById,
note: approvalNoteValue.isEmpty ? null : approvalNoteValue,
),
@@ -1573,7 +1620,10 @@ class _InboundPageState extends State<InboundPage> {
if (!mounted) {
return;
}
SuperportToast.success(context, '입고가 등록되었습니다.');
SuperportToast.success(
context,
'입고가 등록되었습니다. (${created.transactionNumber})',
);
navigator.pop();
} catch (error) {
updateSaving(false);
@@ -1719,20 +1769,11 @@ class _InboundPageState extends State<InboundPage> {
width: 240,
child: SuperportFormField(
label: '트랜잭션번호',
required: true,
errorText: transactionNumberError,
child: ShadInput(
controller: transactionNumberController,
readOnly: initial != null,
enabled: initial == null,
placeholder: const Text('예: IN-2024-0001'),
onChanged: (_) {
if (transactionNumberError != null) {
setState(() {
transactionNumberError = null;
});
}
},
child: Text(
assignedTransactionNo ?? '저장 시 자동 생성',
style: assignedTransactionNo == null
? theme.textTheme.muted
: theme.textTheme.p,
),
),
),
@@ -1740,20 +1781,11 @@ class _InboundPageState extends State<InboundPage> {
width: 240,
child: SuperportFormField(
label: '결재번호',
required: true,
errorText: approvalNumberError,
child: ShadInput(
controller: approvalNumberController,
readOnly: initial != null,
enabled: initial == null,
placeholder: const Text('예: APP-2024-0001'),
onChanged: (_) {
if (approvalNumberError != null) {
setState(() {
approvalNumberError = null;
});
}
},
child: Text(
assignedApprovalNo ?? '저장 시 자동 생성',
style: assignedApprovalNo == null
? theme.textTheme.muted
: theme.textTheme.p,
),
),
),
@@ -1926,8 +1958,6 @@ class _InboundPageState extends State<InboundPage> {
statusValue.dispose();
writerController.dispose();
remarkController.dispose();
transactionNumberController.dispose();
approvalNumberController.dispose();
approvalNoteController.dispose();
transactionTypeController.dispose();
processedAt.dispose();
@@ -2271,10 +2301,6 @@ _InboundFormValidation _validateInboundForm({
required TextEditingController writerController,
required InventoryEmployeeSuggestion? writerSelection,
required bool requireWriterSelection,
required TextEditingController transactionNumberController,
required bool transactionNumberRequired,
required TextEditingController approvalNumberController,
required bool approvalNumberRequired,
required InventoryWarehouseOption? warehouseSelection,
required String statusValue,
required List<_LineItemDraft> drafts,
@@ -2282,8 +2308,6 @@ _InboundFormValidation _validateInboundForm({
}) {
var isValid = true;
String? writerError;
String? transactionNumberError;
String? approvalNumberError;
String? warehouseError;
String? statusError;
String? headerNotice;
@@ -2299,18 +2323,6 @@ _InboundFormValidation _validateInboundForm({
isValid = false;
}
final transactionNumber = transactionNumberController.text.trim();
if (transactionNumberRequired && transactionNumber.isEmpty) {
transactionNumberError = '거래번호를 입력하세요.';
isValid = false;
}
final approvalNumber = approvalNumberController.text.trim();
if (approvalNumberRequired && approvalNumber.isEmpty) {
approvalNumberError = '결재번호를 입력하세요.';
isValid = false;
}
if (warehouseSelection == null) {
warehouseError = '창고를 선택하세요.';
isValid = false;
@@ -2369,8 +2381,6 @@ _InboundFormValidation _validateInboundForm({
return _InboundFormValidation(
isValid: isValid,
writerError: writerError,
transactionNumberError: transactionNumberError,
approvalNumberError: approvalNumberError,
warehouseError: warehouseError,
statusError: statusError,
headerNotice: headerNotice,
@@ -2386,8 +2396,6 @@ class _InboundFormValidation {
const _InboundFormValidation({
required this.isValid,
this.writerError,
this.transactionNumberError,
this.approvalNumberError,
this.warehouseError,
this.statusError,
this.headerNotice,
@@ -2395,8 +2403,6 @@ class _InboundFormValidation {
final bool isValid;
final String? writerError;
final String? transactionNumberError;
final String? approvalNumberError;
final String? warehouseError;
final String? statusError;
final String? headerNotice;

View File

@@ -43,6 +43,15 @@ class InboundDetailView extends StatelessWidget {
value: dateFormatter.format(record.processedAt),
),
_DetailChip(label: '창고', value: record.warehouse),
if (record.warehouseCode != null &&
record.warehouseCode!.trim().isNotEmpty)
_DetailChip(label: '창고 코드', value: record.warehouseCode!),
if (record.warehouseZipcode != null &&
record.warehouseZipcode!.trim().isNotEmpty)
_DetailChip(label: '창고 우편번호', value: record.warehouseZipcode!),
if (record.warehouseAddress != null &&
record.warehouseAddress!.trim().isNotEmpty)
_DetailChip(label: '창고 주소', value: record.warehouseAddress!),
_DetailChip(label: '트랜잭션 유형', value: record.transactionType),
_DetailChip(label: '상태', value: record.status),
_DetailChip(label: '작성자', value: record.writer),

View File

@@ -11,6 +11,9 @@ class OutboundRecord {
required this.processedAt,
required this.warehouse,
this.warehouseId,
this.warehouseCode,
this.warehouseZipcode,
this.warehouseAddress,
required this.status,
this.statusId,
required this.writer,
@@ -31,6 +34,9 @@ class OutboundRecord {
processedAt: transaction.transactionDate,
warehouse: transaction.warehouse.name,
warehouseId: transaction.warehouse.id,
warehouseCode: transaction.warehouse.code,
warehouseZipcode: transaction.warehouse.zipcode,
warehouseAddress: transaction.warehouse.addressLine,
status: transaction.status.name,
statusId: transaction.status.id,
writer: transaction.createdBy.name,
@@ -54,6 +60,9 @@ class OutboundRecord {
final DateTime processedAt;
final String warehouse;
final int? warehouseId;
final String? warehouseCode;
final String? warehouseZipcode;
final String? warehouseAddress;
final String status;
final int? statusId;
final String writer;

View File

@@ -79,6 +79,10 @@ class _OutboundPageState extends State<OutboundPage> {
String? _errorMessage;
Set<int> _processingTransactionIds = {};
String? _routeSelectedNumber;
String? _pendingDetailNumber;
bool _suppressNextRouteSelection = false;
late List<String> _statusOptions;
final Map<String, LookupItem> _statusLookup = {};
LookupItem? _transactionTypeLookup;
@@ -256,6 +260,26 @@ class _OutboundPageState extends State<OutboundPage> {
controller.transactionType ?? _transactionTypeLookup;
_refreshSelection();
});
final detailNumber = _pendingDetailNumber;
if (detailNumber != null) {
_pendingDetailNumber = null;
OutboundRecord? matched;
for (final record in _records) {
if (record.transactionNumber == detailNumber) {
matched = record;
break;
}
}
if (matched != null) {
final target = matched;
WidgetsBinding.instance.addPostFrameCallback((_) {
if (!mounted) {
return;
}
_showDetailDialog(target);
});
}
}
}
@override
@@ -580,10 +604,7 @@ class _OutboundPageState extends State<OutboundPage> {
),
onRowTap: (rowIndex) {
final record = visibleRecords[rowIndex];
setState(() {
_selectedRecord = record;
});
_showDetailDialog(record);
_selectRecord(record, openDetail: true);
},
),
),
@@ -748,6 +769,24 @@ class _OutboundPageState extends State<OutboundPage> {
return records;
}
void _selectRecord(OutboundRecord? record, {bool openDetail = false}) {
if (!mounted) return;
setState(() {
_selectedRecord = record;
});
final selectedNumber = record?.transactionNumber;
_routeSelectedNumber = selectedNumber;
_updateRoute(selected: selectedNumber);
if (openDetail && record != null) {
WidgetsBinding.instance.addPostFrameCallback((_) {
if (!mounted) {
return;
}
_showDetailDialog(record);
});
}
}
List<String> _buildRecordRow(OutboundRecord record) {
final primaryItem = record.items.isNotEmpty ? record.items.first : null;
return [
@@ -848,18 +887,14 @@ class _OutboundPageState extends State<OutboundPage> {
Future<void> _handleCreate() async {
final record = await _showOutboundFormDialog();
if (record != null) {
setState(() {
_selectedRecord = record;
});
_selectRecord(record, openDetail: true);
}
}
Future<void> _handleEdit(OutboundRecord record) async {
final updated = await _showOutboundFormDialog(initial: record);
if (updated != null) {
setState(() {
_selectedRecord = updated;
});
_selectRecord(updated, openDetail: true);
}
}
@@ -1163,6 +1198,10 @@ class _OutboundPageState extends State<OutboundPage> {
(params['order'] ?? '').toLowerCase() == 'asc';
final pageSizeParam = int.tryParse(params['page_size'] ?? '');
final pageParam = int.tryParse(params['page'] ?? '');
final selectedParam = params['selected']?.trim();
final selectedNumber = selectedParam == null || selectedParam.isEmpty
? null
: selectedParam;
final warehouseId = warehouseIdParam;
final warehouseLabel = warehouseParam != null && warehouseParam.isNotEmpty
@@ -1232,6 +1271,13 @@ class _OutboundPageState extends State<OutboundPage> {
_sortAscending = resolvedSortAscending;
_pageSize = pageSize;
_currentPage = page;
_routeSelectedNumber = selectedNumber;
if (_suppressNextRouteSelection) {
_pendingDetailNumber = null;
} else {
_pendingDetailNumber = selectedNumber;
}
_suppressNextRouteSelection = false;
_refreshSelection();
}
@@ -1265,6 +1311,16 @@ class _OutboundPageState extends State<OutboundPage> {
return;
}
final selectedNumber = _routeSelectedNumber;
if (selectedNumber != null) {
for (final record in filtered) {
if (record.transactionNumber == selectedNumber) {
_selectedRecord = record;
return;
}
}
}
final current = _selectedRecord;
if (current != null) {
OutboundRecord? matched;
@@ -1286,7 +1342,7 @@ class _OutboundPageState extends State<OutboundPage> {
_selectedRecord = filtered.first;
}
void _updateRoute({int? page, int? pageSize}) {
void _updateRoute({int? page, int? pageSize, String? selected}) {
if (!mounted) return;
final targetPage = page ?? _currentPage;
final targetPageSize = pageSize ?? _pageSize;
@@ -1336,6 +1392,10 @@ class _OutboundPageState extends State<OutboundPage> {
if (targetPageSize != OutboundTableSpec.pageSizeOptions.first) {
params['page_size'] = targetPageSize.toString();
}
final selectedNumber = selected ?? _selectedRecord?.transactionNumber;
if (selectedNumber != null && selectedNumber.isNotEmpty) {
params['selected'] = selectedNumber;
}
final uri = Uri(
path: widget.routeUri.path,
@@ -1349,6 +1409,7 @@ class _OutboundPageState extends State<OutboundPage> {
if (router == null) {
return;
}
_suppressNextRouteSelection = true;
router.go(newLocation);
}
@@ -1510,12 +1571,8 @@ class _OutboundPageState extends State<OutboundPage> {
final transactionTypeController = TextEditingController(
text: transactionTypeValue,
);
final transactionNumberController = TextEditingController(
text: initial?.transactionNumber ?? '',
);
final approvalNumberController = TextEditingController(
text: initial?.raw?.approval?.approvalNo ?? '',
);
final assignedTransactionNo = initial?.transactionNumber;
final assignedApprovalNo = initial?.raw?.approval?.approvalNo;
final approvalNoteController = TextEditingController();
final drafts =
@@ -1530,8 +1587,6 @@ class _OutboundPageState extends State<OutboundPage> {
};
String? writerError;
String? transactionNumberError;
String? approvalNumberError;
String? customerError;
String? warehouseError;
String? statusError;
@@ -1561,10 +1616,6 @@ class _OutboundPageState extends State<OutboundPage> {
writerController: writerController,
writerSelection: writerSelection,
requireWriterSelection: initial == null,
transactionNumberController: transactionNumberController,
transactionNumberRequired: initial == null,
approvalNumberController: approvalNumberController,
approvalNumberRequired: initial == null,
warehouseSelection: warehouseSelection,
statusValue: statusValue.value,
selectedCustomers: customerSelection
@@ -1575,8 +1626,6 @@ class _OutboundPageState extends State<OutboundPage> {
);
writerError = validation.writerError;
transactionNumberError = validation.transactionNumberError;
approvalNumberError = validation.approvalNumberError;
customerError = validation.customerError;
warehouseError = validation.warehouseError;
statusError = validation.statusError;
@@ -1621,8 +1670,6 @@ class _OutboundPageState extends State<OutboundPage> {
final remarkText = remarkController.text.trim();
final remarkValue = remarkText.isEmpty ? null : remarkText;
final transactionNoValue = transactionNumberController.text.trim();
final approvalNoValue = approvalNumberController.text.trim();
final approvalNoteValue = approvalNoteController.text.trim();
final transactionId = initial?.id;
@@ -1715,7 +1762,10 @@ class _OutboundPageState extends State<OutboundPage> {
if (!mounted) {
return;
}
SuperportToast.success(context, '출고 정보가 수정되었습니다.');
SuperportToast.success(
context,
'출고 정보가 수정되었습니다. (${updated.transactionNumber})',
);
navigator.pop();
return;
}
@@ -1749,7 +1799,6 @@ class _OutboundPageState extends State<OutboundPage> {
final created = await controller.createTransaction(
StockTransactionCreateInput(
transactionNo: transactionNoValue,
transactionTypeId: transactionTypeLookup.id,
transactionStatusId: statusItem.id,
warehouseId: warehouseId,
@@ -1759,7 +1808,6 @@ class _OutboundPageState extends State<OutboundPage> {
lines: createLines,
customers: createCustomers,
approval: StockTransactionApprovalInput(
approvalNo: approvalNoValue,
requestedById: createdById,
note: approvalNoteValue.isEmpty ? null : approvalNoteValue,
),
@@ -1770,7 +1818,10 @@ class _OutboundPageState extends State<OutboundPage> {
if (!mounted) {
return;
}
SuperportToast.success(context, '출고가 등록되었습니다.');
SuperportToast.success(
context,
'출고가 등록되었습니다. (${created.transactionNumber})',
);
navigator.pop();
} catch (error) {
updateSaving(false);
@@ -1911,20 +1962,11 @@ class _OutboundPageState extends State<OutboundPage> {
width: 240,
child: SuperportFormField(
label: '트랜잭션번호',
required: true,
errorText: transactionNumberError,
child: ShadInput(
controller: transactionNumberController,
readOnly: initial != null,
enabled: initial == null,
placeholder: const Text('예: OUT-2024-0001'),
onChanged: (_) {
if (transactionNumberError != null) {
setState(() {
transactionNumberError = null;
});
}
},
child: Text(
assignedTransactionNo ?? '저장 시 자동 생성',
style: assignedTransactionNo == null
? theme.textTheme.muted
: theme.textTheme.p,
),
),
),
@@ -1932,20 +1974,11 @@ class _OutboundPageState extends State<OutboundPage> {
width: 240,
child: SuperportFormField(
label: '결재번호',
required: true,
errorText: approvalNumberError,
child: ShadInput(
controller: approvalNumberController,
readOnly: initial != null,
enabled: initial == null,
placeholder: const Text('예: APP-2024-0001'),
onChanged: (_) {
if (approvalNumberError != null) {
setState(() {
approvalNumberError = null;
});
}
},
child: Text(
assignedApprovalNo ?? '저장 시 자동 생성',
style: assignedApprovalNo == null
? theme.textTheme.muted
: theme.textTheme.p,
),
),
),
@@ -2163,8 +2196,6 @@ class _OutboundPageState extends State<OutboundPage> {
writerController.dispose();
remarkController.dispose();
transactionTypeController.dispose();
transactionNumberController.dispose();
approvalNumberController.dispose();
approvalNoteController.dispose();
processedAt.dispose();
@@ -2418,10 +2449,6 @@ _OutboundFormValidation _validateOutboundForm({
required TextEditingController writerController,
required InventoryEmployeeSuggestion? writerSelection,
required bool requireWriterSelection,
required TextEditingController transactionNumberController,
required bool transactionNumberRequired,
required TextEditingController approvalNumberController,
required bool approvalNumberRequired,
required InventoryWarehouseOption? warehouseSelection,
required String statusValue,
required List<InventoryCustomerOption> selectedCustomers,
@@ -2430,8 +2457,6 @@ _OutboundFormValidation _validateOutboundForm({
}) {
var isValid = true;
String? writerError;
String? transactionNumberError;
String? approvalNumberError;
String? customerError;
String? warehouseError;
String? statusError;
@@ -2448,18 +2473,6 @@ _OutboundFormValidation _validateOutboundForm({
isValid = false;
}
final transactionNumber = transactionNumberController.text.trim();
if (transactionNumberRequired && transactionNumber.isEmpty) {
transactionNumberError = '거래번호를 입력하세요.';
isValid = false;
}
final approvalNumber = approvalNumberController.text.trim();
if (approvalNumberRequired && approvalNumber.isEmpty) {
approvalNumberError = '결재번호를 입력하세요.';
isValid = false;
}
if (warehouseSelection == null) {
warehouseError = '창고를 선택하세요.';
isValid = false;
@@ -2523,8 +2536,6 @@ _OutboundFormValidation _validateOutboundForm({
return _OutboundFormValidation(
isValid: isValid,
writerError: writerError,
transactionNumberError: transactionNumberError,
approvalNumberError: approvalNumberError,
customerError: customerError,
warehouseError: warehouseError,
statusError: statusError,
@@ -2536,8 +2547,6 @@ class _OutboundFormValidation {
const _OutboundFormValidation({
required this.isValid,
this.writerError,
this.transactionNumberError,
this.approvalNumberError,
this.customerError,
this.warehouseError,
this.statusError,
@@ -2546,8 +2555,6 @@ class _OutboundFormValidation {
final bool isValid;
final String? writerError;
final String? transactionNumberError;
final String? approvalNumberError;
final String? customerError;
final String? warehouseError;
final String? statusError;

View File

@@ -43,6 +43,15 @@ class OutboundDetailView extends StatelessWidget {
value: dateFormatter.format(record.processedAt),
),
_DetailChip(label: '창고', value: record.warehouse),
if (record.warehouseCode != null &&
record.warehouseCode!.trim().isNotEmpty)
_DetailChip(label: '창고 코드', value: record.warehouseCode!),
if (record.warehouseZipcode != null &&
record.warehouseZipcode!.trim().isNotEmpty)
_DetailChip(label: '창고 우편번호', value: record.warehouseZipcode!),
if (record.warehouseAddress != null &&
record.warehouseAddress!.trim().isNotEmpty)
_DetailChip(label: '창고 주소', value: record.warehouseAddress!),
_DetailChip(label: '트랜잭션 유형', value: record.transactionType),
_DetailChip(label: '상태', value: record.status),
_DetailChip(label: '작성자', value: record.writer),

View File

@@ -12,6 +12,9 @@ class RentalRecord {
required this.processedAt,
required this.warehouse,
this.warehouseId,
this.warehouseCode,
this.warehouseZipcode,
this.warehouseAddress,
required this.status,
this.statusId,
required this.writer,
@@ -34,6 +37,9 @@ class RentalRecord {
processedAt: transaction.transactionDate,
warehouse: transaction.warehouse.name,
warehouseId: transaction.warehouse.id,
warehouseCode: transaction.warehouse.code,
warehouseZipcode: transaction.warehouse.zipcode,
warehouseAddress: transaction.warehouse.addressLine,
status: transaction.status.name,
statusId: transaction.status.id,
writer: transaction.createdBy.name,
@@ -59,6 +65,9 @@ class RentalRecord {
final DateTime processedAt;
final String warehouse;
final int? warehouseId;
final String? warehouseCode;
final String? warehouseZipcode;
final String? warehouseAddress;
final String status;
final int? statusId;
final String writer;

View File

@@ -80,6 +80,10 @@ class _RentalPageState extends State<RentalPage> {
String? _errorMessage;
Set<int> _processingTransactionIds = {};
String? _routeSelectedNumber;
String? _pendingDetailNumber;
bool _suppressNextRouteSelection = false;
late List<String> _statusOptions;
final Map<String, LookupItem> _statusLookup = {};
LookupItem? _rentTransactionType;
@@ -193,6 +197,26 @@ class _RentalPageState extends State<RentalPage> {
);
_refreshSelection();
});
final detailNumber = _pendingDetailNumber;
if (detailNumber != null) {
_pendingDetailNumber = null;
RentalRecord? matched;
for (final record in _records) {
if (record.transactionNumber == detailNumber) {
matched = record;
break;
}
}
if (matched != null) {
final target = matched;
WidgetsBinding.instance.addPostFrameCallback((_) {
if (!mounted) {
return;
}
_showDetailDialog(target);
});
}
}
}
@override
@@ -525,10 +549,7 @@ class _RentalPageState extends State<RentalPage> {
),
onRowTap: (rowIndex) {
final record = visibleRecords[rowIndex];
setState(() {
_selectedRecord = record;
});
_showDetailDialog(record);
_selectRecord(record, openDetail: true);
},
),
),
@@ -708,6 +729,24 @@ class _RentalPageState extends State<RentalPage> {
return records;
}
void _selectRecord(RentalRecord? record, {bool openDetail = false}) {
if (!mounted) return;
setState(() {
_selectedRecord = record;
});
final selectedNumber = record?.transactionNumber;
_routeSelectedNumber = selectedNumber;
_updateRoute(selected: selectedNumber);
if (openDetail && record != null) {
WidgetsBinding.instance.addPostFrameCallback((_) {
if (!mounted) {
return;
}
_showDetailDialog(record);
});
}
}
List<String> _buildRecordRow(RentalRecord record) {
return [
record.number.split('-').last,
@@ -805,18 +844,14 @@ class _RentalPageState extends State<RentalPage> {
Future<void> _handleCreate() async {
final record = await _showRentalFormDialog();
if (record != null) {
setState(() {
_selectedRecord = record;
});
_selectRecord(record, openDetail: true);
}
}
Future<void> _handleEdit(RentalRecord record) async {
final updated = await _showRentalFormDialog(initial: record);
if (updated != null) {
setState(() {
_selectedRecord = updated;
});
_selectRecord(updated, openDetail: true);
}
}
@@ -1113,6 +1148,10 @@ class _RentalPageState extends State<RentalPage> {
(params['order'] ?? '').toLowerCase() == 'asc';
final pageSizeParam = int.tryParse(params['page_size'] ?? '');
final pageParam = int.tryParse(params['page'] ?? '');
final selectedParam = params['selected']?.trim();
final selectedNumber = selectedParam == null || selectedParam.isEmpty
? null
: selectedParam;
final warehouseId = warehouseIdParam;
final warehouseLabel = warehouseParam != null && warehouseParam.isNotEmpty
@@ -1166,6 +1205,13 @@ class _RentalPageState extends State<RentalPage> {
_sortAscending = resolvedSortAscending;
_pageSize = pageSize;
_currentPage = page;
_routeSelectedNumber = selectedNumber;
if (_suppressNextRouteSelection) {
_pendingDetailNumber = null;
} else {
_pendingDetailNumber = selectedNumber;
}
_suppressNextRouteSelection = false;
_refreshSelection();
}
@@ -1181,9 +1227,7 @@ class _RentalPageState extends State<RentalPage> {
void _goToPage(int page) {
final totalItems = _result?.total ?? _filteredRecords.length;
final totalPages = _calculateTotalPages(totalItems);
final int target = page < 1
? 1
: (page > totalPages ? totalPages : page);
final int target = page < 1 ? 1 : (page > totalPages ? totalPages : page);
if (target == _currentPage) {
return;
}
@@ -1201,6 +1245,16 @@ class _RentalPageState extends State<RentalPage> {
return;
}
final selectedNumber = _routeSelectedNumber;
if (selectedNumber != null) {
for (final record in filtered) {
if (record.transactionNumber == selectedNumber) {
_selectedRecord = record;
return;
}
}
}
final current = _selectedRecord;
if (current != null) {
RentalRecord? matched;
@@ -1222,7 +1276,7 @@ class _RentalPageState extends State<RentalPage> {
_selectedRecord = filtered.first;
}
void _updateRoute({int? page, int? pageSize}) {
void _updateRoute({int? page, int? pageSize, String? selected}) {
if (!mounted) return;
final targetPage = page ?? _currentPage;
final targetPageSize = pageSize ?? _pageSize;
@@ -1276,6 +1330,10 @@ class _RentalPageState extends State<RentalPage> {
if (targetPageSize != RentalTableSpec.pageSizeOptions.first) {
params['page_size'] = targetPageSize.toString();
}
final selectedNumber = selected ?? _selectedRecord?.transactionNumber;
if (selectedNumber != null && selectedNumber.isNotEmpty) {
params['selected'] = selectedNumber;
}
final uri = Uri(
path: widget.routeUri.path,
@@ -1289,6 +1347,7 @@ class _RentalPageState extends State<RentalPage> {
if (router == null) {
return;
}
_suppressNextRouteSelection = true;
router.go(newLocation);
}
@@ -1488,12 +1547,8 @@ class _RentalPageState extends State<RentalPage> {
final transactionTypeController = TextEditingController(
text: _transactionTypeForRental(rentalTypeValue.value),
);
final transactionNumberController = TextEditingController(
text: initial?.transactionNumber ?? '',
);
final approvalNumberController = TextEditingController(
text: initial?.raw?.approval?.approvalNo ?? '',
);
final assignedTransactionNo = initial?.transactionNumber;
final assignedApprovalNo = initial?.raw?.approval?.approvalNo;
final approvalNoteController = TextEditingController();
final drafts =
@@ -1505,8 +1560,6 @@ class _RentalPageState extends State<RentalPage> {
RentalRecord? result;
String? writerError;
String? transactionNumberError;
String? approvalNumberError;
String? customerError;
String? warehouseError;
String? statusError;
@@ -1538,10 +1591,6 @@ class _RentalPageState extends State<RentalPage> {
writerController: writerController,
writerSelection: writerSelection,
requireWriterSelection: initial == null,
transactionNumberController: transactionNumberController,
transactionNumberRequired: initial == null,
approvalNumberController: approvalNumberController,
approvalNumberRequired: initial == null,
warehouseSelection: warehouseSelection,
statusValue: statusValue.value,
selectedCustomers: customerSelection
@@ -1552,8 +1601,6 @@ class _RentalPageState extends State<RentalPage> {
);
writerError = validation.writerError;
transactionNumberError = validation.transactionNumberError;
approvalNumberError = validation.approvalNumberError;
customerError = validation.customerError;
warehouseError = validation.warehouseError;
statusError = validation.statusError;
@@ -1599,8 +1646,6 @@ class _RentalPageState extends State<RentalPage> {
final remarkText = remarkController.text.trim();
final remarkValue = remarkText.isEmpty ? null : remarkText;
final transactionNoValue = transactionNumberController.text.trim();
final approvalNoValue = approvalNumberController.text.trim();
final approvalNoteValue = approvalNoteController.text.trim();
final transactionId = initial?.id;
final initialRecord = initial;
@@ -1693,7 +1738,10 @@ class _RentalPageState extends State<RentalPage> {
if (!mounted) {
return;
}
SuperportToast.success(context, '대여 정보가 수정되었습니다.');
SuperportToast.success(
context,
'대여 정보가 수정되었습니다. (${updated.transactionNumber})',
);
navigator.pop();
return;
}
@@ -1728,7 +1776,6 @@ class _RentalPageState extends State<RentalPage> {
final transactionTypeId = selectedLookup.id;
final created = await controller.createTransaction(
StockTransactionCreateInput(
transactionNo: transactionNoValue,
transactionTypeId: transactionTypeId,
transactionStatusId: statusItem.id,
warehouseId: warehouseId,
@@ -1739,7 +1786,6 @@ class _RentalPageState extends State<RentalPage> {
lines: createLines,
customers: createCustomers,
approval: StockTransactionApprovalInput(
approvalNo: approvalNoValue,
requestedById: createdById,
note: approvalNoteValue.isEmpty ? null : approvalNoteValue,
),
@@ -1750,7 +1796,10 @@ class _RentalPageState extends State<RentalPage> {
if (!mounted) {
return;
}
SuperportToast.success(context, '대여가 등록되었습니다.');
SuperportToast.success(
context,
'대여가 등록되었습니다. (${created.transactionNumber})',
);
navigator.pop();
} catch (error) {
updateSaving(false);
@@ -1918,70 +1967,26 @@ class _RentalPageState extends State<RentalPage> {
),
SizedBox(
width: 240,
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
_FormFieldLabel(
label: '트랜잭션번호',
child: ShadInput(
controller: transactionNumberController,
readOnly: initial != null,
enabled: initial == null,
placeholder: const Text('예: RENT-2024-0001'),
onChanged: (_) {
if (transactionNumberError != null) {
setState(() {
transactionNumberError = null;
});
}
},
),
),
if (transactionNumberError != null)
Padding(
padding: const EdgeInsets.only(top: 6),
child: Text(
transactionNumberError!,
style: theme.textTheme.small.copyWith(
color: theme.colorScheme.destructive,
),
),
),
],
child: _FormFieldLabel(
label: '트랜잭션번호',
child: Text(
assignedTransactionNo ?? '저장 시 자동 생성',
style: assignedTransactionNo == null
? theme.textTheme.muted
: theme.textTheme.p,
),
),
),
SizedBox(
width: 240,
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
_FormFieldLabel(
label: '결재번호',
child: ShadInput(
controller: approvalNumberController,
readOnly: initial != null,
enabled: initial == null,
placeholder: const Text('예: APP-2024-0001'),
onChanged: (_) {
if (approvalNumberError != null) {
setState(() {
approvalNumberError = null;
});
}
},
),
),
if (approvalNumberError != null)
Padding(
padding: const EdgeInsets.only(top: 6),
child: Text(
approvalNumberError!,
style: theme.textTheme.small.copyWith(
color: theme.colorScheme.destructive,
),
),
),
],
child: _FormFieldLabel(
label: '결재번호',
child: Text(
assignedApprovalNo ?? '저장 시 자동 생성',
style: assignedApprovalNo == null
? theme.textTheme.muted
: theme.textTheme.p,
),
),
),
SizedBox(
@@ -2247,8 +2252,6 @@ class _RentalPageState extends State<RentalPage> {
writerController.dispose();
remarkController.dispose();
transactionTypeController.dispose();
transactionNumberController.dispose();
approvalNumberController.dispose();
approvalNoteController.dispose();
processedAt.dispose();
returnDue.dispose();
@@ -2523,10 +2526,6 @@ _RentalFormValidation _validateRentalForm({
required TextEditingController writerController,
required InventoryEmployeeSuggestion? writerSelection,
required bool requireWriterSelection,
required TextEditingController transactionNumberController,
required bool transactionNumberRequired,
required TextEditingController approvalNumberController,
required bool approvalNumberRequired,
required InventoryWarehouseOption? warehouseSelection,
required String statusValue,
required List<InventoryCustomerOption> selectedCustomers,
@@ -2535,8 +2534,6 @@ _RentalFormValidation _validateRentalForm({
}) {
var isValid = true;
String? writerError;
String? transactionNumberError;
String? approvalNumberError;
String? customerError;
String? warehouseError;
String? statusError;
@@ -2553,18 +2550,6 @@ _RentalFormValidation _validateRentalForm({
isValid = false;
}
final transactionNumber = transactionNumberController.text.trim();
if (transactionNumberRequired && transactionNumber.isEmpty) {
transactionNumberError = '거래번호를 입력하세요.';
isValid = false;
}
final approvalNumber = approvalNumberController.text.trim();
if (approvalNumberRequired && approvalNumber.isEmpty) {
approvalNumberError = '결재번호를 입력하세요.';
isValid = false;
}
if (warehouseSelection == null) {
warehouseError = '창고를 선택하세요.';
isValid = false;
@@ -2628,8 +2613,6 @@ _RentalFormValidation _validateRentalForm({
return _RentalFormValidation(
isValid: isValid,
writerError: writerError,
transactionNumberError: transactionNumberError,
approvalNumberError: approvalNumberError,
customerError: customerError,
warehouseError: warehouseError,
statusError: statusError,
@@ -2641,8 +2624,6 @@ class _RentalFormValidation {
const _RentalFormValidation({
required this.isValid,
this.writerError,
this.transactionNumberError,
this.approvalNumberError,
this.customerError,
this.warehouseError,
this.statusError,
@@ -2651,8 +2632,6 @@ class _RentalFormValidation {
final bool isValid;
final String? writerError;
final String? transactionNumberError;
final String? approvalNumberError;
final String? customerError;
final String? warehouseError;
final String? statusError;

View File

@@ -1,7 +1,6 @@
/// 재고 트랜잭션 생성 입력 모델.
class StockTransactionCreateInput {
StockTransactionCreateInput({
this.transactionNo,
required this.transactionTypeId,
required this.transactionStatusId,
required this.warehouseId,
@@ -14,7 +13,6 @@ class StockTransactionCreateInput {
this.approval,
});
final String? transactionNo;
final int transactionTypeId;
final int transactionStatusId;
final int warehouseId;
@@ -29,8 +27,6 @@ class StockTransactionCreateInput {
Map<String, dynamic> toPayload() {
final sanitizedNote = note?.trim();
return {
if (transactionNo != null && transactionNo!.trim().isNotEmpty)
'transaction_no': transactionNo,
'transaction_type_id': transactionTypeId,
'transaction_status_id': transactionStatusId,
'warehouse_id': warehouseId,
@@ -213,23 +209,21 @@ class StockTransactionListFilter {
/// 재고 트랜잭션 생성 시 결재(Approval) 정보를 담는 입력 모델.
class StockTransactionApprovalInput {
StockTransactionApprovalInput({
required this.approvalNo,
required this.requestedById,
this.approvalStatusId,
this.note,
});
final String approvalNo;
final int requestedById;
final int? approvalStatusId;
final String? note;
Map<String, dynamic> toJson() {
final trimmedNote = note?.trim();
return {
'approval_no': approvalNo,
if (approvalStatusId != null) 'approval_status_id': approvalStatusId,
'requested_by_id': requestedById,
'note': note?.trim(),
if (trimmedNote != null && trimmedNote.isNotEmpty) 'note': trimmedNote,
};
}
}