- EquipmentHistoryDialog: 좌우 분할 상세 패널(데스크톱) + 하단 시트(모바일) 추가 • 거래일/유형/수량/창고/장비/회사/비고/생성·수정일 표시 • 리스트 항목 선택 하이라이트, ID 복사 기능 - EquipmentListController: 이력 최신순 정렬 후 상태/일자 결정(오래된 순 가정 제거) - EquipmentList(ShadTable): 체크박스 선택 시 컨트롤러 선택집합과 동기화 - ResponseInterceptor: equipment-history는 transaction_type 로깅, 장비 목록은 status 로깅으로 혼동 제거
811 lines
29 KiB
Dart
811 lines
29 KiB
Dart
import 'package:flutter/material.dart';
|
|
import 'package:flutter/services.dart';
|
|
import 'package:get_it/get_it.dart';
|
|
import 'package:superport/core/constants/app_constants.dart';
|
|
import 'package:superport/data/models/equipment_history_dto.dart';
|
|
import 'package:superport/services/equipment_service.dart';
|
|
import 'package:superport/core/errors/failures.dart';
|
|
import 'package:intl/intl.dart';
|
|
import 'package:superport/screens/common/theme_shadcn.dart';
|
|
|
|
/// 장비 이력을 표시하는 팝업 다이얼로그
|
|
class EquipmentHistoryDialog extends StatefulWidget {
|
|
final int equipmentId;
|
|
final String equipmentName;
|
|
|
|
const EquipmentHistoryDialog({
|
|
super.key,
|
|
required this.equipmentId,
|
|
required this.equipmentName,
|
|
});
|
|
|
|
@override
|
|
State<EquipmentHistoryDialog> createState() => _EquipmentHistoryDialogState();
|
|
|
|
/// 다이얼로그를 표시하는 정적 메서드
|
|
static Future<bool?> show({
|
|
required BuildContext context,
|
|
required int equipmentId,
|
|
required String equipmentName,
|
|
}) {
|
|
return showDialog<bool>(
|
|
context: context,
|
|
barrierDismissible: true,
|
|
builder: (BuildContext context) {
|
|
return EquipmentHistoryDialog(
|
|
equipmentId: equipmentId,
|
|
equipmentName: equipmentName,
|
|
);
|
|
},
|
|
);
|
|
}
|
|
}
|
|
|
|
class _EquipmentHistoryDialogState extends State<EquipmentHistoryDialog> {
|
|
final EquipmentService _equipmentService = GetIt.instance<EquipmentService>();
|
|
final ScrollController _scrollController = ScrollController();
|
|
final TextEditingController _searchController = TextEditingController();
|
|
|
|
List<EquipmentHistoryDto> _histories = [];
|
|
List<EquipmentHistoryDto> _filteredHistories = [];
|
|
bool _isLoading = false;
|
|
bool _isInitialLoad = true;
|
|
String? _error;
|
|
int _currentPage = 1;
|
|
final int _perPage = AppConstants.historyPageSize;
|
|
bool _hasMore = true;
|
|
String _searchQuery = '';
|
|
|
|
// 상세보기 상태
|
|
EquipmentHistoryDto? _selectedHistory;
|
|
|
|
@override
|
|
void initState() {
|
|
super.initState();
|
|
_loadHistory(isInitialLoad: true);
|
|
_scrollController.addListener(_onScroll);
|
|
_searchController.addListener(_onSearchChanged);
|
|
}
|
|
|
|
@override
|
|
void dispose() {
|
|
_scrollController.dispose();
|
|
_searchController.dispose();
|
|
super.dispose();
|
|
}
|
|
|
|
void _onScroll() {
|
|
if (_scrollController.position.pixels >=
|
|
_scrollController.position.maxScrollExtent - 200) {
|
|
_loadMoreHistory();
|
|
}
|
|
}
|
|
|
|
void _onSearchChanged() {
|
|
setState(() {
|
|
_searchQuery = _searchController.text.toLowerCase();
|
|
_filterHistories();
|
|
});
|
|
}
|
|
|
|
void _filterHistories() {
|
|
if (_searchQuery.isEmpty) {
|
|
_filteredHistories = List.from(_histories);
|
|
} else {
|
|
_filteredHistories = _histories.where((history) {
|
|
final remarks = (history.remark ?? '').toLowerCase();
|
|
final type = _getTransactionTypeText(history.transactionType).toLowerCase();
|
|
|
|
return remarks.contains(_searchQuery) ||
|
|
type.contains(_searchQuery);
|
|
}).toList();
|
|
}
|
|
}
|
|
|
|
void _selectHistory(EquipmentHistoryDto history) {
|
|
setState(() {
|
|
_selectedHistory = history;
|
|
});
|
|
}
|
|
|
|
Future<void> _loadHistory({
|
|
bool isRefresh = false,
|
|
bool isInitialLoad = false
|
|
}) async {
|
|
if (isRefresh) {
|
|
_currentPage = 1;
|
|
_hasMore = true;
|
|
_histories.clear();
|
|
_filteredHistories.clear();
|
|
}
|
|
|
|
if (!isInitialLoad && (!_hasMore || (!isRefresh && _isLoading))) {
|
|
return;
|
|
}
|
|
|
|
if (!mounted) return;
|
|
|
|
setState(() {
|
|
_isLoading = true;
|
|
_error = null;
|
|
if (isInitialLoad) _isInitialLoad = false;
|
|
});
|
|
|
|
try {
|
|
final histories = await _equipmentService.getEquipmentHistory(
|
|
widget.equipmentId,
|
|
page: _currentPage,
|
|
perPage: _perPage,
|
|
).timeout(
|
|
const Duration(seconds: 10),
|
|
onTimeout: () {
|
|
throw Exception('API 호출 시간 초과 (10초)');
|
|
},
|
|
);
|
|
|
|
if (!mounted) return;
|
|
|
|
setState(() {
|
|
if (isRefresh) {
|
|
_histories = histories.cast<EquipmentHistoryDto>();
|
|
} else {
|
|
_histories.addAll(histories.cast<EquipmentHistoryDto>());
|
|
}
|
|
_filterHistories();
|
|
_hasMore = histories.length == _perPage;
|
|
if (_hasMore) _currentPage++;
|
|
_isLoading = false;
|
|
});
|
|
} on Failure catch (e) {
|
|
if (!mounted) return;
|
|
setState(() {
|
|
_error = e.message;
|
|
_isLoading = false;
|
|
});
|
|
} catch (e) {
|
|
if (!mounted) return;
|
|
setState(() {
|
|
_error = '이력을 불러오는 중 오류가 발생했습니다: $e';
|
|
_isLoading = false;
|
|
});
|
|
}
|
|
}
|
|
|
|
Future<void> _loadMoreHistory() async {
|
|
await _loadHistory();
|
|
}
|
|
|
|
String _formatDate(DateTime? date) {
|
|
if (date == null) return '-';
|
|
return DateFormat('yyyy-MM-dd HH:mm').format(date);
|
|
}
|
|
|
|
String _getTransactionTypeText(String? type) {
|
|
switch (type) {
|
|
case 'I':
|
|
return '입고';
|
|
case 'O':
|
|
return '출고';
|
|
case 'R':
|
|
return '대여';
|
|
case 'T':
|
|
return '반납';
|
|
case 'D':
|
|
return '폐기';
|
|
default:
|
|
return type ?? '-';
|
|
}
|
|
}
|
|
|
|
Color _getTransactionTypeColor(String? type) {
|
|
switch (type) {
|
|
case 'I':
|
|
return ShadcnTheme.equipmentIn;
|
|
case 'O':
|
|
return ShadcnTheme.equipmentOut;
|
|
case 'R':
|
|
return ShadcnTheme.equipmentRepair;
|
|
case 'T':
|
|
return ShadcnTheme.equipmentRent;
|
|
case 'D':
|
|
return ShadcnTheme.destructive;
|
|
default:
|
|
return ShadcnTheme.secondary;
|
|
}
|
|
}
|
|
|
|
Widget _buildHistoryItem(EquipmentHistoryDto history) {
|
|
final typeColor = _getTransactionTypeColor(history.transactionType);
|
|
final typeText = _getTransactionTypeText(history.transactionType);
|
|
final isSelected = _selectedHistory?.id != null && _selectedHistory?.id == history.id;
|
|
|
|
return Container(
|
|
margin: const EdgeInsets.only(bottom: 8),
|
|
decoration: BoxDecoration(
|
|
color: isSelected ? ShadcnTheme.cardHover : ShadcnTheme.card,
|
|
borderRadius: BorderRadius.circular(8),
|
|
border: Border.all(color: isSelected ? ShadcnTheme.primary : ShadcnTheme.border),
|
|
boxShadow: ShadcnTheme.shadowSm,
|
|
),
|
|
child: Material(
|
|
color: Colors.transparent,
|
|
borderRadius: BorderRadius.circular(8),
|
|
child: InkWell(
|
|
borderRadius: BorderRadius.circular(8),
|
|
onTap: () => _selectHistory(history),
|
|
child: Padding(
|
|
padding: const EdgeInsets.all(12),
|
|
child: Row(
|
|
crossAxisAlignment: CrossAxisAlignment.start,
|
|
children: [
|
|
// 타입 아이콘
|
|
Container(
|
|
width: 40,
|
|
height: 40,
|
|
decoration: BoxDecoration(
|
|
color: typeColor.withValues(alpha: 0.1),
|
|
borderRadius: BorderRadius.circular(8),
|
|
),
|
|
child: Center(
|
|
child: Text(
|
|
typeText,
|
|
style: TextStyle(
|
|
color: typeColor,
|
|
fontWeight: FontWeight.bold,
|
|
fontSize: 11,
|
|
),
|
|
),
|
|
),
|
|
),
|
|
const SizedBox(width: 12),
|
|
// 내용
|
|
Expanded(
|
|
child: Column(
|
|
crossAxisAlignment: CrossAxisAlignment.start,
|
|
children: [
|
|
Row(
|
|
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
|
children: [
|
|
Expanded(
|
|
child: Text(
|
|
history.remark ?? '비고 없음',
|
|
style: const TextStyle(
|
|
fontSize: 14,
|
|
fontWeight: FontWeight.w500,
|
|
),
|
|
overflow: TextOverflow.ellipsis,
|
|
maxLines: 2,
|
|
),
|
|
),
|
|
Container(
|
|
padding: const EdgeInsets.symmetric(
|
|
horizontal: 8,
|
|
vertical: 2,
|
|
),
|
|
decoration: BoxDecoration(
|
|
color: Colors.grey.shade100,
|
|
borderRadius: BorderRadius.circular(4),
|
|
),
|
|
child: Text(
|
|
'수량: ${history.quantity}',
|
|
style: const TextStyle(
|
|
fontSize: 12,
|
|
fontWeight: FontWeight.w500,
|
|
),
|
|
),
|
|
),
|
|
],
|
|
),
|
|
const SizedBox(height: 4),
|
|
Row(
|
|
children: [
|
|
Icon(
|
|
Icons.access_time,
|
|
size: 14,
|
|
color: Colors.grey.shade600,
|
|
),
|
|
const SizedBox(width: 4),
|
|
Text(
|
|
_formatDate(history.transactedAt),
|
|
style: TextStyle(
|
|
fontSize: 12,
|
|
color: Colors.grey.shade600,
|
|
),
|
|
),
|
|
// userName 필드는 백엔드에 없으므로 제거됨
|
|
],
|
|
),
|
|
],
|
|
),
|
|
),
|
|
],
|
|
),
|
|
),
|
|
),
|
|
),
|
|
);
|
|
}
|
|
|
|
@override
|
|
Widget build(BuildContext context) {
|
|
final screenSize = MediaQuery.of(context).size;
|
|
final bool isDesktop = screenSize.width > 768;
|
|
|
|
// 반응형 크기 설정
|
|
final dialogWidth = isDesktop
|
|
? screenSize.width * 0.7
|
|
: screenSize.width * 0.95;
|
|
final dialogHeight = isDesktop
|
|
? screenSize.height * 0.75
|
|
: screenSize.height * 0.85;
|
|
|
|
return Dialog(
|
|
backgroundColor: Colors.transparent,
|
|
insetPadding: EdgeInsets.symmetric(
|
|
horizontal: isDesktop ? 40 : 10,
|
|
vertical: isDesktop ? 40 : 20,
|
|
),
|
|
child: KeyboardListener(
|
|
focusNode: FocusNode(),
|
|
autofocus: true,
|
|
onKeyEvent: (KeyEvent event) {
|
|
if (event is KeyDownEvent &&
|
|
event.logicalKey == LogicalKeyboardKey.escape) {
|
|
Navigator.of(context).pop();
|
|
}
|
|
},
|
|
child: Container(
|
|
width: dialogWidth,
|
|
height: dialogHeight,
|
|
constraints: BoxConstraints(
|
|
maxWidth: 900,
|
|
maxHeight: 700,
|
|
minWidth: 320,
|
|
minHeight: 400,
|
|
),
|
|
decoration: BoxDecoration(
|
|
color: ShadcnTheme.background,
|
|
borderRadius: BorderRadius.circular(12),
|
|
boxShadow: ShadcnTheme.shadowLg,
|
|
),
|
|
child: Column(
|
|
children: [
|
|
// 헤더
|
|
Container(
|
|
padding: const EdgeInsets.all(16),
|
|
decoration: BoxDecoration(
|
|
color: ShadcnTheme.card,
|
|
borderRadius: const BorderRadius.only(
|
|
topLeft: Radius.circular(12),
|
|
topRight: Radius.circular(12),
|
|
),
|
|
border: Border(
|
|
bottom: BorderSide(color: ShadcnTheme.border),
|
|
),
|
|
),
|
|
child: Row(
|
|
children: [
|
|
Icon(
|
|
Icons.history,
|
|
color: Colors.grey.shade700,
|
|
),
|
|
const SizedBox(width: 8),
|
|
Expanded(
|
|
child: Column(
|
|
crossAxisAlignment: CrossAxisAlignment.start,
|
|
children: [
|
|
const Text(
|
|
'장비 이력',
|
|
style: TextStyle(
|
|
fontSize: 16,
|
|
fontWeight: FontWeight.bold,
|
|
),
|
|
),
|
|
Text(
|
|
widget.equipmentName,
|
|
style: TextStyle(
|
|
fontSize: 12,
|
|
color: Colors.grey.shade600,
|
|
),
|
|
overflow: TextOverflow.ellipsis,
|
|
),
|
|
],
|
|
),
|
|
),
|
|
// 검색 필드
|
|
SizedBox(
|
|
width: 200,
|
|
height: 36,
|
|
child: TextField(
|
|
controller: _searchController,
|
|
decoration: InputDecoration(
|
|
hintText: '검색...',
|
|
hintStyle: TextStyle(fontSize: 13),
|
|
prefixIcon: const Icon(Icons.search, size: 18),
|
|
filled: true,
|
|
fillColor: Colors.grey.shade100,
|
|
border: OutlineInputBorder(
|
|
borderRadius: BorderRadius.circular(8),
|
|
borderSide: BorderSide.none,
|
|
),
|
|
contentPadding: const EdgeInsets.symmetric(
|
|
horizontal: 12,
|
|
vertical: 8,
|
|
),
|
|
),
|
|
style: const TextStyle(fontSize: 13),
|
|
),
|
|
),
|
|
const SizedBox(width: 8),
|
|
// 새로고침 버튼
|
|
IconButton(
|
|
icon: const Icon(Icons.refresh),
|
|
onPressed: _isLoading ? null : () => _loadHistory(isRefresh: true),
|
|
tooltip: '새로고침',
|
|
),
|
|
// 닫기 버튼
|
|
IconButton(
|
|
icon: const Icon(Icons.close),
|
|
onPressed: () => Navigator.of(context).pop(),
|
|
tooltip: '닫기',
|
|
),
|
|
],
|
|
),
|
|
),
|
|
// 본문
|
|
Expanded(
|
|
child: Builder(
|
|
builder: (context) {
|
|
// 초기 로딩
|
|
if ((_isInitialLoad || _isLoading) && _histories.isEmpty) {
|
|
return Center(
|
|
child: Column(
|
|
mainAxisAlignment: MainAxisAlignment.center,
|
|
children: [
|
|
const CircularProgressIndicator(),
|
|
const SizedBox(height: 16),
|
|
Text('이력을 불러오는 중...'),
|
|
],
|
|
),
|
|
);
|
|
}
|
|
|
|
// 에러 상태
|
|
if (_error != null && _histories.isEmpty) {
|
|
return Center(
|
|
child: Column(
|
|
mainAxisAlignment: MainAxisAlignment.center,
|
|
children: [
|
|
Icon(
|
|
Icons.error_outline,
|
|
size: 48,
|
|
color: Colors.red.shade400,
|
|
),
|
|
const SizedBox(height: 16),
|
|
Text(
|
|
'오류 발생',
|
|
style: TextStyle(
|
|
fontSize: 16,
|
|
fontWeight: FontWeight.bold,
|
|
),
|
|
),
|
|
const SizedBox(height: 8),
|
|
Padding(
|
|
padding: const EdgeInsets.symmetric(horizontal: 32),
|
|
child: Text(
|
|
_error!,
|
|
textAlign: TextAlign.center,
|
|
style: TextStyle(
|
|
color: Colors.red.shade600,
|
|
fontSize: 13,
|
|
),
|
|
),
|
|
),
|
|
const SizedBox(height: 16),
|
|
ElevatedButton.icon(
|
|
icon: const Icon(Icons.refresh, size: 16),
|
|
label: const Text('다시 시도'),
|
|
onPressed: () => _loadHistory(isRefresh: true),
|
|
style: ElevatedButton.styleFrom(
|
|
padding: const EdgeInsets.symmetric(
|
|
horizontal: 16,
|
|
vertical: 8,
|
|
),
|
|
),
|
|
),
|
|
],
|
|
),
|
|
);
|
|
}
|
|
|
|
// 데이터 없음
|
|
if (_filteredHistories.isEmpty) {
|
|
return Center(
|
|
child: Column(
|
|
mainAxisAlignment: MainAxisAlignment.center,
|
|
children: [
|
|
Icon(
|
|
Icons.inbox_outlined,
|
|
size: 48,
|
|
color: Colors.grey.shade400,
|
|
),
|
|
const SizedBox(height: 16),
|
|
Text(
|
|
_searchQuery.isNotEmpty
|
|
? '검색 결과가 없습니다.'
|
|
: '이력이 없습니다.',
|
|
style: TextStyle(
|
|
fontSize: 14,
|
|
color: Colors.grey.shade600,
|
|
),
|
|
),
|
|
if (_searchQuery.isNotEmpty) ...[
|
|
const SizedBox(height: 8),
|
|
TextButton(
|
|
onPressed: () {
|
|
_searchController.clear();
|
|
},
|
|
child: const Text('검색 초기화'),
|
|
),
|
|
],
|
|
],
|
|
),
|
|
);
|
|
}
|
|
|
|
// 이력 목록 + 상세 (데스크톱: 분할, 모바일: 목록만)
|
|
if (isDesktop) {
|
|
return Padding(
|
|
padding: const EdgeInsets.all(16),
|
|
child: Row(
|
|
children: [
|
|
// 목록
|
|
Expanded(
|
|
flex: 6,
|
|
child: ListView.builder(
|
|
controller: _scrollController,
|
|
itemCount: _filteredHistories.length + (_hasMore && _searchQuery.isEmpty ? 1 : 0),
|
|
itemBuilder: (context, index) {
|
|
if (index == _filteredHistories.length) {
|
|
return const Padding(
|
|
padding: EdgeInsets.all(16.0),
|
|
child: Center(child: CircularProgressIndicator()),
|
|
);
|
|
}
|
|
return _buildHistoryItem(_filteredHistories[index]);
|
|
},
|
|
),
|
|
),
|
|
const SizedBox(width: 16),
|
|
// 상세 패널
|
|
Expanded(
|
|
flex: 5,
|
|
child: _buildDetailPanel(),
|
|
),
|
|
],
|
|
),
|
|
);
|
|
}
|
|
// 모바일: 목록만, 항목 탭 시 상세 다이얼로그 표시
|
|
return Padding(
|
|
padding: const EdgeInsets.all(16),
|
|
child: ListView.builder(
|
|
controller: _scrollController,
|
|
itemCount: _filteredHistories.length + (_hasMore && _searchQuery.isEmpty ? 1 : 0),
|
|
itemBuilder: (context, index) {
|
|
if (index == _filteredHistories.length) {
|
|
return const Padding(
|
|
padding: EdgeInsets.all(16.0),
|
|
child: Center(child: CircularProgressIndicator()),
|
|
);
|
|
}
|
|
final item = _filteredHistories[index];
|
|
return GestureDetector(
|
|
onTap: () {
|
|
_selectHistory(item);
|
|
_showDetailBottomSheet(context);
|
|
},
|
|
child: _buildHistoryItem(item),
|
|
);
|
|
},
|
|
),
|
|
);
|
|
},
|
|
),
|
|
),
|
|
// 푸터
|
|
Container(
|
|
padding: const EdgeInsets.all(12),
|
|
decoration: BoxDecoration(
|
|
color: ShadcnTheme.card,
|
|
borderRadius: const BorderRadius.only(
|
|
bottomLeft: Radius.circular(12),
|
|
bottomRight: Radius.circular(12),
|
|
),
|
|
border: Border(
|
|
top: BorderSide(color: ShadcnTheme.border),
|
|
),
|
|
),
|
|
child: Row(
|
|
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
|
children: [
|
|
Text(
|
|
'총 ${_histories.length}개 이력',
|
|
style: TextStyle(
|
|
fontSize: 12,
|
|
color: Colors.grey.shade600,
|
|
),
|
|
),
|
|
if (_isLoading && _histories.isNotEmpty)
|
|
Row(
|
|
children: [
|
|
const SizedBox(
|
|
width: 12,
|
|
height: 12,
|
|
child: CircularProgressIndicator(
|
|
strokeWidth: 2,
|
|
),
|
|
),
|
|
const SizedBox(width: 8),
|
|
Text(
|
|
'더 불러오는 중...',
|
|
style: TextStyle(
|
|
fontSize: 12,
|
|
color: Colors.grey.shade600,
|
|
),
|
|
),
|
|
],
|
|
),
|
|
],
|
|
),
|
|
),
|
|
],
|
|
),
|
|
),
|
|
),
|
|
);
|
|
}
|
|
|
|
Widget _buildDetailPanel() {
|
|
final h = _selectedHistory;
|
|
return Container(
|
|
decoration: BoxDecoration(
|
|
color: ShadcnTheme.card,
|
|
borderRadius: BorderRadius.circular(8),
|
|
border: Border.all(color: ShadcnTheme.border),
|
|
),
|
|
padding: const EdgeInsets.all(16),
|
|
child: h == null
|
|
? Center(
|
|
child: Text(
|
|
'왼쪽에서 이력을 선택하세요',
|
|
style: TextStyle(color: Colors.grey.shade600),
|
|
),
|
|
)
|
|
: Column(
|
|
crossAxisAlignment: CrossAxisAlignment.start,
|
|
children: [
|
|
Row(
|
|
children: [
|
|
Icon(Icons.receipt_long, color: Colors.grey.shade700),
|
|
const SizedBox(width: 8),
|
|
const Text('이력 상세', style: TextStyle(fontWeight: FontWeight.bold)),
|
|
const Spacer(),
|
|
if (h.id != null)
|
|
IconButton(
|
|
tooltip: 'ID 복사',
|
|
icon: const Icon(Icons.copy, size: 18),
|
|
onPressed: () {
|
|
Clipboard.setData(ClipboardData(text: h.id.toString()));
|
|
ScaffoldMessenger.of(context).showSnackBar(
|
|
const SnackBar(content: Text('ID가 복사되었습니다')),
|
|
);
|
|
},
|
|
),
|
|
],
|
|
),
|
|
const SizedBox(height: 12),
|
|
_detailRow('거래일', _formatDate(h.transactedAt)),
|
|
_detailRow('유형', _getTransactionTypeText(h.transactionType)),
|
|
_detailRow('수량', h.quantity.toString()),
|
|
_detailRow('창고', h.warehouseName ?? (h.warehousesId > 0 ? '#${h.warehousesId}' : '-')),
|
|
_detailRow('장비', h.equipment?.serialNumber ?? h.equipmentSerial ?? '-'),
|
|
_detailRow('회사', h.companies.isNotEmpty
|
|
? h.companies.map((e) => e['name'] ?? e['id']?.toString() ?? '-').join(', ')
|
|
: '-'),
|
|
const SizedBox(height: 8),
|
|
const Text('비고', style: TextStyle(fontWeight: FontWeight.w600)),
|
|
const SizedBox(height: 4),
|
|
Container(
|
|
width: double.infinity,
|
|
padding: const EdgeInsets.all(12),
|
|
decoration: BoxDecoration(
|
|
color: ShadcnTheme.background,
|
|
borderRadius: BorderRadius.circular(6),
|
|
border: Border.all(color: ShadcnTheme.border),
|
|
),
|
|
child: Text(h.remark?.isNotEmpty == true ? h.remark! : '없음'),
|
|
),
|
|
const SizedBox(height: 12),
|
|
Divider(color: ShadcnTheme.border),
|
|
const SizedBox(height: 8),
|
|
_detailRow('생성일', _formatDate(h.createdAt)),
|
|
_detailRow('수정일', _formatDate(h.updatedAt)),
|
|
],
|
|
),
|
|
);
|
|
}
|
|
|
|
Widget _detailRow(String label, String value) {
|
|
return Padding(
|
|
padding: const EdgeInsets.only(bottom: 8.0),
|
|
child: Row(
|
|
crossAxisAlignment: CrossAxisAlignment.start,
|
|
children: [
|
|
SizedBox(width: 80, child: Text(label, style: const TextStyle(fontWeight: FontWeight.w600))),
|
|
const SizedBox(width: 8),
|
|
Expanded(child: Text(value)),
|
|
],
|
|
),
|
|
);
|
|
}
|
|
|
|
void _showDetailBottomSheet(BuildContext context) {
|
|
if (_selectedHistory == null) return;
|
|
final h = _selectedHistory!;
|
|
showModalBottomSheet(
|
|
context: context,
|
|
isScrollControlled: true,
|
|
backgroundColor: ShadcnTheme.card,
|
|
shape: const RoundedRectangleBorder(
|
|
borderRadius: BorderRadius.vertical(top: Radius.circular(12)),
|
|
),
|
|
builder: (context) {
|
|
return Padding(
|
|
padding: const EdgeInsets.fromLTRB(16, 16, 16, 24),
|
|
child: SingleChildScrollView(
|
|
child: Column(
|
|
crossAxisAlignment: CrossAxisAlignment.start,
|
|
children: [
|
|
Row(
|
|
children: [
|
|
const Text('이력 상세', style: TextStyle(fontWeight: FontWeight.bold)),
|
|
const Spacer(),
|
|
IconButton(
|
|
icon: const Icon(Icons.close),
|
|
onPressed: () => Navigator.pop(context),
|
|
)
|
|
],
|
|
),
|
|
const SizedBox(height: 12),
|
|
_detailRow('거래일', _formatDate(h.transactedAt)),
|
|
_detailRow('유형', _getTransactionTypeText(h.transactionType)),
|
|
_detailRow('수량', h.quantity.toString()),
|
|
_detailRow('창고', h.warehouseName ?? (h.warehousesId > 0 ? '#${h.warehousesId}' : '-')),
|
|
_detailRow('장비', h.equipment?.serialNumber ?? h.equipmentSerial ?? '-'),
|
|
_detailRow('회사', h.companies.isNotEmpty
|
|
? h.companies.map((e) => e['name'] ?? e['id']?.toString() ?? '-').join(', ')
|
|
: '-'),
|
|
const SizedBox(height: 8),
|
|
const Text('비고', style: TextStyle(fontWeight: FontWeight.w600)),
|
|
const SizedBox(height: 4),
|
|
Container(
|
|
width: double.infinity,
|
|
padding: const EdgeInsets.all(12),
|
|
decoration: BoxDecoration(
|
|
color: ShadcnTheme.background,
|
|
borderRadius: BorderRadius.circular(6),
|
|
border: Border.all(color: ShadcnTheme.border),
|
|
),
|
|
child: Text(h.remark?.isNotEmpty == true ? h.remark! : '없음'),
|
|
),
|
|
const SizedBox(height: 16),
|
|
],
|
|
),
|
|
),
|
|
);
|
|
},
|
|
);
|
|
}
|
|
}
|