From a13c485302c03aad3678feb6150ebdc49ad757ee Mon Sep 17 00:00:00 2001 From: JiWoong Sul Date: Thu, 24 Jul 2025 15:55:05 +0900 Subject: [PATCH] =?UTF-8?q?feat:=20API=20=ED=86=B5=ED=95=A9=202=EC=B0=A8?= =?UTF-8?q?=20=EC=9E=91=EC=97=85=20=EC=99=84=EB=A3=8C?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 자동 로그인 구현: 앱 시작 시 토큰 확인 후 적절한 화면으로 라우팅 - AuthInterceptor 개선: AuthService와 통합하여 토큰 관리 일원화 - 로그아웃 기능 개선: AuthService를 사용한 API 로그아웃 처리 - 대시보드 API 연동: MockDataService에서 실제 API로 완전 전환 - Dashboard DTO 모델 생성 (OverviewStats, RecentActivity 등) - DashboardRemoteDataSource 및 DashboardService 구현 - OverviewController를 ChangeNotifier 패턴으로 개선 - OverviewScreenRedesign에 Provider 패턴 적용 - API 통합 진행 상황 문서 업데이트 --- doc/API_Integration_Plan.md | 72 +++- .../remote/dashboard_remote_datasource.dart | 132 ++++++ .../remote/interceptors/auth_interceptor.dart | 70 +-- .../equipment_status_distribution.dart | 17 + ...equipment_status_distribution.freezed.dart | 246 +++++++++++ .../equipment_status_distribution.g.dart | 25 ++ .../models/dashboard/expiring_license.dart | 19 + .../dashboard/expiring_license.freezed.dart | 291 +++++++++++++ .../models/dashboard/expiring_license.g.dart | 29 ++ lib/data/models/dashboard/overview_stats.dart | 23 + .../dashboard/overview_stats.freezed.dart | 403 ++++++++++++++++++ .../models/dashboard/overview_stats.g.dart | 35 ++ .../models/dashboard/recent_activity.dart | 19 + .../dashboard/recent_activity.freezed.dart | 291 +++++++++++++ .../models/dashboard/recent_activity.g.dart | 28 ++ lib/di/injection_container.dart | 8 + .../repositories/equipment_repository.dart | 68 +++ lib/main.dart | 26 +- lib/screens/common/app_layout_redesign.dart | 42 +- lib/screens/login/login_screen.dart | 6 +- .../login/widgets/login_view_redesign.dart | 55 ++- .../controllers/overview_controller.dart | 215 +++++++--- .../overview/overview_screen_redesign.dart | 182 +++++--- lib/services/dashboard_service.dart | 42 ++ 24 files changed, 2138 insertions(+), 206 deletions(-) create mode 100644 lib/data/datasources/remote/dashboard_remote_datasource.dart create mode 100644 lib/data/models/dashboard/equipment_status_distribution.dart create mode 100644 lib/data/models/dashboard/equipment_status_distribution.freezed.dart create mode 100644 lib/data/models/dashboard/equipment_status_distribution.g.dart create mode 100644 lib/data/models/dashboard/expiring_license.dart create mode 100644 lib/data/models/dashboard/expiring_license.freezed.dart create mode 100644 lib/data/models/dashboard/expiring_license.g.dart create mode 100644 lib/data/models/dashboard/overview_stats.dart create mode 100644 lib/data/models/dashboard/overview_stats.freezed.dart create mode 100644 lib/data/models/dashboard/overview_stats.g.dart create mode 100644 lib/data/models/dashboard/recent_activity.dart create mode 100644 lib/data/models/dashboard/recent_activity.freezed.dart create mode 100644 lib/data/models/dashboard/recent_activity.g.dart create mode 100644 lib/domain/repositories/equipment_repository.dart create mode 100644 lib/services/dashboard_service.dart diff --git a/doc/API_Integration_Plan.md b/doc/API_Integration_Plan.md index aed75ff..36e9d86 100644 --- a/doc/API_Integration_Plan.md +++ b/doc/API_Integration_Plan.md @@ -892,7 +892,9 @@ class ErrorHandler { ## 🔄 구현 진행 상황 (2025-07-24) -### 완료된 작업 +### 🎯 완료된 작업 + +#### 1차 작업 (2025-07-24 오전) 1. **Auth 관련 DTO 모델 생성** - LoginRequest, LoginResponse, TokenResponse, RefreshTokenRequest - AuthUser, LogoutRequest @@ -916,13 +918,67 @@ class ErrorHandler { - AuthRemoteDataSource, AuthService DI 등록 - GetIt을 통한 의존성 관리 -### 다음 작업 -1. API 서버 실행 및 연동 테스트 -2. 자동 로그인 구현 -3. AuthInterceptor 개선 (AuthService 사용) -4. 로그아웃 기능 UI 추가 -5. 대시보드 및 기타 화면 API 연동 +#### 2차 작업 (2025-07-24 오후) +6. **자동 로그인 구현 ✅** + - main.dart에 FutureBuilder를 사용하여 토큰 확인 + - 유효한 토큰이 있으면 홈 화면, 없으면 로그인 화면으로 라우팅 + - LoginScreen에서 로그인 성공 시 pushNamedAndRemoveUntil 사용 + +7. **AuthInterceptor 개선 ✅** + - AuthService를 DI로 주입받도록 변경 + - 토큰 가져오기, 갱신, 삭제 로직을 AuthService로 일원화 + - 401 에러 시 자동 토큰 갱신 및 재시도 로직 개선 + +8. **로그아웃 기능 개선 ✅** + - AppLayoutRedesign에 AuthService import 추가 + - 로그아웃 버튼 클릭 시 AuthService.logout() 호출 + - 로딩 다이얼로그 및 에러 처리 추가 + +9. **대시보드 API 연동 ✅** + - **DTO 모델 생성**: OverviewStats, RecentActivity, EquipmentStatusDistribution, ExpiringLicense + - **DashboardRemoteDataSource 구현**: 모든 API 엔드포인트 연동 + - **DashboardService 구현**: 비즈니스 로직 처리 + - **OverviewController 개선**: ChangeNotifier 패턴으로 변경, API 사용 + - **OverviewScreenRedesign 수정**: Provider 패턴 적용, 로딩/에러 상태 처리 + - **DI 등록**: DashboardRemoteDataSource, DashboardService 등록 + +10. **API 서버 설정 ✅** + - .env 파일 생성 및 환경 변수 설정 + - JWT 비밀키 및 데이터베이스 연결 정보 설정 + +### 📦 다음 작업 +1. **API 서버 실행 및 테스트** + - Docker Compose로 PostgreSQL, Redis 실행 + - cargo run으로 API 서버 실행 + - Flutter 앱과 연동 테스트 + +2. **장비 관리 API 연동** + - EquipmentDTO 모델 생성 + - EquipmentRemoteDataSource 구현 + - EquipmentService 생성 + - 장비 목록/상세/입고/출고 화면 API 연동 + +3. **회사/사용자 관리 API 연동** + - CompanyService, UserService 구현 + - 각 화면 API 연동 + +4. **성능 최적화** + - 캐싱 전략 구현 + - 페이지네이션 및 무한 스크롤 + - 이미지 로딩 최적화 + +### 📈 진행률 +- **전체 API 통합**: 30% 완료 +- **인증 시스템**: 100% 완료 +- **대시보드**: 100% 완료 +- **장비 관리**: 0% (대기 중) +- **회사/사용자 관리**: 0% (대기 중) + +### 📋 주요 부분 +- **한글 입력**: 모든 API 요청/응답에서 UTF-8 인코딩 적용 +- **사이드 이펙트 방지**: MockDataService와 API 서비스 공존 가능 +- **에러 처리**: 네트워크 오류, 서버 오류, 인증 오류 분리 처리 --- -_마지막 업데이트: 2025-07-24_ (인증 시스템 구현 완료) \ No newline at end of file +_마지막 업데이트: 2025-07-24 오후_ (자동 로그인, AuthInterceptor 개선, 로그아웃 기능, 대시보드 API 연동 완료) \ 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 new file mode 100644 index 0000000..ebbab20 --- /dev/null +++ b/lib/data/datasources/remote/dashboard_remote_datasource.dart @@ -0,0 +1,132 @@ +import 'package:dartz/dartz.dart'; +import 'package:dio/dio.dart'; +import 'package:injectable/injectable.dart'; +import 'package:superport/core/constants/api_endpoints.dart'; +import 'package:superport/core/errors/exceptions.dart'; +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/overview_stats.dart'; +import 'package:superport/data/models/dashboard/recent_activity.dart'; + +abstract class DashboardRemoteDataSource { + Future> getOverviewStats(); + Future>> getRecentActivities(); + Future> getEquipmentStatusDistribution(); + Future>> getExpiringLicenses({int days = 30}); +} + +@LazySingleton(as: DashboardRemoteDataSource) +class DashboardRemoteDataSourceImpl implements DashboardRemoteDataSource { + final ApiClient _apiClient; + + DashboardRemoteDataSourceImpl(this._apiClient); + + @override + Future> getOverviewStats() async { + try { + final response = await _apiClient.get('/overview/stats'); + + if (response.data != null) { + final stats = OverviewStats.fromJson(response.data); + return Right(stats); + } else { + return Left(ServerFailure('응답 데이터가 없습니다')); + } + } on DioException catch (e) { + return Left(_handleDioError(e)); + } catch (e) { + return Left(ServerFailure('통계 데이터를 가져오는 중 오류가 발생했습니다')); + } + } + + @override + Future>> getRecentActivities() async { + try { + final response = await _apiClient.get('/overview/recent-activities'); + + if (response.data != null && response.data is List) { + final activities = (response.data as List) + .map((json) => RecentActivity.fromJson(json)) + .toList(); + return Right(activities); + } else { + return Left(ServerFailure('응답 데이터가 올바르지 않습니다')); + } + } on DioException catch (e) { + return Left(_handleDioError(e)); + } catch (e) { + return Left(ServerFailure('최근 활동을 가져오는 중 오류가 발생했습니다')); + } + } + + @override + Future> getEquipmentStatusDistribution() async { + try { + final response = await _apiClient.get('/equipment/status-distribution'); + + if (response.data != null) { + final distribution = EquipmentStatusDistribution.fromJson(response.data); + return Right(distribution); + } else { + return Left(ServerFailure('응답 데이터가 없습니다')); + } + } on DioException catch (e) { + return Left(_handleDioError(e)); + } catch (e) { + return Left(ServerFailure('장비 상태 분포를 가져오는 중 오류가 발생했습니다')); + } + } + + @override + Future>> getExpiringLicenses({int days = 30}) async { + try { + final response = await _apiClient.get( + '/licenses/expiring-soon', + queryParameters: {'days': days}, + ); + + if (response.data != null && response.data is List) { + final licenses = (response.data as List) + .map((json) => ExpiringLicense.fromJson(json)) + .toList(); + return Right(licenses); + } else { + return Left(ServerFailure('응답 데이터가 올바르지 않습니다')); + } + } on DioException catch (e) { + return Left(_handleDioError(e)); + } catch (e) { + return Left(ServerFailure('만료 예정 라이선스를 가져오는 중 오류가 발생했습니다')); + } + } + + Failure _handleDioError(DioException error) { + switch (error.type) { + case DioExceptionType.connectionTimeout: + case DioExceptionType.sendTimeout: + case DioExceptionType.receiveTimeout: + return NetworkFailure('네트워크 연결 시간이 초과되었습니다'); + case DioExceptionType.connectionError: + return NetworkFailure('서버에 연결할 수 없습니다'); + case DioExceptionType.badResponse: + final statusCode = error.response?.statusCode ?? 0; + final message = error.response?.data?['message'] ?? '서버 오류가 발생했습니다'; + + if (statusCode == 401) { + return AuthFailure('인증이 만료되었습니다'); + } else if (statusCode == 403) { + return AuthFailure('접근 권한이 없습니다'); + } else if (statusCode >= 400 && statusCode < 500) { + return ServerFailure(message); + } else { + return ServerFailure('서버 오류가 발생했습니다 ($statusCode)'); + } + case DioExceptionType.cancel: + return ServerFailure('요청이 취소되었습니다'); + default: + return ServerFailure('알 수 없는 오류가 발생했습니다'); + } + } +} \ No newline at end of file diff --git a/lib/data/datasources/remote/interceptors/auth_interceptor.dart b/lib/data/datasources/remote/interceptors/auth_interceptor.dart index 24f7256..8bd35ef 100644 --- a/lib/data/datasources/remote/interceptors/auth_interceptor.dart +++ b/lib/data/datasources/remote/interceptors/auth_interceptor.dart @@ -1,11 +1,15 @@ import 'package:dio/dio.dart'; -import 'package:flutter_secure_storage/flutter_secure_storage.dart'; -import '../../../../core/constants/app_constants.dart'; +import 'package:get_it/get_it.dart'; import '../../../../core/constants/api_endpoints.dart'; +import '../../../../services/auth_service.dart'; /// 인증 인터셉터 class AuthInterceptor extends Interceptor { - final _storage = const FlutterSecureStorage(); + late final AuthService _authService; + + AuthInterceptor() { + _authService = GetIt.instance(); + } @override void onRequest( @@ -19,7 +23,7 @@ class AuthInterceptor extends Interceptor { } // 저장된 액세스 토큰 가져오기 - final accessToken = await _storage.read(key: AppConstants.accessTokenKey); + final accessToken = await _authService.getAccessToken(); if (accessToken != null) { options.headers['Authorization'] = 'Bearer $accessToken'; @@ -36,12 +40,17 @@ class AuthInterceptor extends Interceptor { // 401 Unauthorized 에러 처리 if (err.response?.statusCode == 401) { // 토큰 갱신 시도 - final refreshSuccess = await _refreshToken(); + final refreshResult = await _authService.refreshToken(); + + final refreshSuccess = refreshResult.fold( + (failure) => false, + (tokenResponse) => true, + ); if (refreshSuccess) { // 새로운 토큰으로 원래 요청 재시도 try { - final newAccessToken = await _storage.read(key: AppConstants.accessTokenKey); + final newAccessToken = await _authService.getAccessToken(); if (newAccessToken != null) { err.requestOptions.headers['Authorization'] = 'Bearer $newAccessToken'; @@ -58,60 +67,13 @@ class AuthInterceptor extends Interceptor { } // 토큰 갱신 실패 시 로그인 화면으로 이동 - await _clearTokens(); + await _authService.clearSession(); // TODO: Navigate to login screen } handler.next(err); } - /// 토큰 갱신 - Future _refreshToken() async { - try { - final refreshToken = await _storage.read(key: AppConstants.refreshTokenKey); - - if (refreshToken == null) { - return false; - } - - final dio = Dio(); - final response = await dio.post( - '${dio.options.baseUrl}${ApiEndpoints.refresh}', - data: { - 'refresh_token': refreshToken, - }, - ); - - if (response.statusCode == 200 && response.data != null) { - final data = response.data; - - // 새로운 토큰 저장 - await _storage.write( - key: AppConstants.accessTokenKey, - value: data['access_token'], - ); - - if (data['refresh_token'] != null) { - await _storage.write( - key: AppConstants.refreshTokenKey, - value: data['refresh_token'], - ); - } - - return true; - } - - return false; - } catch (e) { - return false; - } - } - - /// 토큰 삭제 - Future _clearTokens() async { - await _storage.delete(key: AppConstants.accessTokenKey); - await _storage.delete(key: AppConstants.refreshTokenKey); - } /// 인증 관련 엔드포인트 확인 bool _isAuthEndpoint(String path) { diff --git a/lib/data/models/dashboard/equipment_status_distribution.dart b/lib/data/models/dashboard/equipment_status_distribution.dart new file mode 100644 index 0000000..3193342 --- /dev/null +++ b/lib/data/models/dashboard/equipment_status_distribution.dart @@ -0,0 +1,17 @@ +import 'package:freezed_annotation/freezed_annotation.dart'; + +part 'equipment_status_distribution.freezed.dart'; +part 'equipment_status_distribution.g.dart'; + +@freezed +class EquipmentStatusDistribution with _$EquipmentStatusDistribution { + const factory EquipmentStatusDistribution({ + required int available, + @JsonKey(name: 'in_use') required int inUse, + required int maintenance, + required int disposed, + }) = _EquipmentStatusDistribution; + + factory EquipmentStatusDistribution.fromJson(Map json) => + _$EquipmentStatusDistributionFromJson(json); +} \ No newline at end of file diff --git a/lib/data/models/dashboard/equipment_status_distribution.freezed.dart b/lib/data/models/dashboard/equipment_status_distribution.freezed.dart new file mode 100644 index 0000000..8d3d5c7 --- /dev/null +++ b/lib/data/models/dashboard/equipment_status_distribution.freezed.dart @@ -0,0 +1,246 @@ +// 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 'equipment_status_distribution.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'); + +EquipmentStatusDistribution _$EquipmentStatusDistributionFromJson( + Map json) { + return _EquipmentStatusDistribution.fromJson(json); +} + +/// @nodoc +mixin _$EquipmentStatusDistribution { + int get available => throw _privateConstructorUsedError; + @JsonKey(name: 'in_use') + int get inUse => throw _privateConstructorUsedError; + int get maintenance => throw _privateConstructorUsedError; + int get disposed => throw _privateConstructorUsedError; + + /// Serializes this EquipmentStatusDistribution to a JSON map. + Map toJson() => throw _privateConstructorUsedError; + + /// Create a copy of EquipmentStatusDistribution + /// with the given fields replaced by the non-null parameter values. + @JsonKey(includeFromJson: false, includeToJson: false) + $EquipmentStatusDistributionCopyWith + get copyWith => throw _privateConstructorUsedError; +} + +/// @nodoc +abstract class $EquipmentStatusDistributionCopyWith<$Res> { + factory $EquipmentStatusDistributionCopyWith( + EquipmentStatusDistribution value, + $Res Function(EquipmentStatusDistribution) then) = + _$EquipmentStatusDistributionCopyWithImpl<$Res, + EquipmentStatusDistribution>; + @useResult + $Res call( + {int available, + @JsonKey(name: 'in_use') int inUse, + int maintenance, + int disposed}); +} + +/// @nodoc +class _$EquipmentStatusDistributionCopyWithImpl<$Res, + $Val extends EquipmentStatusDistribution> + implements $EquipmentStatusDistributionCopyWith<$Res> { + _$EquipmentStatusDistributionCopyWithImpl(this._value, this._then); + + // ignore: unused_field + final $Val _value; + // ignore: unused_field + final $Res Function($Val) _then; + + /// Create a copy of EquipmentStatusDistribution + /// with the given fields replaced by the non-null parameter values. + @pragma('vm:prefer-inline') + @override + $Res call({ + Object? available = null, + Object? inUse = null, + Object? maintenance = null, + Object? disposed = null, + }) { + return _then(_value.copyWith( + available: null == available + ? _value.available + : available // ignore: cast_nullable_to_non_nullable + as int, + inUse: null == inUse + ? _value.inUse + : inUse // ignore: cast_nullable_to_non_nullable + as int, + maintenance: null == maintenance + ? _value.maintenance + : maintenance // ignore: cast_nullable_to_non_nullable + as int, + disposed: null == disposed + ? _value.disposed + : disposed // ignore: cast_nullable_to_non_nullable + as int, + ) as $Val); + } +} + +/// @nodoc +abstract class _$$EquipmentStatusDistributionImplCopyWith<$Res> + implements $EquipmentStatusDistributionCopyWith<$Res> { + factory _$$EquipmentStatusDistributionImplCopyWith( + _$EquipmentStatusDistributionImpl value, + $Res Function(_$EquipmentStatusDistributionImpl) then) = + __$$EquipmentStatusDistributionImplCopyWithImpl<$Res>; + @override + @useResult + $Res call( + {int available, + @JsonKey(name: 'in_use') int inUse, + int maintenance, + int disposed}); +} + +/// @nodoc +class __$$EquipmentStatusDistributionImplCopyWithImpl<$Res> + extends _$EquipmentStatusDistributionCopyWithImpl<$Res, + _$EquipmentStatusDistributionImpl> + implements _$$EquipmentStatusDistributionImplCopyWith<$Res> { + __$$EquipmentStatusDistributionImplCopyWithImpl( + _$EquipmentStatusDistributionImpl _value, + $Res Function(_$EquipmentStatusDistributionImpl) _then) + : super(_value, _then); + + /// Create a copy of EquipmentStatusDistribution + /// with the given fields replaced by the non-null parameter values. + @pragma('vm:prefer-inline') + @override + $Res call({ + Object? available = null, + Object? inUse = null, + Object? maintenance = null, + Object? disposed = null, + }) { + return _then(_$EquipmentStatusDistributionImpl( + available: null == available + ? _value.available + : available // ignore: cast_nullable_to_non_nullable + as int, + inUse: null == inUse + ? _value.inUse + : inUse // ignore: cast_nullable_to_non_nullable + as int, + maintenance: null == maintenance + ? _value.maintenance + : maintenance // ignore: cast_nullable_to_non_nullable + as int, + disposed: null == disposed + ? _value.disposed + : disposed // ignore: cast_nullable_to_non_nullable + as int, + )); + } +} + +/// @nodoc +@JsonSerializable() +class _$EquipmentStatusDistributionImpl + implements _EquipmentStatusDistribution { + const _$EquipmentStatusDistributionImpl( + {required this.available, + @JsonKey(name: 'in_use') required this.inUse, + required this.maintenance, + required this.disposed}); + + factory _$EquipmentStatusDistributionImpl.fromJson( + Map json) => + _$$EquipmentStatusDistributionImplFromJson(json); + + @override + final int available; + @override + @JsonKey(name: 'in_use') + final int inUse; + @override + final int maintenance; + @override + final int disposed; + + @override + String toString() { + return 'EquipmentStatusDistribution(available: $available, inUse: $inUse, maintenance: $maintenance, disposed: $disposed)'; + } + + @override + bool operator ==(Object other) { + return identical(this, other) || + (other.runtimeType == runtimeType && + other is _$EquipmentStatusDistributionImpl && + (identical(other.available, available) || + other.available == available) && + (identical(other.inUse, inUse) || other.inUse == inUse) && + (identical(other.maintenance, maintenance) || + other.maintenance == maintenance) && + (identical(other.disposed, disposed) || + other.disposed == disposed)); + } + + @JsonKey(includeFromJson: false, includeToJson: false) + @override + int get hashCode => + Object.hash(runtimeType, available, inUse, maintenance, disposed); + + /// Create a copy of EquipmentStatusDistribution + /// with the given fields replaced by the non-null parameter values. + @JsonKey(includeFromJson: false, includeToJson: false) + @override + @pragma('vm:prefer-inline') + _$$EquipmentStatusDistributionImplCopyWith<_$EquipmentStatusDistributionImpl> + get copyWith => __$$EquipmentStatusDistributionImplCopyWithImpl< + _$EquipmentStatusDistributionImpl>(this, _$identity); + + @override + Map toJson() { + return _$$EquipmentStatusDistributionImplToJson( + this, + ); + } +} + +abstract class _EquipmentStatusDistribution + implements EquipmentStatusDistribution { + const factory _EquipmentStatusDistribution( + {required final int available, + @JsonKey(name: 'in_use') required final int inUse, + required final int maintenance, + required final int disposed}) = _$EquipmentStatusDistributionImpl; + + factory _EquipmentStatusDistribution.fromJson(Map json) = + _$EquipmentStatusDistributionImpl.fromJson; + + @override + int get available; + @override + @JsonKey(name: 'in_use') + int get inUse; + @override + int get maintenance; + @override + int get disposed; + + /// Create a copy of EquipmentStatusDistribution + /// with the given fields replaced by the non-null parameter values. + @override + @JsonKey(includeFromJson: false, includeToJson: false) + _$$EquipmentStatusDistributionImplCopyWith<_$EquipmentStatusDistributionImpl> + get copyWith => throw _privateConstructorUsedError; +} diff --git a/lib/data/models/dashboard/equipment_status_distribution.g.dart b/lib/data/models/dashboard/equipment_status_distribution.g.dart new file mode 100644 index 0000000..1b10aa5 --- /dev/null +++ b/lib/data/models/dashboard/equipment_status_distribution.g.dart @@ -0,0 +1,25 @@ +// GENERATED CODE - DO NOT MODIFY BY HAND + +part of 'equipment_status_distribution.dart'; + +// ************************************************************************** +// JsonSerializableGenerator +// ************************************************************************** + +_$EquipmentStatusDistributionImpl _$$EquipmentStatusDistributionImplFromJson( + Map json) => + _$EquipmentStatusDistributionImpl( + available: (json['available'] as num).toInt(), + inUse: (json['in_use'] as num).toInt(), + maintenance: (json['maintenance'] as num).toInt(), + disposed: (json['disposed'] as num).toInt(), + ); + +Map _$$EquipmentStatusDistributionImplToJson( + _$EquipmentStatusDistributionImpl instance) => + { + 'available': instance.available, + 'in_use': instance.inUse, + 'maintenance': instance.maintenance, + 'disposed': instance.disposed, + }; diff --git a/lib/data/models/dashboard/expiring_license.dart b/lib/data/models/dashboard/expiring_license.dart new file mode 100644 index 0000000..15d486d --- /dev/null +++ b/lib/data/models/dashboard/expiring_license.dart @@ -0,0 +1,19 @@ +import 'package:freezed_annotation/freezed_annotation.dart'; + +part 'expiring_license.freezed.dart'; +part 'expiring_license.g.dart'; + +@freezed +class ExpiringLicense with _$ExpiringLicense { + const factory ExpiringLicense({ + required int id, + @JsonKey(name: 'license_name') required String licenseName, + @JsonKey(name: 'company_name') required String companyName, + @JsonKey(name: 'expiry_date') required DateTime expiryDate, + @JsonKey(name: 'days_remaining') required int daysRemaining, + @JsonKey(name: 'license_type') required String licenseType, + }) = _ExpiringLicense; + + factory ExpiringLicense.fromJson(Map json) => + _$ExpiringLicenseFromJson(json); +} \ No newline at end of file diff --git a/lib/data/models/dashboard/expiring_license.freezed.dart b/lib/data/models/dashboard/expiring_license.freezed.dart new file mode 100644 index 0000000..19fd0ce --- /dev/null +++ b/lib/data/models/dashboard/expiring_license.freezed.dart @@ -0,0 +1,291 @@ +// 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 'expiring_license.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'); + +ExpiringLicense _$ExpiringLicenseFromJson(Map json) { + return _ExpiringLicense.fromJson(json); +} + +/// @nodoc +mixin _$ExpiringLicense { + int get id => throw _privateConstructorUsedError; + @JsonKey(name: 'license_name') + String get licenseName => throw _privateConstructorUsedError; + @JsonKey(name: 'company_name') + String get companyName => throw _privateConstructorUsedError; + @JsonKey(name: 'expiry_date') + DateTime get expiryDate => throw _privateConstructorUsedError; + @JsonKey(name: 'days_remaining') + int get daysRemaining => throw _privateConstructorUsedError; + @JsonKey(name: 'license_type') + String get licenseType => throw _privateConstructorUsedError; + + /// Serializes this ExpiringLicense to a JSON map. + Map toJson() => throw _privateConstructorUsedError; + + /// Create a copy of ExpiringLicense + /// with the given fields replaced by the non-null parameter values. + @JsonKey(includeFromJson: false, includeToJson: false) + $ExpiringLicenseCopyWith get copyWith => + throw _privateConstructorUsedError; +} + +/// @nodoc +abstract class $ExpiringLicenseCopyWith<$Res> { + factory $ExpiringLicenseCopyWith( + ExpiringLicense value, $Res Function(ExpiringLicense) then) = + _$ExpiringLicenseCopyWithImpl<$Res, ExpiringLicense>; + @useResult + $Res call( + {int id, + @JsonKey(name: 'license_name') String licenseName, + @JsonKey(name: 'company_name') String companyName, + @JsonKey(name: 'expiry_date') DateTime expiryDate, + @JsonKey(name: 'days_remaining') int daysRemaining, + @JsonKey(name: 'license_type') String licenseType}); +} + +/// @nodoc +class _$ExpiringLicenseCopyWithImpl<$Res, $Val extends ExpiringLicense> + implements $ExpiringLicenseCopyWith<$Res> { + _$ExpiringLicenseCopyWithImpl(this._value, this._then); + + // ignore: unused_field + final $Val _value; + // ignore: unused_field + final $Res Function($Val) _then; + + /// Create a copy of ExpiringLicense + /// with the given fields replaced by the non-null parameter values. + @pragma('vm:prefer-inline') + @override + $Res call({ + Object? id = null, + Object? licenseName = null, + Object? companyName = null, + Object? expiryDate = null, + Object? daysRemaining = null, + Object? licenseType = null, + }) { + return _then(_value.copyWith( + id: null == id + ? _value.id + : id // ignore: cast_nullable_to_non_nullable + as int, + licenseName: null == licenseName + ? _value.licenseName + : licenseName // ignore: cast_nullable_to_non_nullable + as String, + companyName: null == companyName + ? _value.companyName + : companyName // ignore: cast_nullable_to_non_nullable + as String, + expiryDate: null == expiryDate + ? _value.expiryDate + : expiryDate // ignore: cast_nullable_to_non_nullable + as DateTime, + daysRemaining: null == daysRemaining + ? _value.daysRemaining + : daysRemaining // ignore: cast_nullable_to_non_nullable + as int, + licenseType: null == licenseType + ? _value.licenseType + : licenseType // ignore: cast_nullable_to_non_nullable + as String, + ) as $Val); + } +} + +/// @nodoc +abstract class _$$ExpiringLicenseImplCopyWith<$Res> + implements $ExpiringLicenseCopyWith<$Res> { + factory _$$ExpiringLicenseImplCopyWith(_$ExpiringLicenseImpl value, + $Res Function(_$ExpiringLicenseImpl) then) = + __$$ExpiringLicenseImplCopyWithImpl<$Res>; + @override + @useResult + $Res call( + {int id, + @JsonKey(name: 'license_name') String licenseName, + @JsonKey(name: 'company_name') String companyName, + @JsonKey(name: 'expiry_date') DateTime expiryDate, + @JsonKey(name: 'days_remaining') int daysRemaining, + @JsonKey(name: 'license_type') String licenseType}); +} + +/// @nodoc +class __$$ExpiringLicenseImplCopyWithImpl<$Res> + extends _$ExpiringLicenseCopyWithImpl<$Res, _$ExpiringLicenseImpl> + implements _$$ExpiringLicenseImplCopyWith<$Res> { + __$$ExpiringLicenseImplCopyWithImpl( + _$ExpiringLicenseImpl _value, $Res Function(_$ExpiringLicenseImpl) _then) + : super(_value, _then); + + /// Create a copy of ExpiringLicense + /// with the given fields replaced by the non-null parameter values. + @pragma('vm:prefer-inline') + @override + $Res call({ + Object? id = null, + Object? licenseName = null, + Object? companyName = null, + Object? expiryDate = null, + Object? daysRemaining = null, + Object? licenseType = null, + }) { + return _then(_$ExpiringLicenseImpl( + id: null == id + ? _value.id + : id // ignore: cast_nullable_to_non_nullable + as int, + licenseName: null == licenseName + ? _value.licenseName + : licenseName // ignore: cast_nullable_to_non_nullable + as String, + companyName: null == companyName + ? _value.companyName + : companyName // ignore: cast_nullable_to_non_nullable + as String, + expiryDate: null == expiryDate + ? _value.expiryDate + : expiryDate // ignore: cast_nullable_to_non_nullable + as DateTime, + daysRemaining: null == daysRemaining + ? _value.daysRemaining + : daysRemaining // ignore: cast_nullable_to_non_nullable + as int, + licenseType: null == licenseType + ? _value.licenseType + : licenseType // ignore: cast_nullable_to_non_nullable + as String, + )); + } +} + +/// @nodoc +@JsonSerializable() +class _$ExpiringLicenseImpl implements _ExpiringLicense { + const _$ExpiringLicenseImpl( + {required this.id, + @JsonKey(name: 'license_name') required this.licenseName, + @JsonKey(name: 'company_name') required this.companyName, + @JsonKey(name: 'expiry_date') required this.expiryDate, + @JsonKey(name: 'days_remaining') required this.daysRemaining, + @JsonKey(name: 'license_type') required this.licenseType}); + + factory _$ExpiringLicenseImpl.fromJson(Map json) => + _$$ExpiringLicenseImplFromJson(json); + + @override + final int id; + @override + @JsonKey(name: 'license_name') + final String licenseName; + @override + @JsonKey(name: 'company_name') + final String companyName; + @override + @JsonKey(name: 'expiry_date') + final DateTime expiryDate; + @override + @JsonKey(name: 'days_remaining') + final int daysRemaining; + @override + @JsonKey(name: 'license_type') + final String licenseType; + + @override + String toString() { + return 'ExpiringLicense(id: $id, licenseName: $licenseName, companyName: $companyName, expiryDate: $expiryDate, daysRemaining: $daysRemaining, licenseType: $licenseType)'; + } + + @override + bool operator ==(Object other) { + return identical(this, other) || + (other.runtimeType == runtimeType && + other is _$ExpiringLicenseImpl && + (identical(other.id, id) || other.id == id) && + (identical(other.licenseName, licenseName) || + other.licenseName == licenseName) && + (identical(other.companyName, companyName) || + other.companyName == companyName) && + (identical(other.expiryDate, expiryDate) || + other.expiryDate == expiryDate) && + (identical(other.daysRemaining, daysRemaining) || + other.daysRemaining == daysRemaining) && + (identical(other.licenseType, licenseType) || + other.licenseType == licenseType)); + } + + @JsonKey(includeFromJson: false, includeToJson: false) + @override + int get hashCode => Object.hash(runtimeType, id, licenseName, companyName, + expiryDate, daysRemaining, licenseType); + + /// Create a copy of ExpiringLicense + /// with the given fields replaced by the non-null parameter values. + @JsonKey(includeFromJson: false, includeToJson: false) + @override + @pragma('vm:prefer-inline') + _$$ExpiringLicenseImplCopyWith<_$ExpiringLicenseImpl> get copyWith => + __$$ExpiringLicenseImplCopyWithImpl<_$ExpiringLicenseImpl>( + this, _$identity); + + @override + Map toJson() { + return _$$ExpiringLicenseImplToJson( + this, + ); + } +} + +abstract class _ExpiringLicense implements ExpiringLicense { + const factory _ExpiringLicense( + {required final int id, + @JsonKey(name: 'license_name') required final String licenseName, + @JsonKey(name: 'company_name') required final String companyName, + @JsonKey(name: 'expiry_date') required final DateTime expiryDate, + @JsonKey(name: 'days_remaining') required final int daysRemaining, + @JsonKey(name: 'license_type') required final String licenseType}) = + _$ExpiringLicenseImpl; + + factory _ExpiringLicense.fromJson(Map json) = + _$ExpiringLicenseImpl.fromJson; + + @override + int get id; + @override + @JsonKey(name: 'license_name') + String get licenseName; + @override + @JsonKey(name: 'company_name') + String get companyName; + @override + @JsonKey(name: 'expiry_date') + DateTime get expiryDate; + @override + @JsonKey(name: 'days_remaining') + int get daysRemaining; + @override + @JsonKey(name: 'license_type') + String get licenseType; + + /// Create a copy of ExpiringLicense + /// with the given fields replaced by the non-null parameter values. + @override + @JsonKey(includeFromJson: false, includeToJson: false) + _$$ExpiringLicenseImplCopyWith<_$ExpiringLicenseImpl> get copyWith => + throw _privateConstructorUsedError; +} diff --git a/lib/data/models/dashboard/expiring_license.g.dart b/lib/data/models/dashboard/expiring_license.g.dart new file mode 100644 index 0000000..924ccf8 --- /dev/null +++ b/lib/data/models/dashboard/expiring_license.g.dart @@ -0,0 +1,29 @@ +// GENERATED CODE - DO NOT MODIFY BY HAND + +part of 'expiring_license.dart'; + +// ************************************************************************** +// JsonSerializableGenerator +// ************************************************************************** + +_$ExpiringLicenseImpl _$$ExpiringLicenseImplFromJson( + Map json) => + _$ExpiringLicenseImpl( + id: (json['id'] as num).toInt(), + licenseName: json['license_name'] as String, + companyName: json['company_name'] as String, + expiryDate: DateTime.parse(json['expiry_date'] as String), + daysRemaining: (json['days_remaining'] as num).toInt(), + licenseType: json['license_type'] as String, + ); + +Map _$$ExpiringLicenseImplToJson( + _$ExpiringLicenseImpl instance) => + { + 'id': instance.id, + 'license_name': instance.licenseName, + 'company_name': instance.companyName, + 'expiry_date': instance.expiryDate.toIso8601String(), + 'days_remaining': instance.daysRemaining, + 'license_type': instance.licenseType, + }; diff --git a/lib/data/models/dashboard/overview_stats.dart b/lib/data/models/dashboard/overview_stats.dart new file mode 100644 index 0000000..698be86 --- /dev/null +++ b/lib/data/models/dashboard/overview_stats.dart @@ -0,0 +1,23 @@ +import 'package:freezed_annotation/freezed_annotation.dart'; + +part 'overview_stats.freezed.dart'; +part 'overview_stats.g.dart'; + +@freezed +class OverviewStats with _$OverviewStats { + const factory OverviewStats({ + @JsonKey(name: 'total_equipment') required int totalEquipment, + @JsonKey(name: 'available_equipment') required int availableEquipment, + @JsonKey(name: 'in_use_equipment') required int inUseEquipment, + @JsonKey(name: 'maintenance_equipment') required int maintenanceEquipment, + @JsonKey(name: 'total_companies') required int totalCompanies, + @JsonKey(name: 'total_users') required int totalUsers, + @JsonKey(name: 'active_licenses') required int activeLicenses, + @JsonKey(name: 'expiring_licenses') required int expiringLicenses, + @JsonKey(name: 'total_rentals') required int totalRentals, + @JsonKey(name: 'active_rentals') required int activeRentals, + }) = _OverviewStats; + + factory OverviewStats.fromJson(Map json) => + _$OverviewStatsFromJson(json); +} \ No newline at end of file diff --git a/lib/data/models/dashboard/overview_stats.freezed.dart b/lib/data/models/dashboard/overview_stats.freezed.dart new file mode 100644 index 0000000..24e1cea --- /dev/null +++ b/lib/data/models/dashboard/overview_stats.freezed.dart @@ -0,0 +1,403 @@ +// 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 'overview_stats.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'); + +OverviewStats _$OverviewStatsFromJson(Map json) { + return _OverviewStats.fromJson(json); +} + +/// @nodoc +mixin _$OverviewStats { + @JsonKey(name: 'total_equipment') + int get totalEquipment => throw _privateConstructorUsedError; + @JsonKey(name: 'available_equipment') + int get availableEquipment => throw _privateConstructorUsedError; + @JsonKey(name: 'in_use_equipment') + int get inUseEquipment => throw _privateConstructorUsedError; + @JsonKey(name: 'maintenance_equipment') + int get maintenanceEquipment => throw _privateConstructorUsedError; + @JsonKey(name: 'total_companies') + int get totalCompanies => throw _privateConstructorUsedError; + @JsonKey(name: 'total_users') + int get totalUsers => throw _privateConstructorUsedError; + @JsonKey(name: 'active_licenses') + int get activeLicenses => throw _privateConstructorUsedError; + @JsonKey(name: 'expiring_licenses') + int get expiringLicenses => throw _privateConstructorUsedError; + @JsonKey(name: 'total_rentals') + int get totalRentals => throw _privateConstructorUsedError; + @JsonKey(name: 'active_rentals') + int get activeRentals => throw _privateConstructorUsedError; + + /// Serializes this OverviewStats to a JSON map. + Map toJson() => throw _privateConstructorUsedError; + + /// Create a copy of OverviewStats + /// with the given fields replaced by the non-null parameter values. + @JsonKey(includeFromJson: false, includeToJson: false) + $OverviewStatsCopyWith get copyWith => + throw _privateConstructorUsedError; +} + +/// @nodoc +abstract class $OverviewStatsCopyWith<$Res> { + factory $OverviewStatsCopyWith( + OverviewStats value, $Res Function(OverviewStats) then) = + _$OverviewStatsCopyWithImpl<$Res, OverviewStats>; + @useResult + $Res call( + {@JsonKey(name: 'total_equipment') int totalEquipment, + @JsonKey(name: 'available_equipment') int availableEquipment, + @JsonKey(name: 'in_use_equipment') int inUseEquipment, + @JsonKey(name: 'maintenance_equipment') int maintenanceEquipment, + @JsonKey(name: 'total_companies') int totalCompanies, + @JsonKey(name: 'total_users') int totalUsers, + @JsonKey(name: 'active_licenses') int activeLicenses, + @JsonKey(name: 'expiring_licenses') int expiringLicenses, + @JsonKey(name: 'total_rentals') int totalRentals, + @JsonKey(name: 'active_rentals') int activeRentals}); +} + +/// @nodoc +class _$OverviewStatsCopyWithImpl<$Res, $Val extends OverviewStats> + implements $OverviewStatsCopyWith<$Res> { + _$OverviewStatsCopyWithImpl(this._value, this._then); + + // ignore: unused_field + final $Val _value; + // ignore: unused_field + final $Res Function($Val) _then; + + /// Create a copy of OverviewStats + /// with the given fields replaced by the non-null parameter values. + @pragma('vm:prefer-inline') + @override + $Res call({ + Object? totalEquipment = null, + Object? availableEquipment = null, + Object? inUseEquipment = null, + Object? maintenanceEquipment = null, + Object? totalCompanies = null, + Object? totalUsers = null, + Object? activeLicenses = null, + Object? expiringLicenses = null, + Object? totalRentals = null, + Object? activeRentals = null, + }) { + return _then(_value.copyWith( + totalEquipment: null == totalEquipment + ? _value.totalEquipment + : totalEquipment // ignore: cast_nullable_to_non_nullable + as int, + availableEquipment: null == availableEquipment + ? _value.availableEquipment + : availableEquipment // ignore: cast_nullable_to_non_nullable + as int, + inUseEquipment: null == inUseEquipment + ? _value.inUseEquipment + : inUseEquipment // ignore: cast_nullable_to_non_nullable + as int, + maintenanceEquipment: null == maintenanceEquipment + ? _value.maintenanceEquipment + : maintenanceEquipment // ignore: cast_nullable_to_non_nullable + as int, + totalCompanies: null == totalCompanies + ? _value.totalCompanies + : totalCompanies // ignore: cast_nullable_to_non_nullable + as int, + totalUsers: null == totalUsers + ? _value.totalUsers + : totalUsers // ignore: cast_nullable_to_non_nullable + as int, + activeLicenses: null == activeLicenses + ? _value.activeLicenses + : activeLicenses // ignore: cast_nullable_to_non_nullable + as int, + expiringLicenses: null == expiringLicenses + ? _value.expiringLicenses + : expiringLicenses // ignore: cast_nullable_to_non_nullable + as int, + totalRentals: null == totalRentals + ? _value.totalRentals + : totalRentals // ignore: cast_nullable_to_non_nullable + as int, + activeRentals: null == activeRentals + ? _value.activeRentals + : activeRentals // ignore: cast_nullable_to_non_nullable + as int, + ) as $Val); + } +} + +/// @nodoc +abstract class _$$OverviewStatsImplCopyWith<$Res> + implements $OverviewStatsCopyWith<$Res> { + factory _$$OverviewStatsImplCopyWith( + _$OverviewStatsImpl value, $Res Function(_$OverviewStatsImpl) then) = + __$$OverviewStatsImplCopyWithImpl<$Res>; + @override + @useResult + $Res call( + {@JsonKey(name: 'total_equipment') int totalEquipment, + @JsonKey(name: 'available_equipment') int availableEquipment, + @JsonKey(name: 'in_use_equipment') int inUseEquipment, + @JsonKey(name: 'maintenance_equipment') int maintenanceEquipment, + @JsonKey(name: 'total_companies') int totalCompanies, + @JsonKey(name: 'total_users') int totalUsers, + @JsonKey(name: 'active_licenses') int activeLicenses, + @JsonKey(name: 'expiring_licenses') int expiringLicenses, + @JsonKey(name: 'total_rentals') int totalRentals, + @JsonKey(name: 'active_rentals') int activeRentals}); +} + +/// @nodoc +class __$$OverviewStatsImplCopyWithImpl<$Res> + extends _$OverviewStatsCopyWithImpl<$Res, _$OverviewStatsImpl> + implements _$$OverviewStatsImplCopyWith<$Res> { + __$$OverviewStatsImplCopyWithImpl( + _$OverviewStatsImpl _value, $Res Function(_$OverviewStatsImpl) _then) + : super(_value, _then); + + /// Create a copy of OverviewStats + /// with the given fields replaced by the non-null parameter values. + @pragma('vm:prefer-inline') + @override + $Res call({ + Object? totalEquipment = null, + Object? availableEquipment = null, + Object? inUseEquipment = null, + Object? maintenanceEquipment = null, + Object? totalCompanies = null, + Object? totalUsers = null, + Object? activeLicenses = null, + Object? expiringLicenses = null, + Object? totalRentals = null, + Object? activeRentals = null, + }) { + return _then(_$OverviewStatsImpl( + totalEquipment: null == totalEquipment + ? _value.totalEquipment + : totalEquipment // ignore: cast_nullable_to_non_nullable + as int, + availableEquipment: null == availableEquipment + ? _value.availableEquipment + : availableEquipment // ignore: cast_nullable_to_non_nullable + as int, + inUseEquipment: null == inUseEquipment + ? _value.inUseEquipment + : inUseEquipment // ignore: cast_nullable_to_non_nullable + as int, + maintenanceEquipment: null == maintenanceEquipment + ? _value.maintenanceEquipment + : maintenanceEquipment // ignore: cast_nullable_to_non_nullable + as int, + totalCompanies: null == totalCompanies + ? _value.totalCompanies + : totalCompanies // ignore: cast_nullable_to_non_nullable + as int, + totalUsers: null == totalUsers + ? _value.totalUsers + : totalUsers // ignore: cast_nullable_to_non_nullable + as int, + activeLicenses: null == activeLicenses + ? _value.activeLicenses + : activeLicenses // ignore: cast_nullable_to_non_nullable + as int, + expiringLicenses: null == expiringLicenses + ? _value.expiringLicenses + : expiringLicenses // ignore: cast_nullable_to_non_nullable + as int, + totalRentals: null == totalRentals + ? _value.totalRentals + : totalRentals // ignore: cast_nullable_to_non_nullable + as int, + activeRentals: null == activeRentals + ? _value.activeRentals + : activeRentals // ignore: cast_nullable_to_non_nullable + as int, + )); + } +} + +/// @nodoc +@JsonSerializable() +class _$OverviewStatsImpl implements _OverviewStats { + const _$OverviewStatsImpl( + {@JsonKey(name: 'total_equipment') required this.totalEquipment, + @JsonKey(name: 'available_equipment') required this.availableEquipment, + @JsonKey(name: 'in_use_equipment') required this.inUseEquipment, + @JsonKey(name: 'maintenance_equipment') + required this.maintenanceEquipment, + @JsonKey(name: 'total_companies') required this.totalCompanies, + @JsonKey(name: 'total_users') required this.totalUsers, + @JsonKey(name: 'active_licenses') required this.activeLicenses, + @JsonKey(name: 'expiring_licenses') required this.expiringLicenses, + @JsonKey(name: 'total_rentals') required this.totalRentals, + @JsonKey(name: 'active_rentals') required this.activeRentals}); + + factory _$OverviewStatsImpl.fromJson(Map json) => + _$$OverviewStatsImplFromJson(json); + + @override + @JsonKey(name: 'total_equipment') + final int totalEquipment; + @override + @JsonKey(name: 'available_equipment') + final int availableEquipment; + @override + @JsonKey(name: 'in_use_equipment') + final int inUseEquipment; + @override + @JsonKey(name: 'maintenance_equipment') + final int maintenanceEquipment; + @override + @JsonKey(name: 'total_companies') + final int totalCompanies; + @override + @JsonKey(name: 'total_users') + final int totalUsers; + @override + @JsonKey(name: 'active_licenses') + final int activeLicenses; + @override + @JsonKey(name: 'expiring_licenses') + final int expiringLicenses; + @override + @JsonKey(name: 'total_rentals') + final int totalRentals; + @override + @JsonKey(name: 'active_rentals') + final int activeRentals; + + @override + String toString() { + return 'OverviewStats(totalEquipment: $totalEquipment, availableEquipment: $availableEquipment, inUseEquipment: $inUseEquipment, maintenanceEquipment: $maintenanceEquipment, totalCompanies: $totalCompanies, totalUsers: $totalUsers, activeLicenses: $activeLicenses, expiringLicenses: $expiringLicenses, totalRentals: $totalRentals, activeRentals: $activeRentals)'; + } + + @override + bool operator ==(Object other) { + return identical(this, other) || + (other.runtimeType == runtimeType && + other is _$OverviewStatsImpl && + (identical(other.totalEquipment, totalEquipment) || + other.totalEquipment == totalEquipment) && + (identical(other.availableEquipment, availableEquipment) || + other.availableEquipment == availableEquipment) && + (identical(other.inUseEquipment, inUseEquipment) || + other.inUseEquipment == inUseEquipment) && + (identical(other.maintenanceEquipment, maintenanceEquipment) || + other.maintenanceEquipment == maintenanceEquipment) && + (identical(other.totalCompanies, totalCompanies) || + other.totalCompanies == totalCompanies) && + (identical(other.totalUsers, totalUsers) || + other.totalUsers == totalUsers) && + (identical(other.activeLicenses, activeLicenses) || + other.activeLicenses == activeLicenses) && + (identical(other.expiringLicenses, expiringLicenses) || + other.expiringLicenses == expiringLicenses) && + (identical(other.totalRentals, totalRentals) || + other.totalRentals == totalRentals) && + (identical(other.activeRentals, activeRentals) || + other.activeRentals == activeRentals)); + } + + @JsonKey(includeFromJson: false, includeToJson: false) + @override + int get hashCode => Object.hash( + runtimeType, + totalEquipment, + availableEquipment, + inUseEquipment, + maintenanceEquipment, + totalCompanies, + totalUsers, + activeLicenses, + expiringLicenses, + totalRentals, + activeRentals); + + /// Create a copy of OverviewStats + /// with the given fields replaced by the non-null parameter values. + @JsonKey(includeFromJson: false, includeToJson: false) + @override + @pragma('vm:prefer-inline') + _$$OverviewStatsImplCopyWith<_$OverviewStatsImpl> get copyWith => + __$$OverviewStatsImplCopyWithImpl<_$OverviewStatsImpl>(this, _$identity); + + @override + Map toJson() { + return _$$OverviewStatsImplToJson( + this, + ); + } +} + +abstract class _OverviewStats implements OverviewStats { + const factory _OverviewStats( + {@JsonKey(name: 'total_equipment') required final int totalEquipment, + @JsonKey(name: 'available_equipment') + required final int availableEquipment, + @JsonKey(name: 'in_use_equipment') required final int inUseEquipment, + @JsonKey(name: 'maintenance_equipment') + required final int maintenanceEquipment, + @JsonKey(name: 'total_companies') required final int totalCompanies, + @JsonKey(name: 'total_users') required final int totalUsers, + @JsonKey(name: 'active_licenses') required final int activeLicenses, + @JsonKey(name: 'expiring_licenses') required final int expiringLicenses, + @JsonKey(name: 'total_rentals') required final int totalRentals, + @JsonKey(name: 'active_rentals') + required final int activeRentals}) = _$OverviewStatsImpl; + + factory _OverviewStats.fromJson(Map json) = + _$OverviewStatsImpl.fromJson; + + @override + @JsonKey(name: 'total_equipment') + int get totalEquipment; + @override + @JsonKey(name: 'available_equipment') + int get availableEquipment; + @override + @JsonKey(name: 'in_use_equipment') + int get inUseEquipment; + @override + @JsonKey(name: 'maintenance_equipment') + int get maintenanceEquipment; + @override + @JsonKey(name: 'total_companies') + int get totalCompanies; + @override + @JsonKey(name: 'total_users') + int get totalUsers; + @override + @JsonKey(name: 'active_licenses') + int get activeLicenses; + @override + @JsonKey(name: 'expiring_licenses') + int get expiringLicenses; + @override + @JsonKey(name: 'total_rentals') + int get totalRentals; + @override + @JsonKey(name: 'active_rentals') + int get activeRentals; + + /// Create a copy of OverviewStats + /// with the given fields replaced by the non-null parameter values. + @override + @JsonKey(includeFromJson: false, includeToJson: false) + _$$OverviewStatsImplCopyWith<_$OverviewStatsImpl> get copyWith => + throw _privateConstructorUsedError; +} diff --git a/lib/data/models/dashboard/overview_stats.g.dart b/lib/data/models/dashboard/overview_stats.g.dart new file mode 100644 index 0000000..9a5144a --- /dev/null +++ b/lib/data/models/dashboard/overview_stats.g.dart @@ -0,0 +1,35 @@ +// GENERATED CODE - DO NOT MODIFY BY HAND + +part of 'overview_stats.dart'; + +// ************************************************************************** +// JsonSerializableGenerator +// ************************************************************************** + +_$OverviewStatsImpl _$$OverviewStatsImplFromJson(Map json) => + _$OverviewStatsImpl( + totalEquipment: (json['total_equipment'] as num).toInt(), + availableEquipment: (json['available_equipment'] as num).toInt(), + inUseEquipment: (json['in_use_equipment'] as num).toInt(), + maintenanceEquipment: (json['maintenance_equipment'] as num).toInt(), + totalCompanies: (json['total_companies'] as num).toInt(), + totalUsers: (json['total_users'] as num).toInt(), + activeLicenses: (json['active_licenses'] as num).toInt(), + expiringLicenses: (json['expiring_licenses'] as num).toInt(), + totalRentals: (json['total_rentals'] as num).toInt(), + activeRentals: (json['active_rentals'] as num).toInt(), + ); + +Map _$$OverviewStatsImplToJson(_$OverviewStatsImpl instance) => + { + 'total_equipment': instance.totalEquipment, + 'available_equipment': instance.availableEquipment, + 'in_use_equipment': instance.inUseEquipment, + 'maintenance_equipment': instance.maintenanceEquipment, + 'total_companies': instance.totalCompanies, + 'total_users': instance.totalUsers, + 'active_licenses': instance.activeLicenses, + 'expiring_licenses': instance.expiringLicenses, + 'total_rentals': instance.totalRentals, + 'active_rentals': instance.activeRentals, + }; diff --git a/lib/data/models/dashboard/recent_activity.dart b/lib/data/models/dashboard/recent_activity.dart new file mode 100644 index 0000000..e4c3e12 --- /dev/null +++ b/lib/data/models/dashboard/recent_activity.dart @@ -0,0 +1,19 @@ +import 'package:freezed_annotation/freezed_annotation.dart'; + +part 'recent_activity.freezed.dart'; +part 'recent_activity.g.dart'; + +@freezed +class RecentActivity with _$RecentActivity { + const factory RecentActivity({ + required int id, + @JsonKey(name: 'activity_type') required String activityType, + required String description, + @JsonKey(name: 'user_name') required String userName, + @JsonKey(name: 'created_at') required DateTime createdAt, + Map? metadata, + }) = _RecentActivity; + + factory RecentActivity.fromJson(Map json) => + _$RecentActivityFromJson(json); +} \ No newline at end of file diff --git a/lib/data/models/dashboard/recent_activity.freezed.dart b/lib/data/models/dashboard/recent_activity.freezed.dart new file mode 100644 index 0000000..f3311c8 --- /dev/null +++ b/lib/data/models/dashboard/recent_activity.freezed.dart @@ -0,0 +1,291 @@ +// 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 'recent_activity.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'); + +RecentActivity _$RecentActivityFromJson(Map json) { + return _RecentActivity.fromJson(json); +} + +/// @nodoc +mixin _$RecentActivity { + int get id => throw _privateConstructorUsedError; + @JsonKey(name: 'activity_type') + String get activityType => throw _privateConstructorUsedError; + String get description => throw _privateConstructorUsedError; + @JsonKey(name: 'user_name') + String get userName => throw _privateConstructorUsedError; + @JsonKey(name: 'created_at') + DateTime get createdAt => throw _privateConstructorUsedError; + Map? get metadata => throw _privateConstructorUsedError; + + /// Serializes this RecentActivity to a JSON map. + Map toJson() => throw _privateConstructorUsedError; + + /// Create a copy of RecentActivity + /// with the given fields replaced by the non-null parameter values. + @JsonKey(includeFromJson: false, includeToJson: false) + $RecentActivityCopyWith get copyWith => + throw _privateConstructorUsedError; +} + +/// @nodoc +abstract class $RecentActivityCopyWith<$Res> { + factory $RecentActivityCopyWith( + RecentActivity value, $Res Function(RecentActivity) then) = + _$RecentActivityCopyWithImpl<$Res, RecentActivity>; + @useResult + $Res call( + {int id, + @JsonKey(name: 'activity_type') String activityType, + String description, + @JsonKey(name: 'user_name') String userName, + @JsonKey(name: 'created_at') DateTime createdAt, + Map? metadata}); +} + +/// @nodoc +class _$RecentActivityCopyWithImpl<$Res, $Val extends RecentActivity> + implements $RecentActivityCopyWith<$Res> { + _$RecentActivityCopyWithImpl(this._value, this._then); + + // ignore: unused_field + final $Val _value; + // ignore: unused_field + final $Res Function($Val) _then; + + /// Create a copy of RecentActivity + /// with the given fields replaced by the non-null parameter values. + @pragma('vm:prefer-inline') + @override + $Res call({ + Object? id = null, + Object? activityType = null, + Object? description = null, + Object? userName = null, + Object? createdAt = null, + Object? metadata = freezed, + }) { + return _then(_value.copyWith( + id: null == id + ? _value.id + : id // ignore: cast_nullable_to_non_nullable + as int, + activityType: null == activityType + ? _value.activityType + : activityType // ignore: cast_nullable_to_non_nullable + as String, + description: null == description + ? _value.description + : description // ignore: cast_nullable_to_non_nullable + as String, + userName: null == userName + ? _value.userName + : userName // ignore: cast_nullable_to_non_nullable + as String, + createdAt: null == createdAt + ? _value.createdAt + : createdAt // ignore: cast_nullable_to_non_nullable + as DateTime, + metadata: freezed == metadata + ? _value.metadata + : metadata // ignore: cast_nullable_to_non_nullable + as Map?, + ) as $Val); + } +} + +/// @nodoc +abstract class _$$RecentActivityImplCopyWith<$Res> + implements $RecentActivityCopyWith<$Res> { + factory _$$RecentActivityImplCopyWith(_$RecentActivityImpl value, + $Res Function(_$RecentActivityImpl) then) = + __$$RecentActivityImplCopyWithImpl<$Res>; + @override + @useResult + $Res call( + {int id, + @JsonKey(name: 'activity_type') String activityType, + String description, + @JsonKey(name: 'user_name') String userName, + @JsonKey(name: 'created_at') DateTime createdAt, + Map? metadata}); +} + +/// @nodoc +class __$$RecentActivityImplCopyWithImpl<$Res> + extends _$RecentActivityCopyWithImpl<$Res, _$RecentActivityImpl> + implements _$$RecentActivityImplCopyWith<$Res> { + __$$RecentActivityImplCopyWithImpl( + _$RecentActivityImpl _value, $Res Function(_$RecentActivityImpl) _then) + : super(_value, _then); + + /// Create a copy of RecentActivity + /// with the given fields replaced by the non-null parameter values. + @pragma('vm:prefer-inline') + @override + $Res call({ + Object? id = null, + Object? activityType = null, + Object? description = null, + Object? userName = null, + Object? createdAt = null, + Object? metadata = freezed, + }) { + return _then(_$RecentActivityImpl( + id: null == id + ? _value.id + : id // ignore: cast_nullable_to_non_nullable + as int, + activityType: null == activityType + ? _value.activityType + : activityType // ignore: cast_nullable_to_non_nullable + as String, + description: null == description + ? _value.description + : description // ignore: cast_nullable_to_non_nullable + as String, + userName: null == userName + ? _value.userName + : userName // ignore: cast_nullable_to_non_nullable + as String, + createdAt: null == createdAt + ? _value.createdAt + : createdAt // ignore: cast_nullable_to_non_nullable + as DateTime, + metadata: freezed == metadata + ? _value._metadata + : metadata // ignore: cast_nullable_to_non_nullable + as Map?, + )); + } +} + +/// @nodoc +@JsonSerializable() +class _$RecentActivityImpl implements _RecentActivity { + const _$RecentActivityImpl( + {required this.id, + @JsonKey(name: 'activity_type') required this.activityType, + required this.description, + @JsonKey(name: 'user_name') required this.userName, + @JsonKey(name: 'created_at') required this.createdAt, + final Map? metadata}) + : _metadata = metadata; + + factory _$RecentActivityImpl.fromJson(Map json) => + _$$RecentActivityImplFromJson(json); + + @override + final int id; + @override + @JsonKey(name: 'activity_type') + final String activityType; + @override + final String description; + @override + @JsonKey(name: 'user_name') + final String userName; + @override + @JsonKey(name: 'created_at') + final DateTime createdAt; + 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 'RecentActivity(id: $id, activityType: $activityType, description: $description, userName: $userName, createdAt: $createdAt, metadata: $metadata)'; + } + + @override + bool operator ==(Object other) { + return identical(this, other) || + (other.runtimeType == runtimeType && + other is _$RecentActivityImpl && + (identical(other.id, id) || other.id == id) && + (identical(other.activityType, activityType) || + other.activityType == activityType) && + (identical(other.description, description) || + other.description == description) && + (identical(other.userName, userName) || + other.userName == userName) && + (identical(other.createdAt, createdAt) || + other.createdAt == createdAt) && + const DeepCollectionEquality().equals(other._metadata, _metadata)); + } + + @JsonKey(includeFromJson: false, includeToJson: false) + @override + int get hashCode => Object.hash(runtimeType, id, activityType, description, + userName, createdAt, const DeepCollectionEquality().hash(_metadata)); + + /// Create a copy of RecentActivity + /// with the given fields replaced by the non-null parameter values. + @JsonKey(includeFromJson: false, includeToJson: false) + @override + @pragma('vm:prefer-inline') + _$$RecentActivityImplCopyWith<_$RecentActivityImpl> get copyWith => + __$$RecentActivityImplCopyWithImpl<_$RecentActivityImpl>( + this, _$identity); + + @override + Map toJson() { + return _$$RecentActivityImplToJson( + this, + ); + } +} + +abstract class _RecentActivity implements RecentActivity { + const factory _RecentActivity( + {required final int id, + @JsonKey(name: 'activity_type') required final String activityType, + required final String description, + @JsonKey(name: 'user_name') required final String userName, + @JsonKey(name: 'created_at') required final DateTime createdAt, + final Map? metadata}) = _$RecentActivityImpl; + + factory _RecentActivity.fromJson(Map json) = + _$RecentActivityImpl.fromJson; + + @override + int get id; + @override + @JsonKey(name: 'activity_type') + String get activityType; + @override + String get description; + @override + @JsonKey(name: 'user_name') + String get userName; + @override + @JsonKey(name: 'created_at') + DateTime get createdAt; + @override + Map? get metadata; + + /// Create a copy of RecentActivity + /// with the given fields replaced by the non-null parameter values. + @override + @JsonKey(includeFromJson: false, includeToJson: false) + _$$RecentActivityImplCopyWith<_$RecentActivityImpl> get copyWith => + throw _privateConstructorUsedError; +} diff --git a/lib/data/models/dashboard/recent_activity.g.dart b/lib/data/models/dashboard/recent_activity.g.dart new file mode 100644 index 0000000..178d2e1 --- /dev/null +++ b/lib/data/models/dashboard/recent_activity.g.dart @@ -0,0 +1,28 @@ +// GENERATED CODE - DO NOT MODIFY BY HAND + +part of 'recent_activity.dart'; + +// ************************************************************************** +// JsonSerializableGenerator +// ************************************************************************** + +_$RecentActivityImpl _$$RecentActivityImplFromJson(Map json) => + _$RecentActivityImpl( + id: (json['id'] as num).toInt(), + activityType: json['activity_type'] as String, + description: json['description'] as String, + userName: json['user_name'] as String, + createdAt: DateTime.parse(json['created_at'] as String), + metadata: json['metadata'] as Map?, + ); + +Map _$$RecentActivityImplToJson( + _$RecentActivityImpl instance) => + { + 'id': instance.id, + 'activity_type': instance.activityType, + 'description': instance.description, + 'user_name': instance.userName, + 'created_at': instance.createdAt.toIso8601String(), + 'metadata': instance.metadata, + }; diff --git a/lib/di/injection_container.dart b/lib/di/injection_container.dart index 274dfe1..edd4cb3 100644 --- a/lib/di/injection_container.dart +++ b/lib/di/injection_container.dart @@ -4,7 +4,9 @@ import 'package:get_it/get_it.dart'; import '../core/config/environment.dart'; import '../data/datasources/remote/api_client.dart'; import '../data/datasources/remote/auth_remote_datasource.dart'; +import '../data/datasources/remote/dashboard_remote_datasource.dart'; import '../services/auth_service.dart'; +import '../services/dashboard_service.dart'; /// GetIt 인스턴스 final getIt = GetIt.instance; @@ -25,11 +27,17 @@ Future setupDependencies() async { getIt.registerLazySingleton( () => AuthRemoteDataSourceImpl(getIt()), ); + getIt.registerLazySingleton( + () => DashboardRemoteDataSourceImpl(getIt()), + ); // 서비스 getIt.registerLazySingleton( () => AuthServiceImpl(getIt(), getIt()), ); + getIt.registerLazySingleton( + () => DashboardServiceImpl(getIt()), + ); // 리포지토리 // TODO: Repositories will be registered here diff --git a/lib/domain/repositories/equipment_repository.dart b/lib/domain/repositories/equipment_repository.dart new file mode 100644 index 0000000..386b572 --- /dev/null +++ b/lib/domain/repositories/equipment_repository.dart @@ -0,0 +1,68 @@ +import 'package:dartz/dartz.dart'; +import '../../core/errors/failures.dart'; +import '../../models/equipment_unified_model.dart'; + +/// 장비 관리 Repository 인터페이스 +abstract class EquipmentRepository { + /// 장비 입고 목록 조회 + Future>> getEquipmentIns({ + int? page, + int? limit, + String? search, + String? sortBy, + String? sortOrder, + }); + + /// 장비 입고 상세 조회 + Future> getEquipmentInById(int id); + + /// 장비 입고 생성 + Future> createEquipmentIn(EquipmentIn equipmentIn); + + /// 장비 입고 수정 + Future> updateEquipmentIn(int id, EquipmentIn equipmentIn); + + /// 장비 입고 삭제 + Future> deleteEquipmentIn(int id); + + /// 장비 출고 목록 조회 + Future>> getEquipmentOuts({ + int? page, + int? limit, + String? search, + String? sortBy, + String? sortOrder, + }); + + /// 장비 출고 상세 조회 + Future> getEquipmentOutById(int id); + + /// 장비 출고 생성 + Future> createEquipmentOut(EquipmentOut equipmentOut); + + /// 장비 출고 수정 + Future> updateEquipmentOut(int id, EquipmentOut equipmentOut); + + /// 장비 출고 삭제 + Future> deleteEquipmentOut(int id); + + /// 장비 일괄 출고 + Future>> createBatchEquipmentOut(List equipmentOuts); + + /// 제조사 목록 조회 + Future>> getManufacturers(); + + /// 장비명 목록 조회 + Future>> getEquipmentNames(); + + /// 장비 이력 조회 + Future>> getEquipmentHistory(int equipmentId); + + /// 장비 검색 + Future>> searchEquipment({ + String? manufacturer, + String? name, + String? category, + String? serialNumber, + }); +} \ No newline at end of file diff --git a/lib/main.dart b/lib/main.dart index 7250d52..ed6cc72 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -1,4 +1,5 @@ import 'package:flutter/material.dart'; +import 'package:get_it/get_it.dart'; import 'package:superport/models/equipment_unified_model.dart'; import 'package:superport/screens/common/app_layout_redesign.dart'; import 'package:superport/screens/common/theme_shadcn.dart'; @@ -8,6 +9,7 @@ import 'package:superport/screens/equipment/equipment_out_form.dart'; import 'package:superport/screens/license/license_form.dart'; // MaintenanceFormScreen으로 사용 import 'package:superport/screens/user/user_form.dart'; import 'package:superport/screens/warehouse_location/warehouse_location_form.dart'; +import 'package:superport/services/auth_service.dart'; import 'package:superport/utils/constants.dart'; import 'package:flutter_localizations/flutter_localizations.dart'; import 'package:superport/screens/login/login_screen.dart'; @@ -29,6 +31,8 @@ class SuperportApp extends StatelessWidget { @override Widget build(BuildContext context) { + final authService = GetIt.instance(); + return MaterialApp( title: 'supERPort', theme: ShadcnTheme.lightTheme, @@ -39,7 +43,26 @@ class SuperportApp extends StatelessWidget { ], supportedLocales: const [Locale('ko', 'KR'), Locale('en', 'US')], locale: const Locale('ko', 'KR'), - initialRoute: '/login', + home: FutureBuilder( + future: authService.isLoggedIn(), + builder: (context, snapshot) { + if (snapshot.connectionState == ConnectionState.waiting) { + return const Scaffold( + body: Center( + child: CircularProgressIndicator(), + ), + ); + } + + if (snapshot.hasData && snapshot.data!) { + // 토큰이 유효하면 홈 화면으로 + return AppLayoutRedesign(initialRoute: Routes.home); + } else { + // 토큰이 없거나 유효하지 않으면 로그인 화면으로 + return const LoginScreen(); + } + }, + ), onGenerateRoute: (settings) { // 로그인 라우트 처리 if (settings.name == '/login') { @@ -182,6 +205,7 @@ class SuperportApp extends StatelessWidget { ); } }, + navigatorKey: GlobalKey(), ); } } diff --git a/lib/screens/common/app_layout_redesign.dart b/lib/screens/common/app_layout_redesign.dart index 2920d50..8358256 100644 --- a/lib/screens/common/app_layout_redesign.dart +++ b/lib/screens/common/app_layout_redesign.dart @@ -1,4 +1,5 @@ import 'package:flutter/material.dart'; +import 'package:get_it/get_it.dart'; import 'package:superport/screens/common/theme_shadcn.dart'; import 'package:superport/screens/common/components/shadcn_components.dart'; import 'package:superport/screens/overview/overview_screen_redesign.dart'; @@ -7,6 +8,7 @@ import 'package:superport/screens/company/company_list_redesign.dart'; import 'package:superport/screens/user/user_list_redesign.dart'; import 'package:superport/screens/license/license_list_redesign.dart'; import 'package:superport/screens/warehouse_location/warehouse_location_list_redesign.dart'; +import 'package:superport/services/auth_service.dart'; import 'package:superport/utils/constants.dart'; /// Microsoft Dynamics 365 스타일의 메인 레이아웃 @@ -430,9 +432,43 @@ class _AppLayoutRedesignState extends State // 로그아웃 버튼 ShadcnButton( text: '로그아웃', - onPressed: () { - Navigator.of(context).pop(); - Navigator.of(context).pushReplacementNamed('/login'); + onPressed: () async { + // 로딩 다이얼로그 표시 + showDialog( + context: context, + barrierDismissible: false, + builder: (context) => Center( + child: CircularProgressIndicator(), + ), + ); + + try { + // AuthService를 사용하여 로그아웃 + final authService = GetIt.instance(); + await authService.logout(); + + // 로딩 다이얼로그와 현재 모달 닫기 + if (context.mounted) { + Navigator.of(context).pop(); // 로딩 다이얼로그 + Navigator.of(context).pop(); // 프로필 메뉴 + // 로그인 화면으로 이동 + Navigator.of(context).pushNamedAndRemoveUntil( + '/login', + (route) => false, + ); + } + } catch (e) { + // 에러 처리 + if (context.mounted) { + Navigator.of(context).pop(); // 로딩 다이얼로그 닫기 + ScaffoldMessenger.of(context).showSnackBar( + SnackBar( + content: Text('로그아웃 중 오류가 발생했습니다.'), + backgroundColor: Colors.red, + ), + ); + } + } }, variant: ShadcnButtonVariant.destructive, fullWidth: true, diff --git a/lib/screens/login/login_screen.dart b/lib/screens/login/login_screen.dart index 090f212..ca5de8d 100644 --- a/lib/screens/login/login_screen.dart +++ b/lib/screens/login/login_screen.dart @@ -22,7 +22,11 @@ class _LoginScreenState extends State { // 로그인 성공 시 콜백 (예: overview로 이동) void _onLoginSuccess() { - Navigator.of(context).pushReplacementNamed('/home'); + // 로그인 성공 시 모든 이전 라우트를 제거하고 홈으로 이동 + Navigator.of(context).pushNamedAndRemoveUntil( + '/home', + (route) => false, + ); } @override diff --git a/lib/screens/login/widgets/login_view_redesign.dart b/lib/screens/login/widgets/login_view_redesign.dart index 9c7af0e..806cb2b 100644 --- a/lib/screens/login/widgets/login_view_redesign.dart +++ b/lib/screens/login/widgets/login_view_redesign.dart @@ -27,8 +27,6 @@ class _LoginViewRedesignState extends State late AnimationController _slideController; late Animation _slideAnimation; - bool _rememberMe = false; - @override void initState() { super.initState(); @@ -80,36 +78,37 @@ class _LoginViewRedesignState extends State child: Consumer( builder: (context, controller, _) { return Scaffold( - backgroundColor: ShadcnTheme.background, - body: SafeArea( - child: Center( - child: SingleChildScrollView( - physics: const BouncingScrollPhysics(), - child: Padding( - padding: const EdgeInsets.all(ShadcnTheme.spacing6), - child: ConstrainedBox( - constraints: const BoxConstraints(maxWidth: 400), - child: FadeTransition( - opacity: _fadeAnimation, - child: SlideTransition( - position: _slideAnimation, - child: Column( - mainAxisAlignment: MainAxisAlignment.center, - children: [ - _buildHeader(), - const SizedBox(height: ShadcnTheme.spacing12), - _buildLoginCard(), - const SizedBox(height: ShadcnTheme.spacing8), - _buildFooter(), - ], + backgroundColor: ShadcnTheme.background, + body: SafeArea( + child: Center( + child: SingleChildScrollView( + physics: const BouncingScrollPhysics(), + child: Padding( + padding: const EdgeInsets.all(ShadcnTheme.spacing6), + child: ConstrainedBox( + constraints: const BoxConstraints(maxWidth: 400), + child: FadeTransition( + opacity: _fadeAnimation, + child: SlideTransition( + position: _slideAnimation, + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + _buildHeader(), + const SizedBox(height: ShadcnTheme.spacing12), + _buildLoginCard(), + const SizedBox(height: ShadcnTheme.spacing8), + _buildFooter(), + ], + ), + ), + ), ), ), ), ), ), - ), - ), - ); + ); }, ), ); @@ -234,7 +233,7 @@ class _LoginViewRedesignState extends State margin: const EdgeInsets.only(bottom: ShadcnTheme.spacing4), decoration: BoxDecoration( color: ShadcnTheme.destructive.withOpacity(0.1), - borderRadius: BorderRadius.circular(ShadcnTheme.borderRadius), + borderRadius: BorderRadius.circular(ShadcnTheme.radiusMd), border: Border.all( color: ShadcnTheme.destructive.withOpacity(0.3), ), diff --git a/lib/screens/overview/controllers/overview_controller.dart b/lib/screens/overview/controllers/overview_controller.dart index 9961fe4..74df544 100644 --- a/lib/screens/overview/controllers/overview_controller.dart +++ b/lib/screens/overview/controllers/overview_controller.dart @@ -1,60 +1,177 @@ import 'package:flutter/material.dart'; -import 'package:superport/services/mock_data_service.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/overview_stats.dart'; +import 'package:superport/data/models/dashboard/recent_activity.dart'; +import 'package:superport/services/dashboard_service.dart'; import 'package:superport/screens/common/theme_tailwind.dart'; -// 대시보드(Overview) 화면의 상태 및 비즈니스 로직을 담당하는 컨트롤러 -class OverviewController { - final MockDataService dataService; +// 대시보드(Overview) 화면의 상태 및 비즈니스 로직을 담답하는 컨트롤러 +class OverviewController extends ChangeNotifier { + final DashboardService _dashboardService = GetIt.instance(); - int totalCompanies = 0; - int totalUsers = 0; - int totalEquipmentIn = 0; - int totalEquipmentOut = 0; - int totalLicenses = 0; - - // 최근 활동 데이터 - List> recentActivities = []; - - OverviewController({required this.dataService}); - - // 데이터 로드 및 통계 계산 - void loadData() { - totalCompanies = dataService.getAllCompanies().length; - totalUsers = dataService.getAllUsers().length; - // 실제 서비스에서는 아래 메서드 구현 필요 - totalEquipmentIn = 32; // 임시 데이터 - totalEquipmentOut = 18; // 임시 데이터 - totalLicenses = dataService.getAllLicenses().length; - _loadRecentActivities(); + // 상태 데이터 + OverviewStats? _overviewStats; + List _recentActivities = []; + EquipmentStatusDistribution? _equipmentStatus; + List _expiringLicenses = []; + + // 로딩 상태 + bool _isLoadingStats = false; + bool _isLoadingActivities = false; + bool _isLoadingEquipmentStatus = false; + bool _isLoadingLicenses = false; + + // 에러 상태 + String? _statsError; + String? _activitiesError; + String? _equipmentStatusError; + String? _licensesError; + + // Getters + OverviewStats? get overviewStats => _overviewStats; + List get recentActivities => _recentActivities; + EquipmentStatusDistribution? get equipmentStatus => _equipmentStatus; + List get expiringLicenses => _expiringLicenses; + + bool get isLoading => _isLoadingStats || _isLoadingActivities || + _isLoadingEquipmentStatus || _isLoadingLicenses; + + String? get error { + return _statsError ?? _activitiesError ?? + _equipmentStatusError ?? _licensesError; } - // 최근 활동 데이터 로드 (임시 데이터) - void _loadRecentActivities() { - recentActivities = [ - { - 'type': '장비 입고', - 'title': '라우터 입고 처리 완료', - 'time': '30분 전', - 'user': '홍길동', - 'icon': Icons.input, - 'color': AppThemeTailwind.success, + OverviewController(); + + // 데이터 로드 + Future loadData() async { + await Future.wait([ + _loadOverviewStats(), + _loadRecentActivities(), + _loadEquipmentStatus(), + _loadExpiringLicenses(), + ]); + } + + // 개별 데이터 로드 메서드 + Future _loadOverviewStats() async { + _isLoadingStats = true; + _statsError = null; + notifyListeners(); + + final result = await _dashboardService.getOverviewStats(); + + result.fold( + (failure) { + _statsError = failure.message; }, - { - 'type': '사용자 추가', - 'title': '새 관리자 등록', - 'time': '1시간 전', - 'user': '김철수', - 'icon': Icons.person_add, - 'color': AppThemeTailwind.primary, + (stats) { + _overviewStats = stats; }, - { - 'type': '장비 출고', - 'title': '모니터 5대 출고 처리', - 'time': '2시간 전', - 'user': '이영희', - 'icon': Icons.output, - 'color': AppThemeTailwind.warning, + ); + + _isLoadingStats = false; + notifyListeners(); + } + + Future _loadRecentActivities() async { + _isLoadingActivities = true; + _activitiesError = null; + notifyListeners(); + + final result = await _dashboardService.getRecentActivities(); + + result.fold( + (failure) { + _activitiesError = failure.message; }, - ]; + (activities) { + _recentActivities = activities; + }, + ); + + _isLoadingActivities = false; + notifyListeners(); + } + + Future _loadEquipmentStatus() async { + _isLoadingEquipmentStatus = true; + _equipmentStatusError = null; + notifyListeners(); + + final result = await _dashboardService.getEquipmentStatusDistribution(); + + result.fold( + (failure) { + _equipmentStatusError = failure.message; + }, + (status) { + _equipmentStatus = status; + }, + ); + + _isLoadingEquipmentStatus = false; + notifyListeners(); + } + + Future _loadExpiringLicenses() async { + _isLoadingLicenses = true; + _licensesError = null; + notifyListeners(); + + final result = await _dashboardService.getExpiringLicenses(days: 30); + + result.fold( + (failure) { + _licensesError = failure.message; + }, + (licenses) { + _expiringLicenses = licenses; + }, + ); + + _isLoadingLicenses = false; + notifyListeners(); + } + + // 활동 타입별 아이콘과 색상 가져오기 + IconData getActivityIcon(String activityType) { + switch (activityType.toLowerCase()) { + case 'equipment_in': + case '장비 입고': + return Icons.input; + case 'equipment_out': + case '장비 출고': + return Icons.output; + case 'user_create': + case '사용자 추가': + return Icons.person_add; + case 'license_create': + case '라이선스 등록': + return Icons.vpn_key; + default: + return Icons.notifications; + } + } + + Color getActivityColor(String activityType) { + switch (activityType.toLowerCase()) { + case 'equipment_in': + case '장비 입고': + return AppThemeTailwind.success; + case 'equipment_out': + case '장비 출고': + return AppThemeTailwind.warning; + case 'user_create': + case '사용자 추가': + return AppThemeTailwind.primary; + case 'license_create': + case '라이선스 등록': + return AppThemeTailwind.info; + default: + return AppThemeTailwind.muted; + } } } diff --git a/lib/screens/overview/overview_screen_redesign.dart b/lib/screens/overview/overview_screen_redesign.dart index 9e357ad..494955d 100644 --- a/lib/screens/overview/overview_screen_redesign.dart +++ b/lib/screens/overview/overview_screen_redesign.dart @@ -14,41 +14,45 @@ class OverviewScreenRedesign extends StatefulWidget { class _OverviewScreenRedesignState extends State { late final OverviewController _controller; - bool _isLoading = true; @override void initState() { super.initState(); - _controller = OverviewController(dataService: MockDataService()); + _controller = OverviewController(); _loadData(); } Future _loadData() async { - setState(() { - _isLoading = true; - }); + await _controller.loadDashboardData(); + } - await Future.delayed(const Duration(milliseconds: 800)); - _controller.loadData(); - - setState(() { - _isLoading = false; - }); + @override + void dispose() { + _controller.dispose(); + super.dispose(); } @override Widget build(BuildContext context) { - if (_isLoading) { - return _buildLoadingState(); - } + return ChangeNotifierProvider.value( + value: _controller, + child: Consumer( + builder: (context, controller, child) { + if (controller.isLoading) { + return _buildLoadingState(); + } - return Container( - color: ShadcnTheme.background, - child: SingleChildScrollView( - padding: const EdgeInsets.all(24), - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ + if (controller.error != null) { + return _buildErrorState(controller.error!); + } + + return Container( + color: ShadcnTheme.background, + child: SingleChildScrollView( + padding: const EdgeInsets.all(24), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ // 환영 섹션 ShadcnCard( padding: const EdgeInsets.all(32), @@ -164,6 +168,9 @@ class _OverviewScreenRedesignState extends State { ], ), ), + ); + }, + ), ); } @@ -183,6 +190,34 @@ class _OverviewScreenRedesignState extends State { ); } + Widget _buildErrorState(String error) { + return Container( + color: ShadcnTheme.background, + child: Center( + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Icon( + Icons.error_outline, + size: 48, + color: ShadcnTheme.error, + ), + const SizedBox(height: ShadcnTheme.spacing4), + Text('오류가 발생했습니다', style: ShadcnTheme.headingH4), + const SizedBox(height: ShadcnTheme.spacing2), + Text(error, style: ShadcnTheme.bodyMuted), + const SizedBox(height: ShadcnTheme.spacing4), + ShadcnButton( + text: '다시 시도', + onPressed: _loadData, + variant: ShadcnButtonVariant.primary, + ), + ], + ), + ), + ); + } + Widget _buildLeftColumn() { return Column( children: [ @@ -261,7 +296,27 @@ class _OverviewScreenRedesignState extends State { ], ), const SizedBox(height: 16), - ...List.generate(5, (index) => _buildActivityItem(index)), + Consumer( + builder: (context, controller, child) { + final activities = controller.recentActivities ?? []; + if (activities.isEmpty) { + return Padding( + padding: const EdgeInsets.symmetric(vertical: 20), + child: Center( + child: Text( + '최근 활동이 없습니다', + style: ShadcnTheme.bodyMuted, + ), + ), + ); + } + return Column( + children: activities.take(5).map((activity) => + _buildActivityItem(activity), + ).toList(), + ); + }, + ), ], ), ), @@ -376,41 +431,41 @@ class _OverviewScreenRedesignState extends State { ); } - Widget _buildActivityItem(int index) { - final activities = [ - { - 'icon': Icons.inventory, - 'title': '장비 입고 처리', - 'subtitle': '크레인 #CR-001 입고 완료', - 'time': '2분 전', - }, - { - 'icon': Icons.local_shipping, - 'title': '장비 출고 처리', - 'subtitle': '포클레인 #FK-005 출고 완료', - 'time': '5분 전', - }, - { - 'icon': Icons.business, - 'title': '회사 등록', - 'subtitle': '새로운 회사 "ABC건설" 등록', - 'time': '10분 전', - }, - { - 'icon': Icons.person_add, - 'title': '사용자 추가', - 'subtitle': '신규 사용자 계정 생성', - 'time': '15분 전', - }, - { - 'icon': Icons.settings, - 'title': '시스템 점검', - 'subtitle': '정기 시스템 점검 완료', - 'time': '30분 전', - }, - ]; + Widget _buildActivityItem(dynamic activity) { + // 아이콘 매핑 + IconData getActivityIcon(String type) { + switch (type) { + case 'equipment_in': + return Icons.inventory; + case 'equipment_out': + return Icons.local_shipping; + case 'company': + return Icons.business; + case 'user': + return Icons.person_add; + default: + return Icons.settings; + } + } - final activity = activities[index]; + // 색상 매핑 + Color getActivityColor(String type) { + switch (type) { + case 'equipment_in': + return ShadcnTheme.success; + case 'equipment_out': + return ShadcnTheme.warning; + case 'company': + return ShadcnTheme.info; + case 'user': + return ShadcnTheme.primary; + default: + return ShadcnTheme.mutedForeground; + } + } + + final color = getActivityColor(activity.type); + final dateFormat = DateFormat('MM/dd HH:mm'); return Padding( padding: const EdgeInsets.symmetric(vertical: 8), @@ -419,12 +474,12 @@ class _OverviewScreenRedesignState extends State { Container( padding: const EdgeInsets.all(8), decoration: BoxDecoration( - color: ShadcnTheme.success.withOpacity(0.1), + color: color.withOpacity(0.1), borderRadius: BorderRadius.circular(6), ), child: Icon( - activity['icon'] as IconData, - color: ShadcnTheme.success, + getActivityIcon(activity.type), + color: color, size: 16, ), ), @@ -434,17 +489,20 @@ class _OverviewScreenRedesignState extends State { crossAxisAlignment: CrossAxisAlignment.start, children: [ Text( - activity['title'] as String, + activity.title, style: ShadcnTheme.bodyMedium, ), Text( - activity['subtitle'] as String, + activity.description, style: ShadcnTheme.bodySmall, ), ], ), ), - Text(activity['time'] as String, style: ShadcnTheme.bodySmall), + Text( + dateFormat.format(activity.createdAt), + style: ShadcnTheme.bodySmall, + ), ], ), ); diff --git a/lib/services/dashboard_service.dart b/lib/services/dashboard_service.dart new file mode 100644 index 0000000..63a9331 --- /dev/null +++ b/lib/services/dashboard_service.dart @@ -0,0 +1,42 @@ +import 'package:dartz/dartz.dart'; +import 'package:injectable/injectable.dart'; +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/overview_stats.dart'; +import 'package:superport/data/models/dashboard/recent_activity.dart'; + +abstract class DashboardService { + Future> getOverviewStats(); + Future>> getRecentActivities(); + Future> getEquipmentStatusDistribution(); + Future>> getExpiringLicenses({int days = 30}); +} + +@LazySingleton(as: DashboardService) +class DashboardServiceImpl implements DashboardService { + final DashboardRemoteDataSource _remoteDataSource; + + DashboardServiceImpl(this._remoteDataSource); + + @override + Future> getOverviewStats() async { + return await _remoteDataSource.getOverviewStats(); + } + + @override + Future>> getRecentActivities() async { + return await _remoteDataSource.getRecentActivities(); + } + + @override + Future> getEquipmentStatusDistribution() async { + return await _remoteDataSource.getEquipmentStatusDistribution(); + } + + @override + Future>> getExpiringLicenses({int days = 30}) async { + return await _remoteDataSource.getExpiringLicenses(days: days); + } +} \ No newline at end of file