API v4 계약 반영하고 보고서·입출고 화면 실연동 강화
This commit is contained in:
15
lib/core/services/file_saver.dart
Normal file
15
lib/core/services/file_saver.dart
Normal file
@@ -0,0 +1,15 @@
|
||||
import 'dart:typed_data';
|
||||
|
||||
import 'file_saver_stub.dart' if (dart.library.html) 'file_saver_web.dart';
|
||||
|
||||
/// 바이트 데이터를 로컬 파일로 저장한다.
|
||||
Future<void> saveFileBytes({
|
||||
required Uint8List bytes,
|
||||
required String filename,
|
||||
required String mimeType,
|
||||
}) async {
|
||||
assert(filename.isNotEmpty, 'filename은 비어 있을 수 없습니다.');
|
||||
assert(bytes.isNotEmpty, 'bytes는 비어 있을 수 없습니다.');
|
||||
|
||||
await saveFileBytesImpl(bytes: bytes, filename: filename, mimeType: mimeType);
|
||||
}
|
||||
10
lib/core/services/file_saver_stub.dart
Normal file
10
lib/core/services/file_saver_stub.dart
Normal file
@@ -0,0 +1,10 @@
|
||||
import 'dart:typed_data';
|
||||
|
||||
/// 웹 외 플랫폼에서 파일 저장이 호출되면 예외를 발생시킨다.
|
||||
Future<void> saveFileBytesImpl({
|
||||
required Uint8List bytes,
|
||||
required String filename,
|
||||
required String mimeType,
|
||||
}) async {
|
||||
throw UnsupportedError('현재 플랫폼에서는 파일 저장을 지원하지 않습니다.');
|
||||
}
|
||||
20
lib/core/services/file_saver_web.dart
Normal file
20
lib/core/services/file_saver_web.dart
Normal file
@@ -0,0 +1,20 @@
|
||||
import 'dart:typed_data';
|
||||
|
||||
import 'package:web/web.dart' as web;
|
||||
|
||||
/// 웹 환경에서 Anchor 요소를 사용해 파일 저장을 트리거한다.
|
||||
Future<void> saveFileBytesImpl({
|
||||
required Uint8List bytes,
|
||||
required String filename,
|
||||
required String mimeType,
|
||||
}) async {
|
||||
final dataUrl = Uri.dataFromBytes(bytes, mimeType: mimeType).toString();
|
||||
final anchor = web.document.createElement('a') as web.HTMLAnchorElement
|
||||
..href = dataUrl
|
||||
..download = filename;
|
||||
anchor.style.display = 'none';
|
||||
|
||||
web.document.body?.append(anchor);
|
||||
anchor.click();
|
||||
anchor.remove();
|
||||
}
|
||||
@@ -214,7 +214,9 @@ class ApprovalStepDto {
|
||||
(json['approver'] as Map<String, dynamic>? ?? const {}),
|
||||
),
|
||||
status: ApprovalStatusDto.fromJson(
|
||||
(json['status'] as Map<String, dynamic>? ?? const {}),
|
||||
(json['status'] as Map<String, dynamic>? ??
|
||||
json['step_status'] as Map<String, dynamic>? ??
|
||||
const {}),
|
||||
),
|
||||
assignedAt: _parseDate(json['assigned_at']) ?? DateTime.now(),
|
||||
decidedAt: _parseDate(json['decided_at']),
|
||||
@@ -263,7 +265,12 @@ class ApprovalHistoryDto {
|
||||
return ApprovalHistoryDto(
|
||||
id: json['id'] as int?,
|
||||
action: ApprovalActionDto.fromJson(
|
||||
(json['action'] as Map<String, dynamic>? ?? const {}),
|
||||
json['action'] is Map<String, dynamic>
|
||||
? json['action'] as Map<String, dynamic>
|
||||
: {
|
||||
'id': json['approval_action_id'],
|
||||
'name': json['approval_action_name'],
|
||||
},
|
||||
),
|
||||
fromStatus: json['from_status'] is Map<String, dynamic>
|
||||
? ApprovalStatusDto.fromJson(
|
||||
|
||||
@@ -175,22 +175,199 @@ class ApprovalRepositoryRemote implements ApprovalRepository {
|
||||
Map<String, dynamic> body,
|
||||
) {
|
||||
final data = body['data'];
|
||||
if (data is Map<String, dynamic>) {
|
||||
if (data['approval'] is Map<String, dynamic>) {
|
||||
return data['approval'] as Map<String, dynamic>;
|
||||
final dataMap = data is Map<String, dynamic> ? data : null;
|
||||
Map<String, dynamic>? approval = _selectApprovalPayload(dataMap);
|
||||
approval ??= _selectApprovalPayload(body);
|
||||
if (approval == null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
final merged = Map<String, dynamic>.from(approval);
|
||||
if (dataMap != null) {
|
||||
final steps = _mergeStepsPayload(
|
||||
existing: merged['steps'],
|
||||
data: dataMap,
|
||||
);
|
||||
if (steps != null) {
|
||||
merged['steps'] = steps;
|
||||
}
|
||||
if (data['approval_data'] is Map<String, dynamic>) {
|
||||
return data['approval_data'] as Map<String, dynamic>;
|
||||
}
|
||||
final hasStatus =
|
||||
data.containsKey('status') || data.containsKey('approval_status');
|
||||
if (data.containsKey('approval_no') && hasStatus) {
|
||||
return data;
|
||||
|
||||
final histories = _mergeHistoriesPayload(
|
||||
existing: merged['histories'],
|
||||
data: dataMap,
|
||||
);
|
||||
if (histories != null) {
|
||||
merged['histories'] = histories;
|
||||
}
|
||||
}
|
||||
if (body['approval'] is Map<String, dynamic>) {
|
||||
return body['approval'] as Map<String, dynamic>;
|
||||
return merged;
|
||||
}
|
||||
|
||||
Map<String, dynamic>? _selectApprovalPayload(Map<String, dynamic>? source) {
|
||||
if (source == null) {
|
||||
return null;
|
||||
}
|
||||
if (source['approval'] is Map<String, dynamic>) {
|
||||
return Map<String, dynamic>.from(
|
||||
source['approval'] as Map<String, dynamic>,
|
||||
);
|
||||
}
|
||||
if (source['approval_data'] is Map<String, dynamic>) {
|
||||
return Map<String, dynamic>.from(
|
||||
source['approval_data'] as Map<String, dynamic>,
|
||||
);
|
||||
}
|
||||
final hasStatus =
|
||||
source.containsKey('status') || source.containsKey('approval_status');
|
||||
if (source.containsKey('approval_no') && hasStatus) {
|
||||
return Map<String, dynamic>.from(source);
|
||||
}
|
||||
if (source['approval'] == null && source['data'] is Map<String, dynamic>) {
|
||||
return _selectApprovalPayload(source['data'] as Map<String, dynamic>);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
List<Map<String, dynamic>>? _mergeStepsPayload({
|
||||
required dynamic existing,
|
||||
required Map<String, dynamic> data,
|
||||
}) {
|
||||
final steps = <Map<String, dynamic>>[];
|
||||
|
||||
void upsert(Map<String, dynamic> step) {
|
||||
final id = step['id'] as int?;
|
||||
final order = step['step_order'] as int?;
|
||||
final index = steps.indexWhere((element) {
|
||||
final elementId = element['id'] as int?;
|
||||
if (elementId != null && id != null) {
|
||||
return elementId == id;
|
||||
}
|
||||
if (order != null) {
|
||||
return element['step_order'] == order;
|
||||
}
|
||||
return false;
|
||||
});
|
||||
final copy = Map<String, dynamic>.from(step);
|
||||
if (index >= 0) {
|
||||
steps[index] = copy;
|
||||
} else {
|
||||
steps.add(copy);
|
||||
}
|
||||
}
|
||||
|
||||
if (existing is List) {
|
||||
for (final item in existing) {
|
||||
if (item is Map<String, dynamic>) {
|
||||
upsert(item);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
final responseSteps = data['steps'];
|
||||
if (responseSteps is List) {
|
||||
for (final item in responseSteps) {
|
||||
if (item is Map<String, dynamic>) {
|
||||
upsert(item);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (data['step'] is Map<String, dynamic>) {
|
||||
upsert(data['step'] as Map<String, dynamic>);
|
||||
}
|
||||
if (data['next_step'] is Map<String, dynamic>) {
|
||||
upsert(data['next_step'] as Map<String, dynamic>);
|
||||
}
|
||||
|
||||
if (steps.isEmpty) {
|
||||
return existing is List
|
||||
? existing
|
||||
.whereType<Map<String, dynamic>>()
|
||||
.map((step) => Map<String, dynamic>.from(step))
|
||||
.toList()
|
||||
: null;
|
||||
}
|
||||
|
||||
steps.sort((a, b) {
|
||||
final orderA = a['step_order'] as int? ?? 0;
|
||||
final orderB = b['step_order'] as int? ?? 0;
|
||||
return orderA.compareTo(orderB);
|
||||
});
|
||||
return steps;
|
||||
}
|
||||
|
||||
List<Map<String, dynamic>>? _mergeHistoriesPayload({
|
||||
required dynamic existing,
|
||||
required Map<String, dynamic> data,
|
||||
}) {
|
||||
final histories = <Map<String, dynamic>>[];
|
||||
|
||||
void append(Map<String, dynamic> history) {
|
||||
histories.add(Map<String, dynamic>.from(history));
|
||||
}
|
||||
|
||||
if (existing is List) {
|
||||
for (final item in existing) {
|
||||
if (item is Map<String, dynamic>) {
|
||||
append(item);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
final responseHistories = data['histories'];
|
||||
if (responseHistories is List) {
|
||||
for (final item in responseHistories) {
|
||||
if (item is Map<String, dynamic>) {
|
||||
append(item);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (data['history'] is Map<String, dynamic>) {
|
||||
append(data['history'] as Map<String, dynamic>);
|
||||
}
|
||||
|
||||
if (histories.isEmpty) {
|
||||
return existing is List
|
||||
? existing
|
||||
.whereType<Map<String, dynamic>>()
|
||||
.map((history) => Map<String, dynamic>.from(history))
|
||||
.toList()
|
||||
: null;
|
||||
}
|
||||
|
||||
DateTime? parseTime(Map<String, dynamic> json) {
|
||||
String? read(dynamic value) {
|
||||
if (value is String && value.trim().isNotEmpty) {
|
||||
return value.trim();
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
final raw =
|
||||
read(json['action_at']) ??
|
||||
read(json['created_at']) ??
|
||||
read(json['updated_at']);
|
||||
if (raw == null) {
|
||||
return null;
|
||||
}
|
||||
return DateTime.tryParse(raw);
|
||||
}
|
||||
|
||||
histories.sort((a, b) {
|
||||
final timeA = parseTime(a);
|
||||
final timeB = parseTime(b);
|
||||
if (timeA == null && timeB == null) {
|
||||
return 0;
|
||||
}
|
||||
if (timeA == null) {
|
||||
return 1;
|
||||
}
|
||||
if (timeB == null) {
|
||||
return -1;
|
||||
}
|
||||
return timeA.compareTo(timeB);
|
||||
});
|
||||
return histories;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -31,10 +31,8 @@ class ApprovalHistoryRepositoryRemote implements ApprovalHistoryRepository {
|
||||
query: {
|
||||
'page': page,
|
||||
'page_size': pageSize,
|
||||
if (query != null && query.isNotEmpty) 'q': query,
|
||||
if (action != null && action.isNotEmpty) 'action': action,
|
||||
if (from != null) 'from': from.toIso8601String(),
|
||||
if (to != null) 'to': to.toIso8601String(),
|
||||
if (from != null) 'action_from': from.toIso8601String(),
|
||||
if (to != null) 'action_to': to.toIso8601String(),
|
||||
},
|
||||
options: Options(responseType: ResponseType.json),
|
||||
);
|
||||
|
||||
@@ -1289,8 +1289,12 @@ class _InboundPageState extends State<InboundPage> {
|
||||
);
|
||||
final remarkController = TextEditingController(text: initial?.remark ?? '');
|
||||
final transactionNumberController = TextEditingController(
|
||||
text: initial?.transactionNumber ?? '저장 시 자동 생성',
|
||||
text: initial?.transactionNumber ?? '',
|
||||
);
|
||||
final approvalNumberController = TextEditingController(
|
||||
text: initial?.raw?.approval?.approvalNo ?? '',
|
||||
);
|
||||
final approvalNoteController = TextEditingController();
|
||||
final transactionTypeValue =
|
||||
initial?.transactionType ??
|
||||
_transactionTypeLookup?.name ??
|
||||
@@ -1311,6 +1315,8 @@ class _InboundPageState extends State<InboundPage> {
|
||||
};
|
||||
|
||||
String? writerError;
|
||||
String? transactionNumberError;
|
||||
String? approvalNumberError;
|
||||
String? warehouseError;
|
||||
String? statusError;
|
||||
String? headerNotice;
|
||||
@@ -1339,12 +1345,18 @@ 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;
|
||||
@@ -1388,6 +1400,9 @@ 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;
|
||||
final initialRecord = initial;
|
||||
@@ -1475,6 +1490,7 @@ class _InboundPageState extends State<InboundPage> {
|
||||
.toList(growable: false);
|
||||
final created = await controller.createTransaction(
|
||||
StockTransactionCreateInput(
|
||||
transactionNo: transactionNoValue,
|
||||
transactionTypeId: transactionTypeLookup.id,
|
||||
transactionStatusId: statusItem.id,
|
||||
warehouseId: warehouseId,
|
||||
@@ -1482,6 +1498,11 @@ class _InboundPageState extends State<InboundPage> {
|
||||
createdById: createdById,
|
||||
note: remarkValue,
|
||||
lines: createLines,
|
||||
approval: StockTransactionApprovalInput(
|
||||
approvalNo: approvalNoValue,
|
||||
requestedById: createdById,
|
||||
note: approvalNoteValue.isEmpty ? null : approvalNoteValue,
|
||||
),
|
||||
),
|
||||
);
|
||||
result = created;
|
||||
@@ -1635,10 +1656,41 @@ class _InboundPageState extends State<InboundPage> {
|
||||
width: 240,
|
||||
child: SuperportFormField(
|
||||
label: '트랜잭션번호',
|
||||
required: true,
|
||||
errorText: transactionNumberError,
|
||||
child: ShadInput(
|
||||
controller: transactionNumberController,
|
||||
readOnly: true,
|
||||
enabled: false,
|
||||
readOnly: initial != null,
|
||||
enabled: initial == null,
|
||||
placeholder: const Text('예: IN-2024-0001'),
|
||||
onChanged: (_) {
|
||||
if (transactionNumberError != null) {
|
||||
setState(() {
|
||||
transactionNumberError = null;
|
||||
});
|
||||
}
|
||||
},
|
||||
),
|
||||
),
|
||||
),
|
||||
SizedBox(
|
||||
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;
|
||||
});
|
||||
}
|
||||
},
|
||||
),
|
||||
),
|
||||
),
|
||||
@@ -1680,6 +1732,16 @@ class _InboundPageState extends State<InboundPage> {
|
||||
),
|
||||
),
|
||||
),
|
||||
SizedBox(
|
||||
width: 500,
|
||||
child: SuperportFormField(
|
||||
label: '결재 메모',
|
||||
child: ShadInput(
|
||||
controller: approvalNoteController,
|
||||
maxLines: 2,
|
||||
),
|
||||
),
|
||||
),
|
||||
SizedBox(
|
||||
width: 500,
|
||||
child: SuperportFormField(
|
||||
@@ -1802,6 +1864,8 @@ class _InboundPageState extends State<InboundPage> {
|
||||
writerController.dispose();
|
||||
remarkController.dispose();
|
||||
transactionNumberController.dispose();
|
||||
approvalNumberController.dispose();
|
||||
approvalNoteController.dispose();
|
||||
transactionTypeController.dispose();
|
||||
processedAt.dispose();
|
||||
|
||||
@@ -2346,6 +2410,10 @@ _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,
|
||||
@@ -2353,6 +2421,8 @@ _InboundFormValidation _validateInboundForm({
|
||||
}) {
|
||||
var isValid = true;
|
||||
String? writerError;
|
||||
String? transactionNumberError;
|
||||
String? approvalNumberError;
|
||||
String? warehouseError;
|
||||
String? statusError;
|
||||
String? headerNotice;
|
||||
@@ -2368,6 +2438,18 @@ _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;
|
||||
@@ -2426,6 +2508,8 @@ _InboundFormValidation _validateInboundForm({
|
||||
return _InboundFormValidation(
|
||||
isValid: isValid,
|
||||
writerError: writerError,
|
||||
transactionNumberError: transactionNumberError,
|
||||
approvalNumberError: approvalNumberError,
|
||||
warehouseError: warehouseError,
|
||||
statusError: statusError,
|
||||
headerNotice: headerNotice,
|
||||
@@ -2441,6 +2525,8 @@ class _InboundFormValidation {
|
||||
const _InboundFormValidation({
|
||||
required this.isValid,
|
||||
this.writerError,
|
||||
this.transactionNumberError,
|
||||
this.approvalNumberError,
|
||||
this.warehouseError,
|
||||
this.statusError,
|
||||
this.headerNotice,
|
||||
@@ -2448,6 +2534,8 @@ class _InboundFormValidation {
|
||||
|
||||
final bool isValid;
|
||||
final String? writerError;
|
||||
final String? transactionNumberError;
|
||||
final String? approvalNumberError;
|
||||
final String? warehouseError;
|
||||
final String? statusError;
|
||||
final String? headerNotice;
|
||||
|
||||
@@ -1435,6 +1435,13 @@ 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 approvalNoteController = TextEditingController();
|
||||
|
||||
final drafts =
|
||||
initial?.items
|
||||
@@ -1448,6 +1455,8 @@ class _OutboundPageState extends State<OutboundPage> {
|
||||
};
|
||||
|
||||
String? writerError;
|
||||
String? transactionNumberError;
|
||||
String? approvalNumberError;
|
||||
String? customerError;
|
||||
String? warehouseError;
|
||||
String? statusError;
|
||||
@@ -1477,6 +1486,10 @@ 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
|
||||
@@ -1487,6 +1500,8 @@ class _OutboundPageState extends State<OutboundPage> {
|
||||
);
|
||||
|
||||
writerError = validation.writerError;
|
||||
transactionNumberError = validation.transactionNumberError;
|
||||
approvalNumberError = validation.approvalNumberError;
|
||||
customerError = validation.customerError;
|
||||
warehouseError = validation.warehouseError;
|
||||
statusError = validation.statusError;
|
||||
@@ -1531,6 +1546,9 @@ 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;
|
||||
|
||||
final lineDrafts = <TransactionLineDraft>[];
|
||||
@@ -1656,6 +1674,7 @@ class _OutboundPageState extends State<OutboundPage> {
|
||||
|
||||
final created = await controller.createTransaction(
|
||||
StockTransactionCreateInput(
|
||||
transactionNo: transactionNoValue,
|
||||
transactionTypeId: transactionTypeLookup.id,
|
||||
transactionStatusId: statusItem.id,
|
||||
warehouseId: warehouseId,
|
||||
@@ -1664,6 +1683,11 @@ class _OutboundPageState extends State<OutboundPage> {
|
||||
note: remarkValue,
|
||||
lines: createLines,
|
||||
customers: createCustomers,
|
||||
approval: StockTransactionApprovalInput(
|
||||
approvalNo: approvalNoValue,
|
||||
requestedById: createdById,
|
||||
note: approvalNoteValue.isEmpty ? null : approvalNoteValue,
|
||||
),
|
||||
),
|
||||
);
|
||||
result = created;
|
||||
@@ -1808,6 +1832,48 @@ class _OutboundPageState extends State<OutboundPage> {
|
||||
),
|
||||
),
|
||||
),
|
||||
SizedBox(
|
||||
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;
|
||||
});
|
||||
}
|
||||
},
|
||||
),
|
||||
),
|
||||
),
|
||||
SizedBox(
|
||||
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;
|
||||
});
|
||||
}
|
||||
},
|
||||
),
|
||||
),
|
||||
),
|
||||
SizedBox(
|
||||
width: 240,
|
||||
child: SuperportFormField(
|
||||
@@ -1846,6 +1912,16 @@ class _OutboundPageState extends State<OutboundPage> {
|
||||
),
|
||||
),
|
||||
),
|
||||
SizedBox(
|
||||
width: 500,
|
||||
child: SuperportFormField(
|
||||
label: '결재 메모',
|
||||
child: ShadInput(
|
||||
controller: approvalNoteController,
|
||||
maxLines: 2,
|
||||
),
|
||||
),
|
||||
),
|
||||
SizedBox(
|
||||
width: 360,
|
||||
child: SuperportFormField(
|
||||
@@ -2012,6 +2088,9 @@ class _OutboundPageState extends State<OutboundPage> {
|
||||
writerController.dispose();
|
||||
remarkController.dispose();
|
||||
transactionTypeController.dispose();
|
||||
transactionNumberController.dispose();
|
||||
approvalNumberController.dispose();
|
||||
approvalNoteController.dispose();
|
||||
processedAt.dispose();
|
||||
|
||||
return result;
|
||||
@@ -2460,6 +2539,10 @@ _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,
|
||||
@@ -2468,6 +2551,8 @@ _OutboundFormValidation _validateOutboundForm({
|
||||
}) {
|
||||
var isValid = true;
|
||||
String? writerError;
|
||||
String? transactionNumberError;
|
||||
String? approvalNumberError;
|
||||
String? customerError;
|
||||
String? warehouseError;
|
||||
String? statusError;
|
||||
@@ -2484,6 +2569,18 @@ _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;
|
||||
@@ -2547,6 +2644,8 @@ _OutboundFormValidation _validateOutboundForm({
|
||||
return _OutboundFormValidation(
|
||||
isValid: isValid,
|
||||
writerError: writerError,
|
||||
transactionNumberError: transactionNumberError,
|
||||
approvalNumberError: approvalNumberError,
|
||||
customerError: customerError,
|
||||
warehouseError: warehouseError,
|
||||
statusError: statusError,
|
||||
@@ -2558,6 +2657,8 @@ class _OutboundFormValidation {
|
||||
const _OutboundFormValidation({
|
||||
required this.isValid,
|
||||
this.writerError,
|
||||
this.transactionNumberError,
|
||||
this.approvalNumberError,
|
||||
this.customerError,
|
||||
this.warehouseError,
|
||||
this.statusError,
|
||||
@@ -2566,6 +2667,8 @@ class _OutboundFormValidation {
|
||||
|
||||
final bool isValid;
|
||||
final String? writerError;
|
||||
final String? transactionNumberError;
|
||||
final String? approvalNumberError;
|
||||
final String? customerError;
|
||||
final String? warehouseError;
|
||||
final String? statusError;
|
||||
|
||||
@@ -1411,6 +1411,13 @@ 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 approvalNoteController = TextEditingController();
|
||||
|
||||
final drafts =
|
||||
initial?.items
|
||||
@@ -1421,6 +1428,8 @@ class _RentalPageState extends State<RentalPage> {
|
||||
|
||||
RentalRecord? result;
|
||||
String? writerError;
|
||||
String? transactionNumberError;
|
||||
String? approvalNumberError;
|
||||
String? customerError;
|
||||
String? warehouseError;
|
||||
String? statusError;
|
||||
@@ -1452,6 +1461,10 @@ 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
|
||||
@@ -1462,6 +1475,8 @@ class _RentalPageState extends State<RentalPage> {
|
||||
);
|
||||
|
||||
writerError = validation.writerError;
|
||||
transactionNumberError = validation.transactionNumberError;
|
||||
approvalNumberError = validation.approvalNumberError;
|
||||
customerError = validation.customerError;
|
||||
warehouseError = validation.warehouseError;
|
||||
statusError = validation.statusError;
|
||||
@@ -1507,6 +1522,9 @@ 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;
|
||||
|
||||
@@ -1633,6 +1651,7 @@ class _RentalPageState extends State<RentalPage> {
|
||||
final transactionTypeId = selectedLookup.id;
|
||||
final created = await controller.createTransaction(
|
||||
StockTransactionCreateInput(
|
||||
transactionNo: transactionNoValue,
|
||||
transactionTypeId: transactionTypeId,
|
||||
transactionStatusId: statusItem.id,
|
||||
warehouseId: warehouseId,
|
||||
@@ -1642,6 +1661,11 @@ class _RentalPageState extends State<RentalPage> {
|
||||
expectedReturnDate: returnDue.value,
|
||||
lines: createLines,
|
||||
customers: createCustomers,
|
||||
approval: StockTransactionApprovalInput(
|
||||
approvalNo: approvalNoValue,
|
||||
requestedById: createdById,
|
||||
note: approvalNoteValue.isEmpty ? null : approvalNoteValue,
|
||||
),
|
||||
),
|
||||
);
|
||||
result = created;
|
||||
@@ -1815,6 +1839,74 @@ 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,
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
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,
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
SizedBox(
|
||||
width: 360,
|
||||
child: _FormFieldLabel(
|
||||
@@ -1939,6 +2031,16 @@ class _RentalPageState extends State<RentalPage> {
|
||||
),
|
||||
),
|
||||
),
|
||||
SizedBox(
|
||||
width: 500,
|
||||
child: _FormFieldLabel(
|
||||
label: '결재 메모',
|
||||
child: ShadInput(
|
||||
controller: approvalNoteController,
|
||||
maxLines: 2,
|
||||
),
|
||||
),
|
||||
),
|
||||
SizedBox(
|
||||
width: 500,
|
||||
child: _FormFieldLabel(
|
||||
@@ -2068,6 +2170,9 @@ class _RentalPageState extends State<RentalPage> {
|
||||
writerController.dispose();
|
||||
remarkController.dispose();
|
||||
transactionTypeController.dispose();
|
||||
transactionNumberController.dispose();
|
||||
approvalNumberController.dispose();
|
||||
approvalNoteController.dispose();
|
||||
processedAt.dispose();
|
||||
returnDue.dispose();
|
||||
|
||||
@@ -2543,6 +2648,10 @@ _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,
|
||||
@@ -2551,6 +2660,8 @@ _RentalFormValidation _validateRentalForm({
|
||||
}) {
|
||||
var isValid = true;
|
||||
String? writerError;
|
||||
String? transactionNumberError;
|
||||
String? approvalNumberError;
|
||||
String? customerError;
|
||||
String? warehouseError;
|
||||
String? statusError;
|
||||
@@ -2567,6 +2678,18 @@ _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;
|
||||
@@ -2630,6 +2753,8 @@ _RentalFormValidation _validateRentalForm({
|
||||
return _RentalFormValidation(
|
||||
isValid: isValid,
|
||||
writerError: writerError,
|
||||
transactionNumberError: transactionNumberError,
|
||||
approvalNumberError: approvalNumberError,
|
||||
customerError: customerError,
|
||||
warehouseError: warehouseError,
|
||||
statusError: statusError,
|
||||
@@ -2641,6 +2766,8 @@ class _RentalFormValidation {
|
||||
const _RentalFormValidation({
|
||||
required this.isValid,
|
||||
this.writerError,
|
||||
this.transactionNumberError,
|
||||
this.approvalNumberError,
|
||||
this.customerError,
|
||||
this.warehouseError,
|
||||
this.statusError,
|
||||
@@ -2649,6 +2776,8 @@ class _RentalFormValidation {
|
||||
|
||||
final bool isValid;
|
||||
final String? writerError;
|
||||
final String? transactionNumberError;
|
||||
final String? approvalNumberError;
|
||||
final String? customerError;
|
||||
final String? warehouseError;
|
||||
final String? statusError;
|
||||
|
||||
@@ -37,6 +37,7 @@ class InventoryEmployeeAutocompleteField extends StatefulWidget {
|
||||
required this.onSuggestionSelected,
|
||||
this.onChanged,
|
||||
this.enabled = true,
|
||||
this.placeholder = '작성자 이름 또는 사번 검색',
|
||||
});
|
||||
|
||||
final TextEditingController controller;
|
||||
@@ -44,6 +45,7 @@ class InventoryEmployeeAutocompleteField extends StatefulWidget {
|
||||
final ValueChanged<InventoryEmployeeSuggestion?> onSuggestionSelected;
|
||||
final VoidCallback? onChanged;
|
||||
final bool enabled;
|
||||
final String placeholder;
|
||||
|
||||
@override
|
||||
State<InventoryEmployeeAutocompleteField> createState() =>
|
||||
@@ -193,7 +195,7 @@ class _InventoryEmployeeAutocompleteFieldState
|
||||
controller: textController,
|
||||
focusNode: focusNode,
|
||||
enabled: widget.enabled,
|
||||
placeholder: const Text('작성자 이름 또는 사번 검색'),
|
||||
placeholder: Text(widget.placeholder),
|
||||
onChanged: (_) => widget.onChanged?.call(),
|
||||
onSubmitted: (_) => onFieldSubmitted(),
|
||||
);
|
||||
|
||||
@@ -84,6 +84,7 @@ class StockTransactionRepositoryRemote implements StockTransactionRepository {
|
||||
Future<StockTransaction> submit(int id) async {
|
||||
final response = await _api.post<Map<String, dynamic>>(
|
||||
'$_basePath/$id/submit',
|
||||
data: {'id': id},
|
||||
options: Options(responseType: ResponseType.json),
|
||||
);
|
||||
return _parseSingle(response.data);
|
||||
@@ -93,6 +94,7 @@ class StockTransactionRepositoryRemote implements StockTransactionRepository {
|
||||
Future<StockTransaction> complete(int id) async {
|
||||
final response = await _api.post<Map<String, dynamic>>(
|
||||
'$_basePath/$id/complete',
|
||||
data: {'id': id},
|
||||
options: Options(responseType: ResponseType.json),
|
||||
);
|
||||
return _parseSingle(response.data);
|
||||
@@ -102,6 +104,7 @@ class StockTransactionRepositoryRemote implements StockTransactionRepository {
|
||||
Future<StockTransaction> approve(int id) async {
|
||||
final response = await _api.post<Map<String, dynamic>>(
|
||||
'$_basePath/$id/approve',
|
||||
data: {'id': id},
|
||||
options: Options(responseType: ResponseType.json),
|
||||
);
|
||||
return _parseSingle(response.data);
|
||||
@@ -111,6 +114,7 @@ class StockTransactionRepositoryRemote implements StockTransactionRepository {
|
||||
Future<StockTransaction> reject(int id) async {
|
||||
final response = await _api.post<Map<String, dynamic>>(
|
||||
'$_basePath/$id/reject',
|
||||
data: {'id': id},
|
||||
options: Options(responseType: ResponseType.json),
|
||||
);
|
||||
return _parseSingle(response.data);
|
||||
@@ -120,6 +124,7 @@ class StockTransactionRepositoryRemote implements StockTransactionRepository {
|
||||
Future<StockTransaction> cancel(int id) async {
|
||||
final response = await _api.post<Map<String, dynamic>>(
|
||||
'$_basePath/$id/cancel',
|
||||
data: {'id': id},
|
||||
options: Options(responseType: ResponseType.json),
|
||||
);
|
||||
return _parseSingle(response.data);
|
||||
|
||||
@@ -1,11 +1,8 @@
|
||||
import 'package:dio/dio.dart';
|
||||
import 'package:superport_v2/core/network/api_client.dart';
|
||||
import 'package:superport_v2/core/network/api_routes.dart';
|
||||
|
||||
import '../../domain/entities/stock_transaction.dart';
|
||||
import '../../domain/entities/stock_transaction_input.dart';
|
||||
import '../../domain/repositories/stock_transaction_repository.dart';
|
||||
import '../dtos/stock_transaction_dto.dart';
|
||||
|
||||
/// 재고 트랜잭션 고객 연결 API를 호출하는 원격 저장소 구현체.
|
||||
class TransactionCustomerRepositoryRemote
|
||||
@@ -19,11 +16,11 @@ class TransactionCustomerRepositoryRemote
|
||||
static const _customerPath = '${ApiRoutes.apiV1}/transaction-customers';
|
||||
|
||||
@override
|
||||
Future<List<StockTransactionCustomer>> addCustomers(
|
||||
Future<void> addCustomers(
|
||||
int transactionId,
|
||||
List<TransactionCustomerCreateInput> customers,
|
||||
) async {
|
||||
final response = await _api.post<Map<String, dynamic>>(
|
||||
await _api.post<void>(
|
||||
'$_basePath/$transactionId/customers',
|
||||
data: {
|
||||
'id': transactionId,
|
||||
@@ -31,17 +28,15 @@ class TransactionCustomerRepositoryRemote
|
||||
.map((customer) => customer.toJson())
|
||||
.toList(growable: false),
|
||||
},
|
||||
options: Options(responseType: ResponseType.json),
|
||||
);
|
||||
return _parseCustomers(response.data);
|
||||
}
|
||||
|
||||
@override
|
||||
Future<List<StockTransactionCustomer>> updateCustomers(
|
||||
Future<void> updateCustomers(
|
||||
int transactionId,
|
||||
List<TransactionCustomerUpdateInput> customers,
|
||||
) async {
|
||||
final response = await _api.patch<Map<String, dynamic>>(
|
||||
await _api.patch<void>(
|
||||
'$_basePath/$transactionId/customers',
|
||||
data: {
|
||||
'id': transactionId,
|
||||
@@ -49,38 +44,11 @@ class TransactionCustomerRepositoryRemote
|
||||
.map((customer) => customer.toJson())
|
||||
.toList(growable: false),
|
||||
},
|
||||
options: Options(responseType: ResponseType.json),
|
||||
);
|
||||
return _parseCustomers(response.data);
|
||||
}
|
||||
|
||||
@override
|
||||
Future<void> deleteCustomer(int customerLinkId) async {
|
||||
await _api.delete<void>('$_customerPath/$customerLinkId');
|
||||
}
|
||||
|
||||
List<StockTransactionCustomer> _parseCustomers(Map<String, dynamic>? body) {
|
||||
final data = _extractData(body);
|
||||
if (data['customers'] is List) {
|
||||
final dto = StockTransactionDto.fromJson(data);
|
||||
return dto.customers;
|
||||
}
|
||||
if (data.containsKey('id')) {
|
||||
final dto = StockTransactionDto.fromJson({
|
||||
'customers': [data],
|
||||
});
|
||||
return dto.customers;
|
||||
}
|
||||
return const [];
|
||||
}
|
||||
|
||||
Map<String, dynamic> _extractData(Map<String, dynamic>? body) {
|
||||
if (body == null) {
|
||||
return <String, dynamic>{};
|
||||
}
|
||||
if (body['data'] is Map<String, dynamic>) {
|
||||
return body['data'] as Map<String, dynamic>;
|
||||
}
|
||||
return body;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,11 +1,8 @@
|
||||
import 'package:dio/dio.dart';
|
||||
import 'package:superport_v2/core/network/api_client.dart';
|
||||
import 'package:superport_v2/core/network/api_routes.dart';
|
||||
|
||||
import '../../domain/entities/stock_transaction.dart';
|
||||
import '../../domain/entities/stock_transaction_input.dart';
|
||||
import '../../domain/repositories/stock_transaction_repository.dart';
|
||||
import '../dtos/stock_transaction_dto.dart';
|
||||
|
||||
/// 재고 트랜잭션 라인 API를 호출하는 원격 저장소 구현체.
|
||||
class TransactionLineRepositoryRemote implements TransactionLineRepository {
|
||||
@@ -18,35 +15,31 @@ class TransactionLineRepositoryRemote implements TransactionLineRepository {
|
||||
static const _linePath = '${ApiRoutes.apiV1}/transaction-lines';
|
||||
|
||||
@override
|
||||
Future<List<StockTransactionLine>> addLines(
|
||||
Future<void> addLines(
|
||||
int transactionId,
|
||||
List<TransactionLineCreateInput> lines,
|
||||
) async {
|
||||
final response = await _api.post<Map<String, dynamic>>(
|
||||
await _api.post<void>(
|
||||
'$_basePath/$transactionId/lines',
|
||||
data: {
|
||||
'id': transactionId,
|
||||
'lines': lines.map((line) => line.toJson()).toList(growable: false),
|
||||
},
|
||||
options: Options(responseType: ResponseType.json),
|
||||
);
|
||||
return _parseLines(response.data);
|
||||
}
|
||||
|
||||
@override
|
||||
Future<List<StockTransactionLine>> updateLines(
|
||||
Future<void> updateLines(
|
||||
int transactionId,
|
||||
List<TransactionLineUpdateInput> lines,
|
||||
) async {
|
||||
final response = await _api.patch<Map<String, dynamic>>(
|
||||
await _api.patch<void>(
|
||||
'$_basePath/$transactionId/lines',
|
||||
data: {
|
||||
'id': transactionId,
|
||||
'lines': lines.map((line) => line.toJson()).toList(growable: false),
|
||||
},
|
||||
options: Options(responseType: ResponseType.json),
|
||||
);
|
||||
return _parseLines(response.data);
|
||||
}
|
||||
|
||||
@override
|
||||
@@ -55,40 +48,10 @@ class TransactionLineRepositoryRemote implements TransactionLineRepository {
|
||||
}
|
||||
|
||||
@override
|
||||
Future<StockTransactionLine> restoreLine(int lineId) async {
|
||||
final response = await _api.post<Map<String, dynamic>>(
|
||||
Future<void> restoreLine(int lineId) async {
|
||||
await _api.post<void>(
|
||||
'$_linePath/$lineId/restore',
|
||||
options: Options(responseType: ResponseType.json),
|
||||
);
|
||||
final lines = _parseLines(response.data);
|
||||
if (lines.isEmpty) {
|
||||
throw StateError('복구된 라인 정보를 찾을 수 없습니다.');
|
||||
}
|
||||
return lines.first;
|
||||
}
|
||||
|
||||
List<StockTransactionLine> _parseLines(Map<String, dynamic>? body) {
|
||||
final data = _extractData(body);
|
||||
if (data['lines'] is List) {
|
||||
final dto = StockTransactionDto.fromJson(data);
|
||||
return dto.lines;
|
||||
}
|
||||
if (data.containsKey('id')) {
|
||||
final dto = StockTransactionDto.fromJson({
|
||||
'lines': [data],
|
||||
});
|
||||
return dto.lines;
|
||||
}
|
||||
return const [];
|
||||
}
|
||||
|
||||
Map<String, dynamic> _extractData(Map<String, dynamic>? body) {
|
||||
if (body == null) {
|
||||
return <String, dynamic>{};
|
||||
}
|
||||
if (body['data'] is Map<String, dynamic>) {
|
||||
return body['data'] as Map<String, dynamic>;
|
||||
}
|
||||
return body;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -11,6 +11,7 @@ class StockTransactionCreateInput {
|
||||
this.expectedReturnDate,
|
||||
this.lines = const [],
|
||||
this.customers = const [],
|
||||
this.approval,
|
||||
});
|
||||
|
||||
final String? transactionNo;
|
||||
@@ -23,6 +24,7 @@ class StockTransactionCreateInput {
|
||||
final DateTime? expectedReturnDate;
|
||||
final List<TransactionLineCreateInput> lines;
|
||||
final List<TransactionCustomerCreateInput> customers;
|
||||
final StockTransactionApprovalInput? approval;
|
||||
|
||||
Map<String, dynamic> toPayload() {
|
||||
return {
|
||||
@@ -42,6 +44,7 @@ class StockTransactionCreateInput {
|
||||
'customers': customers
|
||||
.map((customer) => customer.toJson())
|
||||
.toList(growable: false),
|
||||
if (approval != null) 'approval': approval!.toJson(),
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -200,3 +203,27 @@ 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() {
|
||||
return {
|
||||
'approval_no': approvalNo,
|
||||
if (approvalStatusId != null) 'approval_status_id': approvalStatusId,
|
||||
'requested_by_id': requestedById,
|
||||
if (note != null && note!.trim().isNotEmpty) 'note': note,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
@@ -47,13 +47,13 @@ abstract class StockTransactionRepository {
|
||||
/// 재고 트랜잭션 라인 저장소 인터페이스.
|
||||
abstract class TransactionLineRepository {
|
||||
/// 라인을 추가한다.
|
||||
Future<List<StockTransactionLine>> addLines(
|
||||
Future<void> addLines(
|
||||
int transactionId,
|
||||
List<TransactionLineCreateInput> lines,
|
||||
);
|
||||
|
||||
/// 라인 정보를 일괄 수정한다.
|
||||
Future<List<StockTransactionLine>> updateLines(
|
||||
Future<void> updateLines(
|
||||
int transactionId,
|
||||
List<TransactionLineUpdateInput> lines,
|
||||
);
|
||||
@@ -62,19 +62,19 @@ abstract class TransactionLineRepository {
|
||||
Future<void> deleteLine(int lineId);
|
||||
|
||||
/// 삭제된 라인을 복구한다.
|
||||
Future<StockTransactionLine> restoreLine(int lineId);
|
||||
Future<void> restoreLine(int lineId);
|
||||
}
|
||||
|
||||
/// 재고 트랜잭션 고객 연결 저장소 인터페이스.
|
||||
abstract class TransactionCustomerRepository {
|
||||
/// 고객 연결을 추가한다.
|
||||
Future<List<StockTransactionCustomer>> addCustomers(
|
||||
Future<void> addCustomers(
|
||||
int transactionId,
|
||||
List<TransactionCustomerCreateInput> customers,
|
||||
);
|
||||
|
||||
/// 고객 연결 정보를 수정한다.
|
||||
Future<List<StockTransactionCustomer>> updateCustomers(
|
||||
Future<void> updateCustomers(
|
||||
int transactionId,
|
||||
List<TransactionCustomerUpdateInput> customers,
|
||||
);
|
||||
|
||||
@@ -7,6 +7,7 @@ import 'package:go_router/go_router.dart';
|
||||
import 'package:shadcn_ui/shadcn_ui.dart';
|
||||
|
||||
import '../../../../core/constants/app_sections.dart';
|
||||
import '../../../../core/network/api_error.dart';
|
||||
import '../../../../core/network/failure.dart';
|
||||
import '../../../../core/permissions/permission_manager.dart';
|
||||
import '../../../../core/permissions/permission_resources.dart';
|
||||
@@ -73,9 +74,10 @@ class _LoginPageState extends State<LoginPage> {
|
||||
if (!mounted) return;
|
||||
final failure = Failure.from(error);
|
||||
final description = failure.describe();
|
||||
final message = description.isEmpty
|
||||
? '권한 정보를 불러오지 못했습니다. 잠시 후 다시 시도하세요.'
|
||||
: description;
|
||||
final hasApiDetails = failure.raw is ApiException;
|
||||
final message = hasApiDetails && description.isNotEmpty
|
||||
? description
|
||||
: '권한 정보를 불러오지 못했습니다. 잠시 후 다시 시도하세요.';
|
||||
setState(() {
|
||||
errorMessage = message;
|
||||
isLoading = false;
|
||||
|
||||
@@ -133,7 +133,7 @@ class GroupPermissionMenuDto {
|
||||
id: json['id'] as int? ?? json['menu_id'] as int,
|
||||
menuCode: code,
|
||||
menuName: fallbackName,
|
||||
path: json['path'] as String?,
|
||||
path: (json['path'] ?? json['route_path']) as String?,
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -49,10 +49,12 @@ class ReportingRepositoryRemote implements ReportingRepository {
|
||||
'from': request.from.toIso8601String(),
|
||||
'to': request.to.toIso8601String(),
|
||||
'format': request.format.apiValue,
|
||||
if (request.transactionTypeId != null)
|
||||
'type_id': request.transactionTypeId,
|
||||
if (request.statusId != null) 'status_id': request.statusId,
|
||||
if (request.warehouseId != null) 'warehouse_id': request.warehouseId,
|
||||
if (request.transactionStatusId != null)
|
||||
'transaction_status_id': request.transactionStatusId,
|
||||
if (request.approvalStatusId != null)
|
||||
'approval_status_id': request.approvalStatusId,
|
||||
if (request.requestedById != null)
|
||||
'requested_by_id': request.requestedById,
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@@ -6,9 +6,9 @@ class ReportExportRequest {
|
||||
required this.from,
|
||||
required this.to,
|
||||
required this.format,
|
||||
this.transactionTypeId,
|
||||
this.statusId,
|
||||
this.warehouseId,
|
||||
this.transactionStatusId,
|
||||
this.approvalStatusId,
|
||||
this.requestedById,
|
||||
});
|
||||
|
||||
/// 조회 시작 일자.
|
||||
@@ -20,12 +20,12 @@ class ReportExportRequest {
|
||||
/// 내보내기 파일 형식.
|
||||
final ReportExportFormat format;
|
||||
|
||||
/// 재고 트랜잭션 유형 식별자.
|
||||
final int? transactionTypeId;
|
||||
/// 트랜잭션 상태 식별자.
|
||||
final int? transactionStatusId;
|
||||
|
||||
/// 결재 상태 식별자.
|
||||
final int? statusId;
|
||||
final int? approvalStatusId;
|
||||
|
||||
/// 창고 식별자.
|
||||
final int? warehouseId;
|
||||
/// 상신자(요청자) 식별자.
|
||||
final int? requestedById;
|
||||
}
|
||||
|
||||
@@ -7,8 +7,11 @@ 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/network/failure.dart';
|
||||
import 'package:superport_v2/core/services/file_saver.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/shared/widgets/employee_autocomplete_field.dart';
|
||||
import 'package:superport_v2/features/masters/warehouse/domain/entities/warehouse.dart';
|
||||
import 'package:superport_v2/features/masters/warehouse/domain/repositories/warehouse_repository.dart';
|
||||
import 'package:superport_v2/features/reporting/domain/entities/report_download_result.dart';
|
||||
@@ -20,7 +23,6 @@ import 'package:superport_v2/widgets/components/empty_state.dart';
|
||||
import 'package:superport_v2/widgets/components/feedback.dart';
|
||||
import 'package:superport_v2/widgets/components/filter_bar.dart';
|
||||
import 'package:superport_v2/widgets/components/superport_date_picker.dart';
|
||||
import 'package:superport_v2/core/network/failure.dart';
|
||||
|
||||
/// 보고서 다운로드 화면 루트 위젯.
|
||||
class ReportingPage extends StatefulWidget {
|
||||
@@ -36,7 +38,6 @@ class _ReportingPageState extends State<ReportingPage> {
|
||||
ReportingRepository? _reportingRepository;
|
||||
InventoryLookupRepository? _lookupRepository;
|
||||
final intl.DateFormat _dateFormat = intl.DateFormat('yyyy.MM.dd');
|
||||
final Map<ReportTypeFilter, LookupItem> _transactionTypeLookup = {};
|
||||
final Map<ReportStatusFilter, LookupItem> _transactionStatusLookup = {};
|
||||
final Map<ReportStatusFilter, LookupItem> _approvalStatusLookup = {};
|
||||
bool _isLoadingLookups = false;
|
||||
@@ -45,12 +46,9 @@ class _ReportingPageState extends State<ReportingPage> {
|
||||
String? _exportError;
|
||||
ReportDownloadResult? _lastResult;
|
||||
ReportExportFormat? _lastFormat;
|
||||
|
||||
static const Map<ReportTypeFilter, List<String>> _transactionTypeKeywords = {
|
||||
ReportTypeFilter.inbound: ['입고', 'inbound'],
|
||||
ReportTypeFilter.outbound: ['출고', 'outbound'],
|
||||
ReportTypeFilter.rental: ['대여', 'rent', 'rental'],
|
||||
};
|
||||
final TextEditingController _requesterController = TextEditingController();
|
||||
InventoryEmployeeSuggestion? _appliedRequester;
|
||||
InventoryEmployeeSuggestion? _pendingRequester;
|
||||
|
||||
static const Map<ReportStatusFilter, List<String>>
|
||||
_transactionStatusKeywords = {
|
||||
@@ -105,6 +103,12 @@ class _ReportingPageState extends State<ReportingPage> {
|
||||
_loadWarehouses();
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
_requesterController.dispose();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
/// 활성 창고 목록을 불러와 드롭다운 옵션을 준비한다.
|
||||
Future<void> _loadWarehouses() async {
|
||||
setState(() {
|
||||
@@ -173,16 +177,12 @@ class _ReportingPageState extends State<ReportingPage> {
|
||||
_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(
|
||||
@@ -228,6 +228,9 @@ class _ReportingPageState extends State<ReportingPage> {
|
||||
_pendingStatus = ReportStatusFilter.all;
|
||||
_appliedWarehouse = WarehouseFilterOption.all;
|
||||
_pendingWarehouse = WarehouseFilterOption.all;
|
||||
_appliedRequester = null;
|
||||
_pendingRequester = null;
|
||||
_requesterController.clear();
|
||||
});
|
||||
}
|
||||
|
||||
@@ -238,6 +241,7 @@ class _ReportingPageState extends State<ReportingPage> {
|
||||
_appliedType = _pendingType;
|
||||
_appliedStatus = _pendingStatus;
|
||||
_appliedWarehouse = _pendingWarehouse;
|
||||
_appliedRequester = _pendingRequester;
|
||||
});
|
||||
}
|
||||
|
||||
@@ -249,7 +253,8 @@ class _ReportingPageState extends State<ReportingPage> {
|
||||
return _appliedDateRange != null ||
|
||||
_appliedType != ReportTypeFilter.all ||
|
||||
_appliedStatus != ReportStatusFilter.all ||
|
||||
_appliedWarehouse != WarehouseFilterOption.all;
|
||||
_appliedWarehouse != WarehouseFilterOption.all ||
|
||||
_appliedRequester != null;
|
||||
}
|
||||
|
||||
bool get _hasAppliedFilters => _hasCustomFilters;
|
||||
@@ -258,7 +263,8 @@ class _ReportingPageState extends State<ReportingPage> {
|
||||
!_isSameRange(_pendingDateRange, _appliedDateRange) ||
|
||||
_pendingType != _appliedType ||
|
||||
_pendingStatus != _appliedStatus ||
|
||||
_pendingWarehouse != _appliedWarehouse;
|
||||
_pendingWarehouse != _appliedWarehouse ||
|
||||
!_isSameRequester(_pendingRequester, _appliedRequester);
|
||||
|
||||
bool _isSameRange(DateTimeRange? a, DateTimeRange? b) {
|
||||
if (identical(a, b)) {
|
||||
@@ -270,6 +276,19 @@ class _ReportingPageState extends State<ReportingPage> {
|
||||
return a.start == b.start && a.end == b.end;
|
||||
}
|
||||
|
||||
bool _isSameRequester(
|
||||
InventoryEmployeeSuggestion? a,
|
||||
InventoryEmployeeSuggestion? b,
|
||||
) {
|
||||
if (identical(a, b)) {
|
||||
return true;
|
||||
}
|
||||
if (a == null || b == null) {
|
||||
return a == b;
|
||||
}
|
||||
return a.id == b.id;
|
||||
}
|
||||
|
||||
WarehouseFilterOption _resolveWarehouseOption(
|
||||
WarehouseFilterOption target,
|
||||
List<WarehouseFilterOption> options,
|
||||
@@ -282,19 +301,6 @@ class _ReportingPageState extends State<ReportingPage> {
|
||||
return options.first;
|
||||
}
|
||||
|
||||
Map<ReportTypeFilter, LookupItem> _mapTransactionTypes(
|
||||
List<LookupItem> items,
|
||||
) {
|
||||
final result = <ReportTypeFilter, LookupItem>{};
|
||||
for (final entry in _transactionTypeKeywords.entries) {
|
||||
final matched = _matchLookup(items, entry.value);
|
||||
if (matched != null) {
|
||||
result[entry.key] = matched;
|
||||
}
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
Map<ReportStatusFilter, LookupItem> _mapStatusByKeyword(
|
||||
List<LookupItem> items,
|
||||
Map<ReportStatusFilter, List<String>> keywords,
|
||||
@@ -326,25 +332,20 @@ class _ReportingPageState extends State<ReportingPage> {
|
||||
return null;
|
||||
}
|
||||
|
||||
int? _resolveTransactionTypeId() {
|
||||
if (_appliedType == ReportTypeFilter.all ||
|
||||
_appliedType == ReportTypeFilter.approval) {
|
||||
return null;
|
||||
}
|
||||
final lookup = _transactionTypeLookup[_appliedType];
|
||||
return lookup?.id;
|
||||
}
|
||||
|
||||
int? _resolveStatusId() {
|
||||
int? _resolveTransactionStatusId() {
|
||||
if (_appliedStatus == ReportStatusFilter.all) {
|
||||
return null;
|
||||
}
|
||||
if (_appliedType == ReportTypeFilter.approval) {
|
||||
return _approvalStatusLookup[_appliedStatus]?.id;
|
||||
}
|
||||
return _transactionStatusLookup[_appliedStatus]?.id;
|
||||
}
|
||||
|
||||
int? _resolveApprovalStatusId() {
|
||||
if (_appliedStatus == ReportStatusFilter.all) {
|
||||
return null;
|
||||
}
|
||||
return _approvalStatusLookup[_appliedStatus]?.id;
|
||||
}
|
||||
|
||||
String _dateRangeLabel(DateTimeRange? range) {
|
||||
if (range == null) {
|
||||
return '기간 선택';
|
||||
@@ -354,6 +355,13 @@ class _ReportingPageState extends State<ReportingPage> {
|
||||
|
||||
String _formatDate(DateTime value) => _dateFormat.format(value);
|
||||
|
||||
String _requesterLabel(InventoryEmployeeSuggestion? suggestion) {
|
||||
if (suggestion == null) {
|
||||
return '전체 상신자';
|
||||
}
|
||||
return '${suggestion.name} (${suggestion.employeeNo})';
|
||||
}
|
||||
|
||||
Future<void> _handleExport(ReportExportFormat format) async {
|
||||
if (_isExporting) {
|
||||
return;
|
||||
@@ -376,9 +384,9 @@ class _ReportingPageState extends State<ReportingPage> {
|
||||
from: range.start,
|
||||
to: range.end,
|
||||
format: format,
|
||||
transactionTypeId: _resolveTransactionTypeId(),
|
||||
statusId: _resolveStatusId(),
|
||||
warehouseId: _appliedWarehouse.id,
|
||||
transactionStatusId: _resolveTransactionStatusId(),
|
||||
approvalStatusId: _resolveApprovalStatusId(),
|
||||
requestedById: _appliedRequester?.id,
|
||||
);
|
||||
try {
|
||||
final result = _appliedType == ReportTypeFilter.approval
|
||||
@@ -394,7 +402,7 @@ class _ReportingPageState extends State<ReportingPage> {
|
||||
if (result.hasDownloadUrl) {
|
||||
SuperportToast.success(context, '다운로드 링크가 준비되었습니다.');
|
||||
} else if (result.hasBytes) {
|
||||
SuperportToast.success(context, '보고서 파일이 준비되었습니다. 저장 기능은 추후 제공 예정입니다.');
|
||||
await _saveBinaryResult(result, format);
|
||||
} else {
|
||||
SuperportToast.info(context, '다운로드 결과를 확인했지만 추가 처리 항목이 없습니다.');
|
||||
}
|
||||
@@ -419,6 +427,51 @@ class _ReportingPageState extends State<ReportingPage> {
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> _saveBinaryResult(
|
||||
ReportDownloadResult result,
|
||||
ReportExportFormat format,
|
||||
) async {
|
||||
final bytes = result.bytes;
|
||||
if (bytes == null || bytes.isEmpty) {
|
||||
return;
|
||||
}
|
||||
final filename = result.filename ?? 'report.${format.apiValue}';
|
||||
final mimeType = result.mimeType ?? _mimeTypeForFormat(format);
|
||||
try {
|
||||
await saveFileBytes(bytes: bytes, filename: filename, mimeType: mimeType);
|
||||
if (!mounted) {
|
||||
return;
|
||||
}
|
||||
SuperportToast.success(context, '보고서 파일 다운로드가 시작되었습니다.');
|
||||
} on UnsupportedError {
|
||||
if (!mounted) {
|
||||
return;
|
||||
}
|
||||
SuperportToast.info(
|
||||
context,
|
||||
'현재 환경에서는 자동 저장을 지원하지 않습니다. 다운로드 링크 요청 기능을 이용하세요.',
|
||||
);
|
||||
} catch (_) {
|
||||
if (!mounted) {
|
||||
return;
|
||||
}
|
||||
SuperportToast.error(context, '파일을 저장하는 중 문제가 발생했습니다. 잠시 후 다시 시도하세요.');
|
||||
}
|
||||
}
|
||||
|
||||
void _notifyPdfUnavailable() {
|
||||
SuperportToast.info(context, 'PDF 다운로드는 현재 지원되지 않습니다.');
|
||||
}
|
||||
|
||||
String _mimeTypeForFormat(ReportExportFormat format) {
|
||||
switch (format) {
|
||||
case ReportExportFormat.xlsx:
|
||||
return 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet';
|
||||
case ReportExportFormat.pdf:
|
||||
return 'application/pdf';
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> _launchDownloadUrl(Uri url) async {
|
||||
try {
|
||||
final opened = await launchUrl(url, mode: LaunchMode.externalApplication);
|
||||
@@ -476,10 +529,7 @@ class _ReportingPageState extends State<ReportingPage> {
|
||||
if (result.downloadUrl != null)
|
||||
_SummaryRow(label: '다운로드 URL', value: result.downloadUrl!.toString()),
|
||||
if (result.hasBytes && (result.downloadUrl == null))
|
||||
const _SummaryRow(
|
||||
label: '상태',
|
||||
value: '바이너리 응답이 준비되었습니다. 저장 기능은 추후 제공 예정입니다.',
|
||||
),
|
||||
const _SummaryRow(label: '상태', value: '바이너리 응답을 받아 자동 다운로드를 실행했습니다.'),
|
||||
];
|
||||
|
||||
return Column(
|
||||
@@ -530,9 +580,7 @@ class _ReportingPageState extends State<ReportingPage> {
|
||||
child: const Text('XLSX 다운로드'),
|
||||
),
|
||||
ShadButton.outline(
|
||||
onPressed: _canExport
|
||||
? () => _handleExport(ReportExportFormat.pdf)
|
||||
: null,
|
||||
onPressed: _canExport ? _notifyPdfUnavailable : null,
|
||||
leading: const Icon(lucide.LucideIcons.fileText, size: 16),
|
||||
child: const Text('PDF 다운로드'),
|
||||
),
|
||||
@@ -648,6 +696,40 @@ class _ReportingPageState extends State<ReportingPage> {
|
||||
],
|
||||
),
|
||||
),
|
||||
SizedBox(
|
||||
width: 260,
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(
|
||||
'상신자',
|
||||
style: theme.textTheme.small.copyWith(
|
||||
fontWeight: FontWeight.w600,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 4),
|
||||
InventoryEmployeeAutocompleteField(
|
||||
key: ValueKey(_pendingRequester?.id ?? 'none'),
|
||||
controller: _requesterController,
|
||||
initialSuggestion: _pendingRequester,
|
||||
placeholder: '상신자 이름 또는 사번 검색',
|
||||
onSuggestionSelected: (suggestion) {
|
||||
setState(() {
|
||||
_pendingRequester = suggestion;
|
||||
});
|
||||
},
|
||||
onChanged: () {
|
||||
if (_requesterController.text.trim().isEmpty &&
|
||||
_pendingRequester != null) {
|
||||
setState(() {
|
||||
_pendingRequester = null;
|
||||
});
|
||||
}
|
||||
},
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
child: Column(
|
||||
@@ -712,6 +794,10 @@ class _ReportingPageState extends State<ReportingPage> {
|
||||
_SummaryRow(label: '유형', value: _appliedType.label),
|
||||
_SummaryRow(label: '창고', value: _appliedWarehouse.label),
|
||||
_SummaryRow(label: '상태', value: _appliedStatus.label),
|
||||
_SummaryRow(
|
||||
label: '상신자',
|
||||
value: _requesterLabel(_appliedRequester),
|
||||
),
|
||||
if (!_canExport)
|
||||
Padding(
|
||||
padding: const EdgeInsets.only(top: 12),
|
||||
|
||||
Reference in New Issue
Block a user