From 844c7bd92f95d19516ebd027d91fe62b70faf24b Mon Sep 17 00:00:00 2001 From: JiWoong Sul Date: Fri, 8 Aug 2025 14:42:20 +0900 Subject: [PATCH] =?UTF-8?q?feat:=20=EB=8C=80=EC=8B=9C=EB=B3=B4=EB=93=9C?= =?UTF-8?q?=EC=97=90=20=EB=9D=BC=EC=9D=B4=EC=84=A0=EC=8A=A4=20=EB=A7=8C?= =?UTF-8?q?=EB=A3=8C=20=EC=9A=94=EC=95=BD=20=EB=B0=8F=20Lookup=20=EB=8D=B0?= =?UTF-8?q?=EC=9D=B4=ED=84=B0=20=EC=BA=90=EC=8B=B1=20=EC=8B=9C=EC=8A=A4?= =?UTF-8?q?=ED=85=9C=20=EA=B5=AC=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - License Expiry Summary API 연동 완료 - 30/60/90일 내 만료 예정 라이선스 요약 표시 - 대시보드 상단에 알림 카드로 통합 - 만료 임박 순서로 색상 구분 (빨강/주황/노랑) - Lookup 데이터 전역 캐싱 시스템 구축 - LookupService 및 RemoteDataSource 생성 - 전체 lookup 데이터 일괄 로드 및 캐싱 - 타입별 필터링 지원 - 새로운 모델 추가 - LicenseExpirySummary (Freezed) - LookupData, LookupCategory, LookupItem 모델 - CLAUDE.md 문서 업데이트 - 미사용 API 활용 계획 추가 - 구현 우선순위 정의 🤖 Generated with Claude Code Co-Authored-By: Claude --- CLAUDE.md | 60 +- .../remote/dashboard_remote_datasource.dart | 21 + .../remote/lookup_remote_datasource.dart | 102 +++ .../dashboard/license_expiry_summary.dart | 38 + .../license_expiry_summary.freezed.dart | 690 ++++++++++++++++++ .../dashboard/license_expiry_summary.g.dart | 61 ++ lib/data/models/lookups/lookup_data.dart | 35 + .../models/lookups/lookup_data.freezed.dart | 656 +++++++++++++++++ lib/data/models/lookups/lookup_data.g.dart | 63 ++ lib/di/injection_container.dart | 8 + .../controllers/overview_controller.dart | 56 +- .../overview/overview_screen_redesign.dart | 82 +++ lib/services/dashboard_service.dart | 7 + lib/services/lookup_service.dart | 165 +++++ test/api_integration_test.dart | 65 ++ 15 files changed, 2105 insertions(+), 4 deletions(-) create mode 100644 lib/data/datasources/remote/lookup_remote_datasource.dart create mode 100644 lib/data/models/dashboard/license_expiry_summary.dart create mode 100644 lib/data/models/dashboard/license_expiry_summary.freezed.dart create mode 100644 lib/data/models/dashboard/license_expiry_summary.g.dart create mode 100644 lib/data/models/lookups/lookup_data.dart create mode 100644 lib/data/models/lookups/lookup_data.freezed.dart create mode 100644 lib/data/models/lookups/lookup_data.g.dart create mode 100644 lib/services/lookup_service.dart create mode 100644 test/api_integration_test.dart diff --git a/CLAUDE.md b/CLAUDE.md index 7f43ade..a06f8c6 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -118,6 +118,58 @@ Infrastructure: priority: LOW ``` +## 🔌 Unused Backend API Integration Plan + +### 현재 백엔드에 구현되었으나 프론트엔드에서 미사용 중인 API + +#### 1. `/overview/license-expiry` - 라이선스 만료 요약 +**용도**: 30일/60일/90일 내 만료 예정인 라이선스 요약 정보 제공 +**활용 계획**: +- **위치**: Dashboard 화면 상단 알림 배너 +- **구현 방법**: + - 만료 임박 라이선스 카운트를 배지로 표시 + - 클릭 시 상세 라이선스 목록으로 이동 + - 관리자/매니저 권한일 때만 표시 +- **예상 효과**: 라이선스 갱신 누락 방지, 사전 대응 가능 + +#### 2. `/lookups` - 전체 조회 데이터 +**용도**: 시스템 전체 드롭다운/셀렉트 박스용 마스터 데이터 제공 +**활용 계획**: +- **위치**: 앱 초기화 시 한 번 호출하여 캐싱 +- **구현 방법**: + - `LookupService` 생성하여 전역 상태 관리 + - 장비 타입, 상태 코드, 제조사 목록 등 일괄 관리 + - 각 화면에서 개별 API 호출 대신 캐시된 데이터 사용 +- **예상 효과**: API 호출 횟수 감소, 응답 속도 향상 + +#### 3. `/lookups/type` - 타입별 조회 데이터 +**용도**: 특정 타입의 조회 데이터만 선택적으로 가져오기 +**활용 계획**: +- **위치**: 대량 데이터 입력 화면 (장비 일괄 등록, Excel 임포트) +- **구현 방법**: + - 필요한 타입만 선택적으로 로드 + - 동적 폼 생성 시 활용 + - 타입별 유효성 검증 규칙 적용 +- **예상 효과**: 메모리 사용량 최적화, 동적 UI 구성 가능 + +#### 4. `/health` - 시스템 상태 체크 +**용도**: API 서버 상태 및 DB 연결 상태 확인 +**활용 계획**: +- **위치**: + - 로그인 화면 하단 (서버 상태 인디케이터) + - 관리자 대시보드 (시스템 모니터링 위젯) +- **구현 방법**: + - 30초 간격 폴링으로 서버 상태 모니터링 + - 연결 실패 시 자동 재시도 및 사용자 알림 + - 서버 점검 시간 사전 공지 표시 +- **예상 효과**: 시스템 안정성 향상, 장애 조기 감지 + +### 구현 우선순위 +1. **Phase 1 (1주차)**: `/overview/license-expiry` - 대시보드 통합 +2. **Phase 2 (2주차)**: `/lookups` - 전역 캐싱 시스템 구축 +3. **Phase 3 (3주차)**: `/health` - 시스템 모니터링 구현 +4. **Phase 4 (4주차)**: `/lookups/type` - 동적 폼 시스템 구축 + ## 📋 TODO List ### Immediate (This Week) @@ -125,18 +177,22 @@ Infrastructure: - [ ] 대시보드 차트 구현 (Chart.js 통합) - [ ] 시리얼 번호 중복 체크 백엔드 구현 - [ ] 권한 체크 누락 화면 수정 +- [ ] `/overview/license-expiry` API 연동 (대시보드 알림 배너) ### Short Term (This Month) - [ ] 장비 대여/반납 기능 구현 - [ ] 고급 검색 필터 구현 - [ ] Excel 내보내기 기능 - [ ] 성능 최적화 (가상 스크롤링) +- [ ] `/lookups` API 활용한 전역 캐싱 시스템 구축 +- [ ] `/health` API 활용한 서버 상태 모니터링 ### Long Term - [ ] 모바일 앱 최적화 - [ ] 푸시 알림 시스템 - [ ] 다국어 지원 (영어) - [ ] 대시보드 커스터마이징 +- [ ] `/lookups/type` API 활용한 동적 폼 시스템 ## 🔑 Key Decisions @@ -194,5 +250,5 @@ API Source Code: /Users/maximilian.j.sul/Documents/flutter/superport_api **Project Stage**: Development (70% Complete) **Next Milestone**: Beta Release (2025-02-01) -**Last Updated**: 2025-01-08 -**Version**: 3.0 \ No newline at end of file +**Last Updated**: 2025-01-09 +**Version**: 3.1 \ No newline at end of file diff --git a/lib/data/datasources/remote/dashboard_remote_datasource.dart b/lib/data/datasources/remote/dashboard_remote_datasource.dart index 6f938ee..4b5befc 100644 --- a/lib/data/datasources/remote/dashboard_remote_datasource.dart +++ b/lib/data/datasources/remote/dashboard_remote_datasource.dart @@ -6,6 +6,7 @@ import 'package:superport/core/errors/failures.dart'; import 'package:superport/data/datasources/remote/api_client.dart'; import 'package:superport/data/models/dashboard/equipment_status_distribution.dart'; import 'package:superport/data/models/dashboard/expiring_license.dart'; +import 'package:superport/data/models/dashboard/license_expiry_summary.dart'; import 'package:superport/data/models/dashboard/overview_stats.dart'; import 'package:superport/data/models/dashboard/recent_activity.dart'; @@ -14,6 +15,7 @@ abstract class DashboardRemoteDataSource { Future>> getRecentActivities(); Future> getEquipmentStatusDistribution(); Future>> getExpiringLicenses({int days = 30}); + Future> getLicenseExpirySummary(); } @LazySingleton(as: DashboardRemoteDataSource) @@ -105,6 +107,25 @@ class DashboardRemoteDataSourceImpl implements DashboardRemoteDataSource { } } + @override + Future> getLicenseExpirySummary() async { + try { + final response = await _apiClient.get('/overview/license-expiry'); + + if (response.data != null && response.data['success'] == true && response.data['data'] != null) { + final summary = LicenseExpirySummary.fromJson(response.data['data']); + return Right(summary); + } else { + final errorMessage = response.data?['error']?['message'] ?? '응답 데이터가 올바르지 않습니다'; + return Left(ServerFailure(message: errorMessage)); + } + } on DioException catch (e) { + return Left(_handleDioError(e)); + } catch (e) { + return Left(ServerFailure(message: '라이선스 만료 요약을 가져오는 중 오류가 발생했습니다: $e')); + } + } + Failure _handleDioError(DioException error) { switch (error.type) { case DioExceptionType.connectionTimeout: diff --git a/lib/data/datasources/remote/lookup_remote_datasource.dart b/lib/data/datasources/remote/lookup_remote_datasource.dart new file mode 100644 index 0000000..e8d7837 --- /dev/null +++ b/lib/data/datasources/remote/lookup_remote_datasource.dart @@ -0,0 +1,102 @@ +import 'package:dartz/dartz.dart'; +import 'package:dio/dio.dart'; +import 'package:injectable/injectable.dart'; +import 'package:superport/core/errors/failures.dart'; +import 'package:superport/data/datasources/remote/api_client.dart'; +import 'package:superport/data/models/lookups/lookup_data.dart'; + +abstract class LookupRemoteDataSource { + Future> getAllLookups(); + Future>>> getLookupsByType(String type); +} + +@LazySingleton(as: LookupRemoteDataSource) +class LookupRemoteDataSourceImpl implements LookupRemoteDataSource { + final ApiClient _apiClient; + + LookupRemoteDataSourceImpl(this._apiClient); + + @override + Future> getAllLookups() async { + try { + final response = await _apiClient.get('/lookups'); + + if (response.data != null && response.data['success'] == true && response.data['data'] != null) { + final lookupData = LookupData.fromJson(response.data['data']); + return Right(lookupData); + } else { + final errorMessage = response.data?['error']?['message'] ?? '응답 데이터가 올바르지 않습니다'; + return Left(ServerFailure(message: errorMessage)); + } + } on DioException catch (e) { + return Left(_handleDioError(e)); + } catch (e) { + return Left(ServerFailure(message: '조회 데이터를 가져오는 중 오류가 발생했습니다: $e')); + } + } + + @override + Future>>> getLookupsByType(String type) async { + try { + final response = await _apiClient.get( + '/lookups/type', + queryParameters: {'lookup_type': type}, + ); + + if (response.data != null && response.data['success'] == true && response.data['data'] != null) { + final data = response.data['data'] as Map; + final result = >{}; + + data.forEach((key, value) { + if (value is List) { + result[key] = value.map((item) => LookupItem.fromJson(item)).toList(); + } + }); + + return Right(result); + } else { + final errorMessage = response.data?['error']?['message'] ?? '응답 데이터가 올바르지 않습니다'; + return Left(ServerFailure(message: errorMessage)); + } + } on DioException catch (e) { + return Left(_handleDioError(e)); + } catch (e) { + return Left(ServerFailure(message: '타입별 조회 데이터를 가져오는 중 오류가 발생했습니다: $e')); + } + } + + Failure _handleDioError(DioException error) { + switch (error.type) { + case DioExceptionType.connectionTimeout: + case DioExceptionType.sendTimeout: + case DioExceptionType.receiveTimeout: + return NetworkFailure(message: '네트워크 연결 시간이 초과되었습니다'); + case DioExceptionType.connectionError: + return NetworkFailure(message: '서버에 연결할 수 없습니다'); + case DioExceptionType.badResponse: + final statusCode = error.response?.statusCode ?? 0; + final errorData = error.response?.data; + + String message; + if (errorData is Map) { + message = errorData['error']?['message'] ?? + errorData['message'] ?? + '서버 오류가 발생했습니다'; + } else { + message = '서버 오류가 발생했습니다'; + } + + if (statusCode == 401) { + return AuthenticationFailure(message: '인증이 만료되었습니다'); + } else if (statusCode == 403) { + return AuthenticationFailure(message: '접근 권한이 없습니다'); + } else { + return ServerFailure(message: message); + } + case DioExceptionType.cancel: + return ServerFailure(message: '요청이 취소되었습니다'); + default: + return ServerFailure(message: '알 수 없는 오류가 발생했습니다'); + } + } +} \ No newline at end of file diff --git a/lib/data/models/dashboard/license_expiry_summary.dart b/lib/data/models/dashboard/license_expiry_summary.dart new file mode 100644 index 0000000..68bcb17 --- /dev/null +++ b/lib/data/models/dashboard/license_expiry_summary.dart @@ -0,0 +1,38 @@ +import 'package:freezed_annotation/freezed_annotation.dart'; + +part 'license_expiry_summary.freezed.dart'; +part 'license_expiry_summary.g.dart'; + +@freezed +class LicenseExpirySummary with _$LicenseExpirySummary { + const factory LicenseExpirySummary({ + @JsonKey(name: 'within_30_days') required int within30Days, + @JsonKey(name: 'within_60_days') required int within60Days, + @JsonKey(name: 'within_90_days') required int within90Days, + @JsonKey(name: 'expired') required int expired, + @JsonKey(name: 'total_active') required int totalActive, + @JsonKey(name: 'licenses') required List licenses, + }) = _LicenseExpirySummary; + + factory LicenseExpirySummary.fromJson(Map json) => + _$LicenseExpirySummaryFromJson(json); +} + +@freezed +class LicenseExpiryDetail with _$LicenseExpiryDetail { + const factory LicenseExpiryDetail({ + required int id, + @JsonKey(name: 'equipment_id') required int equipmentId, + @JsonKey(name: 'equipment_name') required String equipmentName, + @JsonKey(name: 'serial_number') required String serialNumber, + @JsonKey(name: 'company_name') required String companyName, + @JsonKey(name: 'license_type') required String licenseType, + @JsonKey(name: 'start_date') required String startDate, + @JsonKey(name: 'end_date') required String endDate, + @JsonKey(name: 'days_remaining') required int daysRemaining, + @JsonKey(name: 'is_expired') required bool isExpired, + }) = _LicenseExpiryDetail; + + factory LicenseExpiryDetail.fromJson(Map json) => + _$LicenseExpiryDetailFromJson(json); +} \ No newline at end of file diff --git a/lib/data/models/dashboard/license_expiry_summary.freezed.dart b/lib/data/models/dashboard/license_expiry_summary.freezed.dart new file mode 100644 index 0000000..c77b123 --- /dev/null +++ b/lib/data/models/dashboard/license_expiry_summary.freezed.dart @@ -0,0 +1,690 @@ +// coverage:ignore-file +// GENERATED CODE - DO NOT MODIFY BY HAND +// ignore_for_file: type=lint +// ignore_for_file: unused_element, deprecated_member_use, deprecated_member_use_from_same_package, use_function_type_syntax_for_parameters, unnecessary_const, avoid_init_to_null, invalid_override_different_default_values_named, prefer_expression_function_bodies, annotate_overrides, invalid_annotation_target, unnecessary_question_mark + +part of 'license_expiry_summary.dart'; + +// ************************************************************************** +// FreezedGenerator +// ************************************************************************** + +T _$identity(T value) => value; + +final _privateConstructorUsedError = UnsupportedError( + 'It seems like you constructed your class using `MyClass._()`. This constructor is only meant to be used by freezed and you are not supposed to need it nor use it.\nPlease check the documentation here for more information: https://github.com/rrousselGit/freezed#adding-getters-and-methods-to-our-models'); + +LicenseExpirySummary _$LicenseExpirySummaryFromJson(Map json) { + return _LicenseExpirySummary.fromJson(json); +} + +/// @nodoc +mixin _$LicenseExpirySummary { + @JsonKey(name: 'within_30_days') + int get within30Days => throw _privateConstructorUsedError; + @JsonKey(name: 'within_60_days') + int get within60Days => throw _privateConstructorUsedError; + @JsonKey(name: 'within_90_days') + int get within90Days => throw _privateConstructorUsedError; + @JsonKey(name: 'expired') + int get expired => throw _privateConstructorUsedError; + @JsonKey(name: 'total_active') + int get totalActive => throw _privateConstructorUsedError; + @JsonKey(name: 'licenses') + List get licenses => throw _privateConstructorUsedError; + + /// Serializes this LicenseExpirySummary to a JSON map. + Map toJson() => throw _privateConstructorUsedError; + + /// Create a copy of LicenseExpirySummary + /// with the given fields replaced by the non-null parameter values. + @JsonKey(includeFromJson: false, includeToJson: false) + $LicenseExpirySummaryCopyWith get copyWith => + throw _privateConstructorUsedError; +} + +/// @nodoc +abstract class $LicenseExpirySummaryCopyWith<$Res> { + factory $LicenseExpirySummaryCopyWith(LicenseExpirySummary value, + $Res Function(LicenseExpirySummary) then) = + _$LicenseExpirySummaryCopyWithImpl<$Res, LicenseExpirySummary>; + @useResult + $Res call( + {@JsonKey(name: 'within_30_days') int within30Days, + @JsonKey(name: 'within_60_days') int within60Days, + @JsonKey(name: 'within_90_days') int within90Days, + @JsonKey(name: 'expired') int expired, + @JsonKey(name: 'total_active') int totalActive, + @JsonKey(name: 'licenses') List licenses}); +} + +/// @nodoc +class _$LicenseExpirySummaryCopyWithImpl<$Res, + $Val extends LicenseExpirySummary> + implements $LicenseExpirySummaryCopyWith<$Res> { + _$LicenseExpirySummaryCopyWithImpl(this._value, this._then); + + // ignore: unused_field + final $Val _value; + // ignore: unused_field + final $Res Function($Val) _then; + + /// Create a copy of LicenseExpirySummary + /// with the given fields replaced by the non-null parameter values. + @pragma('vm:prefer-inline') + @override + $Res call({ + Object? within30Days = null, + Object? within60Days = null, + Object? within90Days = null, + Object? expired = null, + Object? totalActive = null, + Object? licenses = null, + }) { + return _then(_value.copyWith( + within30Days: null == within30Days + ? _value.within30Days + : within30Days // ignore: cast_nullable_to_non_nullable + as int, + within60Days: null == within60Days + ? _value.within60Days + : within60Days // ignore: cast_nullable_to_non_nullable + as int, + within90Days: null == within90Days + ? _value.within90Days + : within90Days // ignore: cast_nullable_to_non_nullable + as int, + expired: null == expired + ? _value.expired + : expired // ignore: cast_nullable_to_non_nullable + as int, + totalActive: null == totalActive + ? _value.totalActive + : totalActive // ignore: cast_nullable_to_non_nullable + as int, + licenses: null == licenses + ? _value.licenses + : licenses // ignore: cast_nullable_to_non_nullable + as List, + ) as $Val); + } +} + +/// @nodoc +abstract class _$$LicenseExpirySummaryImplCopyWith<$Res> + implements $LicenseExpirySummaryCopyWith<$Res> { + factory _$$LicenseExpirySummaryImplCopyWith(_$LicenseExpirySummaryImpl value, + $Res Function(_$LicenseExpirySummaryImpl) then) = + __$$LicenseExpirySummaryImplCopyWithImpl<$Res>; + @override + @useResult + $Res call( + {@JsonKey(name: 'within_30_days') int within30Days, + @JsonKey(name: 'within_60_days') int within60Days, + @JsonKey(name: 'within_90_days') int within90Days, + @JsonKey(name: 'expired') int expired, + @JsonKey(name: 'total_active') int totalActive, + @JsonKey(name: 'licenses') List licenses}); +} + +/// @nodoc +class __$$LicenseExpirySummaryImplCopyWithImpl<$Res> + extends _$LicenseExpirySummaryCopyWithImpl<$Res, _$LicenseExpirySummaryImpl> + implements _$$LicenseExpirySummaryImplCopyWith<$Res> { + __$$LicenseExpirySummaryImplCopyWithImpl(_$LicenseExpirySummaryImpl _value, + $Res Function(_$LicenseExpirySummaryImpl) _then) + : super(_value, _then); + + /// Create a copy of LicenseExpirySummary + /// with the given fields replaced by the non-null parameter values. + @pragma('vm:prefer-inline') + @override + $Res call({ + Object? within30Days = null, + Object? within60Days = null, + Object? within90Days = null, + Object? expired = null, + Object? totalActive = null, + Object? licenses = null, + }) { + return _then(_$LicenseExpirySummaryImpl( + within30Days: null == within30Days + ? _value.within30Days + : within30Days // ignore: cast_nullable_to_non_nullable + as int, + within60Days: null == within60Days + ? _value.within60Days + : within60Days // ignore: cast_nullable_to_non_nullable + as int, + within90Days: null == within90Days + ? _value.within90Days + : within90Days // ignore: cast_nullable_to_non_nullable + as int, + expired: null == expired + ? _value.expired + : expired // ignore: cast_nullable_to_non_nullable + as int, + totalActive: null == totalActive + ? _value.totalActive + : totalActive // ignore: cast_nullable_to_non_nullable + as int, + licenses: null == licenses + ? _value._licenses + : licenses // ignore: cast_nullable_to_non_nullable + as List, + )); + } +} + +/// @nodoc +@JsonSerializable() +class _$LicenseExpirySummaryImpl implements _LicenseExpirySummary { + const _$LicenseExpirySummaryImpl( + {@JsonKey(name: 'within_30_days') required this.within30Days, + @JsonKey(name: 'within_60_days') required this.within60Days, + @JsonKey(name: 'within_90_days') required this.within90Days, + @JsonKey(name: 'expired') required this.expired, + @JsonKey(name: 'total_active') required this.totalActive, + @JsonKey(name: 'licenses') + required final List licenses}) + : _licenses = licenses; + + factory _$LicenseExpirySummaryImpl.fromJson(Map json) => + _$$LicenseExpirySummaryImplFromJson(json); + + @override + @JsonKey(name: 'within_30_days') + final int within30Days; + @override + @JsonKey(name: 'within_60_days') + final int within60Days; + @override + @JsonKey(name: 'within_90_days') + final int within90Days; + @override + @JsonKey(name: 'expired') + final int expired; + @override + @JsonKey(name: 'total_active') + final int totalActive; + final List _licenses; + @override + @JsonKey(name: 'licenses') + List get licenses { + if (_licenses is EqualUnmodifiableListView) return _licenses; + // ignore: implicit_dynamic_type + return EqualUnmodifiableListView(_licenses); + } + + @override + String toString() { + return 'LicenseExpirySummary(within30Days: $within30Days, within60Days: $within60Days, within90Days: $within90Days, expired: $expired, totalActive: $totalActive, licenses: $licenses)'; + } + + @override + bool operator ==(Object other) { + return identical(this, other) || + (other.runtimeType == runtimeType && + other is _$LicenseExpirySummaryImpl && + (identical(other.within30Days, within30Days) || + other.within30Days == within30Days) && + (identical(other.within60Days, within60Days) || + other.within60Days == within60Days) && + (identical(other.within90Days, within90Days) || + other.within90Days == within90Days) && + (identical(other.expired, expired) || other.expired == expired) && + (identical(other.totalActive, totalActive) || + other.totalActive == totalActive) && + const DeepCollectionEquality().equals(other._licenses, _licenses)); + } + + @JsonKey(includeFromJson: false, includeToJson: false) + @override + int get hashCode => Object.hash( + runtimeType, + within30Days, + within60Days, + within90Days, + expired, + totalActive, + const DeepCollectionEquality().hash(_licenses)); + + /// Create a copy of LicenseExpirySummary + /// with the given fields replaced by the non-null parameter values. + @JsonKey(includeFromJson: false, includeToJson: false) + @override + @pragma('vm:prefer-inline') + _$$LicenseExpirySummaryImplCopyWith<_$LicenseExpirySummaryImpl> + get copyWith => + __$$LicenseExpirySummaryImplCopyWithImpl<_$LicenseExpirySummaryImpl>( + this, _$identity); + + @override + Map toJson() { + return _$$LicenseExpirySummaryImplToJson( + this, + ); + } +} + +abstract class _LicenseExpirySummary implements LicenseExpirySummary { + const factory _LicenseExpirySummary( + {@JsonKey(name: 'within_30_days') required final int within30Days, + @JsonKey(name: 'within_60_days') required final int within60Days, + @JsonKey(name: 'within_90_days') required final int within90Days, + @JsonKey(name: 'expired') required final int expired, + @JsonKey(name: 'total_active') required final int totalActive, + @JsonKey(name: 'licenses') + required final List licenses}) = + _$LicenseExpirySummaryImpl; + + factory _LicenseExpirySummary.fromJson(Map json) = + _$LicenseExpirySummaryImpl.fromJson; + + @override + @JsonKey(name: 'within_30_days') + int get within30Days; + @override + @JsonKey(name: 'within_60_days') + int get within60Days; + @override + @JsonKey(name: 'within_90_days') + int get within90Days; + @override + @JsonKey(name: 'expired') + int get expired; + @override + @JsonKey(name: 'total_active') + int get totalActive; + @override + @JsonKey(name: 'licenses') + List get licenses; + + /// Create a copy of LicenseExpirySummary + /// with the given fields replaced by the non-null parameter values. + @override + @JsonKey(includeFromJson: false, includeToJson: false) + _$$LicenseExpirySummaryImplCopyWith<_$LicenseExpirySummaryImpl> + get copyWith => throw _privateConstructorUsedError; +} + +LicenseExpiryDetail _$LicenseExpiryDetailFromJson(Map json) { + return _LicenseExpiryDetail.fromJson(json); +} + +/// @nodoc +mixin _$LicenseExpiryDetail { + int get id => throw _privateConstructorUsedError; + @JsonKey(name: 'equipment_id') + int get equipmentId => throw _privateConstructorUsedError; + @JsonKey(name: 'equipment_name') + String get equipmentName => throw _privateConstructorUsedError; + @JsonKey(name: 'serial_number') + String get serialNumber => throw _privateConstructorUsedError; + @JsonKey(name: 'company_name') + String get companyName => throw _privateConstructorUsedError; + @JsonKey(name: 'license_type') + String get licenseType => throw _privateConstructorUsedError; + @JsonKey(name: 'start_date') + String get startDate => throw _privateConstructorUsedError; + @JsonKey(name: 'end_date') + String get endDate => throw _privateConstructorUsedError; + @JsonKey(name: 'days_remaining') + int get daysRemaining => throw _privateConstructorUsedError; + @JsonKey(name: 'is_expired') + bool get isExpired => throw _privateConstructorUsedError; + + /// Serializes this LicenseExpiryDetail to a JSON map. + Map toJson() => throw _privateConstructorUsedError; + + /// Create a copy of LicenseExpiryDetail + /// with the given fields replaced by the non-null parameter values. + @JsonKey(includeFromJson: false, includeToJson: false) + $LicenseExpiryDetailCopyWith get copyWith => + throw _privateConstructorUsedError; +} + +/// @nodoc +abstract class $LicenseExpiryDetailCopyWith<$Res> { + factory $LicenseExpiryDetailCopyWith( + LicenseExpiryDetail value, $Res Function(LicenseExpiryDetail) then) = + _$LicenseExpiryDetailCopyWithImpl<$Res, LicenseExpiryDetail>; + @useResult + $Res call( + {int id, + @JsonKey(name: 'equipment_id') int equipmentId, + @JsonKey(name: 'equipment_name') String equipmentName, + @JsonKey(name: 'serial_number') String serialNumber, + @JsonKey(name: 'company_name') String companyName, + @JsonKey(name: 'license_type') String licenseType, + @JsonKey(name: 'start_date') String startDate, + @JsonKey(name: 'end_date') String endDate, + @JsonKey(name: 'days_remaining') int daysRemaining, + @JsonKey(name: 'is_expired') bool isExpired}); +} + +/// @nodoc +class _$LicenseExpiryDetailCopyWithImpl<$Res, $Val extends LicenseExpiryDetail> + implements $LicenseExpiryDetailCopyWith<$Res> { + _$LicenseExpiryDetailCopyWithImpl(this._value, this._then); + + // ignore: unused_field + final $Val _value; + // ignore: unused_field + final $Res Function($Val) _then; + + /// Create a copy of LicenseExpiryDetail + /// with the given fields replaced by the non-null parameter values. + @pragma('vm:prefer-inline') + @override + $Res call({ + Object? id = null, + Object? equipmentId = null, + Object? equipmentName = null, + Object? serialNumber = null, + Object? companyName = null, + Object? licenseType = null, + Object? startDate = null, + Object? endDate = null, + Object? daysRemaining = null, + Object? isExpired = null, + }) { + return _then(_value.copyWith( + id: null == id + ? _value.id + : id // ignore: cast_nullable_to_non_nullable + as int, + equipmentId: null == equipmentId + ? _value.equipmentId + : equipmentId // ignore: cast_nullable_to_non_nullable + as int, + equipmentName: null == equipmentName + ? _value.equipmentName + : equipmentName // ignore: cast_nullable_to_non_nullable + as String, + serialNumber: null == serialNumber + ? _value.serialNumber + : serialNumber // ignore: cast_nullable_to_non_nullable + as String, + companyName: null == companyName + ? _value.companyName + : companyName // ignore: cast_nullable_to_non_nullable + as String, + licenseType: null == licenseType + ? _value.licenseType + : licenseType // ignore: cast_nullable_to_non_nullable + as String, + startDate: null == startDate + ? _value.startDate + : startDate // ignore: cast_nullable_to_non_nullable + as String, + endDate: null == endDate + ? _value.endDate + : endDate // ignore: cast_nullable_to_non_nullable + as String, + daysRemaining: null == daysRemaining + ? _value.daysRemaining + : daysRemaining // ignore: cast_nullable_to_non_nullable + as int, + isExpired: null == isExpired + ? _value.isExpired + : isExpired // ignore: cast_nullable_to_non_nullable + as bool, + ) as $Val); + } +} + +/// @nodoc +abstract class _$$LicenseExpiryDetailImplCopyWith<$Res> + implements $LicenseExpiryDetailCopyWith<$Res> { + factory _$$LicenseExpiryDetailImplCopyWith(_$LicenseExpiryDetailImpl value, + $Res Function(_$LicenseExpiryDetailImpl) then) = + __$$LicenseExpiryDetailImplCopyWithImpl<$Res>; + @override + @useResult + $Res call( + {int id, + @JsonKey(name: 'equipment_id') int equipmentId, + @JsonKey(name: 'equipment_name') String equipmentName, + @JsonKey(name: 'serial_number') String serialNumber, + @JsonKey(name: 'company_name') String companyName, + @JsonKey(name: 'license_type') String licenseType, + @JsonKey(name: 'start_date') String startDate, + @JsonKey(name: 'end_date') String endDate, + @JsonKey(name: 'days_remaining') int daysRemaining, + @JsonKey(name: 'is_expired') bool isExpired}); +} + +/// @nodoc +class __$$LicenseExpiryDetailImplCopyWithImpl<$Res> + extends _$LicenseExpiryDetailCopyWithImpl<$Res, _$LicenseExpiryDetailImpl> + implements _$$LicenseExpiryDetailImplCopyWith<$Res> { + __$$LicenseExpiryDetailImplCopyWithImpl(_$LicenseExpiryDetailImpl _value, + $Res Function(_$LicenseExpiryDetailImpl) _then) + : super(_value, _then); + + /// Create a copy of LicenseExpiryDetail + /// with the given fields replaced by the non-null parameter values. + @pragma('vm:prefer-inline') + @override + $Res call({ + Object? id = null, + Object? equipmentId = null, + Object? equipmentName = null, + Object? serialNumber = null, + Object? companyName = null, + Object? licenseType = null, + Object? startDate = null, + Object? endDate = null, + Object? daysRemaining = null, + Object? isExpired = null, + }) { + return _then(_$LicenseExpiryDetailImpl( + id: null == id + ? _value.id + : id // ignore: cast_nullable_to_non_nullable + as int, + equipmentId: null == equipmentId + ? _value.equipmentId + : equipmentId // ignore: cast_nullable_to_non_nullable + as int, + equipmentName: null == equipmentName + ? _value.equipmentName + : equipmentName // ignore: cast_nullable_to_non_nullable + as String, + serialNumber: null == serialNumber + ? _value.serialNumber + : serialNumber // ignore: cast_nullable_to_non_nullable + as String, + companyName: null == companyName + ? _value.companyName + : companyName // ignore: cast_nullable_to_non_nullable + as String, + licenseType: null == licenseType + ? _value.licenseType + : licenseType // ignore: cast_nullable_to_non_nullable + as String, + startDate: null == startDate + ? _value.startDate + : startDate // ignore: cast_nullable_to_non_nullable + as String, + endDate: null == endDate + ? _value.endDate + : endDate // ignore: cast_nullable_to_non_nullable + as String, + daysRemaining: null == daysRemaining + ? _value.daysRemaining + : daysRemaining // ignore: cast_nullable_to_non_nullable + as int, + isExpired: null == isExpired + ? _value.isExpired + : isExpired // ignore: cast_nullable_to_non_nullable + as bool, + )); + } +} + +/// @nodoc +@JsonSerializable() +class _$LicenseExpiryDetailImpl implements _LicenseExpiryDetail { + const _$LicenseExpiryDetailImpl( + {required this.id, + @JsonKey(name: 'equipment_id') required this.equipmentId, + @JsonKey(name: 'equipment_name') required this.equipmentName, + @JsonKey(name: 'serial_number') required this.serialNumber, + @JsonKey(name: 'company_name') required this.companyName, + @JsonKey(name: 'license_type') required this.licenseType, + @JsonKey(name: 'start_date') required this.startDate, + @JsonKey(name: 'end_date') required this.endDate, + @JsonKey(name: 'days_remaining') required this.daysRemaining, + @JsonKey(name: 'is_expired') required this.isExpired}); + + factory _$LicenseExpiryDetailImpl.fromJson(Map json) => + _$$LicenseExpiryDetailImplFromJson(json); + + @override + final int id; + @override + @JsonKey(name: 'equipment_id') + final int equipmentId; + @override + @JsonKey(name: 'equipment_name') + final String equipmentName; + @override + @JsonKey(name: 'serial_number') + final String serialNumber; + @override + @JsonKey(name: 'company_name') + final String companyName; + @override + @JsonKey(name: 'license_type') + final String licenseType; + @override + @JsonKey(name: 'start_date') + final String startDate; + @override + @JsonKey(name: 'end_date') + final String endDate; + @override + @JsonKey(name: 'days_remaining') + final int daysRemaining; + @override + @JsonKey(name: 'is_expired') + final bool isExpired; + + @override + String toString() { + return 'LicenseExpiryDetail(id: $id, equipmentId: $equipmentId, equipmentName: $equipmentName, serialNumber: $serialNumber, companyName: $companyName, licenseType: $licenseType, startDate: $startDate, endDate: $endDate, daysRemaining: $daysRemaining, isExpired: $isExpired)'; + } + + @override + bool operator ==(Object other) { + return identical(this, other) || + (other.runtimeType == runtimeType && + other is _$LicenseExpiryDetailImpl && + (identical(other.id, id) || other.id == id) && + (identical(other.equipmentId, equipmentId) || + other.equipmentId == equipmentId) && + (identical(other.equipmentName, equipmentName) || + other.equipmentName == equipmentName) && + (identical(other.serialNumber, serialNumber) || + other.serialNumber == serialNumber) && + (identical(other.companyName, companyName) || + other.companyName == companyName) && + (identical(other.licenseType, licenseType) || + other.licenseType == licenseType) && + (identical(other.startDate, startDate) || + other.startDate == startDate) && + (identical(other.endDate, endDate) || other.endDate == endDate) && + (identical(other.daysRemaining, daysRemaining) || + other.daysRemaining == daysRemaining) && + (identical(other.isExpired, isExpired) || + other.isExpired == isExpired)); + } + + @JsonKey(includeFromJson: false, includeToJson: false) + @override + int get hashCode => Object.hash( + runtimeType, + id, + equipmentId, + equipmentName, + serialNumber, + companyName, + licenseType, + startDate, + endDate, + daysRemaining, + isExpired); + + /// Create a copy of LicenseExpiryDetail + /// with the given fields replaced by the non-null parameter values. + @JsonKey(includeFromJson: false, includeToJson: false) + @override + @pragma('vm:prefer-inline') + _$$LicenseExpiryDetailImplCopyWith<_$LicenseExpiryDetailImpl> get copyWith => + __$$LicenseExpiryDetailImplCopyWithImpl<_$LicenseExpiryDetailImpl>( + this, _$identity); + + @override + Map toJson() { + return _$$LicenseExpiryDetailImplToJson( + this, + ); + } +} + +abstract class _LicenseExpiryDetail implements LicenseExpiryDetail { + const factory _LicenseExpiryDetail( + {required final int id, + @JsonKey(name: 'equipment_id') required final int equipmentId, + @JsonKey(name: 'equipment_name') required final String equipmentName, + @JsonKey(name: 'serial_number') required final String serialNumber, + @JsonKey(name: 'company_name') required final String companyName, + @JsonKey(name: 'license_type') required final String licenseType, + @JsonKey(name: 'start_date') required final String startDate, + @JsonKey(name: 'end_date') required final String endDate, + @JsonKey(name: 'days_remaining') required final int daysRemaining, + @JsonKey(name: 'is_expired') required final bool isExpired}) = + _$LicenseExpiryDetailImpl; + + factory _LicenseExpiryDetail.fromJson(Map json) = + _$LicenseExpiryDetailImpl.fromJson; + + @override + int get id; + @override + @JsonKey(name: 'equipment_id') + int get equipmentId; + @override + @JsonKey(name: 'equipment_name') + String get equipmentName; + @override + @JsonKey(name: 'serial_number') + String get serialNumber; + @override + @JsonKey(name: 'company_name') + String get companyName; + @override + @JsonKey(name: 'license_type') + String get licenseType; + @override + @JsonKey(name: 'start_date') + String get startDate; + @override + @JsonKey(name: 'end_date') + String get endDate; + @override + @JsonKey(name: 'days_remaining') + int get daysRemaining; + @override + @JsonKey(name: 'is_expired') + bool get isExpired; + + /// Create a copy of LicenseExpiryDetail + /// with the given fields replaced by the non-null parameter values. + @override + @JsonKey(includeFromJson: false, includeToJson: false) + _$$LicenseExpiryDetailImplCopyWith<_$LicenseExpiryDetailImpl> get copyWith => + throw _privateConstructorUsedError; +} diff --git a/lib/data/models/dashboard/license_expiry_summary.g.dart b/lib/data/models/dashboard/license_expiry_summary.g.dart new file mode 100644 index 0000000..24e0dcc --- /dev/null +++ b/lib/data/models/dashboard/license_expiry_summary.g.dart @@ -0,0 +1,61 @@ +// GENERATED CODE - DO NOT MODIFY BY HAND + +part of 'license_expiry_summary.dart'; + +// ************************************************************************** +// JsonSerializableGenerator +// ************************************************************************** + +_$LicenseExpirySummaryImpl _$$LicenseExpirySummaryImplFromJson( + Map json) => + _$LicenseExpirySummaryImpl( + within30Days: (json['within_30_days'] as num).toInt(), + within60Days: (json['within_60_days'] as num).toInt(), + within90Days: (json['within_90_days'] as num).toInt(), + expired: (json['expired'] as num).toInt(), + totalActive: (json['total_active'] as num).toInt(), + licenses: (json['licenses'] as List) + .map((e) => LicenseExpiryDetail.fromJson(e as Map)) + .toList(), + ); + +Map _$$LicenseExpirySummaryImplToJson( + _$LicenseExpirySummaryImpl instance) => + { + 'within_30_days': instance.within30Days, + 'within_60_days': instance.within60Days, + 'within_90_days': instance.within90Days, + 'expired': instance.expired, + 'total_active': instance.totalActive, + 'licenses': instance.licenses, + }; + +_$LicenseExpiryDetailImpl _$$LicenseExpiryDetailImplFromJson( + Map json) => + _$LicenseExpiryDetailImpl( + id: (json['id'] as num).toInt(), + equipmentId: (json['equipment_id'] as num).toInt(), + equipmentName: json['equipment_name'] as String, + serialNumber: json['serial_number'] as String, + companyName: json['company_name'] as String, + licenseType: json['license_type'] as String, + startDate: json['start_date'] as String, + endDate: json['end_date'] as String, + daysRemaining: (json['days_remaining'] as num).toInt(), + isExpired: json['is_expired'] as bool, + ); + +Map _$$LicenseExpiryDetailImplToJson( + _$LicenseExpiryDetailImpl instance) => + { + 'id': instance.id, + 'equipment_id': instance.equipmentId, + 'equipment_name': instance.equipmentName, + 'serial_number': instance.serialNumber, + 'company_name': instance.companyName, + 'license_type': instance.licenseType, + 'start_date': instance.startDate, + 'end_date': instance.endDate, + 'days_remaining': instance.daysRemaining, + 'is_expired': instance.isExpired, + }; diff --git a/lib/data/models/lookups/lookup_data.dart b/lib/data/models/lookups/lookup_data.dart new file mode 100644 index 0000000..f628598 --- /dev/null +++ b/lib/data/models/lookups/lookup_data.dart @@ -0,0 +1,35 @@ +import 'package:freezed_annotation/freezed_annotation.dart'; + +part 'lookup_data.freezed.dart'; +part 'lookup_data.g.dart'; + +@freezed +class LookupData with _$LookupData { + const factory LookupData({ + @JsonKey(name: 'equipment_types') required List equipmentTypes, + @JsonKey(name: 'equipment_statuses') required List equipmentStatuses, + @JsonKey(name: 'license_types') required List licenseTypes, + @JsonKey(name: 'manufacturers') required List manufacturers, + @JsonKey(name: 'user_roles') required List userRoles, + @JsonKey(name: 'company_statuses') required List companyStatuses, + @JsonKey(name: 'warehouse_types') required List warehouseTypes, + }) = _LookupData; + + factory LookupData.fromJson(Map json) => + _$LookupDataFromJson(json); +} + +@freezed +class LookupItem with _$LookupItem { + const factory LookupItem({ + required String code, + required String name, + String? description, + @JsonKey(name: 'display_order') int? displayOrder, + @JsonKey(name: 'is_active') @Default(true) bool isActive, + Map? metadata, + }) = _LookupItem; + + factory LookupItem.fromJson(Map json) => + _$LookupItemFromJson(json); +} \ No newline at end of file diff --git a/lib/data/models/lookups/lookup_data.freezed.dart b/lib/data/models/lookups/lookup_data.freezed.dart new file mode 100644 index 0000000..615b8e1 --- /dev/null +++ b/lib/data/models/lookups/lookup_data.freezed.dart @@ -0,0 +1,656 @@ +// coverage:ignore-file +// GENERATED CODE - DO NOT MODIFY BY HAND +// ignore_for_file: type=lint +// ignore_for_file: unused_element, deprecated_member_use, deprecated_member_use_from_same_package, use_function_type_syntax_for_parameters, unnecessary_const, avoid_init_to_null, invalid_override_different_default_values_named, prefer_expression_function_bodies, annotate_overrides, invalid_annotation_target, unnecessary_question_mark + +part of 'lookup_data.dart'; + +// ************************************************************************** +// FreezedGenerator +// ************************************************************************** + +T _$identity(T value) => value; + +final _privateConstructorUsedError = UnsupportedError( + 'It seems like you constructed your class using `MyClass._()`. This constructor is only meant to be used by freezed and you are not supposed to need it nor use it.\nPlease check the documentation here for more information: https://github.com/rrousselGit/freezed#adding-getters-and-methods-to-our-models'); + +LookupData _$LookupDataFromJson(Map json) { + return _LookupData.fromJson(json); +} + +/// @nodoc +mixin _$LookupData { + @JsonKey(name: 'equipment_types') + List get equipmentTypes => throw _privateConstructorUsedError; + @JsonKey(name: 'equipment_statuses') + List get equipmentStatuses => throw _privateConstructorUsedError; + @JsonKey(name: 'license_types') + List get licenseTypes => throw _privateConstructorUsedError; + @JsonKey(name: 'manufacturers') + List get manufacturers => throw _privateConstructorUsedError; + @JsonKey(name: 'user_roles') + List get userRoles => throw _privateConstructorUsedError; + @JsonKey(name: 'company_statuses') + List get companyStatuses => throw _privateConstructorUsedError; + @JsonKey(name: 'warehouse_types') + List get warehouseTypes => throw _privateConstructorUsedError; + + /// Serializes this LookupData to a JSON map. + Map toJson() => throw _privateConstructorUsedError; + + /// Create a copy of LookupData + /// with the given fields replaced by the non-null parameter values. + @JsonKey(includeFromJson: false, includeToJson: false) + $LookupDataCopyWith get copyWith => + throw _privateConstructorUsedError; +} + +/// @nodoc +abstract class $LookupDataCopyWith<$Res> { + factory $LookupDataCopyWith( + LookupData value, $Res Function(LookupData) then) = + _$LookupDataCopyWithImpl<$Res, LookupData>; + @useResult + $Res call( + {@JsonKey(name: 'equipment_types') List equipmentTypes, + @JsonKey(name: 'equipment_statuses') List equipmentStatuses, + @JsonKey(name: 'license_types') List licenseTypes, + @JsonKey(name: 'manufacturers') List manufacturers, + @JsonKey(name: 'user_roles') List userRoles, + @JsonKey(name: 'company_statuses') List companyStatuses, + @JsonKey(name: 'warehouse_types') List warehouseTypes}); +} + +/// @nodoc +class _$LookupDataCopyWithImpl<$Res, $Val extends LookupData> + implements $LookupDataCopyWith<$Res> { + _$LookupDataCopyWithImpl(this._value, this._then); + + // ignore: unused_field + final $Val _value; + // ignore: unused_field + final $Res Function($Val) _then; + + /// Create a copy of LookupData + /// with the given fields replaced by the non-null parameter values. + @pragma('vm:prefer-inline') + @override + $Res call({ + Object? equipmentTypes = null, + Object? equipmentStatuses = null, + Object? licenseTypes = null, + Object? manufacturers = null, + Object? userRoles = null, + Object? companyStatuses = null, + Object? warehouseTypes = null, + }) { + return _then(_value.copyWith( + equipmentTypes: null == equipmentTypes + ? _value.equipmentTypes + : equipmentTypes // ignore: cast_nullable_to_non_nullable + as List, + equipmentStatuses: null == equipmentStatuses + ? _value.equipmentStatuses + : equipmentStatuses // ignore: cast_nullable_to_non_nullable + as List, + licenseTypes: null == licenseTypes + ? _value.licenseTypes + : licenseTypes // ignore: cast_nullable_to_non_nullable + as List, + manufacturers: null == manufacturers + ? _value.manufacturers + : manufacturers // ignore: cast_nullable_to_non_nullable + as List, + userRoles: null == userRoles + ? _value.userRoles + : userRoles // ignore: cast_nullable_to_non_nullable + as List, + companyStatuses: null == companyStatuses + ? _value.companyStatuses + : companyStatuses // ignore: cast_nullable_to_non_nullable + as List, + warehouseTypes: null == warehouseTypes + ? _value.warehouseTypes + : warehouseTypes // ignore: cast_nullable_to_non_nullable + as List, + ) as $Val); + } +} + +/// @nodoc +abstract class _$$LookupDataImplCopyWith<$Res> + implements $LookupDataCopyWith<$Res> { + factory _$$LookupDataImplCopyWith( + _$LookupDataImpl value, $Res Function(_$LookupDataImpl) then) = + __$$LookupDataImplCopyWithImpl<$Res>; + @override + @useResult + $Res call( + {@JsonKey(name: 'equipment_types') List equipmentTypes, + @JsonKey(name: 'equipment_statuses') List equipmentStatuses, + @JsonKey(name: 'license_types') List licenseTypes, + @JsonKey(name: 'manufacturers') List manufacturers, + @JsonKey(name: 'user_roles') List userRoles, + @JsonKey(name: 'company_statuses') List companyStatuses, + @JsonKey(name: 'warehouse_types') List warehouseTypes}); +} + +/// @nodoc +class __$$LookupDataImplCopyWithImpl<$Res> + extends _$LookupDataCopyWithImpl<$Res, _$LookupDataImpl> + implements _$$LookupDataImplCopyWith<$Res> { + __$$LookupDataImplCopyWithImpl( + _$LookupDataImpl _value, $Res Function(_$LookupDataImpl) _then) + : super(_value, _then); + + /// Create a copy of LookupData + /// with the given fields replaced by the non-null parameter values. + @pragma('vm:prefer-inline') + @override + $Res call({ + Object? equipmentTypes = null, + Object? equipmentStatuses = null, + Object? licenseTypes = null, + Object? manufacturers = null, + Object? userRoles = null, + Object? companyStatuses = null, + Object? warehouseTypes = null, + }) { + return _then(_$LookupDataImpl( + equipmentTypes: null == equipmentTypes + ? _value._equipmentTypes + : equipmentTypes // ignore: cast_nullable_to_non_nullable + as List, + equipmentStatuses: null == equipmentStatuses + ? _value._equipmentStatuses + : equipmentStatuses // ignore: cast_nullable_to_non_nullable + as List, + licenseTypes: null == licenseTypes + ? _value._licenseTypes + : licenseTypes // ignore: cast_nullable_to_non_nullable + as List, + manufacturers: null == manufacturers + ? _value._manufacturers + : manufacturers // ignore: cast_nullable_to_non_nullable + as List, + userRoles: null == userRoles + ? _value._userRoles + : userRoles // ignore: cast_nullable_to_non_nullable + as List, + companyStatuses: null == companyStatuses + ? _value._companyStatuses + : companyStatuses // ignore: cast_nullable_to_non_nullable + as List, + warehouseTypes: null == warehouseTypes + ? _value._warehouseTypes + : warehouseTypes // ignore: cast_nullable_to_non_nullable + as List, + )); + } +} + +/// @nodoc +@JsonSerializable() +class _$LookupDataImpl implements _LookupData { + const _$LookupDataImpl( + {@JsonKey(name: 'equipment_types') + required final List equipmentTypes, + @JsonKey(name: 'equipment_statuses') + required final List equipmentStatuses, + @JsonKey(name: 'license_types') + required final List licenseTypes, + @JsonKey(name: 'manufacturers') + required final List manufacturers, + @JsonKey(name: 'user_roles') required final List userRoles, + @JsonKey(name: 'company_statuses') + required final List companyStatuses, + @JsonKey(name: 'warehouse_types') + required final List warehouseTypes}) + : _equipmentTypes = equipmentTypes, + _equipmentStatuses = equipmentStatuses, + _licenseTypes = licenseTypes, + _manufacturers = manufacturers, + _userRoles = userRoles, + _companyStatuses = companyStatuses, + _warehouseTypes = warehouseTypes; + + factory _$LookupDataImpl.fromJson(Map json) => + _$$LookupDataImplFromJson(json); + + final List _equipmentTypes; + @override + @JsonKey(name: 'equipment_types') + List get equipmentTypes { + if (_equipmentTypes is EqualUnmodifiableListView) return _equipmentTypes; + // ignore: implicit_dynamic_type + return EqualUnmodifiableListView(_equipmentTypes); + } + + final List _equipmentStatuses; + @override + @JsonKey(name: 'equipment_statuses') + List get equipmentStatuses { + if (_equipmentStatuses is EqualUnmodifiableListView) + return _equipmentStatuses; + // ignore: implicit_dynamic_type + return EqualUnmodifiableListView(_equipmentStatuses); + } + + final List _licenseTypes; + @override + @JsonKey(name: 'license_types') + List get licenseTypes { + if (_licenseTypes is EqualUnmodifiableListView) return _licenseTypes; + // ignore: implicit_dynamic_type + return EqualUnmodifiableListView(_licenseTypes); + } + + final List _manufacturers; + @override + @JsonKey(name: 'manufacturers') + List get manufacturers { + if (_manufacturers is EqualUnmodifiableListView) return _manufacturers; + // ignore: implicit_dynamic_type + return EqualUnmodifiableListView(_manufacturers); + } + + final List _userRoles; + @override + @JsonKey(name: 'user_roles') + List get userRoles { + if (_userRoles is EqualUnmodifiableListView) return _userRoles; + // ignore: implicit_dynamic_type + return EqualUnmodifiableListView(_userRoles); + } + + final List _companyStatuses; + @override + @JsonKey(name: 'company_statuses') + List get companyStatuses { + if (_companyStatuses is EqualUnmodifiableListView) return _companyStatuses; + // ignore: implicit_dynamic_type + return EqualUnmodifiableListView(_companyStatuses); + } + + final List _warehouseTypes; + @override + @JsonKey(name: 'warehouse_types') + List get warehouseTypes { + if (_warehouseTypes is EqualUnmodifiableListView) return _warehouseTypes; + // ignore: implicit_dynamic_type + return EqualUnmodifiableListView(_warehouseTypes); + } + + @override + String toString() { + return 'LookupData(equipmentTypes: $equipmentTypes, equipmentStatuses: $equipmentStatuses, licenseTypes: $licenseTypes, manufacturers: $manufacturers, userRoles: $userRoles, companyStatuses: $companyStatuses, warehouseTypes: $warehouseTypes)'; + } + + @override + bool operator ==(Object other) { + return identical(this, other) || + (other.runtimeType == runtimeType && + other is _$LookupDataImpl && + const DeepCollectionEquality() + .equals(other._equipmentTypes, _equipmentTypes) && + const DeepCollectionEquality() + .equals(other._equipmentStatuses, _equipmentStatuses) && + const DeepCollectionEquality() + .equals(other._licenseTypes, _licenseTypes) && + const DeepCollectionEquality() + .equals(other._manufacturers, _manufacturers) && + const DeepCollectionEquality() + .equals(other._userRoles, _userRoles) && + const DeepCollectionEquality() + .equals(other._companyStatuses, _companyStatuses) && + const DeepCollectionEquality() + .equals(other._warehouseTypes, _warehouseTypes)); + } + + @JsonKey(includeFromJson: false, includeToJson: false) + @override + int get hashCode => Object.hash( + runtimeType, + const DeepCollectionEquality().hash(_equipmentTypes), + const DeepCollectionEquality().hash(_equipmentStatuses), + const DeepCollectionEquality().hash(_licenseTypes), + const DeepCollectionEquality().hash(_manufacturers), + const DeepCollectionEquality().hash(_userRoles), + const DeepCollectionEquality().hash(_companyStatuses), + const DeepCollectionEquality().hash(_warehouseTypes)); + + /// Create a copy of LookupData + /// with the given fields replaced by the non-null parameter values. + @JsonKey(includeFromJson: false, includeToJson: false) + @override + @pragma('vm:prefer-inline') + _$$LookupDataImplCopyWith<_$LookupDataImpl> get copyWith => + __$$LookupDataImplCopyWithImpl<_$LookupDataImpl>(this, _$identity); + + @override + Map toJson() { + return _$$LookupDataImplToJson( + this, + ); + } +} + +abstract class _LookupData implements LookupData { + const factory _LookupData( + {@JsonKey(name: 'equipment_types') + required final List equipmentTypes, + @JsonKey(name: 'equipment_statuses') + required final List equipmentStatuses, + @JsonKey(name: 'license_types') + required final List licenseTypes, + @JsonKey(name: 'manufacturers') + required final List manufacturers, + @JsonKey(name: 'user_roles') required final List userRoles, + @JsonKey(name: 'company_statuses') + required final List companyStatuses, + @JsonKey(name: 'warehouse_types') + required final List warehouseTypes}) = _$LookupDataImpl; + + factory _LookupData.fromJson(Map json) = + _$LookupDataImpl.fromJson; + + @override + @JsonKey(name: 'equipment_types') + List get equipmentTypes; + @override + @JsonKey(name: 'equipment_statuses') + List get equipmentStatuses; + @override + @JsonKey(name: 'license_types') + List get licenseTypes; + @override + @JsonKey(name: 'manufacturers') + List get manufacturers; + @override + @JsonKey(name: 'user_roles') + List get userRoles; + @override + @JsonKey(name: 'company_statuses') + List get companyStatuses; + @override + @JsonKey(name: 'warehouse_types') + List get warehouseTypes; + + /// Create a copy of LookupData + /// with the given fields replaced by the non-null parameter values. + @override + @JsonKey(includeFromJson: false, includeToJson: false) + _$$LookupDataImplCopyWith<_$LookupDataImpl> get copyWith => + throw _privateConstructorUsedError; +} + +LookupItem _$LookupItemFromJson(Map json) { + return _LookupItem.fromJson(json); +} + +/// @nodoc +mixin _$LookupItem { + String get code => throw _privateConstructorUsedError; + String get name => throw _privateConstructorUsedError; + String? get description => throw _privateConstructorUsedError; + @JsonKey(name: 'display_order') + int? get displayOrder => throw _privateConstructorUsedError; + @JsonKey(name: 'is_active') + bool get isActive => throw _privateConstructorUsedError; + Map? get metadata => throw _privateConstructorUsedError; + + /// Serializes this LookupItem to a JSON map. + Map toJson() => throw _privateConstructorUsedError; + + /// Create a copy of LookupItem + /// with the given fields replaced by the non-null parameter values. + @JsonKey(includeFromJson: false, includeToJson: false) + $LookupItemCopyWith get copyWith => + throw _privateConstructorUsedError; +} + +/// @nodoc +abstract class $LookupItemCopyWith<$Res> { + factory $LookupItemCopyWith( + LookupItem value, $Res Function(LookupItem) then) = + _$LookupItemCopyWithImpl<$Res, LookupItem>; + @useResult + $Res call( + {String code, + String name, + String? description, + @JsonKey(name: 'display_order') int? displayOrder, + @JsonKey(name: 'is_active') bool isActive, + Map? metadata}); +} + +/// @nodoc +class _$LookupItemCopyWithImpl<$Res, $Val extends LookupItem> + implements $LookupItemCopyWith<$Res> { + _$LookupItemCopyWithImpl(this._value, this._then); + + // ignore: unused_field + final $Val _value; + // ignore: unused_field + final $Res Function($Val) _then; + + /// Create a copy of LookupItem + /// with the given fields replaced by the non-null parameter values. + @pragma('vm:prefer-inline') + @override + $Res call({ + Object? code = null, + Object? name = null, + Object? description = freezed, + Object? displayOrder = freezed, + Object? isActive = null, + Object? metadata = freezed, + }) { + return _then(_value.copyWith( + code: null == code + ? _value.code + : code // ignore: cast_nullable_to_non_nullable + as String, + name: null == name + ? _value.name + : name // ignore: cast_nullable_to_non_nullable + as String, + description: freezed == description + ? _value.description + : description // ignore: cast_nullable_to_non_nullable + as String?, + displayOrder: freezed == displayOrder + ? _value.displayOrder + : displayOrder // ignore: cast_nullable_to_non_nullable + as int?, + isActive: null == isActive + ? _value.isActive + : isActive // ignore: cast_nullable_to_non_nullable + as bool, + metadata: freezed == metadata + ? _value.metadata + : metadata // ignore: cast_nullable_to_non_nullable + as Map?, + ) as $Val); + } +} + +/// @nodoc +abstract class _$$LookupItemImplCopyWith<$Res> + implements $LookupItemCopyWith<$Res> { + factory _$$LookupItemImplCopyWith( + _$LookupItemImpl value, $Res Function(_$LookupItemImpl) then) = + __$$LookupItemImplCopyWithImpl<$Res>; + @override + @useResult + $Res call( + {String code, + String name, + String? description, + @JsonKey(name: 'display_order') int? displayOrder, + @JsonKey(name: 'is_active') bool isActive, + Map? metadata}); +} + +/// @nodoc +class __$$LookupItemImplCopyWithImpl<$Res> + extends _$LookupItemCopyWithImpl<$Res, _$LookupItemImpl> + implements _$$LookupItemImplCopyWith<$Res> { + __$$LookupItemImplCopyWithImpl( + _$LookupItemImpl _value, $Res Function(_$LookupItemImpl) _then) + : super(_value, _then); + + /// Create a copy of LookupItem + /// with the given fields replaced by the non-null parameter values. + @pragma('vm:prefer-inline') + @override + $Res call({ + Object? code = null, + Object? name = null, + Object? description = freezed, + Object? displayOrder = freezed, + Object? isActive = null, + Object? metadata = freezed, + }) { + return _then(_$LookupItemImpl( + code: null == code + ? _value.code + : code // ignore: cast_nullable_to_non_nullable + as String, + name: null == name + ? _value.name + : name // ignore: cast_nullable_to_non_nullable + as String, + description: freezed == description + ? _value.description + : description // ignore: cast_nullable_to_non_nullable + as String?, + displayOrder: freezed == displayOrder + ? _value.displayOrder + : displayOrder // ignore: cast_nullable_to_non_nullable + as int?, + isActive: null == isActive + ? _value.isActive + : isActive // ignore: cast_nullable_to_non_nullable + as bool, + metadata: freezed == metadata + ? _value._metadata + : metadata // ignore: cast_nullable_to_non_nullable + as Map?, + )); + } +} + +/// @nodoc +@JsonSerializable() +class _$LookupItemImpl implements _LookupItem { + const _$LookupItemImpl( + {required this.code, + required this.name, + this.description, + @JsonKey(name: 'display_order') this.displayOrder, + @JsonKey(name: 'is_active') this.isActive = true, + final Map? metadata}) + : _metadata = metadata; + + factory _$LookupItemImpl.fromJson(Map json) => + _$$LookupItemImplFromJson(json); + + @override + final String code; + @override + final String name; + @override + final String? description; + @override + @JsonKey(name: 'display_order') + final int? displayOrder; + @override + @JsonKey(name: 'is_active') + final bool isActive; + final Map? _metadata; + @override + Map? get metadata { + final value = _metadata; + if (value == null) return null; + if (_metadata is EqualUnmodifiableMapView) return _metadata; + // ignore: implicit_dynamic_type + return EqualUnmodifiableMapView(value); + } + + @override + String toString() { + return 'LookupItem(code: $code, name: $name, description: $description, displayOrder: $displayOrder, isActive: $isActive, metadata: $metadata)'; + } + + @override + bool operator ==(Object other) { + return identical(this, other) || + (other.runtimeType == runtimeType && + other is _$LookupItemImpl && + (identical(other.code, code) || other.code == code) && + (identical(other.name, name) || other.name == name) && + (identical(other.description, description) || + other.description == description) && + (identical(other.displayOrder, displayOrder) || + other.displayOrder == displayOrder) && + (identical(other.isActive, isActive) || + other.isActive == isActive) && + const DeepCollectionEquality().equals(other._metadata, _metadata)); + } + + @JsonKey(includeFromJson: false, includeToJson: false) + @override + int get hashCode => Object.hash(runtimeType, code, name, description, + displayOrder, isActive, const DeepCollectionEquality().hash(_metadata)); + + /// Create a copy of LookupItem + /// with the given fields replaced by the non-null parameter values. + @JsonKey(includeFromJson: false, includeToJson: false) + @override + @pragma('vm:prefer-inline') + _$$LookupItemImplCopyWith<_$LookupItemImpl> get copyWith => + __$$LookupItemImplCopyWithImpl<_$LookupItemImpl>(this, _$identity); + + @override + Map toJson() { + return _$$LookupItemImplToJson( + this, + ); + } +} + +abstract class _LookupItem implements LookupItem { + const factory _LookupItem( + {required final String code, + required final String name, + final String? description, + @JsonKey(name: 'display_order') final int? displayOrder, + @JsonKey(name: 'is_active') final bool isActive, + final Map? metadata}) = _$LookupItemImpl; + + factory _LookupItem.fromJson(Map json) = + _$LookupItemImpl.fromJson; + + @override + String get code; + @override + String get name; + @override + String? get description; + @override + @JsonKey(name: 'display_order') + int? get displayOrder; + @override + @JsonKey(name: 'is_active') + bool get isActive; + @override + Map? get metadata; + + /// Create a copy of LookupItem + /// with the given fields replaced by the non-null parameter values. + @override + @JsonKey(includeFromJson: false, includeToJson: false) + _$$LookupItemImplCopyWith<_$LookupItemImpl> get copyWith => + throw _privateConstructorUsedError; +} diff --git a/lib/data/models/lookups/lookup_data.g.dart b/lib/data/models/lookups/lookup_data.g.dart new file mode 100644 index 0000000..36db2be --- /dev/null +++ b/lib/data/models/lookups/lookup_data.g.dart @@ -0,0 +1,63 @@ +// GENERATED CODE - DO NOT MODIFY BY HAND + +part of 'lookup_data.dart'; + +// ************************************************************************** +// JsonSerializableGenerator +// ************************************************************************** + +_$LookupDataImpl _$$LookupDataImplFromJson(Map json) => + _$LookupDataImpl( + equipmentTypes: (json['equipment_types'] as List) + .map((e) => LookupItem.fromJson(e as Map)) + .toList(), + equipmentStatuses: (json['equipment_statuses'] as List) + .map((e) => LookupItem.fromJson(e as Map)) + .toList(), + licenseTypes: (json['license_types'] as List) + .map((e) => LookupItem.fromJson(e as Map)) + .toList(), + manufacturers: (json['manufacturers'] as List) + .map((e) => LookupItem.fromJson(e as Map)) + .toList(), + userRoles: (json['user_roles'] as List) + .map((e) => LookupItem.fromJson(e as Map)) + .toList(), + companyStatuses: (json['company_statuses'] as List) + .map((e) => LookupItem.fromJson(e as Map)) + .toList(), + warehouseTypes: (json['warehouse_types'] as List) + .map((e) => LookupItem.fromJson(e as Map)) + .toList(), + ); + +Map _$$LookupDataImplToJson(_$LookupDataImpl instance) => + { + 'equipment_types': instance.equipmentTypes, + 'equipment_statuses': instance.equipmentStatuses, + 'license_types': instance.licenseTypes, + 'manufacturers': instance.manufacturers, + 'user_roles': instance.userRoles, + 'company_statuses': instance.companyStatuses, + 'warehouse_types': instance.warehouseTypes, + }; + +_$LookupItemImpl _$$LookupItemImplFromJson(Map json) => + _$LookupItemImpl( + code: json['code'] as String, + name: json['name'] as String, + description: json['description'] as String?, + displayOrder: (json['display_order'] as num?)?.toInt(), + isActive: json['is_active'] as bool? ?? true, + metadata: json['metadata'] as Map?, + ); + +Map _$$LookupItemImplToJson(_$LookupItemImpl instance) => + { + 'code': instance.code, + 'name': instance.name, + 'description': instance.description, + 'display_order': instance.displayOrder, + 'is_active': instance.isActive, + 'metadata': instance.metadata, + }; diff --git a/lib/di/injection_container.dart b/lib/di/injection_container.dart index 391e173..82789e6 100644 --- a/lib/di/injection_container.dart +++ b/lib/di/injection_container.dart @@ -9,6 +9,7 @@ import '../data/datasources/remote/equipment_remote_datasource.dart'; import '../data/datasources/remote/company_remote_datasource.dart'; import '../data/datasources/remote/user_remote_datasource.dart'; import '../data/datasources/remote/license_remote_datasource.dart'; +import '../data/datasources/remote/lookup_remote_datasource.dart'; import '../data/datasources/remote/warehouse_remote_datasource.dart'; import '../services/auth_service.dart'; import '../services/dashboard_service.dart'; @@ -16,6 +17,7 @@ import '../services/equipment_service.dart'; import '../services/company_service.dart'; import '../services/user_service.dart'; import '../services/license_service.dart'; +import '../services/lookup_service.dart'; import '../services/warehouse_service.dart'; /// GetIt 인스턴스 @@ -52,6 +54,9 @@ Future setupDependencies() async { getIt.registerLazySingleton( () => LicenseRemoteDataSourceImpl(apiClient: getIt()), ); + getIt.registerLazySingleton( + () => LookupRemoteDataSourceImpl(getIt()), + ); getIt.registerLazySingleton( () => WarehouseRemoteDataSourceImpl(apiClient: getIt()), ); @@ -75,6 +80,9 @@ Future setupDependencies() async { getIt.registerLazySingleton( () => LicenseService(getIt()), ); + getIt.registerLazySingleton( + () => LookupService(getIt()), + ); getIt.registerLazySingleton( () => WarehouseService(), ); diff --git a/lib/screens/overview/controllers/overview_controller.dart b/lib/screens/overview/controllers/overview_controller.dart index deca7d3..3a56420 100644 --- a/lib/screens/overview/controllers/overview_controller.dart +++ b/lib/screens/overview/controllers/overview_controller.dart @@ -2,6 +2,7 @@ import 'package:flutter/material.dart'; import 'package:get_it/get_it.dart'; import 'package:superport/data/models/dashboard/equipment_status_distribution.dart'; import 'package:superport/data/models/dashboard/expiring_license.dart'; +import 'package:superport/data/models/dashboard/license_expiry_summary.dart'; import 'package:superport/data/models/dashboard/overview_stats.dart'; import 'package:superport/data/models/dashboard/recent_activity.dart'; import 'package:superport/services/dashboard_service.dart'; @@ -17,35 +18,53 @@ class OverviewController extends ChangeNotifier { List _recentActivities = []; EquipmentStatusDistribution? _equipmentStatus; List _expiringLicenses = []; + LicenseExpirySummary? _licenseExpirySummary; // 로딩 상태 bool _isLoadingStats = false; bool _isLoadingActivities = false; bool _isLoadingEquipmentStatus = false; bool _isLoadingLicenses = false; + bool _isLoadingLicenseExpiry = false; // 에러 상태 String? _statsError; String? _activitiesError; String? _equipmentStatusError; String? _licensesError; + String? _licenseExpiryError; // Getters OverviewStats? get overviewStats => _overviewStats; List get recentActivities => _recentActivities; EquipmentStatusDistribution? get equipmentStatus => _equipmentStatus; List get expiringLicenses => _expiringLicenses; + LicenseExpirySummary? get licenseExpirySummary => _licenseExpirySummary; // 추가 getter int get totalCompanies => _overviewStats?.totalCompanies ?? 0; int get totalUsers => _overviewStats?.totalUsers ?? 0; bool get isLoading => _isLoadingStats || _isLoadingActivities || - _isLoadingEquipmentStatus || _isLoadingLicenses; + _isLoadingEquipmentStatus || _isLoadingLicenses || + _isLoadingLicenseExpiry; String? get error { return _statsError ?? _activitiesError ?? - _equipmentStatusError ?? _licensesError; + _equipmentStatusError ?? _licensesError ?? _licenseExpiryError; + } + + // 라이선스 만료 알림 여부 + bool get hasExpiringLicenses { + if (_licenseExpirySummary == null) return false; + return (_licenseExpirySummary!.within30Days > 0 || + _licenseExpirySummary!.expired > 0); + } + + // 긴급 라이선스 수 (30일 이내 또는 만료) + int get urgentLicenseCount { + if (_licenseExpirySummary == null) return 0; + return _licenseExpirySummary!.within30Days + _licenseExpirySummary!.expired; } OverviewController(); @@ -58,6 +77,7 @@ class OverviewController extends ChangeNotifier { _loadRecentActivities(), _loadEquipmentStatus(), _loadExpiringLicenses(), + _loadLicenseExpirySummary(), ], eagerError: false); // 하나의 작업이 실패해도 다른 작업 계속 진행 } catch (e) { DebugLogger.logError('대시보드 데이터 로드 중 오류', error: e); @@ -233,6 +253,38 @@ class OverviewController extends ChangeNotifier { notifyListeners(); } + Future _loadLicenseExpirySummary() async { + _isLoadingLicenseExpiry = true; + _licenseExpiryError = null; + notifyListeners(); + + try { + final result = await _dashboardService.getLicenseExpirySummary(); + + result.fold( + (failure) { + _licenseExpiryError = failure.message; + DebugLogger.logError('라이선스 만료 요약 로드 실패', error: failure.message); + }, + (summary) { + _licenseExpirySummary = summary; + DebugLogger.log('라이선스 만료 요약 로드 성공', tag: 'DASHBOARD', data: { + 'within30Days': summary.within30Days, + 'within60Days': summary.within60Days, + 'within90Days': summary.within90Days, + 'expired': summary.expired, + }); + }, + ); + } catch (e) { + _licenseExpiryError = '라이선스 만료 요약을 불러올 수 없습니다'; + DebugLogger.logError('라이선스 만료 요약 로드 예외', error: e); + } + + _isLoadingLicenseExpiry = false; + notifyListeners(); + } + // 활동 타입별 아이콘과 색상 가져오기 IconData getActivityIcon(String activityType) { switch (activityType.toLowerCase()) { diff --git a/lib/screens/overview/overview_screen_redesign.dart b/lib/screens/overview/overview_screen_redesign.dart index 4fc3b99..35222dd 100644 --- a/lib/screens/overview/overview_screen_redesign.dart +++ b/lib/screens/overview/overview_screen_redesign.dart @@ -55,6 +55,12 @@ class _OverviewScreenRedesignState extends State { child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ + // 라이선스 만료 알림 배너 (조건부 표시) + if (controller.hasExpiringLicenses) ...[ + _buildLicenseExpiryBanner(controller), + const SizedBox(height: 16), + ], + // 환영 섹션 ShadcnCard( padding: const EdgeInsets.all(32), @@ -375,6 +381,82 @@ class _OverviewScreenRedesignState extends State { ); } + Widget _buildLicenseExpiryBanner(OverviewController controller) { + final summary = controller.licenseExpirySummary; + if (summary == null) return const SizedBox.shrink(); + + Color bannerColor = ShadcnTheme.warning; + String bannerText = ''; + IconData bannerIcon = Icons.warning_amber_rounded; + + if (summary.expired > 0) { + bannerColor = ShadcnTheme.destructive; + bannerText = '${summary.expired}개 라이선스 만료'; + bannerIcon = Icons.error_outline; + } else if (summary.within30Days > 0) { + bannerColor = ShadcnTheme.warning; + bannerText = '${summary.within30Days}개 라이선스 30일 내 만료 예정'; + bannerIcon = Icons.warning_amber_rounded; + } else if (summary.within60Days > 0) { + bannerColor = ShadcnTheme.primary; + bannerText = '${summary.within60Days}개 라이선스 60일 내 만료 예정'; + bannerIcon = Icons.info_outline; + } + + return Container( + padding: const EdgeInsets.all(16), + decoration: BoxDecoration( + color: bannerColor.withValues(alpha: 0.1), + border: Border.all(color: bannerColor.withValues(alpha: 0.3)), + borderRadius: BorderRadius.circular(8), + ), + child: Row( + children: [ + Icon(bannerIcon, color: bannerColor, size: 24), + const SizedBox(width: 12), + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + '라이선스 관리 필요', + style: ShadcnTheme.bodyMedium.copyWith( + fontWeight: FontWeight.bold, + color: bannerColor, + ), + ), + const SizedBox(height: 4), + Text( + bannerText, + style: ShadcnTheme.bodySmall.copyWith( + color: ShadcnTheme.foreground, + ), + ), + ], + ), + ), + TextButton( + onPressed: () { + // 라이선스 목록 페이지로 이동 + Navigator.pushNamed(context, '/licenses'); + }, + child: Row( + mainAxisSize: MainAxisSize.min, + children: [ + Text( + '상세 보기', + style: TextStyle(color: bannerColor), + ), + const SizedBox(width: 4), + Icon(Icons.arrow_forward, color: bannerColor, size: 16), + ], + ), + ), + ], + ), + ); + } + Widget _buildStatCard( String title, String value, diff --git a/lib/services/dashboard_service.dart b/lib/services/dashboard_service.dart index 63a9331..5aea03b 100644 --- a/lib/services/dashboard_service.dart +++ b/lib/services/dashboard_service.dart @@ -4,6 +4,7 @@ import 'package:superport/core/errors/failures.dart'; import 'package:superport/data/datasources/remote/dashboard_remote_datasource.dart'; import 'package:superport/data/models/dashboard/equipment_status_distribution.dart'; import 'package:superport/data/models/dashboard/expiring_license.dart'; +import 'package:superport/data/models/dashboard/license_expiry_summary.dart'; import 'package:superport/data/models/dashboard/overview_stats.dart'; import 'package:superport/data/models/dashboard/recent_activity.dart'; @@ -12,6 +13,7 @@ abstract class DashboardService { Future>> getRecentActivities(); Future> getEquipmentStatusDistribution(); Future>> getExpiringLicenses({int days = 30}); + Future> getLicenseExpirySummary(); } @LazySingleton(as: DashboardService) @@ -39,4 +41,9 @@ class DashboardServiceImpl implements DashboardService { Future>> getExpiringLicenses({int days = 30}) async { return await _remoteDataSource.getExpiringLicenses(days: days); } + + @override + Future> getLicenseExpirySummary() async { + return await _remoteDataSource.getLicenseExpirySummary(); + } } \ No newline at end of file diff --git a/lib/services/lookup_service.dart b/lib/services/lookup_service.dart new file mode 100644 index 0000000..c8517f2 --- /dev/null +++ b/lib/services/lookup_service.dart @@ -0,0 +1,165 @@ +import 'package:flutter/foundation.dart'; +import 'package:injectable/injectable.dart'; +import 'package:superport/data/datasources/remote/lookup_remote_datasource.dart'; +import 'package:superport/data/models/lookups/lookup_data.dart'; + +@lazySingleton +class LookupService extends ChangeNotifier { + final LookupRemoteDataSource _dataSource; + + LookupData? _lookupData; + bool _isLoading = false; + String? _error; + DateTime? _lastFetchTime; + + // 캐시 유효 시간 (30분) + static const Duration _cacheValidDuration = Duration(minutes: 30); + + LookupService(this._dataSource); + + // Getters + LookupData? get lookupData => _lookupData; + bool get isLoading => _isLoading; + String? get error => _error; + bool get hasData => _lookupData != null; + + // 캐시가 유효한지 확인 + bool get isCacheValid { + if (_lastFetchTime == null) return false; + return DateTime.now().difference(_lastFetchTime!) < _cacheValidDuration; + } + + // 장비 타입 목록 + List get equipmentTypes => _lookupData?.equipmentTypes ?? []; + + // 장비 상태 목록 + List get equipmentStatuses => _lookupData?.equipmentStatuses ?? []; + + // 라이선스 타입 목록 + List get licenseTypes => _lookupData?.licenseTypes ?? []; + + // 제조사 목록 + List get manufacturers => _lookupData?.manufacturers ?? []; + + // 사용자 역할 목록 + List get userRoles => _lookupData?.userRoles ?? []; + + // 회사 상태 목록 + List get companyStatuses => _lookupData?.companyStatuses ?? []; + + // 창고 타입 목록 + List get warehouseTypes => _lookupData?.warehouseTypes ?? []; + + // 전체 조회 데이터 로드 + Future loadAllLookups({bool forceRefresh = false}) async { + // 캐시가 유효하고 강제 새로고침이 아니면 캐시 사용 + if (!forceRefresh && isCacheValid && hasData) { + return; + } + + _isLoading = true; + _error = null; + notifyListeners(); + + try { + final result = await _dataSource.getAllLookups(); + + result.fold( + (failure) { + _error = failure.message; + _isLoading = false; + notifyListeners(); + }, + (data) { + _lookupData = data; + _lastFetchTime = DateTime.now(); + _error = null; + _isLoading = false; + notifyListeners(); + }, + ); + } catch (e) { + _error = '조회 데이터 로드 중 오류가 발생했습니다: $e'; + _isLoading = false; + notifyListeners(); + } + } + + // 특정 타입의 조회 데이터만 로드 + Future>?> loadLookupsByType(String type) async { + try { + final result = await _dataSource.getLookupsByType(type); + + return result.fold( + (failure) { + _error = failure.message; + notifyListeners(); + return null; + }, + (data) { + // 부분 업데이트 (필요한 경우) + _updatePartialData(type, data); + return data; + }, + ); + } catch (e) { + _error = '타입별 조회 데이터 로드 중 오류가 발생했습니다: $e'; + notifyListeners(); + return null; + } + } + + // 부분 데이터 업데이트 + void _updatePartialData(String type, Map> data) { + if (_lookupData == null) { + // 전체 데이터가 없으면 부분 데이터만으로 초기화 + _lookupData = LookupData( + equipmentTypes: data['equipment_types'] ?? [], + equipmentStatuses: data['equipment_statuses'] ?? [], + licenseTypes: data['license_types'] ?? [], + manufacturers: data['manufacturers'] ?? [], + userRoles: data['user_roles'] ?? [], + companyStatuses: data['company_statuses'] ?? [], + warehouseTypes: data['warehouse_types'] ?? [], + ); + } else { + // 기존 데이터의 특정 부분만 업데이트 + _lookupData = _lookupData!.copyWith( + equipmentTypes: data['equipment_types'] ?? _lookupData!.equipmentTypes, + equipmentStatuses: data['equipment_statuses'] ?? _lookupData!.equipmentStatuses, + licenseTypes: data['license_types'] ?? _lookupData!.licenseTypes, + manufacturers: data['manufacturers'] ?? _lookupData!.manufacturers, + userRoles: data['user_roles'] ?? _lookupData!.userRoles, + companyStatuses: data['company_statuses'] ?? _lookupData!.companyStatuses, + warehouseTypes: data['warehouse_types'] ?? _lookupData!.warehouseTypes, + ); + } + notifyListeners(); + } + + // 코드로 아이템 찾기 + LookupItem? findByCode(List items, String code) { + try { + return items.firstWhere((item) => item.code == code); + } catch (_) { + return null; + } + } + + // 이름으로 아이템 찾기 + LookupItem? findByName(List items, String name) { + try { + return items.firstWhere((item) => item.name == name); + } catch (_) { + return null; + } + } + + // 캐시 클리어 + void clearCache() { + _lookupData = null; + _lastFetchTime = null; + _error = null; + notifyListeners(); + } +} \ No newline at end of file diff --git a/test/api_integration_test.dart b/test/api_integration_test.dart new file mode 100644 index 0000000..250d4d4 --- /dev/null +++ b/test/api_integration_test.dart @@ -0,0 +1,65 @@ +import 'package:flutter_test/flutter_test.dart'; +import 'package:get_it/get_it.dart'; +import 'package:superport/di/injection_container.dart'; +import 'package:superport/data/datasources/remote/dashboard_remote_datasource.dart'; +import 'package:superport/data/datasources/remote/lookup_remote_datasource.dart'; +import 'package:superport/services/lookup_service.dart'; + +void main() { + setUpAll(() async { + await setupDependencies(); + }); + + tearDownAll(() { + GetIt.instance.reset(); + }); + + group('New API Integration Tests', () { + test('DashboardRemoteDataSource should have getLicenseExpirySummary method', () { + final dataSource = GetIt.instance(); + expect(dataSource, isNotNull); + + // 메서드가 존재하는지 확인 + expect(dataSource.getLicenseExpirySummary, isA()); + }); + + test('LookupRemoteDataSource should be registered', () { + final dataSource = GetIt.instance(); + expect(dataSource, isNotNull); + + // 메서드들이 존재하는지 확인 + expect(dataSource.getAllLookups, isA()); + expect(dataSource.getLookupsByType, isA()); + }); + + test('LookupService should be registered', () { + final service = GetIt.instance(); + expect(service, isNotNull); + + // 프로퍼티와 메서드 확인 + expect(service.hasData, isFalse); // 초기 상태 + expect(service.loadAllLookups, isA()); + expect(service.loadLookupsByType, isA()); + }); + + test('License expiry summary API endpoint should be callable', () async { + final dataSource = GetIt.instance(); + + // API 호출 (실제 네트워크 호출이므로 실패할 수 있음) + final result = await dataSource.getLicenseExpirySummary(); + + // Either 타입 확인 + expect(result.isLeft() || result.isRight(), isTrue); + }); + + test('Lookups API endpoint should be callable', () async { + final dataSource = GetIt.instance(); + + // API 호출 (실제 네트워크 호출이므로 실패할 수 있음) + final result = await dataSource.getAllLookups(); + + // Either 타입 확인 + expect(result.isLeft() || result.isRight(), isTrue); + }); + }); +} \ No newline at end of file