- 전체 371개 파일 중 82개 미사용 파일 식별 - Phase 1: 33개 파일 삭제 예정 (100% 안전) - Phase 2: 30개 파일 삭제 검토 예정 - Phase 3: 19개 파일 수동 검토 예정 🤖 Generated with Claude Code Co-Authored-By: Claude <noreply@anthropic.com>
584 lines
20 KiB
Dart
584 lines
20 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 '../../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();
|
|
});
|
|
}
|
|
|
|
@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),
|
|
ShadButton(
|
|
onPressed:
|
|
() => controller.loadMaintenances(refresh: true),
|
|
child: const Text('다시 시도'),
|
|
),
|
|
],
|
|
),
|
|
);
|
|
}
|
|
|
|
return _isCalendarView
|
|
? MaintenanceCalendar(
|
|
maintenances: controller.maintenances,
|
|
onDateSelected: (date) {
|
|
// 날짜 선택시 해당 날짜의 유지보수 표시
|
|
_showMaintenancesForDate(date, controller.maintenances);
|
|
},
|
|
)
|
|
: _buildListView(controller);
|
|
},
|
|
),
|
|
),
|
|
],
|
|
),
|
|
);
|
|
}
|
|
|
|
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) {
|
|
// 백엔드에 통계 API가 없으므로 빈 위젯 반환
|
|
return const SizedBox.shrink();
|
|
},
|
|
);
|
|
}
|
|
|
|
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: ShadInput(
|
|
placeholder: const Text('장비명, 일련번호로 검색'),
|
|
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 Container(
|
|
margin: const EdgeInsets.only(bottom: 12),
|
|
child: ShadCard(
|
|
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) {
|
|
// 백엔드 스키마 기준으로 상태 판단
|
|
if (maintenance.endedAt != null) {
|
|
return ShadBadge.secondary(
|
|
child: const Text('완료'),
|
|
);
|
|
} else if (maintenance.startedAt != null) {
|
|
return ShadBadge(
|
|
child: const Text('진행중'),
|
|
);
|
|
} else {
|
|
return ShadBadge.outline(
|
|
child: const Text('예정'),
|
|
);
|
|
}
|
|
}
|
|
|
|
Widget _buildTypeChip(String type) {
|
|
return ShadBadge.destructive(
|
|
child: Text(type == 'O' ? '현장' : '원격'),
|
|
);
|
|
}
|
|
|
|
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 _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);
|
|
},
|
|
),
|
|
),
|
|
],
|
|
),
|
|
),
|
|
);
|
|
}
|
|
}
|