- 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>
244 lines
7.2 KiB
Dart
244 lines
7.2 KiB
Dart
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();
|
|
}
|
|
} |