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,244 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:superport/data/models/maintenance_stats_dto.dart';
|
||||
import 'package:superport/domain/usecases/get_maintenance_stats_usecase.dart';
|
||||
|
||||
/// 유지보수 대시보드 컨트롤러
|
||||
/// 60일내, 30일내, 7일내, 만료된 계약 통계를 관리합니다.
|
||||
class MaintenanceDashboardController extends ChangeNotifier {
|
||||
final GetMaintenanceStatsUseCase _getMaintenanceStatsUseCase;
|
||||
|
||||
MaintenanceDashboardController({
|
||||
required GetMaintenanceStatsUseCase getMaintenanceStatsUseCase,
|
||||
}) : _getMaintenanceStatsUseCase = getMaintenanceStatsUseCase;
|
||||
|
||||
// === 상태 관리 ===
|
||||
MaintenanceStatsDto _stats = const MaintenanceStatsDto();
|
||||
List<MaintenanceStatusCardData> _dashboardCards = [];
|
||||
|
||||
bool _isLoading = false;
|
||||
bool _isRefreshing = false;
|
||||
String? _errorMessage;
|
||||
DateTime? _lastUpdated;
|
||||
|
||||
// === Getters ===
|
||||
MaintenanceStatsDto get stats => _stats;
|
||||
List<MaintenanceStatusCardData> get dashboardCards => _dashboardCards;
|
||||
bool get isLoading => _isLoading;
|
||||
bool get isRefreshing => _isRefreshing;
|
||||
String? get errorMessage => _errorMessage;
|
||||
DateTime? get lastUpdated => _lastUpdated;
|
||||
|
||||
// === 대시보드 카드 상태별 조회 ===
|
||||
|
||||
/// 60일 내 만료 예정 계약 카드 데이터
|
||||
MaintenanceStatusCardData get expiring60DaysCard => MaintenanceStatusCardData(
|
||||
title: '60일 내',
|
||||
count: _stats.expiring60Days,
|
||||
subtitle: '만료 예정',
|
||||
status: _stats.expiring60Days > 0
|
||||
? MaintenanceCardStatus.warning
|
||||
: MaintenanceCardStatus.active,
|
||||
actionLabel: '계획하기',
|
||||
);
|
||||
|
||||
/// 30일 내 만료 예정 계약 카드 데이터
|
||||
MaintenanceStatusCardData get expiring30DaysCard => MaintenanceStatusCardData(
|
||||
title: '30일 내',
|
||||
count: _stats.expiring30Days,
|
||||
subtitle: '만료 예정',
|
||||
status: _stats.expiring30Days > 0
|
||||
? MaintenanceCardStatus.urgent
|
||||
: MaintenanceCardStatus.active,
|
||||
actionLabel: '예약하기',
|
||||
);
|
||||
|
||||
/// 7일 내 만료 예정 계약 카드 데이터
|
||||
MaintenanceStatusCardData get expiring7DaysCard => MaintenanceStatusCardData(
|
||||
title: '7일 내',
|
||||
count: _stats.expiring7Days,
|
||||
subtitle: '만료 임박',
|
||||
status: _stats.expiring7Days > 0
|
||||
? MaintenanceCardStatus.critical
|
||||
: MaintenanceCardStatus.active,
|
||||
actionLabel: '즉시 처리',
|
||||
);
|
||||
|
||||
/// 만료된 계약 카드 데이터
|
||||
MaintenanceStatusCardData get expiredContractsCard => MaintenanceStatusCardData(
|
||||
title: '만료됨',
|
||||
count: _stats.expiredContracts,
|
||||
subtitle: '조치 필요',
|
||||
status: _stats.expiredContracts > 0
|
||||
? MaintenanceCardStatus.expired
|
||||
: MaintenanceCardStatus.active,
|
||||
actionLabel: '갱신하기',
|
||||
);
|
||||
|
||||
// === 추가 통계 정보 ===
|
||||
|
||||
/// 총 위험도 점수 (0.0 ~ 1.0)
|
||||
double get riskScore => _stats.riskScore;
|
||||
|
||||
/// 위험도 상태
|
||||
MaintenanceCardStatus get riskStatus => _stats.riskStatus;
|
||||
|
||||
/// 위험도 설명
|
||||
String get riskDescription {
|
||||
switch (riskStatus) {
|
||||
case MaintenanceCardStatus.critical:
|
||||
return '높은 위험 - 즉시 조치 필요';
|
||||
case MaintenanceCardStatus.urgent:
|
||||
return '중간 위험 - 빠른 대응 필요';
|
||||
case MaintenanceCardStatus.warning:
|
||||
return '낮은 위험 - 주의 관찰';
|
||||
default:
|
||||
return '안전 상태';
|
||||
}
|
||||
}
|
||||
|
||||
/// 매출 위험 금액 (포맷된 문자열)
|
||||
String get formattedRevenueAtRisk {
|
||||
final amount = _stats.totalRevenueAtRisk;
|
||||
if (amount >= 1000000) {
|
||||
return '${(amount / 1000000).toStringAsFixed(1)}백만원';
|
||||
} else if (amount >= 10000) {
|
||||
return '${(amount / 10000).toStringAsFixed(0)}만원';
|
||||
} else {
|
||||
return '${amount.toStringAsFixed(0)}원';
|
||||
}
|
||||
}
|
||||
|
||||
/// 완료율 (백분율 문자열)
|
||||
String get formattedCompletionRate {
|
||||
return '${(_stats.completionRate * 100).toStringAsFixed(1)}%';
|
||||
}
|
||||
|
||||
// === 데이터 로딩 메서드 ===
|
||||
|
||||
/// 대시보드 통계 초기 로딩
|
||||
Future<void> loadDashboardStats() async {
|
||||
if (_isLoading) return; // 중복 호출 방지
|
||||
|
||||
_isLoading = true;
|
||||
_errorMessage = null;
|
||||
notifyListeners();
|
||||
|
||||
try {
|
||||
_stats = await _getMaintenanceStatsUseCase.getMaintenanceStats();
|
||||
_dashboardCards = _stats.dashboardCards;
|
||||
_lastUpdated = DateTime.now();
|
||||
_errorMessage = null;
|
||||
|
||||
} catch (e) {
|
||||
_errorMessage = e.toString();
|
||||
// 오류 발생 시 기본값 설정 (UX 개선)
|
||||
_stats = const MaintenanceStatsDto();
|
||||
_dashboardCards = [];
|
||||
|
||||
debugPrint('대시보드 통계 로딩 오류: $e');
|
||||
} finally {
|
||||
_isLoading = false;
|
||||
notifyListeners();
|
||||
}
|
||||
}
|
||||
|
||||
/// 데이터 새로고침 (Pull-to-Refresh)
|
||||
Future<void> refreshDashboardStats() async {
|
||||
if (_isRefreshing) return;
|
||||
|
||||
_isRefreshing = true;
|
||||
_errorMessage = null;
|
||||
notifyListeners();
|
||||
|
||||
try {
|
||||
_stats = await _getMaintenanceStatsUseCase.getMaintenanceStats();
|
||||
_dashboardCards = _stats.dashboardCards;
|
||||
_lastUpdated = DateTime.now();
|
||||
_errorMessage = null;
|
||||
|
||||
} catch (e) {
|
||||
_errorMessage = e.toString();
|
||||
debugPrint('대시보드 통계 새로고침 오류: $e');
|
||||
} finally {
|
||||
_isRefreshing = false;
|
||||
notifyListeners();
|
||||
}
|
||||
}
|
||||
|
||||
/// 특정 기간의 만료 예정 계약 수 조회
|
||||
Future<int> getExpiringCount(int days) async {
|
||||
try {
|
||||
return await _getMaintenanceStatsUseCase.getExpiringContractsCount(days: days);
|
||||
} catch (e) {
|
||||
debugPrint('만료 예정 계약 조회 오류 ($days일): $e');
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
|
||||
/// 계약 타입별 통계 조회
|
||||
Future<Map<String, int>> getContractsByType() async {
|
||||
try {
|
||||
return await _getMaintenanceStatsUseCase.getContractsByType();
|
||||
} catch (e) {
|
||||
debugPrint('계약 타입별 통계 조회 오류: $e');
|
||||
return {'V': 0, 'R': 0};
|
||||
}
|
||||
}
|
||||
|
||||
// === 오류 처리 및 재시도 ===
|
||||
|
||||
/// 오류 메시지 초기화
|
||||
void clearError() {
|
||||
_errorMessage = null;
|
||||
notifyListeners();
|
||||
}
|
||||
|
||||
/// 재시도 (오류 발생 후)
|
||||
Future<void> retry() async {
|
||||
await loadDashboardStats();
|
||||
}
|
||||
|
||||
// === 유틸리티 메서드 ===
|
||||
|
||||
/// 통계 데이터가 유효한지 확인
|
||||
bool get hasValidData => _stats.updatedAt != null;
|
||||
|
||||
/// 마지막 업데이트 이후 경과 시간
|
||||
String get timeSinceLastUpdate {
|
||||
if (_lastUpdated == null) return '업데이트 없음';
|
||||
|
||||
final now = DateTime.now();
|
||||
final difference = now.difference(_lastUpdated!);
|
||||
|
||||
if (difference.inMinutes < 1) {
|
||||
return '방금 전';
|
||||
} else if (difference.inMinutes < 60) {
|
||||
return '${difference.inMinutes}분 전';
|
||||
} else if (difference.inHours < 24) {
|
||||
return '${difference.inHours}시간 전';
|
||||
} else {
|
||||
return '${difference.inDays}일 전';
|
||||
}
|
||||
}
|
||||
|
||||
/// 데이터 새로고침이 필요한지 확인 (5분 기준)
|
||||
bool get needsRefresh {
|
||||
if (_lastUpdated == null) return true;
|
||||
return DateTime.now().difference(_lastUpdated!).inMinutes > 5;
|
||||
}
|
||||
|
||||
/// 자동 새로고침 (필요 시에만)
|
||||
Future<void> autoRefreshIfNeeded() async {
|
||||
if (needsRefresh && !_isLoading && !_isRefreshing) {
|
||||
await refreshDashboardStats();
|
||||
}
|
||||
}
|
||||
|
||||
// === 정리 메서드 ===
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
// 필요한 경우 타이머나 구독 해제
|
||||
super.dispose();
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user