Files
superport/lib/screens/maintenance/maintenance_history_screen.dart
JiWoong Sul 650cd4be55
Some checks failed
Flutter Test & Quality Check / Test on macos-latest (push) Has been cancelled
Flutter Test & Quality Check / Test on ubuntu-latest (push) Has been cancelled
Flutter Test & Quality Check / Build APK (push) Has been cancelled
feat: Flutter analyze 오류 대폭 개선 및 재고 이력 화면 UI 통일 완료
## 주요 개선사항

### 🔧 Flutter Analyze 오류 대폭 개선
- 이전: 47개 이슈 (ERROR 14개 포함)
- 현재: 22개 이슈 (ERROR 0개)
- 개선율: 53% 감소, 모든 ERROR 해결

### 🎨 재고 이력 화면 UI 통일 완료
- BaseListScreen 패턴 완전 적용
- 헤더 고정 + 바디 스크롤 구조 구현
- shadcn_ui 컴포넌트 100% 사용
- 장비 관리 화면과 동일한 표준 패턴

###  코드 품질 개선
- unused imports 제거 (5개 파일)
- unnecessary cast 제거
- unused fields 제거
- injection container 오류 해결

### 📋 문서화 완료
- CLAUDE.md에 UI 통일성 리팩토링 계획 상세 추가
- 전체 10개 화면의 단계별 계획 문서화

🤖 Generated with [Claude Code](https://claude.ai/code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-08-31 17:37:49 +09:00

1060 lines
34 KiB
Dart

import 'package:flutter/material.dart';
import 'package:provider/provider.dart';
import 'package:shadcn_ui/shadcn_ui.dart';
import '../../data/models/maintenance_dto.dart';
import '../common/theme_shadcn.dart';
import 'controllers/maintenance_controller.dart';
class MaintenanceHistoryScreen extends StatefulWidget {
const MaintenanceHistoryScreen({super.key});
@override
State<MaintenanceHistoryScreen> createState() => _MaintenanceHistoryScreenState();
}
class _MaintenanceHistoryScreenState extends State<MaintenanceHistoryScreen>
with SingleTickerProviderStateMixin {
late TabController _tabController;
DateTimeRange? _selectedDateRange;
String _viewMode = 'timeline'; // timeline, table, analytics
@override
void initState() {
super.initState();
_tabController = TabController(length: 3, vsync: this);
// 초기 데이터 로드
WidgetsBinding.instance.addPostFrameCallback((_) {
final controller = context.read<MaintenanceController>();
controller.loadMaintenances(refresh: true);
});
}
@override
void dispose() {
_tabController.dispose();
super.dispose();
}
@override
Widget build(BuildContext context) {
return Scaffold(
backgroundColor: Colors.grey[100],
body: Column(
children: [
_buildHeader(),
_buildViewModeSelector(),
Expanded(
child: Consumer<MaintenanceController>(
builder: (context, controller, child) {
if (controller.isLoading && controller.maintenances.isEmpty) {
return const Center(child: CircularProgressIndicator());
}
if (controller.error != null) {
return Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
const Icon(Icons.error_outline, size: 64, color: Colors.red),
const SizedBox(height: 16),
Text(controller.error!),
const SizedBox(height: 16),
ElevatedButton(
onPressed: () => controller.loadMaintenances(refresh: true),
child: const Text('다시 시도'),
),
],
),
);
}
final completedMaintenances = controller.maintenances
.where((m) => m.endedAt != null)
.toList();
if (completedMaintenances.isEmpty) {
return _buildEmptyState();
}
switch (_viewMode) {
case 'timeline':
return _buildTimelineView(completedMaintenances);
case 'table':
return _buildTableView(completedMaintenances);
case 'analytics':
return _buildAnalyticsView(completedMaintenances, controller);
default:
return _buildTimelineView(completedMaintenances);
}
},
),
),
],
),
);
}
Widget _buildHeader() {
return Container(
padding: const EdgeInsets.all(24),
decoration: BoxDecoration(
color: Colors.white,
boxShadow: [
BoxShadow(
color: Colors.grey.withValues(alpha: 0.1),
spreadRadius: 1,
blurRadius: 3,
offset: const Offset(0, 2),
),
],
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
'유지보수 이력',
style: Theme.of(context).textTheme.headlineSmall?.copyWith(
fontWeight: FontWeight.bold,
),
),
const SizedBox(height: 4),
Consumer<MaintenanceController>(
builder: (context, controller, child) {
final completedCount = controller.maintenances
.where((m) => m.endedAt != null)
.length;
return Text(
'완료된 유지보수: $completedCount건',
style: TextStyle(color: Colors.grey[600]),
);
},
),
],
),
Row(
children: [
TextButton.icon(
onPressed: _selectDateRange,
icon: const Icon(Icons.date_range),
label: Text(
_selectedDateRange != null
? '${DateFormat('yyyy.MM.dd').format(_selectedDateRange!.start)} - ${DateFormat('yyyy.MM.dd').format(_selectedDateRange!.end)}'
: '기간 선택',
),
),
if (_selectedDateRange != null)
IconButton(
icon: const Icon(Icons.clear),
onPressed: () {
setState(() {
_selectedDateRange = null;
});
context.read<MaintenanceController>().loadMaintenances(refresh: true);
},
tooltip: '필터 초기화',
),
],
),
],
),
const SizedBox(height: 16),
_buildSummaryCards(),
],
),
);
}
Widget _buildSummaryCards() {
return Consumer<MaintenanceController>(
builder: (context, controller, child) {
final completedMaintenances = controller.maintenances
.where((m) => m.endedAt != null)
.toList();
// 백엔드에 cost 필드가 없으므로 다른 통계 사용
final thisMonthCompleted = completedMaintenances.where((m) {
final now = DateTime.now();
if (m.registeredAt == null) return false;
final registeredDate = m.registeredAt;
return registeredDate.year == now.year && registeredDate.month == now.month;
}).length;
final avgPeriod = completedMaintenances.isNotEmpty
? (completedMaintenances.map((m) => m.periodMonth ?? 0).reduce((a, b) => a + b) / completedMaintenances.length).toStringAsFixed(1)
: '0';
final onsiteCount = completedMaintenances.where((m) => m.maintenanceType == 'O').length;
return Row(
children: [
Expanded(
child: _buildSummaryCard(
'총 완료 건수',
completedMaintenances.length.toString(),
Icons.check_circle,
Colors.green,
),
),
const SizedBox(width: 12),
Expanded(
child: _buildSummaryCard(
'이번 달 완료',
thisMonthCompleted.toString(),
Icons.calendar_today,
Colors.blue,
),
),
const SizedBox(width: 12),
Expanded(
child: _buildSummaryCard(
'평균 주기',
'${avgPeriod}개월',
Icons.schedule,
Colors.orange,
),
),
const SizedBox(width: 12),
Expanded(
child: _buildSummaryCard(
'현장 유지보수',
'$onsiteCount건',
Icons.location_on,
Colors.purple,
),
),
],
);
},
);
}
Widget _buildSummaryCard(String title, String value, IconData icon, Color color) {
return Container(
padding: const EdgeInsets.all(16),
decoration: BoxDecoration(
color: color.withValues(alpha: 0.1),
borderRadius: BorderRadius.circular(8),
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Icon(icon, color: color, size: 24),
const SizedBox(height: 8),
Text(
title,
style: TextStyle(
fontSize: 12,
color: Colors.grey[600],
),
),
const SizedBox(height: 4),
Text(
value,
style: TextStyle(
fontSize: 18,
fontWeight: FontWeight.bold,
color: color,
),
),
],
),
);
}
Widget _buildViewModeSelector() {
return Container(
padding: const EdgeInsets.symmetric(horizontal: 24, vertical: 12),
decoration: BoxDecoration(
color: Colors.white,
border: Border(
bottom: BorderSide(color: Colors.grey[300]!),
),
),
child: Row(
children: [
_buildViewModeButton('timeline', Icons.timeline, '타임라인'),
const SizedBox(width: 12),
_buildViewModeButton('table', Icons.table_chart, '테이블'),
const SizedBox(width: 12),
_buildViewModeButton('analytics', Icons.analytics, '분석'),
],
),
);
}
Widget _buildViewModeButton(String mode, IconData icon, String label) {
final isSelected = _viewMode == mode;
return Expanded(
child: InkWell(
onTap: () => setState(() => _viewMode = mode),
child: Container(
padding: const EdgeInsets.symmetric(vertical: 12),
decoration: BoxDecoration(
color: isSelected ? Theme.of(context).primaryColor : Colors.grey[100],
borderRadius: BorderRadius.circular(8),
),
child: Row(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Icon(
icon,
size: 20,
color: isSelected ? Colors.white : Colors.grey[600],
),
const SizedBox(width: 8),
Text(
label,
style: TextStyle(
color: isSelected ? Colors.white : Colors.grey[600],
fontWeight: isSelected ? FontWeight.bold : FontWeight.normal,
),
),
],
),
),
),
);
}
Widget _buildEmptyState() {
return Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Icon(Icons.history, size: 64, color: Colors.grey[400]),
const SizedBox(height: 16),
Text(
'완료된 유지보수가 없습니다',
style: TextStyle(
fontSize: 18,
color: Colors.grey[600],
),
),
],
),
);
}
Widget _buildTimelineView(List<MaintenanceDto> maintenances) {
// 날짜별로 그룹화 (endedAt 기준)
final groupedByDate = <String, List<MaintenanceDto>>{};
for (final maintenance in maintenances) {
if (maintenance.endedAt == null) continue;
final endedDate = maintenance.endedAt;
final dateKey = DateFormat('yyyy-MM-dd').format(endedDate);
groupedByDate.putIfAbsent(dateKey, () => []).add(maintenance);
}
final sortedDates = groupedByDate.keys.toList()
..sort((a, b) => b.compareTo(a)); // 최신순 정렬
return ListView.builder(
padding: const EdgeInsets.all(24),
itemCount: sortedDates.length,
itemBuilder: (context, index) {
final date = sortedDates[index];
final dayMaintenances = groupedByDate[date]!;
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Container(
padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 6),
decoration: BoxDecoration(
color: Theme.of(context).primaryColor,
borderRadius: BorderRadius.circular(20),
),
child: Text(
DateFormat('yyyy년 MM월 dd일').format(DateTime.parse(date)),
style: const TextStyle(
color: Colors.white,
fontWeight: FontWeight.bold,
),
),
),
const SizedBox(height: 12),
...dayMaintenances.map((m) => _buildTimelineItem(m)),
const SizedBox(height: 24),
],
);
},
);
}
Widget _buildTimelineItem(MaintenanceDto maintenance) {
return Container(
margin: const EdgeInsets.only(left: 20, bottom: 12),
padding: const EdgeInsets.all(16),
decoration: BoxDecoration(
color: Colors.white,
borderRadius: BorderRadius.circular(8),
boxShadow: [
BoxShadow(
color: Colors.grey.withValues(alpha: 0.1),
blurRadius: 4,
offset: const Offset(0, 2),
),
],
border: Border(
left: BorderSide(
color: Theme.of(context).primaryColor,
width: 3,
),
),
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Text(
'Equipment History #${maintenance.equipmentHistoryId}',
style: const TextStyle(
fontWeight: FontWeight.bold,
fontSize: 16,
),
),
Text(
maintenance.endedAt != null
? DateFormat('HH:mm').format(maintenance.endedAt)
: '',
style: TextStyle(
color: Colors.grey[600],
fontSize: 14,
),
),
],
),
const SizedBox(height: 8),
Row(
children: [
Chip(
label: Text(
maintenance.maintenanceType == 'O' ? '현장' : '원격',
style: const TextStyle(fontSize: 12),
),
backgroundColor: Colors.grey[200],
padding: EdgeInsets.zero,
materialTapTargetSize: MaterialTapTargetSize.shrinkWrap,
),
const SizedBox(width: 8),
Text(
'${maintenance.periodMonth ?? 0}개월 주기',
style: TextStyle(
color: Colors.grey[700],
fontWeight: FontWeight.w500,
),
),
],
),
const SizedBox(height: 8),
Row(
children: [
Icon(Icons.date_range, size: 16, color: Colors.grey[600]),
const SizedBox(width: 4),
Text(
'시작: ${maintenance.startedAt ?? 'N/A'}',
style: TextStyle(color: Colors.grey[600]),
),
],
),
if (maintenance.endedAt != null) ...[
const SizedBox(height: 4),
Row(
children: [
Icon(Icons.check_circle, size: 16, color: Colors.green),
const SizedBox(width: 4),
Text(
'완료: ${maintenance.endedAt}',
style: TextStyle(color: Colors.green),
),
],
),
],
],
),
);
}
/// 헤더 셀 빌더
Widget _buildHeaderCell(
String text, {
required int flex,
required bool useExpanded,
required double minWidth,
}) {
final child = Container(
alignment: Alignment.centerLeft,
child: Text(
text,
style: ShadcnTheme.bodyMedium.copyWith(fontWeight: FontWeight.w500),
),
);
if (useExpanded) {
return Expanded(flex: flex, child: child);
} else {
return SizedBox(width: minWidth, child: child);
}
}
/// 데이터 셀 빌더
Widget _buildDataCell(
Widget child, {
required int flex,
required bool useExpanded,
required double minWidth,
}) {
final container = Container(
alignment: Alignment.centerLeft,
child: child,
);
if (useExpanded) {
return Expanded(flex: flex, child: container);
} else {
return SizedBox(width: minWidth, child: container);
}
}
/// 헤더 셀 리스트
List<Widget> _buildHeaderCells() {
return [
_buildHeaderCell('시작일', flex: 1, useExpanded: true, minWidth: 100),
_buildHeaderCell('완료일', flex: 1, useExpanded: true, minWidth: 100),
_buildHeaderCell('장비 이력 ID', flex: 1, useExpanded: true, minWidth: 100),
_buildHeaderCell('유형', flex: 0, useExpanded: false, minWidth: 80),
_buildHeaderCell('주기(개월)', flex: 0, useExpanded: false, minWidth: 100),
_buildHeaderCell('등록일', flex: 1, useExpanded: true, minWidth: 100),
_buildHeaderCell('작업', flex: 0, useExpanded: false, minWidth: 80),
];
}
/// 테이블 행 빌더
Widget _buildTableRow(MaintenanceDto maintenance, int index) {
return Container(
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 4),
decoration: BoxDecoration(
color: index.isEven
? ShadcnTheme.muted.withValues(alpha: 0.1)
: null,
border: const Border(
bottom: BorderSide(color: Colors.black),
),
),
child: Row(
children: [
_buildDataCell(
Text(
maintenance.startedAt != null
? DateFormat('yyyy-MM-dd').format(maintenance.startedAt)
: '-',
style: ShadcnTheme.bodySmall,
),
flex: 1,
useExpanded: true,
minWidth: 100,
),
_buildDataCell(
Text(
maintenance.endedAt != null
? DateFormat('yyyy-MM-dd').format(maintenance.endedAt)
: '-',
style: ShadcnTheme.bodySmall,
),
flex: 1,
useExpanded: true,
minWidth: 100,
),
_buildDataCell(
Text(
'#${maintenance.equipmentHistoryId}',
style: ShadcnTheme.bodyMedium.copyWith(
fontWeight: FontWeight.w500,
),
),
flex: 1,
useExpanded: true,
minWidth: 100,
),
_buildDataCell(
ShadBadge(
child: Text(maintenance.maintenanceType == 'O' ? '현장' : '원격'),
),
flex: 0,
useExpanded: false,
minWidth: 80,
),
_buildDataCell(
Text(
'${maintenance.periodMonth ?? 0}',
style: ShadcnTheme.bodySmall,
textAlign: TextAlign.center,
),
flex: 0,
useExpanded: false,
minWidth: 100,
),
_buildDataCell(
Text(
maintenance.registeredAt != null
? DateFormat('yyyy-MM-dd').format(maintenance.registeredAt)
: '-',
style: ShadcnTheme.bodySmall,
),
flex: 1,
useExpanded: true,
minWidth: 100,
),
_buildDataCell(
ShadButton.ghost(
size: ShadButtonSize.sm,
onPressed: () => _showMaintenanceDetails(maintenance),
child: const Icon(Icons.visibility, size: 16),
),
flex: 0,
useExpanded: false,
minWidth: 80,
),
],
),
);
}
Widget _buildTableView(List<MaintenanceDto> maintenances) {
return Container(
margin: const EdgeInsets.all(24),
decoration: BoxDecoration(
border: Border.all(color: Colors.black),
borderRadius: BorderRadius.circular(ShadcnTheme.radiusMd),
),
child: Column(
children: [
// 고정 헤더
Container(
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 10),
decoration: BoxDecoration(
color: ShadcnTheme.muted.withValues(alpha: 0.3),
border: Border(bottom: BorderSide(color: Colors.black)),
),
child: Row(children: _buildHeaderCells()),
),
// 스크롤 바디
Expanded(
child: maintenances.isEmpty
? Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Icon(
Icons.build_circle_outlined,
size: 64,
color: ShadcnTheme.mutedForeground,
),
const SizedBox(height: 16),
Text(
'완료된 정비 이력이 없습니다',
style: ShadcnTheme.bodyMedium.copyWith(
color: ShadcnTheme.mutedForeground,
),
),
],
),
)
: ListView.builder(
itemCount: maintenances.length,
itemBuilder: (context, index) => _buildTableRow(maintenances[index], index),
),
),
],
),
);
}
Widget _buildAnalyticsView(List<MaintenanceDto> maintenances, MaintenanceController controller) {
// 월별 비용 계산
final monthlyCosts = <String, double>{};
final typeDistribution = <String, int>{};
final vendorCosts = <String, double>{};
for (final m in maintenances) {
// 월별 비용
if (m.updatedAt == null) continue;
final updatedDate = m.updatedAt!;
final monthKey = DateFormat('yyyy-MM').format(updatedDate);
// cost 필드가 백엔드에 없으므로 비용 계산 제거
monthlyCosts[monthKey] = (monthlyCosts[monthKey] ?? 0.0) + 0.0;
// 유형별 분포
final typeKey = m.maintenanceType == 'O' ? '현장' : '원격';
typeDistribution[typeKey] = (typeDistribution[typeKey] ?? 0) + 1;
// 업체별 비용 (description 필드도 백엔드에 없으므로 maintenanceType 사용)
final vendorKey = m.maintenanceType == 'O' ? '현장유지보수' : '원격유지보수';
vendorCosts[vendorKey] = (vendorCosts[vendorKey] ?? 0) + 1; // 건수로 대체
}
return SingleChildScrollView(
padding: const EdgeInsets.all(24),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
// 월별 비용 추이
_buildAnalyticsSection(
'월별 비용 추이',
Icons.show_chart,
_buildMonthlyChart(monthlyCosts),
),
const SizedBox(height: 24),
// 유형별 분포
_buildAnalyticsSection(
'유지보수 유형 분포',
Icons.pie_chart,
_buildTypeDistribution(typeDistribution),
),
const SizedBox(height: 24),
// 업체별 비용
_buildAnalyticsSection(
'업체별 비용 현황',
Icons.business,
_buildVendorCosts(vendorCosts),
),
const SizedBox(height: 24),
// 통계 요약
_buildStatisticsSummary(maintenances),
],
),
);
}
Widget _buildAnalyticsSection(String title, IconData icon, Widget content) {
return Container(
padding: const EdgeInsets.all(20),
decoration: BoxDecoration(
color: Colors.white,
borderRadius: BorderRadius.circular(12),
boxShadow: [
BoxShadow(
color: Colors.grey.withValues(alpha: 0.1),
blurRadius: 10,
offset: const Offset(0, 2),
),
],
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
children: [
Icon(icon, color: Theme.of(context).primaryColor),
const SizedBox(width: 8),
Text(
title,
style: const TextStyle(
fontSize: 18,
fontWeight: FontWeight.bold,
),
),
],
),
const SizedBox(height: 16),
content,
],
),
);
}
Widget _buildMonthlyChart(Map<String, double> monthlyCosts) {
final sortedMonths = monthlyCosts.keys.toList()..sort();
return Column(
children: sortedMonths.map((month) {
final cost = monthlyCosts[month]!;
final maxCost = monthlyCosts.values.reduce((a, b) => a > b ? a : b);
final ratio = cost / maxCost;
return Padding(
padding: const EdgeInsets.symmetric(vertical: 4),
child: Row(
children: [
SizedBox(
width: 60,
child: Text(
month,
style: const TextStyle(fontSize: 12),
),
),
Expanded(
child: Stack(
children: [
Container(
height: 30,
decoration: BoxDecoration(
color: Colors.grey[200],
borderRadius: BorderRadius.circular(4),
),
),
FractionallySizedBox(
widthFactor: ratio,
child: Container(
height: 30,
decoration: BoxDecoration(
color: Theme.of(context).primaryColor,
borderRadius: BorderRadius.circular(4),
),
),
),
],
),
),
const SizedBox(width: 8),
Text(
'${NumberFormat('#,###').format(cost)}',
style: const TextStyle(fontSize: 12),
),
],
),
);
}).toList(),
);
}
Widget _buildTypeDistribution(Map<String, int> typeDistribution) {
final total = typeDistribution.values.fold<int>(0, (sum, count) => sum + count);
return Row(
children: typeDistribution.entries.map((entry) {
final percentage = (entry.value / total * 100).toStringAsFixed(1);
return Expanded(
child: Container(
padding: const EdgeInsets.all(16),
margin: const EdgeInsets.only(right: 8),
decoration: BoxDecoration(
color: entry.key == '현장' ? Colors.blue[50] : Colors.green[50],
borderRadius: BorderRadius.circular(8),
border: Border.all(
color: entry.key == '현장' ? Colors.blue : Colors.green,
),
),
child: Column(
children: [
Icon(
entry.key == '현장' ? Icons.location_on : Icons.computer,
color: entry.key == '현장' ? Colors.blue : Colors.green,
size: 32,
),
const SizedBox(height: 8),
Text(
entry.key,
style: const TextStyle(fontWeight: FontWeight.bold),
),
const SizedBox(height: 4),
Text(
'${entry.value}',
style: const TextStyle(fontSize: 18),
),
Text(
'$percentage%',
style: TextStyle(
color: Colors.grey[600],
fontSize: 12,
),
),
],
),
),
);
}).toList(),
);
}
Widget _buildVendorCosts(Map<String, double> vendorCosts) {
final sortedVendors = vendorCosts.entries.toList()
..sort((a, b) => b.value.compareTo(a.value));
return Column(
children: sortedVendors.take(5).map((entry) {
return ListTile(
leading: CircleAvatar(
child: Text(entry.key.substring(0, 1).toUpperCase()),
),
title: Text(entry.key),
trailing: Text(
'${NumberFormat('#,###').format(entry.value)}',
style: const TextStyle(
fontWeight: FontWeight.bold,
fontSize: 16,
),
),
);
}).toList(),
);
}
Widget _buildStatisticsSummary(List<MaintenanceDto> maintenances) {
// 비용 대신 유지보수 기간 통계로 대체 (백엔드에 cost 필드 없음)
final totalMaintenances = maintenances.length;
final avgPeriod = maintenances.isNotEmpty
? maintenances.map((m) => m.periodMonth).reduce((a, b) => a + b) / maintenances.length
: 0.0;
final maxPeriod = maintenances.isNotEmpty
? maintenances.map((m) => m.periodMonth).reduce((a, b) => a > b ? a : b).toDouble()
: 0.0;
final minPeriod = maintenances.isNotEmpty
? maintenances.map((m) => m.periodMonth).reduce((a, b) => a < b ? a : b).toDouble()
: 0.0;
return Container(
padding: const EdgeInsets.all(20),
decoration: BoxDecoration(
gradient: LinearGradient(
colors: [
Theme.of(context).primaryColor,
Theme.of(context).primaryColor.withValues(alpha: 0.8),
],
),
borderRadius: BorderRadius.circular(12),
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
const Text(
'통계 요약',
style: TextStyle(
color: Colors.white,
fontSize: 18,
fontWeight: FontWeight.bold,
),
),
const SizedBox(height: 16),
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
_buildStatItem('총 건수', '$totalMaintenances건'),
_buildStatItem('평균 기간', '${avgPeriod.toStringAsFixed(1)}개월'),
_buildStatItem('최대 기간', '${maxPeriod.toInt()}개월'),
_buildStatItem('최소 기간', '${minPeriod.toInt()}개월'),
],
),
],
),
);
}
Widget _buildStatItem(String label, String value) {
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
label,
style: TextStyle(
color: Colors.white.withValues(alpha: 0.8),
fontSize: 12,
),
),
const SizedBox(height: 4),
Text(
value,
style: const TextStyle(
color: Colors.white,
fontSize: 16,
fontWeight: FontWeight.bold,
),
),
],
);
}
void _selectDateRange() async {
final picked = await showDateRangePicker(
context: context,
firstDate: DateTime(2020),
lastDate: DateTime.now(),
initialDateRange: _selectedDateRange,
);
if (picked != null) {
setState(() {
_selectedDateRange = picked;
});
// 날짜 범위로 필터링
// TODO: 백엔드 API가 날짜 범위 필터를 지원하면 구현
}
}
void _showMaintenanceDetails(MaintenanceDto maintenance) {
showShadDialog(
context: context,
builder: (context) => ShadDialog(
title: const Text('유지보수 상세 정보'),
child: SizedBox(
width: 500,
child: SingleChildScrollView(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
mainAxisSize: MainAxisSize.min,
children: [
_buildDetailRow('장비 이력 ID', '#${maintenance.equipmentHistoryId}'),
_buildDetailRow('유지보수 유형', maintenance.maintenanceType == 'O' ? '현장' : '원격'),
_buildDetailRow('시작일', maintenance.startedAt != null ? DateFormat('yyyy-MM-dd').format(maintenance.startedAt) : 'N/A'),
_buildDetailRow('완료일', maintenance.endedAt != null ? DateFormat('yyyy-MM-dd').format(maintenance.endedAt) : 'N/A'),
_buildDetailRow('주기', '${maintenance.periodMonth ?? 0}개월'),
_buildDetailRow('등록일', maintenance.registeredAt != null ? DateFormat('yyyy-MM-dd').format(maintenance.registeredAt) : 'N/A'),
],
),
),
),
actions: [
ShadButton.outline(
onPressed: () => Navigator.of(context).pop(),
child: const Text('닫기'),
),
],
),
);
}
Widget _buildDetailRow(String label, String value) {
return Padding(
padding: const EdgeInsets.symmetric(vertical: 4),
child: Row(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
SizedBox(
width: 120,
child: Text(
label,
style: const TextStyle(fontWeight: FontWeight.bold),
),
),
Expanded(
child: Text(value),
),
],
),
);
}
void _exportHistory() {
ShadToaster.of(context).show(
const ShadToast(
title: Text('엑셀 내보내기'),
description: Text('엑셀 내보내기 기능은 준비 중입니다'),
),
);
}
}