Files
superport/lib/screens/inventory/stock_out_form.dart

399 lines
18 KiB
Dart

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('출고 처리'),
);
},
),
],
),
],
),
),
),
),
),
),
),
);
}
}