사용하지 않는 파일 정리 전 백업 (Phase 10 완료 후 상태)
This commit is contained in:
399
lib/screens/inventory/stock_out_form.dart
Normal file
399
lib/screens/inventory/stock_out_form.dart
Normal file
@@ -0,0 +1,399 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:provider/provider.dart';
|
||||
import 'package:shadcn_ui/shadcn_ui.dart';
|
||||
import '../../screens/equipment/controllers/equipment_history_controller.dart';
|
||||
import '../../data/models/equipment_history_dto.dart';
|
||||
|
||||
class StockOutForm extends StatefulWidget {
|
||||
const StockOutForm({super.key});
|
||||
|
||||
@override
|
||||
State<StockOutForm> createState() => _StockOutFormState();
|
||||
}
|
||||
|
||||
class _StockOutFormState extends State<StockOutForm> {
|
||||
final _formKey = GlobalKey<ShadFormState>();
|
||||
|
||||
int? _selectedEquipmentId;
|
||||
int? _selectedWarehouseId;
|
||||
int _quantity = 1;
|
||||
DateTime _transactionDate = DateTime.now();
|
||||
String? _notes;
|
||||
String? _destination;
|
||||
String? _recipientName;
|
||||
String? _recipientContact;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
WidgetsBinding.instance.addPostFrameCallback((_) {
|
||||
// 재고 현황 로드
|
||||
context.read<EquipmentHistoryController>().loadInventoryStatus();
|
||||
});
|
||||
}
|
||||
|
||||
Future<void> _handleSubmit() async {
|
||||
if (_formKey.currentState?.saveAndValidate() ?? false) {
|
||||
final controller = context.read<EquipmentHistoryController>();
|
||||
|
||||
// 재고 부족 체크
|
||||
final currentStock = await controller.getAvailableStock(
|
||||
_selectedEquipmentId!,
|
||||
warehouseId: _selectedWarehouseId!,
|
||||
);
|
||||
|
||||
if (currentStock < _quantity) {
|
||||
ShadToaster.of(context).show(
|
||||
ShadToast.destructive(
|
||||
title: const Text('재고 부족'),
|
||||
description: Text('현재 재고: $currentStock개, 요청 수량: $_quantity개'),
|
||||
),
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
// 백엔드 스키마에 맞는 요청 데이터 생성
|
||||
final request = EquipmentHistoryRequestDto(
|
||||
equipmentsId: _selectedEquipmentId!,
|
||||
warehousesId: _selectedWarehouseId!,
|
||||
transactionType: 'O', // 출고
|
||||
quantity: _quantity,
|
||||
transactedAt: _transactionDate,
|
||||
remark: _notes,
|
||||
);
|
||||
|
||||
await controller.createHistory(request);
|
||||
|
||||
if (controller.error == null && mounted) {
|
||||
ShadToaster.of(context).show(
|
||||
const ShadToast(
|
||||
title: Text('출고 처리 완료'),
|
||||
description: Text('장비가 성공적으로 출고되었습니다'),
|
||||
),
|
||||
);
|
||||
Navigator.pop(context, true);
|
||||
} else if (controller.error != null && mounted) {
|
||||
ShadToaster.of(context).show(
|
||||
ShadToast.destructive(
|
||||
title: const Text('출고 처리 실패'),
|
||||
description: Text(controller.error!),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final theme = ShadTheme.of(context);
|
||||
|
||||
return Scaffold(
|
||||
backgroundColor: theme.colorScheme.background,
|
||||
appBar: AppBar(
|
||||
backgroundColor: theme.colorScheme.card,
|
||||
title: const Text('장비 출고 처리'),
|
||||
leading: IconButton(
|
||||
icon: const Icon(Icons.arrow_back),
|
||||
onPressed: () => Navigator.pop(context),
|
||||
),
|
||||
),
|
||||
body: SingleChildScrollView(
|
||||
padding: const EdgeInsets.all(24),
|
||||
child: Center(
|
||||
child: Container(
|
||||
constraints: const BoxConstraints(maxWidth: 600),
|
||||
child: ShadCard(
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(24),
|
||||
child: ShadForm(
|
||||
key: _formKey,
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(
|
||||
'출고 정보 입력',
|
||||
style: theme.textTheme.h3,
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
Text(
|
||||
'출고할 장비와 수령 정보를 입력하세요',
|
||||
style: theme.textTheme.muted,
|
||||
),
|
||||
const SizedBox(height: 24),
|
||||
|
||||
// 창고 선택
|
||||
Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
const Text('출고 창고', style: TextStyle(fontSize: 14, fontWeight: FontWeight.w500)),
|
||||
const SizedBox(height: 8),
|
||||
ShadSelect<int>(
|
||||
placeholder: const Text('창고 선택'),
|
||||
options: [
|
||||
const ShadOption(value: 1, child: Text('본사 창고')),
|
||||
const ShadOption(value: 2, child: Text('지사 창고')),
|
||||
const ShadOption(value: 3, child: Text('외부 창고')),
|
||||
],
|
||||
selectedOptionBuilder: (context, value) {
|
||||
switch (value) {
|
||||
case 1:
|
||||
return const Text('본사 창고');
|
||||
case 2:
|
||||
return const Text('지사 창고');
|
||||
case 3:
|
||||
return const Text('외부 창고');
|
||||
default:
|
||||
return const Text('');
|
||||
}
|
||||
},
|
||||
onChanged: (value) {
|
||||
setState(() {
|
||||
_selectedWarehouseId = value;
|
||||
_selectedEquipmentId = null; // 창고 변경 시 장비 선택 초기화
|
||||
});
|
||||
},
|
||||
),
|
||||
],
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
|
||||
// 장비 선택 (창고별 재고가 있는 장비만 표시)
|
||||
if (_selectedWarehouseId != null)
|
||||
FutureBuilder<List<int>>(
|
||||
future: context.read<EquipmentHistoryController>().getAvailableEquipments(warehouseId: _selectedWarehouseId),
|
||||
builder: (context, snapshot) {
|
||||
if (snapshot.connectionState == ConnectionState.waiting) {
|
||||
return const Center(child: CircularProgressIndicator());
|
||||
}
|
||||
|
||||
if (!snapshot.hasData || snapshot.data!.isEmpty) {
|
||||
return Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
const Text('장비', style: TextStyle(fontWeight: FontWeight.bold)),
|
||||
const SizedBox(height: 8),
|
||||
Container(
|
||||
width: double.infinity,
|
||||
padding: const EdgeInsets.all(16),
|
||||
decoration: BoxDecoration(
|
||||
border: Border.all(color: Colors.grey),
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
),
|
||||
child: const Text('해당 창고에 재고가 있는 장비가 없습니다'),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
final availableEquipmentIds = snapshot.data!;
|
||||
|
||||
return Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
const Text('장비', style: TextStyle(fontWeight: FontWeight.bold)),
|
||||
const SizedBox(height: 8),
|
||||
ShadSelect<int>(
|
||||
placeholder: const Text('장비 선택'),
|
||||
options: availableEquipmentIds.map((equipmentId) {
|
||||
return ShadOption(
|
||||
value: equipmentId,
|
||||
child: FutureBuilder<int>(
|
||||
future: context.read<EquipmentHistoryController>().getAvailableStock(
|
||||
equipmentId,
|
||||
warehouseId: _selectedWarehouseId,
|
||||
),
|
||||
builder: (context, stockSnapshot) {
|
||||
final stock = stockSnapshot.data ?? 0;
|
||||
return Text('장비 ID: $equipmentId (재고: $stock개)');
|
||||
},
|
||||
),
|
||||
);
|
||||
}).toList(),
|
||||
selectedOptionBuilder: (context, value) {
|
||||
return FutureBuilder<int>(
|
||||
future: context.read<EquipmentHistoryController>().getAvailableStock(
|
||||
value,
|
||||
warehouseId: _selectedWarehouseId,
|
||||
),
|
||||
builder: (context, stockSnapshot) {
|
||||
final stock = stockSnapshot.data ?? 0;
|
||||
return Text('장비 ID: $value (재고: $stock개)');
|
||||
},
|
||||
);
|
||||
},
|
||||
onChanged: (value) {
|
||||
setState(() {
|
||||
_selectedEquipmentId = value;
|
||||
});
|
||||
},
|
||||
),
|
||||
if (_selectedEquipmentId != null)
|
||||
Padding(
|
||||
padding: const EdgeInsets.only(top: 8),
|
||||
child: FutureBuilder<int>(
|
||||
future: context.read<EquipmentHistoryController>().getAvailableStock(
|
||||
_selectedEquipmentId!,
|
||||
warehouseId: _selectedWarehouseId,
|
||||
),
|
||||
builder: (context, stockSnapshot) {
|
||||
final stock = stockSnapshot.data ?? 0;
|
||||
return Text(
|
||||
'현재 재고: $stock개',
|
||||
style: theme.textTheme.small.copyWith(
|
||||
color: theme.colorScheme.primary,
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
},
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
|
||||
// 수량
|
||||
ShadInputFormField(
|
||||
label: const Text('출고 수량'),
|
||||
keyboardType: TextInputType.number,
|
||||
controller: TextEditingController(text: '1'),
|
||||
validator: (v) {
|
||||
if (_quantity <= 0) {
|
||||
return '수량은 1 이상이어야 합니다';
|
||||
}
|
||||
// 재고 체크는 submit 시에만 async로 처리
|
||||
return null;
|
||||
},
|
||||
onChanged: (value) {
|
||||
setState(() {
|
||||
_quantity = int.tryParse(value) ?? 1;
|
||||
});
|
||||
},
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
|
||||
// 출고일
|
||||
GestureDetector(
|
||||
onTap: () async {
|
||||
final picked = await showDatePicker(
|
||||
context: context,
|
||||
initialDate: _transactionDate,
|
||||
firstDate: DateTime(2020),
|
||||
lastDate: DateTime.now(),
|
||||
);
|
||||
if (picked != null) {
|
||||
setState(() {
|
||||
_transactionDate = picked;
|
||||
});
|
||||
}
|
||||
},
|
||||
child: AbsorbPointer(
|
||||
child: ShadInputFormField(
|
||||
label: const Text('출고일'),
|
||||
controller: TextEditingController(
|
||||
text: '${_transactionDate.year}-${_transactionDate.month.toString().padLeft(2, '0')}-${_transactionDate.day.toString().padLeft(2, '0')}',
|
||||
),
|
||||
readOnly: true,
|
||||
),
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
|
||||
// 출고 목적지
|
||||
ShadInputFormField(
|
||||
label: const Text('출고 목적지'),
|
||||
placeholder: const Text('예: 고객사명, 부서명'),
|
||||
validator: (v) {
|
||||
if (_destination == null || _destination!.isEmpty) {
|
||||
return '출고 목적지를 입력하세요';
|
||||
}
|
||||
return null;
|
||||
},
|
||||
onChanged: (value) {
|
||||
_destination = value;
|
||||
},
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
|
||||
// 수령인 정보
|
||||
ShadInputFormField(
|
||||
label: const Text('수령인'),
|
||||
placeholder: const Text('수령인 이름'),
|
||||
onChanged: (value) {
|
||||
_recipientName = value.isEmpty ? null : value;
|
||||
},
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
|
||||
ShadInputFormField(
|
||||
label: const Text('수령인 연락처'),
|
||||
placeholder: const Text('010-0000-0000'),
|
||||
onChanged: (value) {
|
||||
_recipientContact = value.isEmpty ? null : value;
|
||||
},
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
|
||||
// 비고
|
||||
ShadInputFormField(
|
||||
label: const Text('비고'),
|
||||
maxLines: 3,
|
||||
placeholder: const Text('출고 사유 및 추가 정보'),
|
||||
onChanged: (value) {
|
||||
String fullNotes = '';
|
||||
if (_destination != null) {
|
||||
fullNotes += '목적지: $_destination';
|
||||
}
|
||||
if (_recipientName != null) {
|
||||
fullNotes += '\n수령인: $_recipientName';
|
||||
}
|
||||
if (_recipientContact != null) {
|
||||
fullNotes += '\n연락처: $_recipientContact';
|
||||
}
|
||||
if (value.isNotEmpty) {
|
||||
fullNotes += '\n비고: $value';
|
||||
}
|
||||
_notes = fullNotes.isEmpty ? null : fullNotes;
|
||||
},
|
||||
),
|
||||
const SizedBox(height: 32),
|
||||
|
||||
// 버튼
|
||||
Row(
|
||||
mainAxisAlignment: MainAxisAlignment.end,
|
||||
children: [
|
||||
ShadButton.outline(
|
||||
child: const Text('취소'),
|
||||
onPressed: () => Navigator.pop(context),
|
||||
),
|
||||
const SizedBox(width: 8),
|
||||
Consumer<EquipmentHistoryController>(
|
||||
builder: (context, controller, _) {
|
||||
return ShadButton(
|
||||
enabled: !controller.isLoading,
|
||||
onPressed: _handleSubmit,
|
||||
child: controller.isLoading
|
||||
? const SizedBox(
|
||||
width: 16,
|
||||
height: 16,
|
||||
child: CircularProgressIndicator(
|
||||
strokeWidth: 2,
|
||||
),
|
||||
)
|
||||
: const Text('출고 처리'),
|
||||
);
|
||||
},
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user