프로젝트 최초 커밋
This commit is contained in:
405
lib/screens/goods/goods_list.dart
Normal file
405
lib/screens/goods/goods_list.dart
Normal file
@@ -0,0 +1,405 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:superport/services/mock_data_service.dart';
|
||||
import 'package:superport/utils/constants.dart';
|
||||
import 'package:superport/screens/common/main_layout.dart';
|
||||
import 'package:superport/screens/common/custom_widgets.dart';
|
||||
import 'package:superport/screens/common/widgets/pagination.dart';
|
||||
import 'package:superport/screens/common/theme_tailwind.dart';
|
||||
import 'package:superport/screens/common/widgets/category_autocomplete_field.dart';
|
||||
|
||||
/// 물품 관리(등록) 화면
|
||||
/// 이름, 제조사, 대분류, 중분류, 소분류만 등록/조회 가능
|
||||
class GoodsListScreen extends StatefulWidget {
|
||||
const GoodsListScreen({super.key});
|
||||
|
||||
@override
|
||||
State<GoodsListScreen> createState() => _GoodsListScreenState();
|
||||
}
|
||||
|
||||
class _GoodsListScreenState extends State<GoodsListScreen> {
|
||||
final MockDataService _dataService = MockDataService();
|
||||
late List<_GoodsItem> _goodsList;
|
||||
int _currentPage = 1;
|
||||
final int _pageSize = 10;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
_loadGoods();
|
||||
}
|
||||
|
||||
void _loadGoods() {
|
||||
final allEquipments = _dataService.getAllEquipmentIns();
|
||||
final goodsSet = <String, _GoodsItem>{};
|
||||
for (final equipmentIn in allEquipments) {
|
||||
final eq = equipmentIn.equipment;
|
||||
final key =
|
||||
'${eq.manufacturer}|${eq.name}|${eq.category}|${eq.subCategory}|${eq.subSubCategory}';
|
||||
goodsSet[key] = _GoodsItem(
|
||||
name: eq.name,
|
||||
manufacturer: eq.manufacturer,
|
||||
category: eq.category,
|
||||
subCategory: eq.subCategory,
|
||||
subSubCategory: eq.subSubCategory,
|
||||
);
|
||||
}
|
||||
setState(() {
|
||||
_goodsList = goodsSet.values.toList();
|
||||
});
|
||||
}
|
||||
|
||||
void _showAddGoodsDialog() async {
|
||||
final result = await showDialog<_GoodsItem>(
|
||||
context: context,
|
||||
builder: (context) => _GoodsFormDialog(),
|
||||
);
|
||||
if (result != null) {
|
||||
setState(() {
|
||||
_goodsList.add(result);
|
||||
});
|
||||
ScaffoldMessenger.of(
|
||||
context,
|
||||
).showSnackBar(const SnackBar(content: Text('물품이 등록되었습니다.')));
|
||||
}
|
||||
}
|
||||
|
||||
void _showEditGoodsDialog(int index) async {
|
||||
final result = await showDialog<_GoodsItem>(
|
||||
context: context,
|
||||
builder: (context) => _GoodsFormDialog(item: _goodsList[index]),
|
||||
);
|
||||
if (result != null) {
|
||||
setState(() {
|
||||
_goodsList[index] = result;
|
||||
});
|
||||
ScaffoldMessenger.of(
|
||||
context,
|
||||
).showSnackBar(const SnackBar(content: Text('물품 정보가 수정되었습니다.')));
|
||||
}
|
||||
}
|
||||
|
||||
void _deleteGoods(int index) {
|
||||
showDialog(
|
||||
context: context,
|
||||
builder:
|
||||
(context) => AlertDialog(
|
||||
title: const Text('삭제 확인'),
|
||||
content: const Text('이 물품 정보를 삭제하시겠습니까?'),
|
||||
actions: [
|
||||
TextButton(
|
||||
onPressed: () => Navigator.pop(context),
|
||||
child: const Text('취소'),
|
||||
),
|
||||
TextButton(
|
||||
onPressed: () {
|
||||
setState(() {
|
||||
_goodsList.removeAt(index);
|
||||
});
|
||||
Navigator.pop(context);
|
||||
ScaffoldMessenger.of(
|
||||
context,
|
||||
).showSnackBar(const SnackBar(content: Text('물품이 삭제되었습니다.')));
|
||||
},
|
||||
child: const Text('삭제'),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final screenWidth = MediaQuery.of(context).size.width;
|
||||
final maxContentWidth = screenWidth > 1200 ? 1200.0 : screenWidth - 32;
|
||||
final int totalCount = _goodsList.length;
|
||||
final int startIndex = (_currentPage - 1) * _pageSize;
|
||||
final int endIndex =
|
||||
(startIndex + _pageSize) > totalCount
|
||||
? totalCount
|
||||
: (startIndex + _pageSize);
|
||||
final pagedGoods = _goodsList.sublist(startIndex, endIndex);
|
||||
|
||||
return MainLayout(
|
||||
title: '물품 관리',
|
||||
currentRoute: Routes.goods,
|
||||
actions: [
|
||||
IconButton(
|
||||
icon: const Icon(Icons.refresh),
|
||||
onPressed: _loadGoods,
|
||||
color: Colors.grey,
|
||||
),
|
||||
],
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(16.0),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
PageTitle(
|
||||
title: '물품 목록',
|
||||
width: maxContentWidth - 32,
|
||||
rightWidget: ElevatedButton.icon(
|
||||
onPressed: _showAddGoodsDialog,
|
||||
icon: const Icon(Icons.add),
|
||||
label: const Text('추가'),
|
||||
style: AppThemeTailwind.primaryButtonStyle,
|
||||
),
|
||||
),
|
||||
Expanded(
|
||||
child: DataTableCard(
|
||||
width: maxContentWidth - 32,
|
||||
child:
|
||||
pagedGoods.isEmpty
|
||||
? const Center(child: Text('등록된 물품이 없습니다.'))
|
||||
: SingleChildScrollView(
|
||||
scrollDirection: Axis.horizontal,
|
||||
child: Container(
|
||||
constraints: BoxConstraints(
|
||||
minWidth: maxContentWidth - 64,
|
||||
),
|
||||
child: SingleChildScrollView(
|
||||
scrollDirection: Axis.vertical,
|
||||
child: DataTable(
|
||||
columns: const [
|
||||
DataColumn(label: Text('번호')),
|
||||
DataColumn(label: Text('이름')),
|
||||
DataColumn(label: Text('제조사')),
|
||||
DataColumn(label: Text('대분류')),
|
||||
DataColumn(label: Text('중분류')),
|
||||
DataColumn(label: Text('소분류')),
|
||||
DataColumn(label: Text('관리')),
|
||||
],
|
||||
rows: List.generate(pagedGoods.length, (i) {
|
||||
final item = pagedGoods[i];
|
||||
final realIndex = startIndex + i;
|
||||
return DataRow(
|
||||
cells: [
|
||||
DataCell(Text('${realIndex + 1}')),
|
||||
DataCell(Text(item.name)),
|
||||
DataCell(Text(item.manufacturer)),
|
||||
DataCell(Text(item.category)),
|
||||
DataCell(Text(item.subCategory)),
|
||||
DataCell(Text(item.subSubCategory)),
|
||||
DataCell(
|
||||
Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
IconButton(
|
||||
icon: const Icon(
|
||||
Icons.edit,
|
||||
color: AppThemeTailwind.primary,
|
||||
),
|
||||
onPressed:
|
||||
() => _showEditGoodsDialog(
|
||||
realIndex,
|
||||
),
|
||||
),
|
||||
IconButton(
|
||||
icon: const Icon(
|
||||
Icons.delete,
|
||||
color: AppThemeTailwind.danger,
|
||||
),
|
||||
onPressed:
|
||||
() => _deleteGoods(realIndex),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
}),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
if (totalCount > _pageSize)
|
||||
Padding(
|
||||
padding: const EdgeInsets.symmetric(vertical: 12),
|
||||
child: Pagination(
|
||||
totalCount: totalCount,
|
||||
currentPage: _currentPage,
|
||||
pageSize: _pageSize,
|
||||
onPageChanged: (page) {
|
||||
setState(() {
|
||||
_currentPage = page;
|
||||
});
|
||||
},
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/// 물품 데이터 모델 (이름, 제조사, 대중소분류)
|
||||
class _GoodsItem {
|
||||
final String name;
|
||||
final String manufacturer;
|
||||
final String category;
|
||||
final String subCategory;
|
||||
final String subSubCategory;
|
||||
|
||||
_GoodsItem({
|
||||
required this.name,
|
||||
required this.manufacturer,
|
||||
required this.category,
|
||||
required this.subCategory,
|
||||
required this.subSubCategory,
|
||||
});
|
||||
}
|
||||
|
||||
/// 물품 등록/수정 폼 다이얼로그
|
||||
class _GoodsFormDialog extends StatefulWidget {
|
||||
final _GoodsItem? item;
|
||||
const _GoodsFormDialog({this.item});
|
||||
@override
|
||||
State<_GoodsFormDialog> createState() => _GoodsFormDialogState();
|
||||
}
|
||||
|
||||
class _GoodsFormDialogState extends State<_GoodsFormDialog> {
|
||||
final _formKey = GlobalKey<FormState>();
|
||||
late String _name;
|
||||
late String _manufacturer;
|
||||
late String _category;
|
||||
late String _subCategory;
|
||||
late String _subSubCategory;
|
||||
|
||||
late final MockDataService _dataService;
|
||||
late final List<String> _manufacturerList;
|
||||
late final List<String> _nameList;
|
||||
late final List<String> _categoryList;
|
||||
late final List<String> _subCategoryList;
|
||||
late final List<String> _subSubCategoryList;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
_name = widget.item?.name ?? '';
|
||||
_manufacturer = widget.item?.manufacturer ?? '';
|
||||
_category = widget.item?.category ?? '';
|
||||
_subCategory = widget.item?.subCategory ?? '';
|
||||
_subSubCategory = widget.item?.subSubCategory ?? '';
|
||||
_dataService = MockDataService();
|
||||
_manufacturerList = _dataService.getAllManufacturers();
|
||||
_nameList = _dataService.getAllEquipmentNames();
|
||||
_categoryList = _dataService.getAllCategories();
|
||||
_subCategoryList = _dataService.getAllSubCategories();
|
||||
_subSubCategoryList = _dataService.getAllSubSubCategories();
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Dialog(
|
||||
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(12)),
|
||||
child: ConstrainedBox(
|
||||
constraints: const BoxConstraints(maxWidth: 420),
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(24.0),
|
||||
child: Form(
|
||||
key: _formKey,
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(
|
||||
widget.item == null ? '신상품 등록' : '신상품 정보 수정',
|
||||
style: const TextStyle(
|
||||
fontSize: 20,
|
||||
fontWeight: FontWeight.bold,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 24),
|
||||
FormFieldWrapper(
|
||||
label: '이름',
|
||||
isRequired: true,
|
||||
child: CategoryAutocompleteField(
|
||||
hintText: '이름을 입력 또는 선택하세요',
|
||||
value: _name,
|
||||
items: _nameList,
|
||||
isRequired: true,
|
||||
onSelect: (v) => setState(() => _name = v),
|
||||
),
|
||||
),
|
||||
FormFieldWrapper(
|
||||
label: '제조사',
|
||||
isRequired: true,
|
||||
child: CategoryAutocompleteField(
|
||||
hintText: '제조사를 입력 또는 선택하세요',
|
||||
value: _manufacturer,
|
||||
items: _manufacturerList,
|
||||
isRequired: true,
|
||||
onSelect: (v) => setState(() => _manufacturer = v),
|
||||
),
|
||||
),
|
||||
FormFieldWrapper(
|
||||
label: '대분류',
|
||||
isRequired: true,
|
||||
child: CategoryAutocompleteField(
|
||||
hintText: '대분류를 입력 또는 선택하세요',
|
||||
value: _category,
|
||||
items: _categoryList,
|
||||
isRequired: true,
|
||||
onSelect: (v) => setState(() => _category = v),
|
||||
),
|
||||
),
|
||||
FormFieldWrapper(
|
||||
label: '중분류',
|
||||
isRequired: true,
|
||||
child: CategoryAutocompleteField(
|
||||
hintText: '중분류를 입력 또는 선택하세요',
|
||||
value: _subCategory,
|
||||
items: _subCategoryList,
|
||||
isRequired: true,
|
||||
onSelect: (v) => setState(() => _subCategory = v),
|
||||
),
|
||||
),
|
||||
FormFieldWrapper(
|
||||
label: '소분류',
|
||||
isRequired: true,
|
||||
child: CategoryAutocompleteField(
|
||||
hintText: '소분류를 입력 또는 선택하세요',
|
||||
value: _subSubCategory,
|
||||
items: _subSubCategoryList,
|
||||
isRequired: true,
|
||||
onSelect: (v) => setState(() => _subSubCategory = v),
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 24),
|
||||
Row(
|
||||
mainAxisAlignment: MainAxisAlignment.end,
|
||||
children: [
|
||||
TextButton(
|
||||
onPressed: () => Navigator.of(context).pop(),
|
||||
child: const Text('취소'),
|
||||
),
|
||||
const SizedBox(width: 8),
|
||||
ElevatedButton(
|
||||
style: AppThemeTailwind.primaryButtonStyle,
|
||||
onPressed: () {
|
||||
if (_formKey.currentState?.validate() ?? false) {
|
||||
Navigator.of(context).pop(
|
||||
_GoodsItem(
|
||||
name: _name,
|
||||
manufacturer: _manufacturer,
|
||||
category: _category,
|
||||
subCategory: _subCategory,
|
||||
subSubCategory: _subSubCategory,
|
||||
),
|
||||
);
|
||||
}
|
||||
},
|
||||
child: Text(widget.item == null ? '등록' : '수정'),
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user