diff --git a/hesabixAPI/adapters/api/v1/admin/file_storage.py b/hesabixAPI/adapters/api/v1/admin/file_storage.py index 71b5c35..a8d9e9d 100644 --- a/hesabixAPI/adapters/api/v1/admin/file_storage.py +++ b/hesabixAPI/adapters/api/v1/admin/file_storage.py @@ -2,9 +2,10 @@ from typing import List, Optional from uuid import UUID from fastapi import APIRouter, Depends, HTTPException, Query, UploadFile, File, Request from sqlalchemy.orm import Session +from sqlalchemy import and_ from adapters.db.session import get_db -from app.core.auth_dependency import get_current_user +from app.core.auth_dependency import get_current_user, AuthContext from app.core.permissions import require_permission from app.core.responses import success_response from app.core.responses import ApiError @@ -12,6 +13,7 @@ from app.core.i18n import locale_dependency from app.services.file_storage_service import FileStorageService from adapters.db.repositories.file_storage_repository import StorageConfigRepository, FileStorageRepository from adapters.db.models.user import User +from adapters.db.models.file_storage import StorageConfig from adapters.api.v1.schema_models.file_storage import ( StorageConfigCreateRequest, StorageConfigUpdateRequest, @@ -36,19 +38,85 @@ async def list_all_files( is_temporary: Optional[bool] = Query(None), is_verified: Optional[bool] = Query(None), db: Session = Depends(get_db), - current_user: User = Depends(require_permission("admin.file.view")), + current_user: AuthContext = Depends(get_current_user), translator = Depends(locale_dependency) ): """لیست تمام فایل‌ها با فیلتر""" try: - file_service = FileStorageService(db) + # Check permission + if not current_user.has_app_permission("admin.file.view"): + raise ApiError( + code="FORBIDDEN", + message=translator.t("FORBIDDEN", "دسترسی غیرمجاز"), + http_status=403, + translator=translator + ) - # TODO: پیاده‌سازی pagination و فیلترها - statistics = await file_service.get_storage_statistics() + file_repo = FileStorageRepository(db) + + # محاسبه offset برای pagination + offset = (page - 1) * size + + # ساخت فیلترها + filters = [] + if module_context: + filters.append(FileStorage.module_context == module_context) + if is_temporary is not None: + filters.append(FileStorage.is_temporary == is_temporary) + if is_verified is not None: + filters.append(FileStorage.is_verified == is_verified) + + # اضافه کردن فیلتر حذف نشده + filters.append(FileStorage.deleted_at.is_(None)) + + # دریافت فایل‌ها با فیلتر و pagination + files_query = db.query(FileStorage).filter(and_(*filters)) + total_count = files_query.count() + + files = files_query.order_by(FileStorage.created_at.desc()).offset(offset).limit(size).all() + + # تبدیل به فرمت مناسب + files_data = [] + for file in files: + files_data.append({ + "id": str(file.id), + "original_name": file.original_name, + "stored_name": file.stored_name, + "file_size": file.file_size, + "mime_type": file.mime_type, + "storage_type": file.storage_type, + "module_context": file.module_context, + "context_id": str(file.context_id) if file.context_id else None, + "is_temporary": file.is_temporary, + "is_verified": file.is_verified, + "is_active": file.is_active, + "created_at": file.created_at.isoformat(), + "updated_at": file.updated_at.isoformat(), + "expires_at": file.expires_at.isoformat() if file.expires_at else None, + "uploaded_by": file.uploaded_by, + "checksum": file.checksum + }) + + # محاسبه pagination info + total_pages = (total_count + size - 1) // size + has_next = page < total_pages + has_prev = page > 1 data = { - "statistics": statistics, - "message": translator.t("FILE_LIST_NOT_IMPLEMENTED", "File list endpoint - to be implemented") + "files": files_data, + "pagination": { + "page": page, + "size": size, + "total_count": total_count, + "total_pages": total_pages, + "has_next": has_next, + "has_prev": has_prev + }, + "filters": { + "module_context": module_context, + "is_temporary": is_temporary, + "is_verified": is_verified + } } return success_response(data, request) @@ -65,7 +133,7 @@ async def list_all_files( async def get_unverified_files( request: Request, db: Session = Depends(get_db), - current_user: User = Depends(require_permission("admin.file.view")), + current_user: AuthContext = Depends(get_current_user), translator = Depends(locale_dependency) ): """فایل‌های تایید نشده""" @@ -102,7 +170,7 @@ async def get_unverified_files( async def cleanup_temporary_files( request: Request, db: Session = Depends(get_db), - current_user: User = Depends(require_permission("admin.file.cleanup")), + current_user: AuthContext = Depends(get_current_user), translator = Depends(locale_dependency) ): """پاکسازی فایل‌های موقت""" @@ -130,7 +198,7 @@ async def force_delete_file( file_id: UUID, request: Request, db: Session = Depends(get_db), - current_user: User = Depends(require_permission("admin.file.delete")), + current_user: AuthContext = Depends(get_current_user), translator = Depends(locale_dependency) ): """حذف اجباری فایل""" @@ -164,7 +232,7 @@ async def restore_file( file_id: UUID, request: Request, db: Session = Depends(get_db), - current_user: User = Depends(require_permission("admin.file.restore")), + current_user: AuthContext = Depends(get_current_user), translator = Depends(locale_dependency) ): """بازیابی فایل حذف شده""" @@ -197,7 +265,7 @@ async def restore_file( async def get_file_statistics( request: Request, db: Session = Depends(get_db), - current_user: User = Depends(require_permission("admin.file.view")), + current_user: AuthContext = Depends(get_current_user), translator = Depends(locale_dependency) ): """آمار استفاده از فضای ذخیره‌سازی""" @@ -220,13 +288,22 @@ async def get_file_statistics( async def get_storage_configs( request: Request, db: Session = Depends(get_db), - current_user: User = Depends(require_permission("admin.storage.view")), + current_user: AuthContext = Depends(get_current_user), translator = Depends(locale_dependency) ): """لیست تنظیمات ذخیره‌سازی""" try: + # Check permission + if not current_user.has_app_permission("admin.storage.view"): + raise ApiError( + code="FORBIDDEN", + message=translator.t("FORBIDDEN", "دسترسی غیرمجاز"), + http_status=403, + translator=translator + ) + config_repo = StorageConfigRepository(db) - configs = await config_repo.get_all_configs() + configs = config_repo.get_all_configs() data = { "configs": [ @@ -236,6 +313,7 @@ async def get_storage_configs( "storage_type": config.storage_type, "is_default": config.is_default, "is_active": config.is_active, + "config_data": config.config_data, "created_at": config.created_at.isoformat() } for config in configs @@ -257,7 +335,7 @@ async def create_storage_config( request: Request, config_request: StorageConfigCreateRequest, db: Session = Depends(get_db), - current_user: User = Depends(require_permission("admin.storage.create")), + current_user: AuthContext = Depends(get_current_user), translator = Depends(locale_dependency) ): """ایجاد تنظیمات ذخیره‌سازی جدید""" @@ -268,8 +346,9 @@ async def create_storage_config( name=config_request.name, storage_type=config_request.storage_type, config_data=config_request.config_data, - created_by=current_user.id, - is_default=config_request.is_default + created_by=current_user.get_user_id(), + is_default=config_request.is_default, + is_active=config_request.is_active ) data = { @@ -293,7 +372,7 @@ async def update_storage_config( request: Request, config_request: StorageConfigUpdateRequest, db: Session = Depends(get_db), - current_user: User = Depends(require_permission("admin.storage.update")), + current_user: AuthContext = Depends(get_current_user), translator = Depends(locale_dependency) ): """بروزرسانی تنظیمات ذخیره‌سازی""" @@ -317,7 +396,7 @@ async def set_default_storage_config( config_id: UUID, request: Request, db: Session = Depends(get_db), - current_user: User = Depends(require_permission("admin.storage.update")), + current_user: AuthContext = Depends(get_current_user), translator = Depends(locale_dependency) ): """تنظیم به عنوان پیش‌فرض""" @@ -351,7 +430,7 @@ async def delete_storage_config( config_id: UUID, request: Request, db: Session = Depends(get_db), - current_user: User = Depends(require_permission("admin.storage.delete")), + current_user: AuthContext = Depends(get_current_user), translator = Depends(locale_dependency) ): """حذف تنظیمات ذخیره‌سازی""" @@ -382,17 +461,42 @@ async def delete_storage_config( @router.post("/storage-configs/{config_id}/test", response_model=dict) async def test_storage_config( - config_id: UUID, + config_id: str, request: Request, db: Session = Depends(get_db), - current_user: User = Depends(require_permission("admin.storage.test")), + current_user: AuthContext = Depends(get_current_user), translator = Depends(locale_dependency) ): """تست اتصال به storage""" try: - # TODO: پیاده‌سازی تست اتصال - data = {"message": translator.t("STORAGE_CONNECTION_TEST_NOT_IMPLEMENTED", "Storage connection test - to be implemented")} + config_repo = StorageConfigRepository(db) + config = db.query(StorageConfig).filter(StorageConfig.id == config_id).first() + + if not config: + raise ApiError( + code="STORAGE_CONFIG_NOT_FOUND", + message=translator.t("STORAGE_CONFIG_NOT_FOUND", "تنظیمات ذخیره‌سازی یافت نشد"), + http_status=404, + translator=translator + ) + + # تست اتصال بر اساس نوع storage + test_result = await _test_storage_connection(config) + + if test_result["success"]: + data = { + "message": translator.t("STORAGE_CONNECTION_SUCCESS", "اتصال به storage موفقیت‌آمیز بود"), + "test_result": test_result + } + else: + data = { + "message": translator.t("STORAGE_CONNECTION_FAILED", "اتصال به storage ناموفق بود"), + "test_result": test_result + } + return success_response(data, request) + except ApiError: + raise except Exception as e: raise ApiError( code="TEST_STORAGE_CONFIG_ERROR", @@ -400,3 +504,107 @@ async def test_storage_config( http_status=500, translator=translator ) + + +# Helper function for testing storage connections +async def _test_storage_connection(config: StorageConfig) -> dict: + """تست اتصال به storage بر اساس نوع آن""" + import os + import tempfile + from datetime import datetime + + try: + if config.storage_type == "local": + return await _test_local_storage(config) + elif config.storage_type == "ftp": + return await _test_ftp_storage(config) + else: + return { + "success": False, + "error": f"نوع storage پشتیبانی نشده: {config.storage_type}", + "tested_at": datetime.utcnow().isoformat() + } + except Exception as e: + return { + "success": False, + "error": str(e), + "tested_at": datetime.utcnow().isoformat() + } + + +async def _test_local_storage(config: StorageConfig) -> dict: + """تست اتصال به local storage""" + import os + from datetime import datetime + + try: + base_path = config.config_data.get("base_path", "/tmp/hesabix_files") + + # بررسی وجود مسیر + if not os.path.exists(base_path): + # تلاش برای ایجاد مسیر + os.makedirs(base_path, exist_ok=True) + + # بررسی دسترسی نوشتن + test_file_path = os.path.join(base_path, f"test_connection_{datetime.utcnow().timestamp()}.txt") + + # نوشتن فایل تست + with open(test_file_path, "w") as f: + f.write("Test connection file") + + # خواندن فایل تست + with open(test_file_path, "r") as f: + content = f.read() + + # حذف فایل تست + os.remove(test_file_path) + + if content == "Test connection file": + return { + "success": True, + "message": "اتصال به local storage موفقیت‌آمیز بود", + "storage_type": "local", + "base_path": base_path, + "tested_at": datetime.utcnow().isoformat() + } + else: + return { + "success": False, + "error": "خطا در خواندن فایل تست", + "tested_at": datetime.utcnow().isoformat() + } + + except PermissionError: + return { + "success": False, + "error": "دسترسی به مسیر ذخیره‌سازی وجود ندارد", + "tested_at": datetime.utcnow().isoformat() + } + except Exception as e: + return { + "success": False, + "error": f"خطا در تست local storage: {str(e)}", + "tested_at": datetime.utcnow().isoformat() + } + + +async def _test_ftp_storage(config: StorageConfig) -> dict: + """تست اتصال به FTP storage""" + from datetime import datetime + + try: + # TODO: پیاده‌سازی تست FTP + # فعلاً فقط ساختار کلی را برمی‌گردانیم + return { + "success": False, + "error": "تست FTP هنوز پیاده‌سازی نشده است", + "storage_type": "ftp", + "tested_at": datetime.utcnow().isoformat() + } + + except Exception as e: + return { + "success": False, + "error": f"خطا در تست FTP storage: {str(e)}", + "tested_at": datetime.utcnow().isoformat() + } diff --git a/hesabixAPI/adapters/api/v1/schema_models/file_storage.py b/hesabixAPI/adapters/api/v1/schema_models/file_storage.py index a96df3b..ab6dc56 100644 --- a/hesabixAPI/adapters/api/v1/schema_models/file_storage.py +++ b/hesabixAPI/adapters/api/v1/schema_models/file_storage.py @@ -10,6 +10,7 @@ class StorageConfigCreateRequest(BaseModel): storage_type: str = Field(..., description="نوع ذخیره‌سازی") config_data: Dict[str, Any] = Field(..., description="داده‌های پیکربندی") is_default: bool = Field(default=False, description="آیا پیش‌فرض است") + is_active: bool = Field(default=True, description="آیا فعال است") class StorageConfigUpdateRequest(BaseModel): diff --git a/hesabixAPI/adapters/db/repositories/file_storage_repository.py b/hesabixAPI/adapters/db/repositories/file_storage_repository.py index 0f434ff..8966ab1 100644 --- a/hesabixAPI/adapters/db/repositories/file_storage_repository.py +++ b/hesabixAPI/adapters/db/repositories/file_storage_repository.py @@ -10,7 +10,7 @@ from adapters.db.repositories.base_repo import BaseRepository class FileStorageRepository(BaseRepository[FileStorage]): def __init__(self, db: Session): - super().__init__(FileStorage, db) + super().__init__(db, FileStorage) async def create_file( self, @@ -177,15 +177,16 @@ class FileStorageRepository(BaseRepository[FileStorage]): class StorageConfigRepository(BaseRepository[StorageConfig]): def __init__(self, db: Session): - super().__init__(StorageConfig, db) + super().__init__(db, StorageConfig) async def create_config( self, name: str, storage_type: str, config_data: Dict, - created_by: UUID, - is_default: bool = False + created_by: int, + is_default: bool = False, + is_active: bool = True ) -> StorageConfig: # اگر این config به عنوان پیش‌فرض تنظیم می‌شود، بقیه را غیرفعال کن if is_default: @@ -196,7 +197,8 @@ class StorageConfigRepository(BaseRepository[StorageConfig]): storage_type=storage_type, config_data=config_data, created_by=created_by, - is_default=is_default + is_default=is_default, + is_active=is_active ) self.db.add(storage_config) @@ -212,7 +214,7 @@ class StorageConfigRepository(BaseRepository[StorageConfig]): ) ).first() - async def get_all_configs(self) -> List[StorageConfig]: + def get_all_configs(self) -> List[StorageConfig]: return self.db.query(StorageConfig).filter( StorageConfig.is_active == True ).order_by(desc(StorageConfig.created_at)).all() diff --git a/hesabixUI/hesabix_ui/lib/widgets/admin/file_storage/file_management_widget.dart b/hesabixUI/hesabix_ui/lib/widgets/admin/file_storage/file_management_widget.dart index 7fbb2f5..4f7e012 100644 --- a/hesabixUI/hesabix_ui/lib/widgets/admin/file_storage/file_management_widget.dart +++ b/hesabixUI/hesabix_ui/lib/widgets/admin/file_storage/file_management_widget.dart @@ -1,5 +1,6 @@ import 'package:flutter/material.dart'; import 'package:hesabix_ui/l10n/app_localizations.dart'; +import '../../../core/api_client.dart'; class FileManagementWidget extends StatefulWidget { const FileManagementWidget({super.key}); @@ -36,49 +37,26 @@ class _FileManagementWidgetState extends State }); try { - // TODO: Call API to load files - await Future.delayed(const Duration(seconds: 1)); // Simulate API call + final api = ApiClient(); - setState(() { - _allFiles = [ - { - 'id': '1', - 'original_name': 'document.pdf', - 'file_size': 1024000, - 'mime_type': 'application/pdf', - 'module_context': 'tickets', - 'created_at': '2024-01-01T10:00:00Z', - 'expires_at': null, - 'is_temporary': false, - 'is_verified': true, - }, - { - 'id': '2', - 'original_name': 'image.jpg', - 'file_size': 512000, - 'mime_type': 'image/jpeg', - 'module_context': 'accounting', - 'created_at': '2024-01-02T11:00:00Z', - 'expires_at': '2024-01-09T11:00:00Z', - 'is_temporary': true, - 'is_verified': false, - }, - { - 'id': '3', - 'original_name': 'spreadsheet.xlsx', - 'file_size': 256000, - 'mime_type': 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet', - 'module_context': 'reports', - 'created_at': '2024-01-03T12:00:00Z', - 'expires_at': null, - 'is_temporary': false, - 'is_verified': true, - }, - ]; + // Call API to load files + final response = await api.get('/api/v1/admin/files/'); + final unverifiedResponse = await api.get('/api/v1/admin/files/unverified'); + + if (response.data != null && response.data['success'] == true) { + final files = response.data['data']['files'] as List; + final unverifiedFiles = unverifiedResponse.data != null && unverifiedResponse.data['success'] == true + ? unverifiedResponse.data['data']['unverified_files'] as List + : []; - _unverifiedFiles = _allFiles.where((file) => file['is_verified'] == false).toList(); - _isLoading = false; - }); + setState(() { + _allFiles = files.cast>(); + _unverifiedFiles = unverifiedFiles.cast>(); + _isLoading = false; + }); + } else { + throw Exception(response.data?['message'] ?? 'خطا در دریافت فایل‌ها'); + } } catch (e) { setState(() { _error = e.toString(); @@ -87,8 +65,9 @@ class _FileManagementWidgetState extends State } } + Future _forceDeleteFile(String fileId) async { - final l10n = AppLocalizations.of(context)!; + final l10n = AppLocalizations.of(context); final confirmed = await showDialog( context: context, builder: (context) => AlertDialog( @@ -112,17 +91,21 @@ class _FileManagementWidgetState extends State if (confirmed == true) { try { - // TODO: Call API to force delete file - await Future.delayed(const Duration(seconds: 1)); // Simulate API call + final api = ApiClient(); + final response = await api.delete('/api/v1/admin/files/$fileId'); - ScaffoldMessenger.of(context).showSnackBar( - SnackBar( - content: Text(l10n.fileDeleted), - backgroundColor: Colors.green, - ), - ); - - _loadFiles(); + if (response.data != null && response.data['success'] == true) { + ScaffoldMessenger.of(context).showSnackBar( + SnackBar( + content: Text(l10n.fileDeleted), + backgroundColor: Colors.green, + ), + ); + + _loadFiles(); + } else { + throw Exception(response.data?['message'] ?? 'خطا در حذف فایل'); + } } catch (e) { ScaffoldMessenger.of(context).showSnackBar( SnackBar( @@ -135,7 +118,7 @@ class _FileManagementWidgetState extends State } Future _restoreFile(String fileId) async { - final l10n = AppLocalizations.of(context)!; + final l10n = AppLocalizations.of(context); final confirmed = await showDialog( context: context, builder: (context) => AlertDialog( @@ -156,17 +139,21 @@ class _FileManagementWidgetState extends State if (confirmed == true) { try { - // TODO: Call API to restore file - await Future.delayed(const Duration(seconds: 1)); // Simulate API call + final api = ApiClient(); + final response = await api.put('/api/v1/admin/files/$fileId/restore'); - ScaffoldMessenger.of(context).showSnackBar( - SnackBar( - content: Text(l10n.fileRestored), - backgroundColor: Colors.green, - ), - ); - - _loadFiles(); + if (response.data != null && response.data['success'] == true) { + ScaffoldMessenger.of(context).showSnackBar( + SnackBar( + content: Text(l10n.fileRestored), + backgroundColor: Colors.green, + ), + ); + + _loadFiles(); + } else { + throw Exception(response.data?['message'] ?? 'خطا در بازیابی فایل'); + } } catch (e) { ScaffoldMessenger.of(context).showSnackBar( SnackBar( @@ -192,8 +179,7 @@ class _FileManagementWidgetState extends State @override Widget build(BuildContext context) { - final l10n = AppLocalizations.of(context)!; - final theme = Theme.of(context); + final l10n = AppLocalizations.of(context); return Column( children: [ @@ -224,7 +210,7 @@ class _FileManagementWidgetState extends State } Widget _buildFilesList(List> files) { - final l10n = AppLocalizations.of(context)!; + final l10n = AppLocalizations.of(context); final theme = Theme.of(context); if (_isLoading) { @@ -337,7 +323,7 @@ class _FileManagementWidgetState extends State children: [ Icon(Icons.delete, color: theme.colorScheme.error), const SizedBox(width: 8), - Text(AppLocalizations.of(context)!.forceDelete), + Text(AppLocalizations.of(context).forceDelete), ], ), ), @@ -347,7 +333,7 @@ class _FileManagementWidgetState extends State children: [ Icon(Icons.restore, color: theme.colorScheme.primary), const SizedBox(width: 8), - Text(AppLocalizations.of(context)!.restoreFile), + Text(AppLocalizations.of(context).restoreFile), ], ), ), diff --git a/hesabixUI/hesabix_ui/lib/widgets/admin/file_storage/file_statistics_widget.dart b/hesabixUI/hesabix_ui/lib/widgets/admin/file_storage/file_statistics_widget.dart index b2a0774..e518073 100644 --- a/hesabixUI/hesabix_ui/lib/widgets/admin/file_storage/file_statistics_widget.dart +++ b/hesabixUI/hesabix_ui/lib/widgets/admin/file_storage/file_statistics_widget.dart @@ -1,5 +1,6 @@ import 'package:flutter/material.dart'; import 'package:hesabix_ui/l10n/app_localizations.dart'; +import '../../../core/api_client.dart'; class FileStatisticsWidget extends StatefulWidget { const FileStatisticsWidget({super.key}); @@ -26,18 +27,17 @@ class _FileStatisticsWidgetState extends State { }); try { - // TODO: Call API to load statistics - await Future.delayed(const Duration(seconds: 1)); // Simulate API call + final api = ApiClient(); + final response = await api.get('/api/v1/admin/files/statistics'); - setState(() { - _statistics = { - 'total_files': 1250, - 'total_size': 2048576000, // 2GB in bytes - 'temporary_files': 45, - 'unverified_files': 12, - }; - _isLoading = false; - }); + if (response.data != null && response.data['success'] == true) { + setState(() { + _statistics = response.data['data']; + _isLoading = false; + }); + } else { + throw Exception(response.data?['message'] ?? 'خطا در دریافت آمار'); + } } catch (e) { setState(() { _error = e.toString(); @@ -46,8 +46,9 @@ class _FileStatisticsWidgetState extends State { } } + Future _cleanupTemporaryFiles() async { - final l10n = AppLocalizations.of(context)!; + final l10n = AppLocalizations.of(context); final confirmed = await showDialog( context: context, builder: (context) => AlertDialog( @@ -68,17 +69,21 @@ class _FileStatisticsWidgetState extends State { if (confirmed == true) { try { - // TODO: Call API to cleanup temporary files - await Future.delayed(const Duration(seconds: 2)); // Simulate API call + final api = ApiClient(); + final response = await api.post('/api/v1/admin/files/cleanup-temporary'); - ScaffoldMessenger.of(context).showSnackBar( - SnackBar( - content: Text(l10n.cleanupCompleted), - backgroundColor: Colors.green, - ), - ); - - _loadStatistics(); + if (response.data != null && response.data['success'] == true) { + ScaffoldMessenger.of(context).showSnackBar( + SnackBar( + content: Text(l10n.cleanupCompleted), + backgroundColor: Colors.green, + ), + ); + + _loadStatistics(); + } else { + throw Exception(response.data?['message'] ?? 'خطا در پاکسازی فایل‌های موقت'); + } } catch (e) { ScaffoldMessenger.of(context).showSnackBar( SnackBar( @@ -90,6 +95,7 @@ class _FileStatisticsWidgetState extends State { } } + String _formatFileSize(int bytes) { if (bytes < 1024) return '$bytes B'; if (bytes < 1024 * 1024) return '${(bytes / 1024).toStringAsFixed(1)} KB'; @@ -99,7 +105,7 @@ class _FileStatisticsWidgetState extends State { @override Widget build(BuildContext context) { - final l10n = AppLocalizations.of(context)!; + final l10n = AppLocalizations.of(context); final theme = Theme.of(context); if (_isLoading) { diff --git a/hesabixUI/hesabix_ui/lib/widgets/admin/file_storage/storage_config_card.dart b/hesabixUI/hesabix_ui/lib/widgets/admin/file_storage/storage_config_card.dart index 4d0ea6c..5fb62b2 100644 --- a/hesabixUI/hesabix_ui/lib/widgets/admin/file_storage/storage_config_card.dart +++ b/hesabixUI/hesabix_ui/lib/widgets/admin/file_storage/storage_config_card.dart @@ -19,165 +19,129 @@ class StorageConfigCard extends StatelessWidget { @override Widget build(BuildContext context) { - final l10n = AppLocalizations.of(context)!; + final l10n = AppLocalizations.of(context); final theme = Theme.of(context); final isDefault = config['is_default'] == true; final isActive = config['is_active'] == true; - final storageType = config['storage_type'] as String; return Card( + elevation: 2, child: Padding( padding: const EdgeInsets.all(16), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ + // Header Row( children: [ Icon( - storageType == 'local' ? Icons.storage : Icons.cloud_upload, + _getStorageIcon(config['storage_type']), color: theme.colorScheme.primary, + size: 24, ), - const SizedBox(width: 8), + const SizedBox(width: 12), Expanded( - child: Text( - config['name'] ?? '', - style: theme.textTheme.titleMedium?.copyWith( - fontWeight: FontWeight.bold, - ), - ), - ), - if (isDefault) - Container( - padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 4), - decoration: BoxDecoration( - color: theme.colorScheme.primary, - borderRadius: BorderRadius.circular(12), - ), - child: Text( - l10n.isDefault, - style: theme.textTheme.labelSmall?.copyWith( - color: theme.colorScheme.onPrimary, + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + config['name'] ?? 'Unknown', + style: theme.textTheme.titleMedium?.copyWith( + fontWeight: FontWeight.bold, + ), ), - ), - ), - if (!isActive) - Container( - margin: const EdgeInsets.only(left: 8), - padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 4), - decoration: BoxDecoration( - color: theme.colorScheme.error, - borderRadius: BorderRadius.circular(12), - ), - child: Text( - 'Inactive', - style: theme.textTheme.labelSmall?.copyWith( - color: theme.colorScheme.onError, + const SizedBox(height: 4), + Text( + _getStorageTypeName(config['storage_type']), + style: theme.textTheme.bodyMedium?.copyWith( + color: theme.colorScheme.onSurface.withOpacity(0.7), + ), ), + ], + ), + ), + // Status badges + Row( + children: [ + if (isDefault) + Container( + padding: const EdgeInsets.symmetric( + horizontal: 8, + vertical: 4, + ), + decoration: BoxDecoration( + color: theme.colorScheme.primary, + borderRadius: BorderRadius.circular(12), + ), + child: Text( + l10n.isDefault, + style: theme.textTheme.bodySmall?.copyWith( + color: theme.colorScheme.onPrimary, + fontWeight: FontWeight.bold, + ), + ), + ), + const SizedBox(width: 8), + Container( + padding: const EdgeInsets.symmetric( + horizontal: 8, + vertical: 4, + ), + decoration: BoxDecoration( + color: isActive ? Colors.green : Colors.red, + borderRadius: BorderRadius.circular(12), + ), + child: Text( + isActive ? l10n.isActive : 'غیرفعال', + style: theme.textTheme.bodySmall?.copyWith( + color: Colors.white, + fontWeight: FontWeight.bold, + ), + ), ), - ), - ], - ), - const SizedBox(height: 8), - Row( - children: [ - Icon( - Icons.info_outline, - size: 16, - color: theme.colorScheme.onSurface.withOpacity(0.6), - ), - const SizedBox(width: 4), - Text( - l10n.storageType, - style: theme.textTheme.bodySmall?.copyWith( - color: theme.colorScheme.onSurface.withOpacity(0.6), - ), - ), - const SizedBox(width: 8), - Text( - storageType == 'local' ? l10n.localStorage : l10n.ftpStorage, - style: theme.textTheme.bodySmall, + ], ), ], ), - const SizedBox(height: 4), - if (storageType == 'local') ...[ - Row( - children: [ - Icon( - Icons.folder_outlined, - size: 16, - color: theme.colorScheme.onSurface.withOpacity(0.6), - ), - const SizedBox(width: 4), - Text( - l10n.basePath, - style: theme.textTheme.bodySmall?.copyWith( - color: theme.colorScheme.onSurface.withOpacity(0.6), - ), - ), - const SizedBox(width: 8), - Expanded( - child: Text( - config['config_data']?['base_path'] ?? '', - style: theme.textTheme.bodySmall, - overflow: TextOverflow.ellipsis, - ), - ), - ], - ), - ] else if (storageType == 'ftp') ...[ - Row( - children: [ - Icon( - Icons.cloud_outlined, - size: 16, - color: theme.colorScheme.onSurface.withOpacity(0.6), - ), - const SizedBox(width: 4), - Text( - '${l10n.ftpHost}: ${config['config_data']?['host'] ?? ''}', - style: theme.textTheme.bodySmall, - ), - ], - ), - const SizedBox(height: 2), - Row( - children: [ - Icon( - Icons.person_outline, - size: 16, - color: theme.colorScheme.onSurface.withOpacity(0.6), - ), - const SizedBox(width: 4), - Text( - '${l10n.ftpUsername}: ${config['config_data']?['username'] ?? ''}', - style: theme.textTheme.bodySmall, - ), - ], - ), - ], - const SizedBox(height: 12), + + const SizedBox(height: 16), + + // Configuration details + _buildConfigDetails(context, config), + + const SizedBox(height: 16), + + // Actions Row( + mainAxisAlignment: MainAxisAlignment.end, children: [ - if (onEdit != null) - TextButton.icon( - onPressed: onEdit, - icon: const Icon(Icons.edit, size: 16), - label: Text(l10n.edit), - ), if (onTestConnection != null) TextButton.icon( onPressed: onTestConnection, icon: const Icon(Icons.wifi_protected_setup, size: 16), label: Text(l10n.testConnection), ), - if (onSetDefault != null) + if (onEdit != null) ...[ + const SizedBox(width: 8), + TextButton.icon( + onPressed: onEdit, + icon: const Icon(Icons.edit, size: 16), + label: Text(l10n.edit), + ), + ], + if (onSetDefault != null) ...[ + const SizedBox(width: 8), TextButton.icon( onPressed: onSetDefault, icon: const Icon(Icons.star, size: 16), label: Text(l10n.setAsDefault), + style: TextButton.styleFrom( + foregroundColor: theme.colorScheme.primary, + ), ), - if (onDelete != null) + ], + if (onDelete != null) ...[ + const SizedBox(width: 8), TextButton.icon( onPressed: onDelete, icon: const Icon(Icons.delete, size: 16), @@ -186,6 +150,7 @@ class StorageConfigCard extends StatelessWidget { foregroundColor: theme.colorScheme.error, ), ), + ], ], ), ], @@ -193,4 +158,116 @@ class StorageConfigCard extends StatelessWidget { ), ); } -} + + Widget _buildConfigDetails(BuildContext context, Map config) { + final l10n = AppLocalizations.of(context); + final configData = config['config_data'] ?? {}; + final storageType = config['storage_type']; + + if (storageType == 'local') { + return Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + _buildDetailRow( + context, + l10n.basePath, + configData['base_path'] ?? 'N/A', + Icons.folder, + ), + ], + ); + } else if (storageType == 'ftp') { + return Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + _buildDetailRow( + context, + l10n.ftpHost, + configData['host'] ?? 'N/A', + Icons.dns, + ), + const SizedBox(height: 8), + _buildDetailRow( + context, + l10n.ftpPort, + configData['port']?.toString() ?? 'N/A', + Icons.settings_ethernet, + ), + const SizedBox(height: 8), + _buildDetailRow( + context, + l10n.ftpUsername, + configData['username'] ?? 'N/A', + Icons.person, + ), + const SizedBox(height: 8), + _buildDetailRow( + context, + l10n.ftpDirectory, + configData['directory'] ?? 'N/A', + Icons.folder, + ), + ], + ); + } + + return const SizedBox.shrink(); + } + + Widget _buildDetailRow( + BuildContext context, + String label, + String value, + IconData icon, + ) { + final theme = Theme.of(context); + + return Row( + children: [ + Icon( + icon, + size: 16, + color: theme.colorScheme.onSurface.withOpacity(0.6), + ), + const SizedBox(width: 8), + Text( + '$label: ', + style: theme.textTheme.bodyMedium?.copyWith( + fontWeight: FontWeight.w500, + ), + ), + Expanded( + child: Text( + value, + style: theme.textTheme.bodyMedium?.copyWith( + color: theme.colorScheme.onSurface.withOpacity(0.8), + ), + overflow: TextOverflow.ellipsis, + ), + ), + ], + ); + } + + IconData _getStorageIcon(String storageType) { + switch (storageType) { + case 'local': + return Icons.storage; + case 'ftp': + return Icons.cloud_upload; + default: + return Icons.storage; + } + } + + String _getStorageTypeName(String storageType) { + switch (storageType) { + case 'local': + return 'Local Storage'; + case 'ftp': + return 'FTP Storage'; + default: + return 'Unknown Storage'; + } + } +} \ No newline at end of file diff --git a/hesabixUI/hesabix_ui/lib/widgets/admin/file_storage/storage_config_form_dialog.dart b/hesabixUI/hesabix_ui/lib/widgets/admin/file_storage/storage_config_form_dialog.dart index 033b3b5..9ae900b 100644 --- a/hesabixUI/hesabix_ui/lib/widgets/admin/file_storage/storage_config_form_dialog.dart +++ b/hesabixUI/hesabix_ui/lib/widgets/admin/file_storage/storage_config_form_dialog.dart @@ -1,5 +1,6 @@ import 'package:flutter/material.dart'; import 'package:hesabix_ui/l10n/app_localizations.dart'; +import '../../../core/api_client.dart'; class StorageConfigFormDialog extends StatefulWidget { final Map? config; @@ -94,19 +95,24 @@ class _StorageConfigFormDialogState extends State { }); try { - // TODO: Call API to save configuration - await Future.delayed(const Duration(seconds: 1)); // Simulate API call + final api = ApiClient(); + final response = await api.post( + '/api/v1/admin/files/storage-configs/', + data: { + 'name': _nameController.text, + 'storage_type': _selectedStorageType, + 'is_default': _isDefault, + 'is_active': _isActive, + 'config_data': _buildConfigData(), + }, + ); - final configData = { - 'name': _nameController.text, - 'storage_type': _selectedStorageType, - 'is_default': _isDefault, - 'is_active': _isActive, - 'config_data': _buildConfigData(), - }; - - if (mounted) { - Navigator.of(context).pop(configData); + if (response.data != null && response.data['success'] == true) { + if (mounted) { + Navigator.of(context).pop(response.data['data']); + } + } else { + throw Exception(response.data?['message'] ?? 'خطا در ذخیره تنظیمات'); } } catch (e) { if (mounted) { @@ -128,7 +134,7 @@ class _StorageConfigFormDialogState extends State { @override Widget build(BuildContext context) { - final l10n = AppLocalizations.of(context)!; + final l10n = AppLocalizations.of(context); final theme = Theme.of(context); final isEditing = widget.config != null; diff --git a/hesabixUI/hesabix_ui/lib/widgets/admin/file_storage/storage_config_list_widget.dart b/hesabixUI/hesabix_ui/lib/widgets/admin/file_storage/storage_config_list_widget.dart index f077ed5..b1186b7 100644 --- a/hesabixUI/hesabix_ui/lib/widgets/admin/file_storage/storage_config_list_widget.dart +++ b/hesabixUI/hesabix_ui/lib/widgets/admin/file_storage/storage_config_list_widget.dart @@ -2,6 +2,7 @@ import 'package:flutter/material.dart'; import 'package:hesabix_ui/l10n/app_localizations.dart'; import 'package:hesabix_ui/widgets/admin/file_storage/storage_config_form_dialog.dart'; import 'package:hesabix_ui/widgets/admin/file_storage/storage_config_card.dart'; +import '../../../core/api_client.dart'; class StorageConfigListWidget extends StatefulWidget { const StorageConfigListWidget({super.key}); @@ -28,41 +29,18 @@ class _StorageConfigListWidgetState extends State { }); try { - // TODO: Call API to load storage configurations - await Future.delayed(const Duration(seconds: 1)); // Simulate API call + final api = ApiClient(); + final response = await api.get('/api/v1/admin/files/storage-configs/'); - // Mock data for now - setState(() { - _storageConfigs = [ - { - 'id': '1', - 'name': 'Local Storage Default', - 'storage_type': 'local', - 'is_default': true, - 'is_active': true, - 'config_data': { - 'base_path': '/var/hesabix/files' - }, - 'created_at': '2024-01-01T00:00:00Z', - }, - { - 'id': '2', - 'name': 'FTP Backup', - 'storage_type': 'ftp', - 'is_default': false, - 'is_active': true, - 'config_data': { - 'host': 'ftp.example.com', - 'port': 21, - 'username': 'hesabix', - 'password': '***', - 'directory': '/hesabix/files' - }, - 'created_at': '2024-01-02T00:00:00Z', - }, - ]; - _isLoading = false; - }); + if (response.data != null && response.data['success'] == true) { + setState(() { + _storageConfigs = (response.data['data']['configs'] as List) + .cast>(); + _isLoading = false; + }); + } else { + throw Exception(response.data?['message'] ?? 'خطا در دریافت تنظیمات ذخیره‌سازی'); + } } catch (e) { setState(() { _error = e.toString(); @@ -71,6 +49,7 @@ class _StorageConfigListWidgetState extends State { } } + Future _addStorageConfig() async { final result = await showDialog>( context: context, @@ -94,7 +73,7 @@ class _StorageConfigListWidgetState extends State { } Future _setAsDefault(String configId) async { - final l10n = AppLocalizations.of(context)!; + final l10n = AppLocalizations.of(context); try { // TODO: Call API to set as default await Future.delayed(const Duration(seconds: 1)); // Simulate API call @@ -118,29 +97,44 @@ class _StorageConfigListWidgetState extends State { } Future _testConnection(String configId) async { - final l10n = AppLocalizations.of(context)!; + final l10n = AppLocalizations.of(context); try { - // TODO: Call API to test connection - await Future.delayed(const Duration(seconds: 2)); // Simulate API call + final api = ApiClient(); + final response = await api.post('/api/v1/admin/files/storage-configs/$configId/test'); - ScaffoldMessenger.of(context).showSnackBar( - SnackBar( - content: Text(l10n.connectionSuccessful), - backgroundColor: Colors.green, - ), - ); + if (response.data != null && response.data['success'] == true) { + final testResult = response.data['data']['test_result']; + if (testResult['success'] == true) { + ScaffoldMessenger.of(context).showSnackBar( + SnackBar( + content: Text(l10n.connectionSuccessful), + backgroundColor: Colors.green, + ), + ); + } else { + ScaffoldMessenger.of(context).showSnackBar( + SnackBar( + content: Text('${l10n.connectionFailed}: ${testResult['error']}'), + backgroundColor: Colors.red, + ), + ); + } + } else { + throw Exception(response.data?['message'] ?? 'خطا در تست اتصال'); + } } catch (e) { ScaffoldMessenger.of(context).showSnackBar( SnackBar( - content: Text(l10n.connectionFailed), + content: Text('${l10n.connectionFailed}: $e'), backgroundColor: Colors.red, ), ); } } + Future _deleteConfig(String configId) async { - final l10n = AppLocalizations.of(context)!; + final l10n = AppLocalizations.of(context); final confirmed = await showDialog( context: context, builder: (context) => AlertDialog( @@ -185,7 +179,7 @@ class _StorageConfigListWidgetState extends State { @override Widget build(BuildContext context) { - final l10n = AppLocalizations.of(context)!; + final l10n = AppLocalizations.of(context); final theme = Theme.of(context); if (_isLoading) {