Files
superport/lib/screens/maintenance/maintenance_schedule_screen.dart

706 lines
23 KiB
Dart

import 'package:flutter/material.dart';
import 'package:provider/provider.dart';
import 'package:intl/intl.dart';
import '../../data/models/maintenance_dto.dart';
import '../../domain/entities/maintenance_schedule.dart';
import 'controllers/maintenance_controller.dart';
import 'maintenance_form_dialog.dart';
import 'components/maintenance_calendar.dart';
class MaintenanceScheduleScreen extends StatefulWidget {
const MaintenanceScheduleScreen({super.key});
@override
State<MaintenanceScheduleScreen> createState() =>
_MaintenanceScheduleScreenState();
}
class _MaintenanceScheduleScreenState extends State<MaintenanceScheduleScreen>
with SingleTickerProviderStateMixin {
late TabController _tabController;
bool _isCalendarView = false;
@override
void initState() {
super.initState();
_tabController = TabController(length: 3, vsync: this);
// 초기 데이터 로드
WidgetsBinding.instance.addPostFrameCallback((_) {
final controller = context.read<MaintenanceController>();
controller.loadMaintenances(refresh: true);
controller.loadAlerts();
controller.loadStatistics();
});
}
@override
void dispose() {
_tabController.dispose();
super.dispose();
}
@override
Widget build(BuildContext context) {
return Scaffold(
backgroundColor: Colors.grey[100],
body: Column(
children: [
_buildHeader(),
_buildFilterBar(),
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: [
Text(
'오류 발생',
style: Theme.of(context).textTheme.headlineSmall,
),
const SizedBox(height: 8),
Text(controller.error!),
const SizedBox(height: 16),
ElevatedButton(
onPressed:
() => controller.loadMaintenances(refresh: true),
child: const Text('다시 시도'),
),
],
),
);
}
return _isCalendarView
? MaintenanceCalendar(
maintenances: controller.maintenances,
onDateSelected: (date) {
// 날짜 선택시 해당 날짜의 유지보수 표시
_showMaintenancesForDate(date, controller.maintenances);
},
)
: _buildListView(controller);
},
),
),
],
),
floatingActionButton: FloatingActionButton.extended(
onPressed: _showCreateMaintenanceDialog,
icon: const Icon(Icons.add),
label: const Text('유지보수 등록'),
backgroundColor: Theme.of(context).primaryColor,
),
);
}
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(
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) {
return Text(
'${controller.totalCount}건 | '
'예정 ${controller.upcomingCount}건 | '
'지연 ${controller.overdueCount}',
style: Theme.of(context).textTheme.bodyMedium?.copyWith(
color: Colors.grey[600],
),
);
},
),
],
),
Row(
children: [
IconButton(
icon: Icon(
_isCalendarView ? Icons.list : Icons.calendar_month,
),
onPressed: () {
setState(() {
_isCalendarView = !_isCalendarView;
});
},
tooltip: _isCalendarView ? '리스트 보기' : '캘린더 보기',
),
const SizedBox(width: 8),
IconButton(
icon: const Icon(Icons.refresh),
onPressed: () {
context.read<MaintenanceController>().loadMaintenances(
refresh: true,
);
},
tooltip: '새로고침',
),
],
),
],
),
const SizedBox(height: 16),
_buildStatisticsCards(),
],
),
);
}
Widget _buildStatisticsCards() {
return Consumer<MaintenanceController>(
builder: (context, controller, child) {
final stats = controller.statistics;
if (stats == null) return const SizedBox.shrink();
return Row(
children: [
Expanded(
child: _buildStatCard(
'전체 유지보수',
(stats['total'] ?? 0).toString(),
Icons.build_circle,
Colors.blue,
),
),
const SizedBox(width: 12),
Expanded(
child: _buildStatCard(
'예정된 항목',
(stats['upcoming'] ?? 0).toString(),
Icons.schedule,
Colors.orange,
),
),
const SizedBox(width: 12),
Expanded(
child: _buildStatCard(
'지연된 항목',
(stats['overdue'] ?? 0).toString(),
Icons.warning,
Colors.red,
),
),
const SizedBox(width: 12),
Expanded(
child: _buildStatCard(
'진행 중',
(stats['inProgress'] ?? 0).toString(),
Icons.schedule_outlined,
Colors.green,
),
),
],
);
},
);
}
Widget _buildStatCard(
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: Row(
children: [
Icon(icon, color: color, size: 32),
const SizedBox(width: 12),
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
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 _buildFilterBar() {
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: [
Expanded(
child: TextField(
decoration: InputDecoration(
hintText: '장비명, 일련번호로 검색',
prefixIcon: const Icon(Icons.search),
border: OutlineInputBorder(
borderRadius: BorderRadius.circular(8),
borderSide: BorderSide(color: Colors.grey[300]!),
),
contentPadding: const EdgeInsets.symmetric(horizontal: 16),
),
onChanged: (value) {
context.read<MaintenanceController>().setSearchQuery(value);
},
),
),
const SizedBox(width: 12),
PopupMenuButton<String>(
child: Container(
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 12),
decoration: BoxDecoration(
border: Border.all(color: Colors.grey[300]!),
borderRadius: BorderRadius.circular(8),
),
child: Row(
children: const [
Icon(Icons.filter_list),
SizedBox(width: 8),
Text('상태 필터'),
],
),
),
onSelected: (status) {
context.read<MaintenanceController>().setMaintenanceFilter(status);
},
itemBuilder:
(context) => [
const PopupMenuItem(value: null, child: Text('전체')),
const PopupMenuItem(
value: 'active',
child: Text('진행중 (시작됨, 완료되지 않음)'),
),
const PopupMenuItem(
value: 'completed',
child: Text('완료됨'),
),
const PopupMenuItem(
value: 'upcoming',
child: Text('예정됨'),
),
],
),
const SizedBox(width: 12),
PopupMenuButton<String>(
child: Container(
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 12),
decoration: BoxDecoration(
border: Border.all(color: Colors.grey[300]!),
borderRadius: BorderRadius.circular(8),
),
child: Row(
children: const [
Icon(Icons.sort),
SizedBox(width: 8),
Text('정렬'),
],
),
),
onSelected: (value) {
final parts = value.split('_');
context.read<MaintenanceController>().setSorting(
parts[0],
parts[1] == 'asc',
);
},
itemBuilder:
(context) => [
const PopupMenuItem(
value: 'started_at_asc',
child: Text('시작일 오름차순'),
),
const PopupMenuItem(
value: 'started_at_desc',
child: Text('시작일 내림차순'),
),
const PopupMenuItem(
value: 'registered_at_desc',
child: Text('최신 등록순'),
),
const PopupMenuItem(
value: 'period_month_desc',
child: Text('주기 긴 순'),
),
],
),
],
),
);
}
Widget _buildListView(MaintenanceController controller) {
if (controller.maintenances.isEmpty) {
return Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Icon(
Icons.build_circle_outlined,
size: 64,
color: Colors.grey[400],
),
const SizedBox(height: 16),
Text(
'등록된 유지보수가 없습니다',
style: Theme.of(
context,
).textTheme.titleLarge?.copyWith(color: Colors.grey[600]),
),
const SizedBox(height: 8),
Text('새로운 유지보수를 등록해주세요', style: TextStyle(color: Colors.grey[500])),
],
),
);
}
return ListView.builder(
padding: const EdgeInsets.all(24),
itemCount: controller.maintenances.length,
itemBuilder: (context, index) {
final maintenance = controller.maintenances[index];
return _buildMaintenanceCard(maintenance);
},
);
}
Widget _buildMaintenanceCard(MaintenanceDto maintenance) {
final schedule = context
.read<MaintenanceController>()
.getScheduleForMaintenance(maintenance.startedAt);
// generateAlert is not available, using null for now
final alert = null; // schedule?.generateAlert();
return Card(
margin: const EdgeInsets.only(bottom: 12),
child: InkWell(
onTap: () => _showMaintenanceDetails(maintenance),
borderRadius: BorderRadius.circular(8),
child: Padding(
padding: const EdgeInsets.all(16),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
children: [
_buildStatusChip(maintenance),
const SizedBox(width: 8),
_buildTypeChip(maintenance.maintenanceType),
const SizedBox(width: 8),
if (alert != null) _buildAlertChip(alert),
const Spacer(),
PopupMenuButton<String>(
onSelected:
(value) => _handleMaintenanceAction(value, maintenance),
itemBuilder:
(context) => [
const PopupMenuItem(value: 'edit', child: Text('수정')),
const PopupMenuItem(
value: 'toggle',
child: Text('상태 변경'),
),
const PopupMenuItem(
value: 'delete',
child: Text('삭제'),
),
],
),
],
),
const SizedBox(height: 12),
Text(
'Equipment History #${maintenance.equipmentHistoryId}',
style: Theme.of(
context,
).textTheme.titleMedium?.copyWith(fontWeight: FontWeight.bold),
),
const SizedBox(height: 8),
Row(
children: [
Icon(Icons.calendar_today, size: 16, color: Colors.grey[600]),
const SizedBox(width: 4),
Text(
'시작일: ${maintenance.startedAt ?? "미정"}',
style: TextStyle(color: Colors.grey[600]),
),
const SizedBox(width: 16),
Icon(Icons.check_circle, size: 16, color: Colors.grey[600]),
const SizedBox(width: 4),
Text(
'완료일: ${maintenance.endedAt ?? "미완료"}',
style: TextStyle(color: Colors.grey[600]),
),
],
),
const SizedBox(height: 8),
Row(
children: [
Icon(Icons.repeat, size: 16, color: Colors.grey[600]),
const SizedBox(width: 4),
Text(
'주기: ${maintenance.periodMonth ?? 0}개월',
style: TextStyle(color: Colors.grey[600]),
),
const SizedBox(width: 16),
Icon(Icons.settings, size: 16, color: Colors.grey[600]),
const SizedBox(width: 4),
Text(
'유형: ${maintenance.maintenanceType == 'O' ? '현장' : '원격'}',
style: TextStyle(color: Colors.grey[600]),
),
],
),
const SizedBox(height: 8),
Row(
children: [
Icon(Icons.schedule, size: 16, color: Colors.grey[600]),
const SizedBox(width: 4),
Text(
'등록일: ${DateFormat('yyyy-MM-dd').format(maintenance.registeredAt)}',
style: TextStyle(color: Colors.grey[600]),
),
],
),
],
),
),
),
);
}
Widget _buildStatusChip(MaintenanceDto maintenance) {
Color color;
String label;
// 백엔드 스키마 기준으로 상태 판단
if (maintenance.endedAt != null) {
// 완료됨
color = Colors.green;
label = '완료';
} else if (maintenance.startedAt != null) {
// 시작됐지만 완료되지 않음 (진행중)
color = Colors.orange;
label = '진행중';
} else {
// 아직 시작되지 않음 (예정)
color = Colors.blue;
label = '예정';
}
return Chip(
label: Text(label, style: const TextStyle(fontSize: 12)),
backgroundColor: color.withValues(alpha: 0.2),
side: BorderSide(color: color),
padding: const EdgeInsets.symmetric(horizontal: 8),
materialTapTargetSize: MaterialTapTargetSize.shrinkWrap,
);
}
Widget _buildTypeChip(String type) {
return Chip(
label: Text(
type == 'O' ? '현장' : '원격',
style: const TextStyle(fontSize: 12),
),
backgroundColor: Colors.grey[200],
padding: const EdgeInsets.symmetric(horizontal: 8),
materialTapTargetSize: MaterialTapTargetSize.shrinkWrap,
);
}
Widget _buildAlertChip(MaintenanceAlert alert) {
Color color;
switch (alert.priority) {
case AlertPriority.critical:
color = Colors.red;
break;
case AlertPriority.high:
color = Colors.orange;
break;
case AlertPriority.medium:
color = Colors.yellow[700]!;
break;
case AlertPriority.low:
color = Colors.blue;
break;
}
return Chip(
label: Text(
'${alert.daysUntilDue < 0 ? "지연 " : ""}${alert.daysUntilDue.abs()}',
style: TextStyle(fontSize: 12, color: Colors.white),
),
backgroundColor: color,
padding: const EdgeInsets.symmetric(horizontal: 8),
materialTapTargetSize: MaterialTapTargetSize.shrinkWrap,
);
}
void _showCreateMaintenanceDialog() {
showDialog(
context: context,
builder: (context) => const MaintenanceFormDialog(),
).then((result) {
if (result == true) {
context.read<MaintenanceController>().loadMaintenances(refresh: true);
}
});
}
void _showMaintenanceDetails(MaintenanceDto maintenance) {
showDialog(
context: context,
builder: (context) => MaintenanceFormDialog(maintenance: maintenance),
).then((result) {
if (result == true) {
context.read<MaintenanceController>().loadMaintenances(refresh: true);
}
});
}
void _handleMaintenanceAction(
String action,
MaintenanceDto maintenance,
) async {
final controller = context.read<MaintenanceController>();
switch (action) {
case 'edit':
_showMaintenanceDetails(maintenance);
break;
case 'toggle':
// TODO: 백엔드 스키마에 맞는 상태 변경 로직 구현 필요
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(content: Text('상태 변경 기능은 준비 중입니다')),
);
break;
case 'delete':
final confirm = await showDialog<bool>(
context: context,
builder:
(context) => AlertDialog(
title: const Text('유지보수 삭제'),
content: const Text('정말로 이 유지보수를 삭제하시겠습니까?'),
actions: [
TextButton(
onPressed: () => Navigator.of(context).pop(false),
child: const Text('취소'),
),
TextButton(
onPressed: () => Navigator.of(context).pop(true),
child: const Text(
'삭제',
style: TextStyle(color: Colors.red),
),
),
],
),
);
if (confirm == true && maintenance.id != null) {
await controller.deleteMaintenance(maintenance.id!);
}
break;
}
}
void _showMaintenancesForDate(
DateTime date,
List<MaintenanceDto> maintenances,
) {
final dateMaintenances =
maintenances.where((m) {
// nextMaintenanceDate 필드가 백엔드에 없으므로 startedAt~endedAt 기간으로 확인
final targetDate = DateTime(date.year, date.month, date.day);
final startDate = DateTime(m.startedAt.year, m.startedAt.month, m.startedAt.day);
final endDate = DateTime(m.endedAt.year, m.endedAt.month, m.endedAt.day);
return (targetDate.isAfter(startDate) || targetDate.isAtSameMomentAs(startDate)) &&
(targetDate.isBefore(endDate) || targetDate.isAtSameMomentAs(endDate));
}).toList();
if (dateMaintenances.isEmpty) return;
showModalBottomSheet(
context: context,
builder:
(context) => Container(
padding: const EdgeInsets.all(16),
child: Column(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
'${DateFormat('yyyy년 MM월 dd일').format(date)} 유지보수',
style: Theme.of(context).textTheme.titleLarge,
),
const SizedBox(height: 16),
...dateMaintenances.map(
(m) => ListTile(
title: Text('Equipment History #${m.equipmentHistoryId}'),
subtitle: Text(
'${m.maintenanceType == "O" ? "현장" : "원격"} | ${m.periodMonth}개월 주기',
),
trailing: Text(
'${DateFormat('yyyy-MM-dd').format(m.endedAt)}', // 종료일로 대체
),
onTap: () {
Navigator.of(context).pop();
_showMaintenanceDetails(m);
},
),
),
],
),
),
);
}
}