feat: V/R 유지보수 시스템 전환 및 대시보드 테이블 형태 완성
- V/R 시스템 완전 전환: WARRANTY/CONTRACT/INSPECTION → V(방문)/R(원격) - 유지보수 대시보드 카드 → StandardDataTable 테이블 형태 전환 - "조회중..." 문제 해결: 백엔드 직접 필드 사용 (equipment_model, company_name) - MaintenanceDto 신규 필드 추가: company_id, company_name, equipment_serial, equipment_model - preloadEquipmentData 비활성화로 불필요한 equipment-history API 호출 제거 - CO-STAR 프레임워크 적용 및 CLAUDE.md v3.0 업데이트 - Flutter Analyze ERROR: 0 유지, 100% shadcn_ui 컴플라이언스 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,537 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:shadcn_ui/shadcn_ui.dart';
|
||||
import 'package:superport/data/models/inventory_history_view_model.dart';
|
||||
import 'package:superport/screens/inventory/controllers/inventory_history_controller.dart';
|
||||
import 'package:superport/screens/common/theme_shadcn.dart';
|
||||
import 'package:superport/screens/common/components/shadcn_components.dart';
|
||||
|
||||
/// 장비 이력 상세보기 다이얼로그
|
||||
/// 특정 장비의 전체 히스토리를 시간순으로 표시
|
||||
class EquipmentHistoryDetailDialog extends StatefulWidget {
|
||||
final int equipmentId;
|
||||
final String equipmentName;
|
||||
final String serialNumber;
|
||||
final InventoryHistoryController controller;
|
||||
|
||||
const EquipmentHistoryDetailDialog({
|
||||
super.key,
|
||||
required this.equipmentId,
|
||||
required this.equipmentName,
|
||||
required this.serialNumber,
|
||||
required this.controller,
|
||||
});
|
||||
|
||||
@override
|
||||
State<EquipmentHistoryDetailDialog> createState() =>
|
||||
_EquipmentHistoryDetailDialogState();
|
||||
}
|
||||
|
||||
class _EquipmentHistoryDetailDialogState
|
||||
extends State<EquipmentHistoryDetailDialog> {
|
||||
List<InventoryHistoryViewModel>? _historyList;
|
||||
bool _isLoading = true;
|
||||
String? _error;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
_loadEquipmentHistory();
|
||||
}
|
||||
|
||||
/// 장비별 이력 로드
|
||||
Future<void> _loadEquipmentHistory() async {
|
||||
setState(() {
|
||||
_isLoading = true;
|
||||
_error = null;
|
||||
});
|
||||
|
||||
try {
|
||||
final histories = await widget.controller.loadEquipmentHistory(widget.equipmentId);
|
||||
setState(() {
|
||||
_historyList = histories;
|
||||
_isLoading = false;
|
||||
});
|
||||
} catch (e) {
|
||||
setState(() {
|
||||
_error = e.toString();
|
||||
_isLoading = false;
|
||||
});
|
||||
print('[EquipmentHistoryDetailDialog] Error loading equipment history: $e');
|
||||
}
|
||||
}
|
||||
|
||||
/// 거래 유형 아이콘 반환
|
||||
IconData _getTransactionIcon(String transactionType) {
|
||||
switch (transactionType) {
|
||||
case 'I':
|
||||
return Icons.arrow_downward; // 입고
|
||||
case 'O':
|
||||
return Icons.arrow_upward; // 출고
|
||||
case 'R':
|
||||
return Icons.share; // 대여
|
||||
case 'D':
|
||||
return Icons.delete_outline; // 폐기
|
||||
default:
|
||||
return Icons.help_outline;
|
||||
}
|
||||
}
|
||||
|
||||
/// 거래 유형 색상 반환
|
||||
Color _getTransactionColor(String transactionType) {
|
||||
switch (transactionType) {
|
||||
case 'I':
|
||||
return Colors.green; // 입고
|
||||
case 'O':
|
||||
return Colors.orange; // 출고
|
||||
case 'R':
|
||||
return Colors.blue; // 대여
|
||||
case 'D':
|
||||
return Colors.red; // 폐기
|
||||
default:
|
||||
return Colors.grey;
|
||||
}
|
||||
}
|
||||
|
||||
/// 타임라인 아이템 빌더
|
||||
Widget _buildTimelineItem(InventoryHistoryViewModel history, int index) {
|
||||
final isFirst = index == 0;
|
||||
final isLast = index == (_historyList?.length ?? 0) - 1;
|
||||
final color = _getTransactionColor(history.transactionType);
|
||||
|
||||
return Row(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
// 타임라인 인디케이터
|
||||
SizedBox(
|
||||
width: 60,
|
||||
child: Column(
|
||||
children: [
|
||||
// 위쪽 연결선
|
||||
if (!isFirst)
|
||||
Container(
|
||||
width: 2,
|
||||
height: 20,
|
||||
color: Colors.grey.withValues(alpha: 0.3),
|
||||
),
|
||||
// 원형 인디케이터
|
||||
Container(
|
||||
width: 32,
|
||||
height: 32,
|
||||
decoration: BoxDecoration(
|
||||
shape: BoxShape.circle,
|
||||
color: color,
|
||||
border: Border.all(
|
||||
color: Colors.white,
|
||||
width: 2,
|
||||
),
|
||||
boxShadow: [
|
||||
BoxShadow(
|
||||
color: color.withValues(alpha: 0.3),
|
||||
blurRadius: 4,
|
||||
offset: const Offset(0, 2),
|
||||
),
|
||||
],
|
||||
),
|
||||
child: Icon(
|
||||
_getTransactionIcon(history.transactionType),
|
||||
color: Colors.white,
|
||||
size: 16,
|
||||
),
|
||||
),
|
||||
// 아래쪽 연결선
|
||||
if (!isLast)
|
||||
Container(
|
||||
width: 2,
|
||||
height: 20,
|
||||
color: Colors.grey.withValues(alpha: 0.3),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 16),
|
||||
// 이력 정보
|
||||
Expanded(
|
||||
child: Container(
|
||||
margin: EdgeInsets.only(bottom: isLast ? 0 : 16),
|
||||
padding: const EdgeInsets.all(16),
|
||||
decoration: BoxDecoration(
|
||||
color: ShadcnTheme.card,
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
border: Border.all(
|
||||
color: color.withValues(alpha: 0.2),
|
||||
width: 1,
|
||||
),
|
||||
boxShadow: [
|
||||
BoxShadow(
|
||||
color: Colors.black.withValues(alpha: 0.05),
|
||||
blurRadius: 4,
|
||||
offset: const Offset(0, 2),
|
||||
),
|
||||
],
|
||||
),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
// 헤더 (거래 유형 + 날짜)
|
||||
Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
Row(
|
||||
children: [
|
||||
ShadcnBadge(
|
||||
text: history.transactionTypeDisplay,
|
||||
variant: _getBadgeVariant(history.transactionType),
|
||||
size: ShadcnBadgeSize.small,
|
||||
),
|
||||
const SizedBox(width: 8),
|
||||
if (isFirst)
|
||||
Container(
|
||||
padding: const EdgeInsets.symmetric(
|
||||
horizontal: 6,
|
||||
vertical: 2,
|
||||
),
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.orange.withValues(alpha: 0.1),
|
||||
borderRadius: BorderRadius.circular(4),
|
||||
border: Border.all(
|
||||
color: Colors.orange.withValues(alpha: 0.3),
|
||||
),
|
||||
),
|
||||
child: const Text(
|
||||
'최근',
|
||||
style: TextStyle(
|
||||
fontSize: 10,
|
||||
color: Colors.orange,
|
||||
fontWeight: FontWeight.w500,
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
Text(
|
||||
history.formattedDate,
|
||||
style: ShadcnTheme.bodySmall.copyWith(
|
||||
color: ShadcnTheme.mutedForeground,
|
||||
fontWeight: FontWeight.w500,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
const SizedBox(height: 12),
|
||||
// 위치 정보
|
||||
Row(
|
||||
children: [
|
||||
Icon(
|
||||
history.isCustomerLocation ? Icons.business : Icons.warehouse,
|
||||
size: 16,
|
||||
color: history.isCustomerLocation ? Colors.blue : Colors.green,
|
||||
),
|
||||
const SizedBox(width: 6),
|
||||
Text(
|
||||
'위치: ',
|
||||
style: ShadcnTheme.bodySmall.copyWith(
|
||||
color: ShadcnTheme.mutedForeground,
|
||||
),
|
||||
),
|
||||
Expanded(
|
||||
child: Text(
|
||||
history.location,
|
||||
style: ShadcnTheme.bodySmall.copyWith(
|
||||
fontWeight: FontWeight.w500,
|
||||
),
|
||||
overflow: TextOverflow.ellipsis,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
// 수량 정보
|
||||
if (history.quantity > 0) ...[
|
||||
const SizedBox(height: 6),
|
||||
Row(
|
||||
children: [
|
||||
Icon(
|
||||
Icons.inventory,
|
||||
size: 16,
|
||||
color: ShadcnTheme.mutedForeground,
|
||||
),
|
||||
const SizedBox(width: 6),
|
||||
Text(
|
||||
'수량: ',
|
||||
style: ShadcnTheme.bodySmall.copyWith(
|
||||
color: ShadcnTheme.mutedForeground,
|
||||
),
|
||||
),
|
||||
Text(
|
||||
'${history.quantity}개',
|
||||
style: ShadcnTheme.bodySmall.copyWith(
|
||||
fontWeight: FontWeight.w500,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
// 비고
|
||||
if (history.remark != null && history.remark!.isNotEmpty) ...[
|
||||
const SizedBox(height: 8),
|
||||
Container(
|
||||
width: double.infinity,
|
||||
padding: const EdgeInsets.all(8),
|
||||
decoration: BoxDecoration(
|
||||
color: ShadcnTheme.muted.withValues(alpha: 0.3),
|
||||
borderRadius: BorderRadius.circular(4),
|
||||
),
|
||||
child: Row(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Icon(
|
||||
Icons.note,
|
||||
size: 14,
|
||||
color: ShadcnTheme.mutedForeground,
|
||||
),
|
||||
const SizedBox(width: 6),
|
||||
Expanded(
|
||||
child: Text(
|
||||
history.remark!,
|
||||
style: ShadcnTheme.bodySmall.copyWith(
|
||||
color: ShadcnTheme.mutedForeground,
|
||||
fontStyle: FontStyle.italic,
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
/// Badge Variant 반환
|
||||
ShadcnBadgeVariant _getBadgeVariant(String transactionType) {
|
||||
switch (transactionType) {
|
||||
case 'I':
|
||||
return ShadcnBadgeVariant.success; // 입고
|
||||
case 'O':
|
||||
return ShadcnBadgeVariant.warning; // 출고
|
||||
case 'R':
|
||||
return ShadcnBadgeVariant.info; // 대여
|
||||
case 'D':
|
||||
return ShadcnBadgeVariant.destructive; // 폐기
|
||||
default:
|
||||
return ShadcnBadgeVariant.secondary;
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return ShadDialog(
|
||||
title: Row(
|
||||
children: [
|
||||
Icon(
|
||||
Icons.history,
|
||||
color: ShadcnTheme.primary,
|
||||
size: 20,
|
||||
),
|
||||
const SizedBox(width: 8),
|
||||
const Text('장비 이력 상세'),
|
||||
],
|
||||
),
|
||||
description: SingleChildScrollView(
|
||||
child: SizedBox(
|
||||
width: 600,
|
||||
height: 500,
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
// 장비 정보 헤더
|
||||
Container(
|
||||
width: double.infinity,
|
||||
padding: const EdgeInsets.all(16),
|
||||
decoration: BoxDecoration(
|
||||
color: ShadcnTheme.muted.withValues(alpha: 0.2),
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
border: Border.all(color: ShadcnTheme.border),
|
||||
),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Row(
|
||||
children: [
|
||||
Icon(
|
||||
Icons.precision_manufacturing,
|
||||
color: ShadcnTheme.primary,
|
||||
size: 18,
|
||||
),
|
||||
const SizedBox(width: 8),
|
||||
Text(
|
||||
widget.equipmentName,
|
||||
style: ShadcnTheme.bodyLarge.copyWith(
|
||||
fontWeight: FontWeight.w600,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
const SizedBox(height: 4),
|
||||
Row(
|
||||
children: [
|
||||
Icon(
|
||||
Icons.qr_code,
|
||||
color: ShadcnTheme.mutedForeground,
|
||||
size: 16,
|
||||
),
|
||||
const SizedBox(width: 6),
|
||||
Text(
|
||||
'시리얼: ${widget.serialNumber}',
|
||||
style: ShadcnTheme.bodySmall.copyWith(
|
||||
color: ShadcnTheme.mutedForeground,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 20),
|
||||
// 이력 목록 헤더
|
||||
Row(
|
||||
children: [
|
||||
Icon(
|
||||
Icons.timeline,
|
||||
color: ShadcnTheme.mutedForeground,
|
||||
size: 16,
|
||||
),
|
||||
const SizedBox(width: 6),
|
||||
Text(
|
||||
'변동 이력 (시간순)',
|
||||
style: ShadcnTheme.bodyMedium.copyWith(
|
||||
fontWeight: FontWeight.w500,
|
||||
color: ShadcnTheme.mutedForeground,
|
||||
),
|
||||
),
|
||||
if (_historyList != null) ...[
|
||||
const SizedBox(width: 8),
|
||||
ShadcnBadge(
|
||||
text: '${_historyList!.length}건',
|
||||
variant: ShadcnBadgeVariant.secondary,
|
||||
size: ShadcnBadgeSize.small,
|
||||
),
|
||||
],
|
||||
],
|
||||
),
|
||||
const SizedBox(height: 12),
|
||||
// 이력 목록
|
||||
Expanded(
|
||||
child: _buildHistoryContent(),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
actions: [
|
||||
ShadcnButton(
|
||||
text: '새로고침',
|
||||
onPressed: _loadEquipmentHistory,
|
||||
variant: ShadcnButtonVariant.secondary,
|
||||
icon: const Icon(Icons.refresh, size: 16),
|
||||
),
|
||||
ShadcnButton(
|
||||
text: '닫기',
|
||||
onPressed: () => Navigator.of(context).pop(),
|
||||
variant: ShadcnButtonVariant.primary,
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
/// 이력 컨텐츠 빌더
|
||||
Widget _buildHistoryContent() {
|
||||
if (_isLoading) {
|
||||
return const Center(
|
||||
child: Column(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
SizedBox(
|
||||
width: 32,
|
||||
height: 32,
|
||||
child: CircularProgressIndicator(strokeWidth: 2),
|
||||
),
|
||||
SizedBox(height: 12),
|
||||
Text('이력을 불러오는 중...'),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
if (_error != null) {
|
||||
return Center(
|
||||
child: Column(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
Icon(
|
||||
Icons.error_outline,
|
||||
size: 48,
|
||||
color: Colors.red.withValues(alpha: 0.6),
|
||||
),
|
||||
const SizedBox(height: 12),
|
||||
Text(
|
||||
'이력을 불러올 수 없습니다',
|
||||
style: ShadcnTheme.bodyMedium.copyWith(
|
||||
color: Colors.red,
|
||||
fontWeight: FontWeight.w500,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
Text(
|
||||
_error!,
|
||||
style: ShadcnTheme.bodySmall.copyWith(
|
||||
color: ShadcnTheme.mutedForeground,
|
||||
),
|
||||
textAlign: TextAlign.center,
|
||||
),
|
||||
const SizedBox(height: 12),
|
||||
ShadcnButton(
|
||||
text: '다시 시도',
|
||||
onPressed: _loadEquipmentHistory,
|
||||
variant: ShadcnButtonVariant.secondary,
|
||||
icon: const Icon(Icons.refresh, size: 16),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
if (_historyList == null || _historyList!.isEmpty) {
|
||||
return Center(
|
||||
child: Column(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
Icon(
|
||||
Icons.inventory_2_outlined,
|
||||
size: 48,
|
||||
color: ShadcnTheme.mutedForeground.withValues(alpha: 0.6),
|
||||
),
|
||||
const SizedBox(height: 12),
|
||||
Text(
|
||||
'등록된 이력이 없습니다',
|
||||
style: ShadcnTheme.bodyMedium.copyWith(
|
||||
color: ShadcnTheme.mutedForeground,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
return ListView.builder(
|
||||
padding: const EdgeInsets.symmetric(vertical: 8),
|
||||
itemCount: _historyList!.length,
|
||||
itemBuilder: (context, index) {
|
||||
return _buildTimelineItem(_historyList![index], index);
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user