사용하지 않는 파일 정리 전 백업 (Phase 10 완료 후 상태)
This commit is contained in:
904
lib/screens/maintenance/maintenance_history_screen.dart
Normal file
904
lib/screens/maintenance/maintenance_history_screen.dart
Normal file
@@ -0,0 +1,904 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:provider/provider.dart';
|
||||
import 'package:intl/intl.dart';
|
||||
import '../../data/models/maintenance_dto.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);
|
||||
}
|
||||
},
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
floatingActionButton: FloatingActionButton(
|
||||
onPressed: _exportHistory,
|
||||
tooltip: '엑셀 내보내기',
|
||||
child: const Icon(Icons.file_download),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
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 _buildTableView(List<MaintenanceDto> maintenances) {
|
||||
return SingleChildScrollView(
|
||||
scrollDirection: Axis.horizontal,
|
||||
child: SingleChildScrollView(
|
||||
padding: const EdgeInsets.all(24),
|
||||
child: DataTable(
|
||||
columns: const [
|
||||
DataColumn(label: Text('시작일')),
|
||||
DataColumn(label: Text('완료일')),
|
||||
DataColumn(label: Text('장비 이력 ID')),
|
||||
DataColumn(label: Text('유형')),
|
||||
DataColumn(label: Text('주기(개월)')),
|
||||
DataColumn(label: Text('등록일')),
|
||||
DataColumn(label: Text('작업')),
|
||||
],
|
||||
rows: maintenances.map((m) {
|
||||
return DataRow(
|
||||
cells: [
|
||||
DataCell(Text(m.startedAt != null ? DateFormat('yyyy-MM-dd').format(m.startedAt!) : '-')),
|
||||
DataCell(Text(m.endedAt != null ? DateFormat('yyyy-MM-dd').format(m.endedAt!) : '-')),
|
||||
DataCell(Text('#${m.equipmentHistoryId}')),
|
||||
DataCell(Text(m.maintenanceType == 'O' ? '현장' : '원격')),
|
||||
DataCell(Text('${m.periodMonth ?? 0}')),
|
||||
DataCell(Text(m.registeredAt != null ? DateFormat('yyyy-MM-dd').format(m.registeredAt!) : '-')),
|
||||
DataCell(
|
||||
IconButton(
|
||||
icon: const Icon(Icons.visibility, size: 20),
|
||||
onPressed: () => _showMaintenanceDetails(m),
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
}).toList(),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
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) {
|
||||
showDialog(
|
||||
context: context,
|
||||
builder: (context) => AlertDialog(
|
||||
title: Text('유지보수 상세 정보'),
|
||||
content: 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: [
|
||||
TextButton(
|
||||
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() {
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
const SnackBar(
|
||||
content: Text('엑셀 내보내기 기능은 준비 중입니다'),
|
||||
backgroundColor: Colors.orange,
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user