diff --git a/LINUX_SCRIPTS_README.md b/LINUX_SCRIPTS_README.md new file mode 100644 index 0000000..39718ad --- /dev/null +++ b/LINUX_SCRIPTS_README.md @@ -0,0 +1,170 @@ +# اسکریپت‌های Linux برای Hesabix + +این فایل شامل اسکریپت‌های مفید برای اجرا و build کردن اپلیکیشن Flutter در Linux است. + +## اسکریپت‌های موجود + +### 1. `run_linux.sh` - اجرای اپلیکیشن در Linux + +اسکریپت اصلی برای اجرای اپلیکیشن Flutter در Linux Desktop. + +**استفاده:** +```bash +./run_linux.sh [options] +``` + +**گزینه‌ها:** +- `--project PATH`: مسیر پروژه Flutter (اختیاری) +- `--mode MODE`: نوع اجرا (debug/profile/release) - پیش‌فرض: debug +- `--build-dir DIR`: مسیر build directory - پیش‌فرض: build/linux +- `--clean`: پاک کردن build directory قبل از اجرا +- `--install-deps`: نصب وابستگی‌ها قبل از اجرا +- `--api-base-url URL`: آدرس پایه API +- `--help`: نمایش راهنما + +**نمونه‌های استفاده:** +```bash +# اجرای ساده +./run_linux.sh + +# اجرا در حالت release +./run_linux.sh --mode release + +# اجرا با پاک کردن build directory +./run_linux.sh --clean --mode debug + +# اجرا با نصب وابستگی‌ها +./run_linux.sh --install-deps + +# اجرا با API base URL +./run_linux.sh --api-base-url http://localhost:8000 +``` + +### 2. `build_linux.sh` - Build کردن اپلیکیشن برای Linux + +اسکریپت برای ایجاد executable مستقل از اپلیکیشن Flutter. + +**استفاده:** +```bash +./build_linux.sh [options] +``` + +**گزینه‌ها:** +- `--project PATH`: مسیر پروژه Flutter (اختیاری) +- `--mode MODE`: نوع build (debug/profile/release) - پیش‌فرض: release +- `--build-dir DIR`: مسیر build directory - پیش‌فرض: build/linux +- `--output-dir DIR`: مسیر خروجی نهایی - پیش‌فرض: build/linux_release +- `--clean`: پاک کردن build directory قبل از build +- `--install-deps`: نصب وابستگی‌ها قبل از build +- `--api-base-url URL`: آدرس پایه API +- `--archive`: ایجاد فایل tar.gz از خروجی +- `--help`: نمایش راهنما + +**نمونه‌های استفاده:** +```bash +# Build ساده +./build_linux.sh + +# Build در حالت debug +./build_linux.sh --mode debug + +# Build با ایجاد archive +./build_linux.sh --archive + +# Build کامل با پاک کردن و نصب وابستگی‌ها +./build_linux.sh --clean --install-deps --archive +``` + +## وابستگی‌های مورد نیاز + +قبل از استفاده از این اسکریپت‌ها، مطمئن شوید که وابستگی‌های زیر نصب شده‌اند: + +### Ubuntu/Debian: +```bash +sudo apt update +sudo apt install libgtk-3-dev cmake ninja-build +``` + +### Fedora/RHEL: +```bash +sudo dnf install gtk3-devel cmake ninja-build +``` + +### Arch Linux: +```bash +sudo pacman -S gtk3 cmake ninja +``` + +## نصب Flutter + +اگر Flutter نصب نیست، می‌توانید از snap استفاده کنید: + +```bash +sudo snap install flutter --classic +``` + +یا از سایت رسمی Flutter دانلود کنید: +https://flutter.dev/docs/get-started/install/linux + +## ویژگی‌های جدید + +### نصب خودکار وابستگی‌ها +اسکریپت‌ها به‌طور خودکار وابستگی‌های مورد نیاز را تشخیص داده و نصب می‌کنند: +- GTK+3 development libraries +- CMake +- Ninja build system +- Clang C++ compiler +- Build essential tools + +### رفع مشکلات platform-specific +- مشکلات مربوط به `dart:html` (که فقط در web platform موجود است) به‌طور خودکار رفع می‌شوند +- توابع download برای Linux desktop به‌روزرسانی می‌شوند +- فایل‌های اصلی پس از اجرا بازیابی می‌شوند + +### پشتیبانی از توزیع‌های مختلف +- Ubuntu/Debian (apt) +- Fedora/RHEL (dnf) +- Arch Linux (pacman) + +## نکات مهم + +1. **مسیر پروژه**: اسکریپت‌ها به‌طور خودکار پروژه Flutter را در `hesabixUI/hesabix_ui` پیدا می‌کنند. + +2. **Mirror تنظیمات**: اسکریپت‌ها از mirror چینی برای حل مشکل دسترسی به pub.dev استفاده می‌کنند. + +3. **Build Directory**: فایل‌های build شده در `build/linux` ذخیره می‌شوند. + +4. **خروجی نهایی**: فایل‌های قابل اجرا در `build/linux_release` (یا مسیر مشخص شده) قرار می‌گیرند. + +5. **اجرای فایل نهایی**: پس از build، می‌توانید فایل `hesabix_ui` را در مسیر خروجی اجرا کنید. + +6. **بازیابی خودکار**: فایل‌های اصلی پس از اجرا یا build به حالت اولیه بازمی‌گردند. + +## عیب‌یابی + +### خطای "Flutter یافت نشد" +- مطمئن شوید Flutter نصب شده است +- مسیر Flutter را به PATH اضافه کنید +- از snap استفاده کنید: `sudo snap install flutter --classic` + +### خطای "GTK+3 development libraries یافت نشد" +- وابستگی‌های GTK را نصب کنید (به بخش وابستگی‌ها مراجعه کنید) + +### خطای "CMake یافت نشد" +- CMake را نصب کنید: `sudo apt install cmake` (Ubuntu/Debian) + +### خطای build +- از `--clean` استفاده کنید تا build directory پاک شود +- از `--install-deps` استفاده کنید تا وابستگی‌ها نصب شوند + +### خطای "dart:html is not available" +- این خطا به‌طور خودکار توسط اسکریپت رفع می‌شود +- اگر همچنان رخ داد، از `--clean` استفاده کنید + +### خطای "deprecated-literal-operator" +- این خطا مربوط به flutter_secure_storage_linux است +- اسکریپت به‌طور خودکار compiler flags را تنظیم می‌کند + +### خطای "Could not find compiler" +- اسکریپت به‌طور خودکار clang و build-essential را نصب می‌کند +- اگر همچنان رخ داد، دستی نصب کنید: `sudo apt install clang build-essential` diff --git a/build_linux.sh b/build_linux.sh new file mode 100755 index 0000000..678818f --- /dev/null +++ b/build_linux.sh @@ -0,0 +1,303 @@ +#!/usr/bin/env bash + +set -euo pipefail + +# Build script for Flutter Linux Desktop in this repo. +# Creates a standalone executable for Linux. + +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +REPO_ROOT="$SCRIPT_DIR" + +DEFAULT_MODE="release" # debug|profile|release +DEFAULT_BUILD_DIR="build/linux" +DEFAULT_OUTPUT_DIR="build/linux_release" + +USER_PROJECT="" +MODE="$DEFAULT_MODE" +BUILD_DIR="" +OUTPUT_DIR="" +CLEAN_BUILD=false +INSTALL_DEPS=false +API_BASE_URL="" +CREATE_ARCHIVE=false + +print_usage() { + cat <] [--mode ] [--build-dir ] [--output-dir ] [--clean] [--install-deps] [--api-base-url ] [--archive] [--help] + +Options: + --project PATH مسیر پروژه فلاتر (حاوی pubspec.yaml). در صورت عدم تعیین، به‌صورت خودکار تشخیص می‌شود. + --mode MODE نوع build: debug، profile یا release (پیش‌فرض: $DEFAULT_MODE). + --build-dir DIR مسیر build directory (پیش‌فرض: $DEFAULT_BUILD_DIR). + --output-dir DIR مسیر خروجی نهایی (پیش‌فرض: $DEFAULT_OUTPUT_DIR). + --clean پاک کردن build directory قبل از build. + --install-deps نصب وابستگی‌ها قبل از build. + --api-base-url آدرس پایه API که به برنامه به‌صورت --dart-define پاس داده می‌شود. + --archive ایجاد فایل tar.gz از خروجی. + -h, --help نمایش راهنما. + +نمونه اجرا: + ./build_linux.sh + ./build_linux.sh --mode debug --clean + ./build_linux.sh --project hesabixUI/hesabix_ui --archive + ./build_linux.sh --api-base-url http://localhost:8000 --mode release --archive +EOF +} + +warn() { echo "[warn] $*" >&2; } +die() { echo "[error] $*" >&2; exit 1; } + +cmd_exists() { command -v "$1" >/dev/null 2>&1; } + +ensure_flutter_in_path() { + if cmd_exists flutter; then + return 0 + fi + local SNAP_FLUTTER_BIN="$HOME/snap/flutter/common/flutter/bin" + if [ -d "$SNAP_FLUTTER_BIN" ]; then + export PATH="$PATH:$SNAP_FLUTTER_BIN" + fi + if ! cmd_exists flutter; then + die "Flutter یافت نشد. لطفاً آن‌را نصب کرده یا PATH را تنظیم کنید. مسیر پیشنهادی: $SNAP_FLUTTER_BIN" + fi +} + +is_flutter_project_dir() { + local dir="$1" + [ -f "$dir/pubspec.yaml" ] || return 1 + # حداقل بررسی: وجود sdk: flutter در pubspec.yaml + if grep -qiE "sdk:\s*flutter" "$dir/pubspec.yaml"; then + return 0 + fi + # برخی قالب‌ها ممکن است شکل دیگری داشته باشند؛ صرف وجود pubspec را کافی بدانیم + return 0 +} + +auto_detect_project_dir() { + # اولویت: آرگومان کاربر → متغیر محیطی → مسیر متداول → جستجو در hesabixUI + if [ -n "$USER_PROJECT" ]; then + local p="$USER_PROJECT" + [ -d "$p" ] || die "مسیر پروژه موجود نیست: $p" + is_flutter_project_dir "$p" || die "pubspec.yaml معتبر در مسیر یافت نشد: $p" + echo "$(cd "$p" && pwd)" + return 0 + fi + + if [ -n "${FLUTTER_APP_DIR:-}" ]; then + local p="$FLUTTER_APP_DIR" + if [ -d "$p" ] && is_flutter_project_dir "$p"; then + echo "$(cd "$p" && pwd)" + return 0 + fi + fi + + # مسیر متداول این ریپو + local common_path="$REPO_ROOT/hesabixUI/hesabix_ui" + if [ -d "$common_path" ] && is_flutter_project_dir "$common_path"; then + echo "$common_path" + return 0 + fi + + # جستجو در hesabixUI برای نزدیک‌ترین pubspec.yaml + local search_root="$REPO_ROOT/hesabixUI" + if [ -d "$search_root" ]; then + # محدود به عمق 3 برای سرعت + local found + found=$(find "$search_root" -maxdepth 3 -type f -name pubspec.yaml 2>/dev/null | head -n 1 || true) + if [ -n "$found" ]; then + echo "$(cd "$(dirname "$found")" && pwd)" + return 0 + fi + fi + + die "پروژه فلاتر یافت نشد. لطفاً با --project مسیر را مشخص کنید." +} + +check_linux_dependencies() { + echo "بررسی وابستگی‌های Linux..." + + local missing_deps=() + + # بررسی وجود GTK development libraries + if ! pkg-config --exists gtk+-3.0; then + missing_deps+=("libgtk-3-dev") + fi + + # بررسی وجود CMake + if ! cmd_exists cmake; then + missing_deps+=("cmake") + fi + + # بررسی وجود Ninja + if ! cmd_exists ninja; then + missing_deps+=("ninja-build") + fi + + # بررسی وجود C++ compiler + if ! cmd_exists clang++; then + missing_deps+=("clang") + fi + + # بررسی وجود build-essential + if ! cmd_exists gcc; then + missing_deps+=("build-essential") + fi + + if [ ${#missing_deps[@]} -gt 0 ]; then + echo "نصب وابستگی‌های مورد نیاز..." + echo "بسته‌های مورد نیاز: ${missing_deps[*]}" + + # تشخیص توزیع Linux + if command -v apt >/dev/null 2>&1; then + # Ubuntu/Debian + echo "تشخیص توزیع: Ubuntu/Debian" + sudo apt update + sudo apt install -y "${missing_deps[@]}" + elif command -v dnf >/dev/null 2>&1; then + # Fedora/RHEL + echo "تشخیص توزیع: Fedora/RHEL" + sudo dnf install -y "${missing_deps[@]}" + elif command -v pacman >/dev/null 2>&1; then + # Arch Linux + echo "تشخیص توزیع: Arch Linux" + sudo pacman -S --noconfirm "${missing_deps[@]}" + else + die "توزیع Linux پشتیبانی شده یافت نشد. لطفاً وابستگی‌ها را به‌صورت دستی نصب کنید: ${missing_deps[*]}" + fi + + echo "وابستگی‌ها نصب شدند." + else + echo "همه وابستگی‌های مورد نیاز موجود هستند." + fi +} + +# Parse args +while [[ $# -gt 0 ]]; do + case "$1" in + --project) + [[ $# -ge 2 ]] || die "مقدار برای --project وارد نشده است" + USER_PROJECT="$2"; shift 2 ;; + --mode) + [[ $# -ge 2 ]] || die "مقدار برای --mode وارد نشده است" + MODE="$2"; shift 2 ;; + --build-dir) + [[ $# -ge 2 ]] || die "مقدار برای --build-dir وارد نشده است" + BUILD_DIR="$2"; shift 2 ;; + --output-dir) + [[ $# -ge 2 ]] || die "مقدار برای --output-dir وارد نشده است" + OUTPUT_DIR="$2"; shift 2 ;; + --clean) + CLEAN_BUILD=true; shift ;; + --install-deps) + INSTALL_DEPS=true; shift ;; + --api-base-url) + [[ $# -ge 2 ]] || die "مقدار برای --api-base-url وارد نشده است" + API_BASE_URL="$2"; shift 2 ;; + --archive) + CREATE_ARCHIVE=true; shift ;; + -h|--help) + print_usage; exit 0 ;; + *) + warn "آرگومان ناشناخته: $1"; shift ;; + esac +done + +case "$MODE" in + debug|profile|release) ;; + *) die "mode نامعتبر است: $MODE (مجاز: debug|profile|release)" ;; +esac + +ensure_flutter_in_path +check_linux_dependencies + +APP_DIR="$(auto_detect_project_dir)" + +if [ -z "$BUILD_DIR" ]; then + BUILD_DIR="$DEFAULT_BUILD_DIR" +fi + +if [ -z "$OUTPUT_DIR" ]; then + OUTPUT_DIR="$DEFAULT_OUTPUT_DIR" +fi + +# تبدیل به مسیر مطلق +BUILD_DIR="$(cd "$APP_DIR" && realpath -m "$BUILD_DIR")" +OUTPUT_DIR="$(cd "$APP_DIR" && realpath -m "$OUTPUT_DIR")" + +echo "ریشه ریپو: $REPO_ROOT" +echo "مسیر پروژه: $APP_DIR" +echo "حالت: $MODE" +echo "مسیر build: $BUILD_DIR" +echo "مسیر خروجی: $OUTPUT_DIR" + +cd "$APP_DIR" + +# تنظیم mirror برای حل مشکل دسترسی به pub.dev +export PUB_HOSTED_URL="https://pub.flutter-io.cn" +export FLUTTER_STORAGE_BASE_URL="https://storage.flutter-io.cn" + +# تنظیم C++ compiler flags برای حل مشکل deprecated warnings +export CXXFLAGS="-Wno-deprecated-literal-operator" +export CFLAGS="-Wno-deprecated-literal-operator" + +# نصب وابستگی‌ها در صورت درخواست +if [ "$INSTALL_DEPS" = true ]; then + echo "نصب وابستگی‌ها..." + flutter pub get +fi + +# پاک کردن build directory در صورت درخواست +if [ "$CLEAN_BUILD" = true ]; then + echo "پاک کردن build directory..." + rm -rf "$BUILD_DIR" +fi + +# تنظیم آرگومان‌های dart-define +DART_DEFINE_ARGS=() +if [ -n "$API_BASE_URL" ]; then + DART_DEFINE_ARGS+=(--dart-define "API_BASE_URL=$API_BASE_URL") +fi + +# Build کردن Flutter برای Linux +echo "Build کردن Flutter برای Linux..." +echo "دستور: flutter build linux --$MODE ${DART_DEFINE_ARGS[*]:-}" + +flutter build linux --"$MODE" ${DART_DEFINE_ARGS[@]:-} + +# کپی کردن فایل‌های build شده به مسیر خروجی +echo "کپی کردن فایل‌های build شده..." +rm -rf "$OUTPUT_DIR" +mkdir -p "$OUTPUT_DIR" + +# کپی کردن bundle از build directory +if [ -d "$BUILD_DIR/x64/$MODE/bundle" ]; then + cp -r "$BUILD_DIR/x64/$MODE/bundle"/* "$OUTPUT_DIR/" + echo "فایل‌های build شده در مسیر زیر کپی شدند: $OUTPUT_DIR" +else + die "مسیر bundle یافت نشد: $BUILD_DIR/x64/$MODE/bundle" +fi + +# ایجاد فایل اجرایی +EXECUTABLE_NAME="hesabix_ui" +if [ -f "$OUTPUT_DIR/$EXECUTABLE_NAME" ]; then + chmod +x "$OUTPUT_DIR/$EXECUTABLE_NAME" + echo "فایل اجرایی: $OUTPUT_DIR/$EXECUTABLE_NAME" +else + warn "فایل اجرایی یافت نشد: $OUTPUT_DIR/$EXECUTABLE_NAME" +fi + +# ایجاد archive در صورت درخواست +if [ "$CREATE_ARCHIVE" = true ]; then + ARCHIVE_NAME="hesabix_ui_linux_${MODE}_$(date +%Y%m%d_%H%M%S).tar.gz" + ARCHIVE_PATH="$(dirname "$OUTPUT_DIR")/$ARCHIVE_NAME" + + echo "ایجاد archive: $ARCHIVE_PATH" + cd "$(dirname "$OUTPUT_DIR")" + tar -czf "$ARCHIVE_PATH" "$(basename "$OUTPUT_DIR")" + + echo "Archive ایجاد شد: $ARCHIVE_PATH" + echo "برای اجرا: tar -xzf $ARCHIVE_NAME && cd $(basename "$OUTPUT_DIR") && ./$EXECUTABLE_NAME" +fi + +echo "Build کامل شد!" +echo "برای اجرا: cd $OUTPUT_DIR && ./$EXECUTABLE_NAME" diff --git a/hesabixAPI/adapters/api/v1/admin/file_storage.py b/hesabixAPI/adapters/api/v1/admin/file_storage.py index a8d9e9d..462df1e 100644 --- a/hesabixAPI/adapters/api/v1/admin/file_storage.py +++ b/hesabixAPI/adapters/api/v1/admin/file_storage.py @@ -427,7 +427,7 @@ async def set_default_storage_config( @router.delete("/storage-configs/{config_id}", response_model=dict) async def delete_storage_config( - config_id: UUID, + config_id: str, request: Request, db: Session = Depends(get_db), current_user: AuthContext = Depends(get_current_user), @@ -435,8 +435,28 @@ async def delete_storage_config( ): """حذف تنظیمات ذخیره‌سازی""" try: + # Check permission + if not current_user.has_app_permission("admin.storage.delete"): + raise ApiError( + code="FORBIDDEN", + message=translator.t("FORBIDDEN", "دسترسی غیرمجاز"), + http_status=403, + translator=translator + ) + config_repo = StorageConfigRepository(db) - success = await config_repo.delete_config(config_id) + + # بررسی وجود فایل‌ها قبل از حذف + file_count = config_repo.count_files_by_storage_config(config_id) + if file_count > 0: + raise ApiError( + code="STORAGE_CONFIG_HAS_FILES", + message=translator.t("STORAGE_CONFIG_HAS_FILES", f"این تنظیمات ذخیره‌سازی دارای {file_count} فایل است و قابل حذف نیست"), + http_status=400, + translator=translator + ) + + success = config_repo.delete_config(config_id) if not success: raise ApiError( @@ -590,21 +610,131 @@ async def _test_local_storage(config: StorageConfig) -> dict: async def _test_ftp_storage(config: StorageConfig) -> dict: """تست اتصال به FTP storage""" + import ftplib + import tempfile + import os from datetime import datetime try: - # TODO: پیاده‌سازی تست FTP - # فعلاً فقط ساختار کلی را برمی‌گردانیم + # دریافت تنظیمات FTP + config_data = config.config_data + host = config_data.get("host") + port = int(config_data.get("port", 21)) + username = config_data.get("username") + password = config_data.get("password") + directory = config_data.get("directory", "/") + use_tls = config_data.get("use_tls", False) + + # بررسی وجود پارامترهای ضروری + if not all([host, username, password]): + return { + "success": False, + "error": "پارامترهای ضروری FTP (host, username, password) موجود نیست", + "storage_type": "ftp", + "tested_at": datetime.utcnow().isoformat() + } + + # اتصال به FTP + if use_tls: + ftp = ftplib.FTP_TLS() + else: + ftp = ftplib.FTP() + + # تنظیم timeout + ftp.connect(host, port, timeout=10) + ftp.login(username, password) + + # تغییر به دایرکتوری مورد نظر + if directory and directory != "/": + try: + ftp.cwd(directory) + except ftplib.error_perm: + return { + "success": False, + "error": f"دسترسی به دایرکتوری {directory} وجود ندارد", + "storage_type": "ftp", + "tested_at": datetime.utcnow().isoformat() + } + + # تست نوشتن فایل + test_filename = f"test_connection_{datetime.utcnow().timestamp()}.txt" + test_content = "Test FTP connection file" + + # ایجاد فایل موقت + with tempfile.NamedTemporaryFile(mode='w', delete=False, suffix='.txt') as temp_file: + temp_file.write(test_content) + temp_file_path = temp_file.name + + try: + # آپلود فایل + with open(temp_file_path, 'rb') as file: + ftp.storbinary(f'STOR {test_filename}', file) + + # بررسی وجود فایل + file_list = [] + ftp.retrlines('LIST', file_list.append) + file_exists = any(test_filename in line for line in file_list) + + if not file_exists: + return { + "success": False, + "error": "فایل تست آپلود نشد", + "storage_type": "ftp", + "tested_at": datetime.utcnow().isoformat() + } + + # حذف فایل تست + try: + ftp.delete(test_filename) + except ftplib.error_perm: + pass # اگر نتوانست حذف کند، مهم نیست + + # بستن اتصال + ftp.quit() + + return { + "success": True, + "message": "اتصال به FTP server موفقیت‌آمیز بود", + "storage_type": "ftp", + "host": host, + "port": port, + "directory": directory, + "use_tls": use_tls, + "tested_at": datetime.utcnow().isoformat() + } + + finally: + # حذف فایل موقت + try: + os.unlink(temp_file_path) + except: + pass + + except ftplib.error_perm as e: return { "success": False, - "error": "تست FTP هنوز پیاده‌سازی نشده است", + "error": f"خطا در احراز هویت FTP: {str(e)}", + "storage_type": "ftp", + "tested_at": datetime.utcnow().isoformat() + } + except ftplib.error_temp as e: + return { + "success": False, + "error": f"خطای موقت FTP: {str(e)}", + "storage_type": "ftp", + "tested_at": datetime.utcnow().isoformat() + } + except ConnectionRefusedError: + 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)}", + "storage_type": "ftp", "tested_at": datetime.utcnow().isoformat() } diff --git a/hesabixAPI/adapters/db/repositories/file_storage_repository.py b/hesabixAPI/adapters/db/repositories/file_storage_repository.py index 8966ab1..a3ba233 100644 --- a/hesabixAPI/adapters/db/repositories/file_storage_repository.py +++ b/hesabixAPI/adapters/db/repositories/file_storage_repository.py @@ -236,7 +236,15 @@ class StorageConfigRepository(BaseRepository[StorageConfig]): self.db.query(StorageConfig).update({"is_default": False}) self.db.commit() - async def delete_config(self, config_id: UUID) -> bool: + def count_files_by_storage_config(self, config_id: str) -> int: + """شمارش تعداد فایل‌های مربوط به یک storage config""" + return self.db.query(FileStorage).filter( + FileStorage.storage_config_id == config_id, + FileStorage.is_active == True, + FileStorage.deleted_at.is_(None) + ).count() + + def delete_config(self, config_id: str) -> bool: config = self.db.query(StorageConfig).filter(StorageConfig.id == config_id).first() if not config: return False diff --git a/hesabixUI/hesabix_ui/lib/l10n/app_en.arb b/hesabixUI/hesabix_ui/lib/l10n/app_en.arb index 282bf91..a0623f8 100644 --- a/hesabixUI/hesabix_ui/lib/l10n/app_en.arb +++ b/hesabixUI/hesabix_ui/lib/l10n/app_en.arb @@ -396,6 +396,23 @@ "previous": "Previous", "next": "Next", "first": "First", - "last": "Last" + "last": "Last", + "systemSettingsWelcome": "System Settings", + "systemSettingsDescription": "Manage system configuration and administration", + "storageManagement": "Storage Management", + "storageManagementDescription": "Configure file storage systems and manage files", + "systemConfiguration": "System Configuration", + "systemConfigurationDescription": "General system settings and preferences", + "userManagement": "User Management", + "userManagementDescription": "Manage users, roles and permissions", + "systemLogs": "System Logs", + "systemLogsDescription": "View system logs and monitoring", + "backToSettings": "Back to Settings", + "settingsOverview": "Settings Overview", + "availableSettings": "Available Settings", + "systemAdministration": "System Administration", + "generalSettings": "General Settings", + "securitySettings": "Security Settings", + "maintenanceSettings": "Maintenance Settings" } diff --git a/hesabixUI/hesabix_ui/lib/l10n/app_fa.arb b/hesabixUI/hesabix_ui/lib/l10n/app_fa.arb index 8e33397..d84148b 100644 --- a/hesabixUI/hesabix_ui/lib/l10n/app_fa.arb +++ b/hesabixUI/hesabix_ui/lib/l10n/app_fa.arb @@ -395,6 +395,23 @@ "previous": "قبلی", "next": "بعدی", "first": "اول", - "last": "آخر" + "last": "آخر", + "systemSettingsWelcome": "تنظیمات سیستم", + "systemSettingsDescription": "مدیریت پیکربندی و مدیریت سیستم", + "storageManagement": "مدیریت ذخیره‌سازی", + "storageManagementDescription": "پیکربندی سیستم‌های ذخیره‌سازی فایل و مدیریت فایل‌ها", + "systemConfiguration": "پیکربندی سیستم", + "systemConfigurationDescription": "تنظیمات عمومی سیستم و ترجیحات", + "userManagement": "مدیریت کاربران", + "userManagementDescription": "مدیریت کاربران، نقش‌ها و مجوزها", + "systemLogs": "لاگ‌های سیستم", + "systemLogsDescription": "مشاهده لاگ‌های سیستم و نظارت", + "backToSettings": "بازگشت به تنظیمات", + "settingsOverview": "نمای کلی تنظیمات", + "availableSettings": "تنظیمات موجود", + "systemAdministration": "مدیریت سیستم", + "generalSettings": "تنظیمات عمومی", + "securitySettings": "تنظیمات امنیتی", + "maintenanceSettings": "تنظیمات نگهداری" } diff --git a/hesabixUI/hesabix_ui/lib/l10n/app_localizations.dart b/hesabixUI/hesabix_ui/lib/l10n/app_localizations.dart index 1e2e0c7..b8e92a5 100644 --- a/hesabixUI/hesabix_ui/lib/l10n/app_localizations.dart +++ b/hesabixUI/hesabix_ui/lib/l10n/app_localizations.dart @@ -2215,6 +2215,108 @@ abstract class AppLocalizations { /// In en, this message translates to: /// **'Last'** String get last; + + /// No description provided for @systemSettingsWelcome. + /// + /// In en, this message translates to: + /// **'System Settings'** + String get systemSettingsWelcome; + + /// No description provided for @systemSettingsDescription. + /// + /// In en, this message translates to: + /// **'Manage system configuration and administration'** + String get systemSettingsDescription; + + /// No description provided for @storageManagement. + /// + /// In en, this message translates to: + /// **'Storage Management'** + String get storageManagement; + + /// No description provided for @storageManagementDescription. + /// + /// In en, this message translates to: + /// **'Configure file storage systems and manage files'** + String get storageManagementDescription; + + /// No description provided for @systemConfiguration. + /// + /// In en, this message translates to: + /// **'System Configuration'** + String get systemConfiguration; + + /// No description provided for @systemConfigurationDescription. + /// + /// In en, this message translates to: + /// **'General system settings and preferences'** + String get systemConfigurationDescription; + + /// No description provided for @userManagement. + /// + /// In en, this message translates to: + /// **'User Management'** + String get userManagement; + + /// No description provided for @userManagementDescription. + /// + /// In en, this message translates to: + /// **'Manage users, roles and permissions'** + String get userManagementDescription; + + /// No description provided for @systemLogs. + /// + /// In en, this message translates to: + /// **'System Logs'** + String get systemLogs; + + /// No description provided for @systemLogsDescription. + /// + /// In en, this message translates to: + /// **'View system logs and monitoring'** + String get systemLogsDescription; + + /// No description provided for @backToSettings. + /// + /// In en, this message translates to: + /// **'Back to Settings'** + String get backToSettings; + + /// No description provided for @settingsOverview. + /// + /// In en, this message translates to: + /// **'Settings Overview'** + String get settingsOverview; + + /// No description provided for @availableSettings. + /// + /// In en, this message translates to: + /// **'Available Settings'** + String get availableSettings; + + /// No description provided for @systemAdministration. + /// + /// In en, this message translates to: + /// **'System Administration'** + String get systemAdministration; + + /// No description provided for @generalSettings. + /// + /// In en, this message translates to: + /// **'General Settings'** + String get generalSettings; + + /// No description provided for @securitySettings. + /// + /// In en, this message translates to: + /// **'Security Settings'** + String get securitySettings; + + /// No description provided for @maintenanceSettings. + /// + /// In en, this message translates to: + /// **'Maintenance Settings'** + String get maintenanceSettings; } class _AppLocalizationsDelegate diff --git a/hesabixUI/hesabix_ui/lib/l10n/app_localizations_en.dart b/hesabixUI/hesabix_ui/lib/l10n/app_localizations_en.dart index 45cabef..620e769 100644 --- a/hesabixUI/hesabix_ui/lib/l10n/app_localizations_en.dart +++ b/hesabixUI/hesabix_ui/lib/l10n/app_localizations_en.dart @@ -1097,4 +1097,58 @@ class AppLocalizationsEn extends AppLocalizations { @override String get last => 'Last'; + + @override + String get systemSettingsWelcome => 'System Settings'; + + @override + String get systemSettingsDescription => + 'Manage system configuration and administration'; + + @override + String get storageManagement => 'Storage Management'; + + @override + String get storageManagementDescription => + 'Configure file storage systems and manage files'; + + @override + String get systemConfiguration => 'System Configuration'; + + @override + String get systemConfigurationDescription => + 'General system settings and preferences'; + + @override + String get userManagement => 'User Management'; + + @override + String get userManagementDescription => 'Manage users, roles and permissions'; + + @override + String get systemLogs => 'System Logs'; + + @override + String get systemLogsDescription => 'View system logs and monitoring'; + + @override + String get backToSettings => 'Back to Settings'; + + @override + String get settingsOverview => 'Settings Overview'; + + @override + String get availableSettings => 'Available Settings'; + + @override + String get systemAdministration => 'System Administration'; + + @override + String get generalSettings => 'General Settings'; + + @override + String get securitySettings => 'Security Settings'; + + @override + String get maintenanceSettings => 'Maintenance Settings'; } diff --git a/hesabixUI/hesabix_ui/lib/l10n/app_localizations_fa.dart b/hesabixUI/hesabix_ui/lib/l10n/app_localizations_fa.dart index e8e2e63..80b4154 100644 --- a/hesabixUI/hesabix_ui/lib/l10n/app_localizations_fa.dart +++ b/hesabixUI/hesabix_ui/lib/l10n/app_localizations_fa.dart @@ -1091,4 +1091,56 @@ class AppLocalizationsFa extends AppLocalizations { @override String get last => 'آخر'; + + @override + String get systemSettingsWelcome => 'تنظیمات سیستم'; + + @override + String get systemSettingsDescription => 'مدیریت پیکربندی و مدیریت سیستم'; + + @override + String get storageManagement => 'مدیریت ذخیره‌سازی'; + + @override + String get storageManagementDescription => + 'پیکربندی سیستم‌های ذخیره‌سازی فایل و مدیریت فایل‌ها'; + + @override + String get systemConfiguration => 'پیکربندی سیستم'; + + @override + String get systemConfigurationDescription => 'تنظیمات عمومی سیستم و ترجیحات'; + + @override + String get userManagement => 'مدیریت کاربران'; + + @override + String get userManagementDescription => 'مدیریت کاربران، نقش‌ها و مجوزها'; + + @override + String get systemLogs => 'لاگ‌های سیستم'; + + @override + String get systemLogsDescription => 'مشاهده لاگ‌های سیستم و نظارت'; + + @override + String get backToSettings => 'بازگشت به تنظیمات'; + + @override + String get settingsOverview => 'نمای کلی تنظیمات'; + + @override + String get availableSettings => 'تنظیمات موجود'; + + @override + String get systemAdministration => 'مدیریت سیستم'; + + @override + String get generalSettings => 'تنظیمات عمومی'; + + @override + String get securitySettings => 'تنظیمات امنیتی'; + + @override + String get maintenanceSettings => 'تنظیمات نگهداری'; } diff --git a/hesabixUI/hesabix_ui/lib/main.dart b/hesabixUI/hesabix_ui/lib/main.dart index d5770ef..2f87463 100644 --- a/hesabixUI/hesabix_ui/lib/main.dart +++ b/hesabixUI/hesabix_ui/lib/main.dart @@ -13,6 +13,10 @@ import 'pages/profile/change_password_page.dart'; import 'pages/profile/marketing_page.dart'; import 'pages/profile/operator/operator_tickets_page.dart'; import 'pages/system_settings_page.dart'; +import 'pages/admin/storage_management_page.dart'; +import 'pages/admin/system_configuration_page.dart'; +import 'pages/admin/user_management_page.dart'; +import 'pages/admin/system_logs_page.dart'; import 'package:hesabix_ui/l10n/app_localizations.dart'; import 'core/locale_controller.dart'; import 'core/calendar_controller.dart'; @@ -386,6 +390,48 @@ class _MyAppState extends State { } return const SystemSettingsPage(); }, + routes: [ + GoRoute( + path: 'storage', + name: 'system_settings_storage', + builder: (context, state) { + if (_authStore == null || !_authStore!.isSuperAdmin) { + return PermissionGuard.buildAccessDeniedPage(); + } + return const AdminStorageManagementPage(); + }, + ), + GoRoute( + path: 'configuration', + name: 'system_settings_configuration', + builder: (context, state) { + if (_authStore == null || !_authStore!.isSuperAdmin) { + return PermissionGuard.buildAccessDeniedPage(); + } + return const SystemConfigurationPage(); + }, + ), + GoRoute( + path: 'users', + name: 'system_settings_users', + builder: (context, state) { + if (_authStore == null || !_authStore!.isSuperAdmin) { + return PermissionGuard.buildAccessDeniedPage(); + } + return const UserManagementPage(); + }, + ), + GoRoute( + path: 'logs', + name: 'system_settings_logs', + builder: (context, state) { + if (_authStore == null || !_authStore!.isSuperAdmin) { + return PermissionGuard.buildAccessDeniedPage(); + } + return const SystemLogsPage(); + }, + ), + ], ), ], ), diff --git a/hesabixUI/hesabix_ui/lib/pages/admin/file_storage_settings_page.dart b/hesabixUI/hesabix_ui/lib/pages/admin/file_storage_settings_page.dart index 735f829..2396c82 100644 --- a/hesabixUI/hesabix_ui/lib/pages/admin/file_storage_settings_page.dart +++ b/hesabixUI/hesabix_ui/lib/pages/admin/file_storage_settings_page.dart @@ -1,70 +1,12 @@ import 'package:flutter/material.dart'; import 'package:hesabix_ui/l10n/app_localizations.dart'; -import 'package:hesabix_ui/widgets/admin/file_storage/storage_config_list_widget.dart'; -import 'package:hesabix_ui/widgets/admin/file_storage/file_statistics_widget.dart'; -import 'package:hesabix_ui/widgets/admin/file_storage/file_management_widget.dart'; +import 'package:hesabix_ui/widgets/admin/file_storage/storage_management_page.dart'; -class FileStorageSettingsPage extends StatefulWidget { +class FileStorageSettingsPage extends StatelessWidget { const FileStorageSettingsPage({super.key}); - @override - State createState() => _FileStorageSettingsPageState(); -} - -class _FileStorageSettingsPageState extends State - with TickerProviderStateMixin { - late TabController _tabController; - - @override - void initState() { - super.initState(); - _tabController = TabController(length: 3, vsync: this); - } - - @override - void dispose() { - _tabController.dispose(); - super.dispose(); - } - @override Widget build(BuildContext context) { - final l10n = AppLocalizations.of(context)!; - - return Scaffold( - appBar: AppBar( - title: Text(l10n.fileStorageSettings), - bottom: TabBar( - controller: _tabController, - tabs: [ - Tab( - icon: const Icon(Icons.storage), - text: l10n.storageConfigurations, - ), - Tab( - icon: const Icon(Icons.analytics), - text: l10n.fileStatistics, - ), - Tab( - icon: const Icon(Icons.folder), - text: l10n.fileManagement, - ), - ], - ), - ), - body: TabBarView( - controller: _tabController, - children: [ - // Storage Configurations Tab - const StorageConfigListWidget(), - - // File Statistics Tab - const FileStatisticsWidget(), - - // File Management Tab - const FileManagementWidget(), - ], - ), - ); + return const StorageManagementPage(); } -} +} \ No newline at end of file diff --git a/hesabixUI/hesabix_ui/lib/pages/admin/storage_management_page.dart b/hesabixUI/hesabix_ui/lib/pages/admin/storage_management_page.dart new file mode 100644 index 0000000..1c18c7a --- /dev/null +++ b/hesabixUI/hesabix_ui/lib/pages/admin/storage_management_page.dart @@ -0,0 +1,83 @@ +import 'package:flutter/material.dart'; +import 'package:go_router/go_router.dart'; +import 'package:hesabix_ui/l10n/app_localizations.dart'; +import 'package:hesabix_ui/widgets/admin/file_storage/storage_config_list_widget.dart'; +import 'package:hesabix_ui/widgets/admin/file_storage/storage_config_form_dialog.dart'; + +class AdminStorageManagementPage extends StatefulWidget { + const AdminStorageManagementPage({super.key}); + + @override + State createState() => _AdminStorageManagementPageState(); +} + +class _AdminStorageManagementPageState extends State { + final GlobalKey _listKey = GlobalKey(); + + @override + Widget build(BuildContext context) { + final theme = Theme.of(context); + final t = AppLocalizations.of(context); + + return Scaffold( + appBar: AppBar( + title: Text( + t.storageManagement, + style: theme.textTheme.headlineSmall?.copyWith( + fontWeight: FontWeight.bold, + color: theme.colorScheme.onPrimary, + ), + ), + backgroundColor: theme.colorScheme.primary, + foregroundColor: theme.colorScheme.onPrimary, + elevation: 0, + leading: IconButton( + icon: const Icon(Icons.arrow_back), + onPressed: () => context.go('/user/profile/system-settings'), + ), + ), + body: Container( + decoration: BoxDecoration( + gradient: LinearGradient( + begin: Alignment.topCenter, + end: Alignment.bottomCenter, + colors: [ + theme.colorScheme.primary.withOpacity(0.1), + theme.colorScheme.surface, + ], + ), + ), + child: StorageConfigListWidget( + key: _listKey, + onRefresh: () => _listKey.currentState?.loadStorageConfigs(), + ), + ), + floatingActionButton: FloatingActionButton.extended( + onPressed: () => _showCreateDialog(context), + icon: const Icon(Icons.add), + label: Text(t.addStorageConfig), + backgroundColor: theme.colorScheme.primary, + foregroundColor: theme.colorScheme.onPrimary, + ), + ); + } + + void _showCreateDialog(BuildContext context) { + showDialog( + context: context, + builder: (context) => StorageConfigFormDialog( + onSaved: () { + // Refresh the list + _listKey.currentState?.loadStorageConfigs(); + // Show success message + ScaffoldMessenger.of(context).showSnackBar( + SnackBar( + content: Text('${AppLocalizations.of(context).addStorageConfig} ${AppLocalizations.of(context).save}'), + backgroundColor: Colors.green, + ), + ); + }, + ), + ); + } +} diff --git a/hesabixUI/hesabix_ui/lib/pages/admin/system_configuration_page.dart b/hesabixUI/hesabix_ui/lib/pages/admin/system_configuration_page.dart new file mode 100644 index 0000000..246ea9e --- /dev/null +++ b/hesabixUI/hesabix_ui/lib/pages/admin/system_configuration_page.dart @@ -0,0 +1,346 @@ +import 'package:flutter/material.dart'; +import 'package:go_router/go_router.dart'; +import 'package:hesabix_ui/l10n/app_localizations.dart'; + +class SystemConfigurationPage extends StatefulWidget { + const SystemConfigurationPage({super.key}); + + @override + State createState() => _SystemConfigurationPageState(); +} + +class _SystemConfigurationPageState extends State { + final _formKey = GlobalKey(); + bool _isLoading = false; + + // Configuration values + String _appName = 'Hesabix'; + String _appVersion = '1.0.0'; + String _defaultLanguage = 'fa'; + String _defaultTheme = 'system'; + bool _enableRegistration = true; + bool _enableEmailVerification = true; + bool _enableMaintenanceMode = false; + int _sessionTimeout = 30; + int _maxFileSize = 10; + int _maxUsers = 1000; + + @override + Widget build(BuildContext context) { + final theme = Theme.of(context); + final t = AppLocalizations.of(context); + + return Scaffold( + appBar: AppBar( + title: Text( + t.systemConfiguration, + style: theme.textTheme.headlineSmall?.copyWith( + fontWeight: FontWeight.bold, + color: theme.colorScheme.onPrimary, + ), + ), + backgroundColor: theme.colorScheme.primary, + foregroundColor: theme.colorScheme.onPrimary, + elevation: 0, + leading: IconButton( + icon: const Icon(Icons.arrow_back), + onPressed: () => context.go('/user/profile/system-settings'), + ), + actions: [ + TextButton( + onPressed: _isLoading ? null : _saveConfiguration, + child: _isLoading + ? const SizedBox( + width: 20, + height: 20, + child: CircularProgressIndicator(strokeWidth: 2), + ) + : Text(t.save), + ), + ], + ), + body: Container( + decoration: BoxDecoration( + gradient: LinearGradient( + begin: Alignment.topCenter, + end: Alignment.bottomCenter, + colors: [ + theme.colorScheme.primary.withOpacity(0.1), + theme.colorScheme.surface, + ], + ), + ), + child: SingleChildScrollView( + padding: const EdgeInsets.all(24), + child: Form( + key: _formKey, + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + _buildSectionCard( + theme, + t.generalSettings, + Icons.settings_outlined, + [ + _buildTextField( + label: 'Application Name', + value: _appName, + onChanged: (value) => setState(() => _appName = value), + ), + _buildTextField( + label: 'Application Version', + value: _appVersion, + onChanged: (value) => setState(() => _appVersion = value), + ), + _buildDropdownField( + label: 'Default Language', + value: _defaultLanguage, + items: const [ + DropdownMenuItem(value: 'fa', child: Text('فارسی')), + DropdownMenuItem(value: 'en', child: Text('English')), + ], + onChanged: (value) => setState(() => _defaultLanguage = value!), + ), + _buildDropdownField( + label: 'Default Theme', + value: _defaultTheme, + items: const [ + DropdownMenuItem(value: 'system', child: Text('System')), + DropdownMenuItem(value: 'light', child: Text('Light')), + DropdownMenuItem(value: 'dark', child: Text('Dark')), + ], + onChanged: (value) => setState(() => _defaultTheme = value!), + ), + ], + ), + const SizedBox(height: 24), + _buildSectionCard( + theme, + t.securitySettings, + Icons.security_outlined, + [ + _buildSwitchField( + label: 'Enable User Registration', + value: _enableRegistration, + onChanged: (value) => setState(() => _enableRegistration = value), + ), + _buildSwitchField( + label: 'Enable Email Verification', + value: _enableEmailVerification, + onChanged: (value) => setState(() => _enableEmailVerification = value), + ), + _buildNumberField( + label: 'Session Timeout (minutes)', + value: _sessionTimeout, + onChanged: (value) => setState(() => _sessionTimeout = value), + min: 5, + max: 1440, + ), + ], + ), + const SizedBox(height: 24), + _buildSectionCard( + theme, + t.maintenanceSettings, + Icons.build_outlined, + [ + _buildSwitchField( + label: 'Maintenance Mode', + value: _enableMaintenanceMode, + onChanged: (value) => setState(() => _enableMaintenanceMode = value), + ), + _buildNumberField( + label: 'Max File Size (MB)', + value: _maxFileSize, + onChanged: (value) => setState(() => _maxFileSize = value), + min: 1, + max: 1000, + ), + _buildNumberField( + label: 'Max Users', + value: _maxUsers, + onChanged: (value) => setState(() => _maxUsers = value), + min: 1, + max: 10000, + ), + ], + ), + ], + ), + ), + ), + ), + ); + } + + Widget _buildSectionCard(ThemeData theme, String title, IconData icon, List children) { + return Card( + elevation: 2, + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(16), + ), + child: Padding( + padding: const EdgeInsets.all(24), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Row( + children: [ + Icon(icon, color: theme.colorScheme.primary), + const SizedBox(width: 12), + Text( + title, + style: theme.textTheme.titleLarge?.copyWith( + fontWeight: FontWeight.bold, + color: theme.colorScheme.onSurface, + ), + ), + ], + ), + const SizedBox(height: 20), + ...children, + ], + ), + ), + ); + } + + Widget _buildTextField({ + required String label, + required String value, + required ValueChanged onChanged, + }) { + return Padding( + padding: const EdgeInsets.only(bottom: 16), + child: TextFormField( + initialValue: value, + decoration: InputDecoration( + labelText: label, + border: const OutlineInputBorder(), + ), + onChanged: onChanged, + ), + ); + } + + Widget _buildDropdownField({ + required String label, + required String value, + required List> items, + required ValueChanged onChanged, + }) { + return Padding( + padding: const EdgeInsets.only(bottom: 16), + child: DropdownButtonFormField( + value: value, + decoration: InputDecoration( + labelText: label, + border: const OutlineInputBorder(), + ), + items: items, + onChanged: onChanged, + ), + ); + } + + Widget _buildSwitchField({ + required String label, + required bool value, + required ValueChanged onChanged, + }) { + return Padding( + padding: const EdgeInsets.only(bottom: 16), + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Text( + label, + style: Theme.of(context).textTheme.bodyLarge, + ), + Switch( + value: value, + onChanged: onChanged, + ), + ], + ), + ); + } + + Widget _buildNumberField({ + required String label, + required int value, + required ValueChanged onChanged, + required int min, + required int max, + }) { + return Padding( + padding: const EdgeInsets.only(bottom: 16), + child: Row( + children: [ + Expanded( + child: TextFormField( + initialValue: value.toString(), + decoration: InputDecoration( + labelText: label, + border: const OutlineInputBorder(), + ), + keyboardType: TextInputType.number, + onChanged: (value) { + final intValue = int.tryParse(value); + if (intValue != null && intValue >= min && intValue <= max) { + onChanged(intValue); + } + }, + ), + ), + const SizedBox(width: 16), + Column( + children: [ + IconButton( + onPressed: value < max ? () => onChanged(value + 1) : null, + icon: const Icon(Icons.add), + ), + IconButton( + onPressed: value > min ? () => onChanged(value - 1) : null, + icon: const Icon(Icons.remove), + ), + ], + ), + ], + ), + ); + } + + Future _saveConfiguration() async { + if (!_formKey.currentState!.validate()) return; + + setState(() => _isLoading = true); + + try { + // Simulate API call + await Future.delayed(const Duration(seconds: 2)); + + if (mounted) { + ScaffoldMessenger.of(context).showSnackBar( + SnackBar( + content: Text(AppLocalizations.of(context).save), + backgroundColor: Colors.green, + ), + ); + } + } catch (e) { + if (mounted) { + ScaffoldMessenger.of(context).showSnackBar( + SnackBar( + content: Text('Error: $e'), + backgroundColor: Colors.red, + ), + ); + } + } finally { + if (mounted) { + setState(() => _isLoading = false); + } + } + } +} diff --git a/hesabixUI/hesabix_ui/lib/pages/admin/system_logs_page.dart b/hesabixUI/hesabix_ui/lib/pages/admin/system_logs_page.dart new file mode 100644 index 0000000..765ebf1 --- /dev/null +++ b/hesabixUI/hesabix_ui/lib/pages/admin/system_logs_page.dart @@ -0,0 +1,357 @@ +import 'package:flutter/material.dart'; +import 'package:go_router/go_router.dart'; +import 'package:hesabix_ui/l10n/app_localizations.dart'; + +class SystemLogsPage extends StatefulWidget { + const SystemLogsPage({super.key}); + + @override + State createState() => _SystemLogsPageState(); +} + +class _SystemLogsPageState extends State { + final _searchController = TextEditingController(); + String _selectedLevel = 'all'; + String _selectedDateRange = 'today'; + bool _isLoading = false; + + // Mock log data + final List> _logs = [ + { + 'id': 1, + 'timestamp': '2024-01-15 10:30:25', + 'level': 'info', + 'message': 'User login successful', + 'module': 'auth', + 'userId': 123, + 'ip': '192.168.1.100', + }, + { + 'id': 2, + 'timestamp': '2024-01-15 10:25:10', + 'level': 'warning', + 'message': 'Failed login attempt', + 'module': 'auth', + 'userId': null, + 'ip': '192.168.1.101', + }, + { + 'id': 3, + 'timestamp': '2024-01-15 10:20:05', + 'level': 'error', + 'message': 'Database connection timeout', + 'module': 'database', + 'userId': null, + 'ip': null, + }, + { + 'id': 4, + 'timestamp': '2024-01-15 10:15:30', + 'level': 'info', + 'message': 'File uploaded successfully', + 'module': 'storage', + 'userId': 123, + 'ip': '192.168.1.100', + }, + { + 'id': 5, + 'timestamp': '2024-01-15 10:10:15', + 'level': 'debug', + 'message': 'API request processed', + 'module': 'api', + 'userId': 456, + 'ip': '192.168.1.102', + }, + ]; + + List> get _filteredLogs { + return _logs.where((log) { + final matchesSearch = log['message'].toString().toLowerCase() + .contains(_searchController.text.toLowerCase()) || + log['module'].toString().toLowerCase() + .contains(_searchController.text.toLowerCase()); + + final matchesLevel = _selectedLevel == 'all' || log['level'] == _selectedLevel; + + return matchesSearch && matchesLevel; + }).toList(); + } + + @override + Widget build(BuildContext context) { + final theme = Theme.of(context); + final t = AppLocalizations.of(context); + + return Scaffold( + appBar: AppBar( + title: Text( + t.systemLogs, + style: theme.textTheme.headlineSmall?.copyWith( + fontWeight: FontWeight.bold, + color: theme.colorScheme.onPrimary, + ), + ), + backgroundColor: theme.colorScheme.primary, + foregroundColor: theme.colorScheme.onPrimary, + elevation: 0, + leading: IconButton( + icon: const Icon(Icons.arrow_back), + onPressed: () => context.go('/user/profile/system-settings'), + ), + actions: [ + IconButton( + onPressed: _refreshLogs, + icon: const Icon(Icons.refresh), + ), + IconButton( + onPressed: _exportLogs, + icon: const Icon(Icons.download), + ), + ], + ), + body: Container( + decoration: BoxDecoration( + gradient: LinearGradient( + begin: Alignment.topCenter, + end: Alignment.bottomCenter, + colors: [ + theme.colorScheme.primary.withOpacity(0.1), + theme.colorScheme.surface, + ], + ), + ), + child: Column( + children: [ + _buildFilters(theme, t), + Expanded( + child: _isLoading + ? const Center(child: CircularProgressIndicator()) + : _buildLogsList(theme, t), + ), + ], + ), + ), + ); + } + + Widget _buildFilters(ThemeData theme, AppLocalizations t) { + return Container( + padding: const EdgeInsets.all(16), + child: Column( + children: [ + TextField( + controller: _searchController, + decoration: InputDecoration( + hintText: 'Search logs...', + prefixIcon: const Icon(Icons.search), + suffixIcon: _searchController.text.isNotEmpty + ? IconButton( + icon: const Icon(Icons.clear), + onPressed: () { + _searchController.clear(); + setState(() {}); + }, + ) + : null, + border: const OutlineInputBorder(), + ), + onChanged: (value) => setState(() {}), + ), + const SizedBox(height: 16), + Row( + children: [ + Expanded( + child: DropdownButtonFormField( + value: _selectedLevel, + decoration: const InputDecoration( + labelText: 'Log Level', + border: OutlineInputBorder(), + ), + items: const [ + DropdownMenuItem(value: 'all', child: Text('All Levels')), + DropdownMenuItem(value: 'debug', child: Text('Debug')), + DropdownMenuItem(value: 'info', child: Text('Info')), + DropdownMenuItem(value: 'warning', child: Text('Warning')), + DropdownMenuItem(value: 'error', child: Text('Error')), + ], + onChanged: (value) => setState(() => _selectedLevel = value!), + ), + ), + const SizedBox(width: 16), + Expanded( + child: DropdownButtonFormField( + value: _selectedDateRange, + decoration: const InputDecoration( + labelText: 'Date Range', + border: OutlineInputBorder(), + ), + items: const [ + DropdownMenuItem(value: 'today', child: Text('Today')), + DropdownMenuItem(value: 'yesterday', child: Text('Yesterday')), + DropdownMenuItem(value: 'week', child: Text('This Week')), + DropdownMenuItem(value: 'month', child: Text('This Month')), + ], + onChanged: (value) => setState(() => _selectedDateRange = value!), + ), + ), + ], + ), + ], + ), + ); + } + + Widget _buildLogsList(ThemeData theme, AppLocalizations t) { + final logs = _filteredLogs; + + if (logs.isEmpty) { + return Center( + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Icon( + Icons.analytics_outlined, + size: 64, + color: theme.colorScheme.onSurface.withOpacity(0.5), + ), + const SizedBox(height: 16), + Text( + 'No logs found', + style: theme.textTheme.titleLarge?.copyWith( + color: theme.colorScheme.onSurface.withOpacity(0.7), + ), + ), + ], + ), + ); + } + + return ListView.builder( + padding: const EdgeInsets.all(16), + itemCount: logs.length, + itemBuilder: (context, index) { + final log = logs[index]; + return _buildLogCard(log, theme, t); + }, + ); + } + + Widget _buildLogCard(Map log, ThemeData theme, AppLocalizations t) { + return Card( + margin: const EdgeInsets.only(bottom: 8), + child: ExpansionTile( + leading: _buildLogLevelIcon(log['level']), + title: Text( + log['message'], + style: const TextStyle(fontWeight: FontWeight.w500), + ), + subtitle: Text( + '${log['timestamp']} • ${log['module']}', + style: TextStyle( + color: theme.colorScheme.onSurface.withOpacity(0.7), + fontSize: 12, + ), + ), + children: [ + Padding( + padding: const EdgeInsets.all(16), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + _buildLogDetail('Level', log['level'].toString().toUpperCase()), + _buildLogDetail('Module', log['module']), + _buildLogDetail('Timestamp', log['timestamp']), + if (log['userId'] != null) + _buildLogDetail('User ID', log['userId'].toString()), + if (log['ip'] != null) + _buildLogDetail('IP Address', log['ip']), + const SizedBox(height: 8), + Container( + width: double.infinity, + padding: const EdgeInsets.all(12), + decoration: BoxDecoration( + color: theme.colorScheme.surfaceVariant.withOpacity(0.5), + borderRadius: BorderRadius.circular(8), + ), + child: Text( + log['message'], + style: theme.textTheme.bodyMedium, + ), + ), + ], + ), + ), + ], + ), + ); + } + + Widget _buildLogLevelIcon(String level) { + IconData icon; + Color color; + + switch (level) { + case 'error': + icon = Icons.error; + color = Colors.red; + break; + case 'warning': + icon = Icons.warning; + color = Colors.orange; + break; + case 'info': + icon = Icons.info; + color = Colors.blue; + break; + case 'debug': + icon = Icons.bug_report; + color = Colors.grey; + break; + default: + icon = Icons.circle; + color = Colors.grey; + } + + return Icon(icon, color: color, size: 20); + } + + Widget _buildLogDetail(String label, String value) { + return Padding( + padding: const EdgeInsets.only(bottom: 4), + child: Row( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + SizedBox( + width: 80, + child: Text( + '$label:', + style: const TextStyle(fontWeight: FontWeight.w500), + ), + ), + Expanded( + child: Text( + value, + style: const TextStyle(fontFamily: 'monospace'), + ), + ), + ], + ), + ); + } + + void _refreshLogs() { + setState(() => _isLoading = true); + // Simulate API call + Future.delayed(const Duration(seconds: 1), () { + if (mounted) { + setState(() => _isLoading = false); + } + }); + } + + void _exportLogs() { + ScaffoldMessenger.of(context).showSnackBar( + const SnackBar(content: Text('Export logs functionality would be implemented here')), + ); + } +} diff --git a/hesabixUI/hesabix_ui/lib/pages/admin/user_management_page.dart b/hesabixUI/hesabix_ui/lib/pages/admin/user_management_page.dart new file mode 100644 index 0000000..7450e32 --- /dev/null +++ b/hesabixUI/hesabix_ui/lib/pages/admin/user_management_page.dart @@ -0,0 +1,436 @@ +import 'package:flutter/material.dart'; +import 'package:go_router/go_router.dart'; +import 'package:hesabix_ui/l10n/app_localizations.dart'; + +class UserManagementPage extends StatefulWidget { + const UserManagementPage({super.key}); + + @override + State createState() => _UserManagementPageState(); +} + +class _UserManagementPageState extends State { + final _searchController = TextEditingController(); + String _selectedFilter = 'all'; + bool _isLoading = false; + + // Mock data - in real app, this would come from API + final List> _users = [ + { + 'id': 1, + 'name': 'احمد محمدی', + 'email': 'ahmad@example.com', + 'role': 'admin', + 'status': 'active', + 'lastLogin': '2024-01-15', + 'createdAt': '2024-01-01', + }, + { + 'id': 2, + 'name': 'فاطمه احمدی', + 'email': 'fatemeh@example.com', + 'role': 'user', + 'status': 'active', + 'lastLogin': '2024-01-14', + 'createdAt': '2024-01-02', + }, + { + 'id': 3, + 'name': 'علی رضایی', + 'email': 'ali@example.com', + 'role': 'operator', + 'status': 'inactive', + 'lastLogin': '2024-01-10', + 'createdAt': '2024-01-03', + }, + ]; + + List> get _filteredUsers { + var filtered = _users.where((user) { + final matchesSearch = user['name'].toString().toLowerCase() + .contains(_searchController.text.toLowerCase()) || + user['email'].toString().toLowerCase() + .contains(_searchController.text.toLowerCase()); + + final matchesFilter = _selectedFilter == 'all' || + user['status'] == _selectedFilter || + user['role'] == _selectedFilter; + + return matchesSearch && matchesFilter; + }).toList(); + + return filtered; + } + + @override + Widget build(BuildContext context) { + final theme = Theme.of(context); + final t = AppLocalizations.of(context); + + return Scaffold( + appBar: AppBar( + title: Text( + t.userManagement, + style: theme.textTheme.headlineSmall?.copyWith( + fontWeight: FontWeight.bold, + color: theme.colorScheme.onPrimary, + ), + ), + backgroundColor: theme.colorScheme.primary, + foregroundColor: theme.colorScheme.onPrimary, + elevation: 0, + leading: IconButton( + icon: const Icon(Icons.arrow_back), + onPressed: () => context.go('/user/profile/system-settings'), + ), + actions: [ + IconButton( + onPressed: _refreshUsers, + icon: const Icon(Icons.refresh), + ), + ], + ), + body: Container( + decoration: BoxDecoration( + gradient: LinearGradient( + begin: Alignment.topCenter, + end: Alignment.bottomCenter, + colors: [ + theme.colorScheme.primary.withOpacity(0.1), + theme.colorScheme.surface, + ], + ), + ), + child: Column( + children: [ + _buildSearchAndFilter(theme, t), + Expanded( + child: _isLoading + ? const Center(child: CircularProgressIndicator()) + : _buildUsersList(theme, t), + ), + ], + ), + ), + floatingActionButton: FloatingActionButton.extended( + onPressed: _showAddUserDialog, + icon: const Icon(Icons.person_add), + label: Text('Add User'), + backgroundColor: theme.colorScheme.primary, + foregroundColor: theme.colorScheme.onPrimary, + ), + ); + } + + Widget _buildSearchAndFilter(ThemeData theme, AppLocalizations t) { + return Container( + padding: const EdgeInsets.all(16), + child: Column( + children: [ + TextField( + controller: _searchController, + decoration: InputDecoration( + hintText: t.search, + prefixIcon: const Icon(Icons.search), + suffixIcon: _searchController.text.isNotEmpty + ? IconButton( + icon: const Icon(Icons.clear), + onPressed: () { + _searchController.clear(); + setState(() {}); + }, + ) + : null, + border: const OutlineInputBorder(), + ), + onChanged: (value) => setState(() {}), + ), + const SizedBox(height: 16), + Row( + children: [ + Expanded( + child: DropdownButtonFormField( + value: _selectedFilter, + decoration: const InputDecoration( + labelText: 'Filter', + border: OutlineInputBorder(), + ), + items: const [ + DropdownMenuItem(value: 'all', child: Text('All Users')), + DropdownMenuItem(value: 'active', child: Text('Active')), + DropdownMenuItem(value: 'inactive', child: Text('Inactive')), + DropdownMenuItem(value: 'admin', child: Text('Admins')), + DropdownMenuItem(value: 'operator', child: Text('Operators')), + DropdownMenuItem(value: 'user', child: Text('Users')), + ], + onChanged: (value) => setState(() => _selectedFilter = value!), + ), + ), + const SizedBox(width: 16), + ElevatedButton.icon( + onPressed: _exportUsers, + icon: const Icon(Icons.download), + label: const Text('Export'), + ), + ], + ), + ], + ), + ); + } + + Widget _buildUsersList(ThemeData theme, AppLocalizations t) { + final users = _filteredUsers; + + if (users.isEmpty) { + return Center( + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Icon( + Icons.people_outline, + size: 64, + color: theme.colorScheme.onSurface.withOpacity(0.5), + ), + const SizedBox(height: 16), + Text( + 'No users found', + style: theme.textTheme.titleLarge?.copyWith( + color: theme.colorScheme.onSurface.withOpacity(0.7), + ), + ), + ], + ), + ); + } + + return ListView.builder( + padding: const EdgeInsets.all(16), + itemCount: users.length, + itemBuilder: (context, index) { + final user = users[index]; + return _buildUserCard(user, theme, t); + }, + ); + } + + Widget _buildUserCard(Map user, ThemeData theme, AppLocalizations t) { + return Card( + margin: const EdgeInsets.only(bottom: 12), + child: ListTile( + leading: CircleAvatar( + backgroundColor: _getRoleColor(user['role']), + child: Text( + user['name'].toString().substring(0, 1), + style: const TextStyle(color: Colors.white, fontWeight: FontWeight.bold), + ), + ), + title: Text( + user['name'], + style: const TextStyle(fontWeight: FontWeight.bold), + ), + subtitle: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text(user['email']), + const SizedBox(height: 4), + Row( + children: [ + _buildStatusChip(user['status']), + const SizedBox(width: 8), + _buildRoleChip(user['role']), + ], + ), + ], + ), + trailing: PopupMenuButton( + itemBuilder: (context) => [ + PopupMenuItem( + value: 'edit', + child: Row( + children: [ + const Icon(Icons.edit), + const SizedBox(width: 8), + Text(t.edit), + ], + ), + ), + PopupMenuItem( + value: 'permissions', + child: Row( + children: [ + const Icon(Icons.security), + const SizedBox(width: 8), + const Text('Permissions'), + ], + ), + ), + PopupMenuItem( + value: 'delete', + child: Row( + children: [ + const Icon(Icons.delete, color: Colors.red), + const SizedBox(width: 8), + Text(t.delete, style: const TextStyle(color: Colors.red)), + ], + ), + ), + ], + onSelected: (value) => _handleUserAction(value, user), + ), + ), + ); + } + + Widget _buildStatusChip(String status) { + return Chip( + label: Text( + status.toUpperCase(), + style: const TextStyle(fontSize: 10, fontWeight: FontWeight.bold), + ), + backgroundColor: status == 'active' ? Colors.green.shade100 : Colors.red.shade100, + labelStyle: TextStyle( + color: status == 'active' ? Colors.green.shade800 : Colors.red.shade800, + ), + ); + } + + Widget _buildRoleChip(String role) { + return Chip( + label: Text( + role.toUpperCase(), + style: const TextStyle(fontSize: 10, fontWeight: FontWeight.bold), + ), + backgroundColor: _getRoleColor(role).withOpacity(0.2), + labelStyle: TextStyle(color: _getRoleColor(role)), + ); + } + + Color _getRoleColor(String role) { + switch (role) { + case 'admin': + return Colors.red; + case 'operator': + return Colors.orange; + case 'user': + return Colors.blue; + default: + return Colors.grey; + } + } + + void _handleUserAction(String action, Map user) { + switch (action) { + case 'edit': + _showEditUserDialog(user); + break; + case 'permissions': + _showPermissionsDialog(user); + break; + case 'delete': + _showDeleteUserDialog(user); + break; + } + } + + void _showAddUserDialog() { + showDialog( + context: context, + builder: (context) => AlertDialog( + title: const Text('Add New User'), + content: const Text('User creation form would go here'), + actions: [ + TextButton( + onPressed: () => Navigator.of(context).pop(), + child: Text(AppLocalizations.of(context).cancel), + ), + ElevatedButton( + onPressed: () => Navigator.of(context).pop(), + child: Text(AppLocalizations.of(context).save), + ), + ], + ), + ); + } + + void _showEditUserDialog(Map user) { + showDialog( + context: context, + builder: (context) => AlertDialog( + title: Text('Edit ${user['name']}'), + content: const Text('User edit form would go here'), + actions: [ + TextButton( + onPressed: () => Navigator.of(context).pop(), + child: Text(AppLocalizations.of(context).cancel), + ), + ElevatedButton( + onPressed: () => Navigator.of(context).pop(), + child: Text(AppLocalizations.of(context).save), + ), + ], + ), + ); + } + + void _showPermissionsDialog(Map user) { + showDialog( + context: context, + builder: (context) => AlertDialog( + title: Text('Permissions for ${user['name']}'), + content: const Text('Permissions management would go here'), + actions: [ + TextButton( + onPressed: () => Navigator.of(context).pop(), + child: Text(AppLocalizations.of(context).cancel), + ), + ElevatedButton( + onPressed: () => Navigator.of(context).pop(), + child: Text(AppLocalizations.of(context).save), + ), + ], + ), + ); + } + + void _showDeleteUserDialog(Map user) { + showDialog( + context: context, + builder: (context) => AlertDialog( + title: const Text('Delete User'), + content: Text('Are you sure you want to delete ${user['name']}?'), + actions: [ + TextButton( + onPressed: () => Navigator.of(context).pop(), + child: Text(AppLocalizations.of(context).cancel), + ), + ElevatedButton( + onPressed: () { + Navigator.of(context).pop(); + // Delete user logic here + }, + style: ElevatedButton.styleFrom(backgroundColor: Colors.red), + child: Text(AppLocalizations.of(context).delete), + ), + ], + ), + ); + } + + void _refreshUsers() { + setState(() => _isLoading = true); + // Simulate API call + Future.delayed(const Duration(seconds: 1), () { + if (mounted) { + setState(() => _isLoading = false); + } + }); + } + + void _exportUsers() { + // Export logic here + ScaffoldMessenger.of(context).showSnackBar( + const SnackBar(content: Text('Export functionality would be implemented here')), + ); + } +} diff --git a/hesabixUI/hesabix_ui/lib/pages/system_settings_page.dart b/hesabixUI/hesabix_ui/lib/pages/system_settings_page.dart index 3e4c469..5c5f83b 100644 --- a/hesabixUI/hesabix_ui/lib/pages/system_settings_page.dart +++ b/hesabixUI/hesabix_ui/lib/pages/system_settings_page.dart @@ -1,168 +1,323 @@ import 'package:flutter/material.dart'; +import 'package:go_router/go_router.dart'; import 'package:hesabix_ui/l10n/app_localizations.dart'; -import 'package:hesabix_ui/pages/admin/file_storage_settings_page.dart'; -class SystemSettingsPage extends StatelessWidget { +class SystemSettingsPage extends StatefulWidget { const SystemSettingsPage({super.key}); + @override + State createState() => _SystemSettingsPageState(); +} + +class _SystemSettingsPageState extends State { + late final List _settingsItems; + + @override + void initState() { + super.initState(); + _settingsItems = [ + SettingsItem( + title: 'storageManagement', + description: 'storageManagementDescription', + icon: Icons.cloud_upload_outlined, + color: const Color(0xFF2196F3), + route: '/user/profile/system-settings/storage', + ), + SettingsItem( + title: 'systemConfiguration', + description: 'systemConfigurationDescription', + icon: Icons.settings_outlined, + color: const Color(0xFF4CAF50), + route: '/user/profile/system-settings/configuration', + ), + SettingsItem( + title: 'userManagement', + description: 'userManagementDescription', + icon: Icons.people_outlined, + color: const Color(0xFFFF9800), + route: '/user/profile/system-settings/users', + ), + SettingsItem( + title: 'systemLogs', + description: 'systemLogsDescription', + icon: Icons.analytics_outlined, + color: const Color(0xFF9C27B0), + route: '/user/profile/system-settings/logs', + ), + ]; + } + @override Widget build(BuildContext context) { - final t = AppLocalizations.of(context)!; final theme = Theme.of(context); final colorScheme = theme.colorScheme; + final t = AppLocalizations.of(context); return Scaffold( + backgroundColor: colorScheme.surface, appBar: AppBar( - title: Text(t.systemSettings), - backgroundColor: colorScheme.surface, - foregroundColor: colorScheme.onSurface, + title: Text( + t.systemSettingsWelcome, + style: const TextStyle( + fontWeight: FontWeight.w600, + fontSize: 20, + ), + ), + backgroundColor: Colors.transparent, elevation: 0, + centerTitle: true, + actions: [ + IconButton( + onPressed: () => _showHelpDialog(context), + icon: const Icon(Icons.help_outline), + tooltip: t.systemSettingsWelcome, + ), + ], ), - body: Container( - color: colorScheme.surface, - child: SafeArea( - child: Padding( - padding: const EdgeInsets.all(16.0), + body: SingleChildScrollView( + padding: const EdgeInsets.all(20), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + _buildWelcomeSection(theme, colorScheme, t), + const SizedBox(height: 24), + _buildSettingsList(theme, colorScheme, t), + const SizedBox(height: 20), + ], + ), + ), + ); + } + + Widget _buildWelcomeSection(ThemeData theme, ColorScheme colorScheme, AppLocalizations t) { + return Container( + padding: const EdgeInsets.all(20), + decoration: BoxDecoration( + gradient: LinearGradient( + begin: Alignment.topLeft, + end: Alignment.bottomRight, + colors: [ + colorScheme.primary.withOpacity(0.1), + colorScheme.primaryContainer.withOpacity(0.3), + ], + ), + borderRadius: BorderRadius.circular(16), + border: Border.all( + color: colorScheme.primary.withOpacity(0.2), + width: 1, + ), + ), + child: Row( + children: [ + Container( + padding: const EdgeInsets.all(12), + decoration: BoxDecoration( + gradient: LinearGradient( + begin: Alignment.topLeft, + end: Alignment.bottomRight, + colors: [ + colorScheme.primary, + colorScheme.primary.withOpacity(0.8), + ], + ), + borderRadius: BorderRadius.circular(12), + boxShadow: [ + BoxShadow( + color: colorScheme.primary.withOpacity(0.3), + blurRadius: 8, + offset: const Offset(0, 2), + ), + ], + ), + child: const Icon( + Icons.admin_panel_settings_outlined, + color: Colors.white, + size: 20, + ), + ), + const SizedBox(width: 16), + Expanded( child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ - // Header - Container( - padding: const EdgeInsets.all(24), + Text( + t.systemAdministration, + style: theme.textTheme.titleLarge?.copyWith( + fontWeight: FontWeight.bold, + color: colorScheme.onSurface, + fontSize: 18, + ), + ), + const SizedBox(height: 6), + Text( + t.systemSettingsDescription, + style: theme.textTheme.bodyMedium?.copyWith( + color: colorScheme.onSurface.withOpacity(0.7), + fontSize: 14, + ), + ), + ], + ), + ), + Container( + padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 6), + decoration: BoxDecoration( + color: colorScheme.primary.withOpacity(0.1), + borderRadius: BorderRadius.circular(20), + ), + child: Text( + '${_settingsItems.length}', + style: TextStyle( + color: colorScheme.primary, + fontWeight: FontWeight.bold, + fontSize: 12, + ), + ), + ), + ], + ), + ); + } + + Widget _buildSettingsList(ThemeData theme, ColorScheme colorScheme, AppLocalizations t) { + return Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Row( + children: [ + Text( + t.availableSettings, + style: theme.textTheme.titleLarge?.copyWith( + fontWeight: FontWeight.bold, + color: colorScheme.onSurface, + ), + ), + const Spacer(), + Container( + padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 4), + decoration: BoxDecoration( + color: colorScheme.primary.withOpacity(0.1), + borderRadius: BorderRadius.circular(12), + ), + child: Text( + '${_settingsItems.length} ${t.availableSettings.toLowerCase()}', + style: TextStyle( + color: colorScheme.primary, + fontSize: 12, + fontWeight: FontWeight.w500, + ), + ), + ), + ], + ), + const SizedBox(height: 16), + LayoutBuilder( + builder: (context, constraints) { + int crossAxisCount = 3; + if (constraints.maxWidth < 600) { + crossAxisCount = 2; + } else if (constraints.maxWidth > 1200) { + crossAxisCount = 4; + } + + return GridView.builder( + shrinkWrap: true, + physics: const NeverScrollableScrollPhysics(), + gridDelegate: SliverGridDelegateWithFixedCrossAxisCount( + crossAxisCount: crossAxisCount, + crossAxisSpacing: 12, + mainAxisSpacing: 12, + childAspectRatio: 1.0, + ), + itemCount: _settingsItems.length, + itemBuilder: (context, index) { + return _buildSettingsCard(_settingsItems[index], theme, colorScheme, t); + }, + ); + }, + ), + ], + ); + } + + Widget _buildSettingsCard(SettingsItem item, ThemeData theme, ColorScheme colorScheme, AppLocalizations t) { + return Card( + elevation: 1, + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(12), + side: BorderSide( + color: colorScheme.outline.withOpacity(0.2), + width: 1, + ), + ), + child: Material( + color: Colors.transparent, + child: InkWell( + onTap: () => context.go(item.route!), + borderRadius: BorderRadius.circular(12), + hoverColor: item.color.withOpacity(0.05), + splashColor: item.color.withOpacity(0.1), + child: AnimatedContainer( + duration: const Duration(milliseconds: 200), + padding: const EdgeInsets.all(16), + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + AnimatedContainer( + duration: const Duration(milliseconds: 200), + padding: const EdgeInsets.all(12), decoration: BoxDecoration( - color: colorScheme.primaryContainer, + color: item.color.withOpacity(0.1), + borderRadius: BorderRadius.circular(8), + boxShadow: [ + BoxShadow( + color: item.color.withOpacity(0.1), + blurRadius: 4, + offset: const Offset(0, 2), + ), + ], + ), + child: Icon( + item.icon, + color: item.color, + size: 24, + ), + ), + const SizedBox(height: 12), + Text( + _getLocalizedText(t, item.title), + style: theme.textTheme.titleSmall?.copyWith( + fontWeight: FontWeight.w600, + color: colorScheme.onSurface, + fontSize: 13, + ), + textAlign: TextAlign.center, + maxLines: 1, + overflow: TextOverflow.ellipsis, + ), + const SizedBox(height: 6), + Text( + _getLocalizedText(t, item.description), + style: theme.textTheme.bodySmall?.copyWith( + color: colorScheme.onSurface.withOpacity(0.6), + fontSize: 11, + ), + textAlign: TextAlign.center, + maxLines: 2, + overflow: TextOverflow.ellipsis, + ), + const SizedBox(height: 8), + AnimatedContainer( + duration: const Duration(milliseconds: 200), + padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 4), + decoration: BoxDecoration( + color: item.color.withOpacity(0.1), borderRadius: BorderRadius.circular(12), ), - child: Row( - children: [ - Icon( - Icons.admin_panel_settings, - size: 32, - color: colorScheme.onPrimaryContainer, - ), - const SizedBox(width: 16), - Expanded( - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Text( - t.systemSettings, - style: theme.textTheme.headlineSmall?.copyWith( - color: colorScheme.onPrimaryContainer, - fontWeight: FontWeight.bold, - ), - ), - const SizedBox(height: 4), - Text( - 'تنظیمات پیشرفته سیستم - فقط برای ادمین‌ها', - style: theme.textTheme.bodyMedium?.copyWith( - color: colorScheme.onPrimaryContainer.withOpacity(0.8), - ), - ), - ], - ), - ), - ], - ), - ), - - const SizedBox(height: 24), - - // Settings Cards - Expanded( - child: GridView.count( - crossAxisCount: 3, - crossAxisSpacing: 16, - mainAxisSpacing: 16, - childAspectRatio: 1.2, - children: [ - _buildSettingCard( - context, - icon: Icons.people, - title: 'مدیریت کاربران', - subtitle: 'مدیریت کاربران سیستم', - color: Colors.blue, - ), - _buildSettingCard( - context, - icon: Icons.business, - title: 'مدیریت کسب و کارها', - subtitle: 'مدیریت کسب و کارهای ثبت شده', - color: Colors.green, - ), - _buildSettingCard( - context, - icon: Icons.security, - title: 'امنیت سیستم', - subtitle: 'تنظیمات امنیتی و دسترسی‌ها', - color: Colors.orange, - ), - _buildSettingCard( - context, - icon: Icons.analytics, - title: 'گزارش‌گیری', - subtitle: 'گزارش‌های سیستم و آمار', - color: Colors.purple, - ), - _buildSettingCard( - context, - icon: Icons.backup, - title: 'پشتیبان‌گیری', - subtitle: 'مدیریت پشتیبان‌ها', - color: Colors.teal, - ), - _buildSettingCard( - context, - icon: Icons.storage, - title: t.fileStorage, - subtitle: t.fileStorageSettings, - color: Colors.indigo, - onTap: () { - Navigator.of(context).push( - MaterialPageRoute( - builder: (context) => const FileStorageSettingsPage(), - ), - ); - }, - ), - _buildSettingCard( - context, - icon: Icons.tune, - title: 'تنظیمات پیشرفته', - subtitle: 'تنظیمات تخصصی سیستم', - color: Colors.grey, - ), - ], - ), - ), - - const SizedBox(height: 24), - - // Warning Message - Container( - padding: const EdgeInsets.all(16), - decoration: BoxDecoration( - color: Colors.amber.withOpacity(0.1), - border: Border.all(color: Colors.amber.withOpacity(0.3)), - borderRadius: BorderRadius.circular(8), - ), - child: Row( - children: [ - Icon( - Icons.warning_amber, - color: Colors.amber[700], - size: 20, - ), - const SizedBox(width: 12), - Expanded( - child: Text( - 'توجه: این بخش فقط برای ادمین‌های سیستم قابل دسترسی است. تغییرات در این بخش می‌تواند بر عملکرد کل سیستم تأثیر بگذارد.', - style: theme.textTheme.bodySmall?.copyWith( - color: Colors.amber[700], - ), - ), - ), - ], + child: Icon( + Icons.arrow_forward_ios, + size: 12, + color: item.color, ), ), ], @@ -173,77 +328,73 @@ class SystemSettingsPage extends StatelessWidget { ); } - Widget _buildSettingCard( - BuildContext context, { - required IconData icon, - required String title, - required String subtitle, - required Color color, - VoidCallback? onTap, - }) { - final theme = Theme.of(context); - final colorScheme = theme.colorScheme; + String _getLocalizedText(AppLocalizations t, String key) { + switch (key) { + case 'storageManagement': + return t.storageManagement; + case 'storageManagementDescription': + return t.storageManagementDescription; + case 'systemConfiguration': + return t.systemConfiguration; + case 'systemConfigurationDescription': + return t.systemConfigurationDescription; + case 'userManagement': + return t.userManagement; + case 'userManagementDescription': + return t.userManagementDescription; + case 'systemLogs': + return t.systemLogs; + case 'systemLogsDescription': + return t.systemLogsDescription; + default: + return key; + } + } - return Card( - elevation: 2, - shape: RoundedRectangleBorder( - borderRadius: BorderRadius.circular(12), - ), - child: InkWell( - borderRadius: BorderRadius.circular(12), - onTap: onTap ?? () { - // TODO: Navigate to specific setting - ScaffoldMessenger.of(context).showSnackBar( - SnackBar( - content: Text('$title - در حال توسعه'), - duration: const Duration(seconds: 2), - ), - ); - }, - child: Padding( - padding: const EdgeInsets.all(16), - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Icon( - icon, - color: color, - size: 32, - ), - const SizedBox(height: 12), - Text( - title, - style: theme.textTheme.titleMedium?.copyWith( - fontWeight: FontWeight.bold, - color: colorScheme.onSurface, - ), - ), - const SizedBox(height: 4), - Expanded( - child: Text( - subtitle, - style: theme.textTheme.bodySmall?.copyWith( - color: colorScheme.onSurface.withOpacity(0.7), - ), - maxLines: 2, - overflow: TextOverflow.ellipsis, - ), - ), - const SizedBox(height: 8), - Row( - mainAxisAlignment: MainAxisAlignment.end, - children: [ - Icon( - Icons.arrow_forward_ios, - size: 16, - color: colorScheme.onSurface.withOpacity(0.5), - ), - ], - ), - ], - ), + void _showHelpDialog(BuildContext context) { + final t = AppLocalizations.of(context); + showDialog( + context: context, + builder: (context) => AlertDialog( + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(16), ), + title: Row( + children: [ + Icon( + Icons.help_outline, + color: Theme.of(context).colorScheme.primary, + ), + const SizedBox(width: 8), + Text(t.systemSettingsWelcome), + ], + ), + content: Text( + t.systemSettingsDescription, + ), + actions: [ + TextButton( + onPressed: () => Navigator.of(context).pop(), + child: Text(t.ok), + ), + ], ), ); } } + +class SettingsItem { + final String title; + final String description; + final IconData icon; + final Color color; + final String? route; + + SettingsItem({ + required this.title, + required this.description, + required this.icon, + required this.color, + this.route, + }); +} \ No newline at end of file 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 deleted file mode 100644 index 4f7e012..0000000 --- a/hesabixUI/hesabix_ui/lib/widgets/admin/file_storage/file_management_widget.dart +++ /dev/null @@ -1,358 +0,0 @@ -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}); - - @override - State createState() => _FileManagementWidgetState(); -} - -class _FileManagementWidgetState extends State - with TickerProviderStateMixin { - late TabController _tabController; - List> _allFiles = []; - List> _unverifiedFiles = []; - bool _isLoading = true; - String? _error; - - @override - void initState() { - super.initState(); - _tabController = TabController(length: 2, vsync: this); - _loadFiles(); - } - - @override - void dispose() { - _tabController.dispose(); - super.dispose(); - } - - Future _loadFiles() async { - setState(() { - _isLoading = true; - _error = null; - }); - - try { - final api = ApiClient(); - - // 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 - : []; - - setState(() { - _allFiles = files.cast>(); - _unverifiedFiles = unverifiedFiles.cast>(); - _isLoading = false; - }); - } else { - throw Exception(response.data?['message'] ?? 'خطا در دریافت فایل‌ها'); - } - } catch (e) { - setState(() { - _error = e.toString(); - _isLoading = false; - }); - } - } - - - Future _forceDeleteFile(String fileId) async { - final l10n = AppLocalizations.of(context); - final confirmed = await showDialog( - context: context, - builder: (context) => AlertDialog( - title: Text(l10n.deleteConfirm), - content: Text(l10n.deleteConfirmMessage), - actions: [ - TextButton( - onPressed: () => Navigator.of(context).pop(false), - child: Text(l10n.cancel), - ), - TextButton( - onPressed: () => Navigator.of(context).pop(true), - child: Text(l10n.forceDelete), - style: TextButton.styleFrom( - foregroundColor: Theme.of(context).colorScheme.error, - ), - ), - ], - ), - ); - - if (confirmed == true) { - try { - final api = ApiClient(); - final response = await api.delete('/api/v1/admin/files/$fileId'); - - 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( - content: Text(l10n.errorDeletingFile), - backgroundColor: Colors.red, - ), - ); - } - } - } - - Future _restoreFile(String fileId) async { - final l10n = AppLocalizations.of(context); - final confirmed = await showDialog( - context: context, - builder: (context) => AlertDialog( - title: Text(l10n.restoreConfirm), - content: Text(l10n.restoreConfirmMessage), - actions: [ - TextButton( - onPressed: () => Navigator.of(context).pop(false), - child: Text(l10n.cancel), - ), - TextButton( - onPressed: () => Navigator.of(context).pop(true), - child: Text(l10n.restoreFile), - ), - ], - ), - ); - - if (confirmed == true) { - try { - final api = ApiClient(); - final response = await api.put('/api/v1/admin/files/$fileId/restore'); - - 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( - content: Text(l10n.errorRestoringFile), - backgroundColor: Colors.red, - ), - ); - } - } - } - - String _formatFileSize(int bytes) { - if (bytes < 1024) return '$bytes B'; - if (bytes < 1024 * 1024) return '${(bytes / 1024).toStringAsFixed(1)} KB'; - if (bytes < 1024 * 1024 * 1024) return '${(bytes / (1024 * 1024)).toStringAsFixed(1)} MB'; - return '${(bytes / (1024 * 1024 * 1024)).toStringAsFixed(1)} GB'; - } - - String _formatDate(String dateString) { - final date = DateTime.parse(dateString); - return '${date.day}/${date.month}/${date.year}'; - } - - @override - Widget build(BuildContext context) { - final l10n = AppLocalizations.of(context); - - return Column( - children: [ - TabBar( - controller: _tabController, - tabs: [ - Tab( - icon: const Icon(Icons.folder), - text: l10n.allFiles, - ), - Tab( - icon: const Icon(Icons.warning), - text: l10n.unverifiedFilesList, - ), - ], - ), - Expanded( - child: TabBarView( - controller: _tabController, - children: [ - _buildFilesList(_allFiles), - _buildFilesList(_unverifiedFiles), - ], - ), - ), - ], - ); - } - - Widget _buildFilesList(List> files) { - final l10n = AppLocalizations.of(context); - final theme = Theme.of(context); - - if (_isLoading) { - return const Center( - child: CircularProgressIndicator(), - ); - } - - if (_error != null) { - return Center( - child: Column( - mainAxisAlignment: MainAxisAlignment.center, - children: [ - Icon( - Icons.error_outline, - size: 64, - color: theme.colorScheme.error, - ), - const SizedBox(height: 16), - Text( - _error!, - style: theme.textTheme.bodyLarge, - textAlign: TextAlign.center, - ), - const SizedBox(height: 16), - ElevatedButton( - onPressed: _loadFiles, - child: Text(l10n.retry), - ), - ], - ), - ); - } - - if (files.isEmpty) { - return Center( - child: Column( - mainAxisAlignment: MainAxisAlignment.center, - children: [ - Icon( - Icons.folder_outlined, - size: 64, - color: theme.colorScheme.onSurface.withOpacity(0.5), - ), - const SizedBox(height: 16), - Text( - l10n.noFilesFound, - style: theme.textTheme.bodyLarge?.copyWith( - color: theme.colorScheme.onSurface.withOpacity(0.7), - ), - ), - ], - ), - ); - } - - return ListView.builder( - padding: const EdgeInsets.all(16), - itemCount: files.length, - itemBuilder: (context, index) { - final file = files[index]; - return Card( - margin: const EdgeInsets.only(bottom: 8), - child: ListTile( - leading: Icon( - _getFileIcon(file['mime_type']), - color: theme.colorScheme.primary, - ), - title: Text( - file['original_name'], - style: theme.textTheme.titleMedium, - ), - subtitle: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Text('${l10n.fileSize}: ${_formatFileSize(file['file_size'])}'), - Text('${l10n.moduleContext}: ${file['module_context']}'), - Text('${l10n.createdAt}: ${_formatDate(file['created_at'])}'), - if (file['is_temporary'] == true) - Text( - '${l10n.isTemporary}: ${file['expires_at'] != null ? _formatDate(file['expires_at']) : 'N/A'}', - style: theme.textTheme.bodySmall?.copyWith( - color: theme.colorScheme.error, - ), - ), - if (file['is_verified'] == false) - Text( - l10n.isVerified, - style: theme.textTheme.bodySmall?.copyWith( - color: theme.colorScheme.error, - ), - ), - ], - ), - trailing: PopupMenuButton( - onSelected: (value) { - switch (value) { - case 'delete': - _forceDeleteFile(file['id']); - break; - case 'restore': - _restoreFile(file['id']); - break; - } - }, - itemBuilder: (context) => [ - PopupMenuItem( - value: 'delete', - child: Row( - children: [ - Icon(Icons.delete, color: theme.colorScheme.error), - const SizedBox(width: 8), - Text(AppLocalizations.of(context).forceDelete), - ], - ), - ), - PopupMenuItem( - value: 'restore', - child: Row( - children: [ - Icon(Icons.restore, color: theme.colorScheme.primary), - const SizedBox(width: 8), - Text(AppLocalizations.of(context).restoreFile), - ], - ), - ), - ], - ), - ), - ); - }, - ); - } - - IconData _getFileIcon(String mimeType) { - if (mimeType.startsWith('image/')) return Icons.image; - if (mimeType.startsWith('video/')) return Icons.video_file; - if (mimeType.startsWith('audio/')) return Icons.audio_file; - if (mimeType.contains('pdf')) return Icons.picture_as_pdf; - if (mimeType.contains('word')) return Icons.description; - if (mimeType.contains('excel') || mimeType.contains('spreadsheet')) return Icons.table_chart; - if (mimeType.contains('zip') || mimeType.contains('rar')) return Icons.archive; - return Icons.insert_drive_file; - } -} 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 deleted file mode 100644 index e518073..0000000 --- a/hesabixUI/hesabix_ui/lib/widgets/admin/file_storage/file_statistics_widget.dart +++ /dev/null @@ -1,341 +0,0 @@ -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}); - - @override - State createState() => _FileStatisticsWidgetState(); -} - -class _FileStatisticsWidgetState extends State { - Map? _statistics; - bool _isLoading = true; - String? _error; - - @override - void initState() { - super.initState(); - _loadStatistics(); - } - - Future _loadStatistics() async { - setState(() { - _isLoading = true; - _error = null; - }); - - try { - final api = ApiClient(); - final response = await api.get('/api/v1/admin/files/statistics'); - - 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(); - _isLoading = false; - }); - } - } - - - Future _cleanupTemporaryFiles() async { - final l10n = AppLocalizations.of(context); - final confirmed = await showDialog( - context: context, - builder: (context) => AlertDialog( - title: Text(l10n.cleanupTemporaryFiles), - content: Text(l10n.deleteConfirmMessage), - actions: [ - TextButton( - onPressed: () => Navigator.of(context).pop(false), - child: Text(l10n.cancel), - ), - TextButton( - onPressed: () => Navigator.of(context).pop(true), - child: Text(l10n.cleanupTemporaryFiles), - ), - ], - ), - ); - - if (confirmed == true) { - try { - final api = ApiClient(); - final response = await api.post('/api/v1/admin/files/cleanup-temporary'); - - 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( - content: Text('Error: $e'), - backgroundColor: Colors.red, - ), - ); - } - } - } - - - String _formatFileSize(int bytes) { - if (bytes < 1024) return '$bytes B'; - if (bytes < 1024 * 1024) return '${(bytes / 1024).toStringAsFixed(1)} KB'; - if (bytes < 1024 * 1024 * 1024) return '${(bytes / (1024 * 1024)).toStringAsFixed(1)} MB'; - return '${(bytes / (1024 * 1024 * 1024)).toStringAsFixed(1)} GB'; - } - - @override - Widget build(BuildContext context) { - final l10n = AppLocalizations.of(context); - final theme = Theme.of(context); - - if (_isLoading) { - return const Center( - child: CircularProgressIndicator(), - ); - } - - if (_error != null) { - return Center( - child: Column( - mainAxisAlignment: MainAxisAlignment.center, - children: [ - Icon( - Icons.error_outline, - size: 64, - color: theme.colorScheme.error, - ), - const SizedBox(height: 16), - Text( - _error!, - style: theme.textTheme.bodyLarge, - textAlign: TextAlign.center, - ), - const SizedBox(height: 16), - ElevatedButton( - onPressed: _loadStatistics, - child: Text(l10n.retry), - ), - ], - ), - ); - } - - return SingleChildScrollView( - padding: const EdgeInsets.all(16), - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Row( - children: [ - Expanded( - child: Text( - l10n.fileStatistics, - style: theme.textTheme.headlineSmall, - ), - ), - ElevatedButton.icon( - onPressed: _cleanupTemporaryFiles, - icon: const Icon(Icons.cleaning_services), - label: Text(l10n.cleanupTemporaryFiles), - style: ElevatedButton.styleFrom( - backgroundColor: theme.colorScheme.error, - foregroundColor: theme.colorScheme.onError, - ), - ), - ], - ), - const SizedBox(height: 24), - - // Statistics Cards - GridView.count( - shrinkWrap: true, - physics: const NeverScrollableScrollPhysics(), - crossAxisCount: MediaQuery.of(context).size.width > 600 ? 2 : 1, - childAspectRatio: 2.5, - crossAxisSpacing: 16, - mainAxisSpacing: 16, - children: [ - _buildStatCard( - context, - l10n.totalFiles, - _statistics!['total_files'].toString(), - Icons.folder, - theme.colorScheme.primary, - ), - _buildStatCard( - context, - l10n.totalSize, - _formatFileSize(_statistics!['total_size']), - Icons.storage, - theme.colorScheme.secondary, - ), - _buildStatCard( - context, - l10n.temporaryFiles, - _statistics!['temporary_files'].toString(), - Icons.schedule, - theme.colorScheme.tertiary, - ), - _buildStatCard( - context, - l10n.unverifiedFiles, - _statistics!['unverified_files'].toString(), - Icons.warning, - theme.colorScheme.error, - ), - ], - ), - - const SizedBox(height: 24), - - // Additional Information - Card( - child: Padding( - padding: const EdgeInsets.all(16), - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Text( - 'Storage Information', - style: theme.textTheme.titleMedium?.copyWith( - fontWeight: FontWeight.bold, - ), - ), - const SizedBox(height: 16), - _buildInfoRow( - context, - 'Average file size', - _formatFileSize(_statistics!['total_size'] ~/ _statistics!['total_files']), - Icons.info_outline, - ), - _buildInfoRow( - context, - 'Storage efficiency', - '95%', - Icons.trending_up, - ), - _buildInfoRow( - context, - 'Last cleanup', - '2 days ago', - Icons.cleaning_services, - ), - ], - ), - ), - ), - ], - ), - ); - } - - Widget _buildStatCard( - BuildContext context, - String title, - String value, - IconData icon, - Color color, - ) { - final theme = Theme.of(context); - - return Card( - child: Padding( - padding: const EdgeInsets.all(16), - child: Row( - children: [ - Container( - padding: const EdgeInsets.all(12), - decoration: BoxDecoration( - color: color.withOpacity(0.1), - borderRadius: BorderRadius.circular(8), - ), - child: Icon( - icon, - color: color, - size: 24, - ), - ), - const SizedBox(width: 16), - Expanded( - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - mainAxisAlignment: MainAxisAlignment.center, - children: [ - Text( - title, - style: theme.textTheme.bodyMedium?.copyWith( - color: theme.colorScheme.onSurface.withOpacity(0.7), - ), - ), - const SizedBox(height: 4), - Text( - value, - style: theme.textTheme.headlineSmall?.copyWith( - fontWeight: FontWeight.bold, - color: color, - ), - ), - ], - ), - ), - ], - ), - ), - ); - } - - Widget _buildInfoRow( - BuildContext context, - String label, - String value, - IconData icon, - ) { - final theme = Theme.of(context); - - return Padding( - padding: const EdgeInsets.symmetric(vertical: 8), - child: Row( - children: [ - Icon( - icon, - size: 16, - color: theme.colorScheme.onSurface.withOpacity(0.6), - ), - const SizedBox(width: 8), - Expanded( - child: Text( - label, - style: theme.textTheme.bodyMedium, - ), - ), - Text( - value, - style: theme.textTheme.bodyMedium?.copyWith( - fontWeight: FontWeight.bold, - ), - ), - ], - ), - ); - } -} 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 5fb62b2..d7c44dc 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 @@ -23,203 +23,405 @@ class StorageConfigCard extends StatelessWidget { final theme = Theme.of(context); final isDefault = config['is_default'] == true; final isActive = config['is_active'] == true; + final storageType = config['storage_type'] ?? 'unknown'; return Card( - elevation: 2, + elevation: 4, + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(16), + side: isDefault + ? BorderSide(color: theme.colorScheme.primary, width: 2) + : BorderSide.none, + ), + child: Container( + decoration: BoxDecoration( + borderRadius: BorderRadius.circular(16), + gradient: isDefault + ? LinearGradient( + begin: Alignment.topLeft, + end: Alignment.bottomRight, + colors: [ + theme.colorScheme.primary.withOpacity(0.05), + theme.colorScheme.primary.withOpacity(0.02), + ], + ) + : null, + ), child: Padding( - padding: const EdgeInsets.all(16), + padding: const EdgeInsets.all(20), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ - // Header + // Header Row( + children: [ + Container( + padding: const EdgeInsets.all(12), + decoration: BoxDecoration( + color: _getStorageColor(storageType).withOpacity(0.1), + borderRadius: BorderRadius.circular(12), + ), + child: Icon( + _getStorageIcon(storageType), + color: _getStorageColor(storageType), + size: 28, + ), + ), + const SizedBox(width: 16), + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Row( + children: [ + Expanded( + child: Text( + config['name'] ?? 'Unknown', + style: theme.textTheme.titleLarge?.copyWith( + fontWeight: FontWeight.bold, + color: theme.colorScheme.onSurface, + ), + ), + ), + if (isDefault) ...[ + Container( + padding: const EdgeInsets.symmetric( + horizontal: 8, + vertical: 4, + ), + decoration: BoxDecoration( + color: theme.colorScheme.primary, + borderRadius: BorderRadius.circular(12), + ), + child: Row( + mainAxisSize: MainAxisSize.min, children: [ Icon( - _getStorageIcon(config['storage_type']), - color: theme.colorScheme.primary, - size: 24, + Icons.star, + size: 16, + color: theme.colorScheme.onPrimary, ), - const SizedBox(width: 12), - Expanded( - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Text( - config['name'] ?? 'Unknown', - style: theme.textTheme.titleMedium?.copyWith( - fontWeight: FontWeight.bold, - ), - ), - const SizedBox(height: 4), - Text( - _getStorageTypeName(config['storage_type']), - style: theme.textTheme.bodyMedium?.copyWith( - color: theme.colorScheme.onSurface.withOpacity(0.7), - ), - ), - ], + const SizedBox(width: 4), + Text( + 'پیش‌فرض', + style: theme.textTheme.bodySmall?.copyWith( + color: theme.colorScheme.onPrimary, + fontWeight: FontWeight.bold, + ), + ), + ], + ), + ), + ], + ], + ), + const SizedBox(height: 4), + Row( + children: [ + Container( + padding: const EdgeInsets.symmetric( + horizontal: 8, + vertical: 4, + ), + decoration: BoxDecoration( + color: _getStorageColor(storageType), + borderRadius: BorderRadius.circular(8), + ), + child: Text( + _getStorageTypeName(storageType), + style: theme.textTheme.bodySmall?.copyWith( + color: Colors.white, + fontWeight: FontWeight.bold, + ), + ), ), - ), - // Status badges - Row( - children: [ - if (isDefault) - Container( - padding: const EdgeInsets.symmetric( - horizontal: 8, - vertical: 4, + const SizedBox(width: 8), + Container( + padding: const EdgeInsets.symmetric( + horizontal: 8, + vertical: 4, + ), + decoration: BoxDecoration( + color: isActive ? Colors.green : Colors.red, + borderRadius: BorderRadius.circular(8), + ), + child: Text( + isActive ? 'فعال' : 'غیرفعال', + style: theme.textTheme.bodySmall?.copyWith( + color: Colors.white, + fontWeight: FontWeight.bold, + ), + ), + ), + ], ), - 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(height: 20), + + // Configuration details + _buildConfigDetails(context, config), + + const SizedBox(height: 20), + + // Actions + Row( + mainAxisAlignment: MainAxisAlignment.end, + children: [ + if (onTestConnection != null) + _buildActionButton( + context: context, + icon: Icons.wifi_protected_setup, + label: l10n.testConnection, + onPressed: onTestConnection!, + color: theme.colorScheme.primary, + ), + if (onEdit != null) ...[ 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, - ), - ), + _buildActionButton( + context: context, + icon: Icons.edit, + label: l10n.edit, + onPressed: onEdit!, + color: theme.colorScheme.secondary, ), ], - ), - ], - ), - - const SizedBox(height: 16), - - // Configuration details - _buildConfigDetails(context, config), - - const SizedBox(height: 16), - - // Actions - Row( - mainAxisAlignment: MainAxisAlignment.end, - children: [ - if (onTestConnection != null) - TextButton.icon( - onPressed: onTestConnection, - icon: const Icon(Icons.wifi_protected_setup, size: 16), - label: Text(l10n.testConnection), - ), - 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 (onSetDefault != null) ...[ + const SizedBox(width: 8), + _buildActionButton( + context: context, + icon: Icons.star, + label: l10n.setAsDefault, + onPressed: onSetDefault!, + color: Colors.orange, ), - ), - ], - if (onDelete != null) ...[ - const SizedBox(width: 8), - TextButton.icon( - onPressed: onDelete, - icon: const Icon(Icons.delete, size: 16), - label: Text(l10n.delete), - style: TextButton.styleFrom( - foregroundColor: theme.colorScheme.error, + ], + if (onDelete != null) ...[ + const SizedBox(width: 8), + _buildActionButton( + context: context, + icon: Icons.delete, + label: l10n.delete, + onPressed: onDelete!, + color: theme.colorScheme.error, ), - ), + ], ], - ], - ), - ], + ), + ], + ), + ), + ), + ); + } + + Widget _buildActionButton({ + required BuildContext context, + required IconData icon, + required String label, + required VoidCallback onPressed, + required Color color, + }) { + return ElevatedButton.icon( + onPressed: onPressed, + icon: Icon(icon, size: 18), + label: Text(label), + style: ElevatedButton.styleFrom( + backgroundColor: color, + foregroundColor: Colors.white, + elevation: 2, + padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 8), + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(8), ), ), ); } Widget _buildConfigDetails(BuildContext context, Map config) { - final l10n = AppLocalizations.of(context); + final theme = Theme.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, - ), - ], - ); + return _buildLocalConfigDetails(context, configData); } else if (storageType == 'ftp') { - return Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - _buildDetailRow( - context, - l10n.ftpHost, - configData['host'] ?? 'N/A', - Icons.dns, + return _buildFtpConfigDetails(context, configData); + } else { + return Container( + padding: const EdgeInsets.all(12), + decoration: BoxDecoration( + color: theme.colorScheme.surfaceVariant.withOpacity(0.5), + borderRadius: BorderRadius.circular(8), + ), + child: Text( + 'نوع ذخیره‌سازی نامشخص', + style: theme.textTheme.bodyMedium?.copyWith( + color: theme.colorScheme.onSurface.withOpacity(0.6), ), - 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, - ) { + Widget _buildLocalConfigDetails(BuildContext context, Map configData) { + final l10n = AppLocalizations.of(context); + final theme = Theme.of(context); + final basePath = configData['base_path'] ?? ''; + + return Container( + padding: const EdgeInsets.all(16), + decoration: BoxDecoration( + color: theme.colorScheme.surfaceVariant.withOpacity(0.3), + borderRadius: BorderRadius.circular(12), + border: Border.all( + color: theme.colorScheme.outline.withOpacity(0.2), + ), + ), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Row( + children: [ + Icon( + Icons.folder_outlined, + size: 20, + color: theme.colorScheme.primary, + ), + const SizedBox(width: 8), + Text( + 'پیکربندی ذخیره‌سازی محلی', + style: theme.textTheme.titleSmall?.copyWith( + fontWeight: FontWeight.bold, + color: theme.colorScheme.onSurface, + ), + ), + ], + ), + const SizedBox(height: 8), + Text( + l10n.basePath, + style: theme.textTheme.bodySmall?.copyWith( + color: theme.colorScheme.onSurface.withOpacity(0.6), + ), + ), + const SizedBox(height: 4), + Container( + padding: const EdgeInsets.all(8), + decoration: BoxDecoration( + color: theme.colorScheme.surface, + borderRadius: BorderRadius.circular(6), + border: Border.all( + color: theme.colorScheme.outline.withOpacity(0.3), + ), + ), + child: Row( + children: [ + Icon( + Icons.folder, + size: 16, + color: theme.colorScheme.primary, + ), + const SizedBox(width: 8), + Expanded( + child: Text( + basePath, + style: theme.textTheme.bodyMedium?.copyWith( + fontFamily: 'monospace', + ), + ), + ), + ], + ), + ), + ], + ), + ); + } + + Widget _buildFtpConfigDetails(BuildContext context, Map configData) { + final l10n = AppLocalizations.of(context); + final theme = Theme.of(context); + final host = configData['host'] ?? ''; + final port = configData['port'] ?? 21; + final username = configData['username'] ?? ''; + final directory = configData['directory'] ?? '/'; + final useTls = configData['use_tls'] == true; + + return Container( + padding: const EdgeInsets.all(16), + decoration: BoxDecoration( + color: theme.colorScheme.surfaceVariant.withOpacity(0.3), + borderRadius: BorderRadius.circular(12), + border: Border.all( + color: theme.colorScheme.outline.withOpacity(0.2), + ), + ), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Row( + children: [ + Icon( + Icons.cloud_outlined, + size: 20, + color: theme.colorScheme.primary, + ), + const SizedBox(width: 8), + Text( + 'پیکربندی FTP', + style: theme.textTheme.titleSmall?.copyWith( + fontWeight: FontWeight.bold, + color: theme.colorScheme.onSurface, + ), + ), + ], + ), + const SizedBox(height: 12), + _buildConfigRow( + context, + Icons.dns, + 'میزبان', + host, + ), + const SizedBox(height: 8), + _buildConfigRow( + context, + Icons.settings_ethernet, + 'پورت', + port.toString(), + ), + const SizedBox(height: 8), + _buildConfigRow( + context, + Icons.person, + l10n.username, + username, + ), + const SizedBox(height: 8), + _buildConfigRow( + context, + Icons.folder, + 'دایرکتوری', + directory, + ), + const SizedBox(height: 8), + _buildConfigRow( + context, + Icons.security, + 'امنیت', + useTls ? 'TLS فعال' : 'TLS غیرفعال', + ), + ], + ), + ); + } + + Widget _buildConfigRow(BuildContext context, IconData icon, String label, String value) { final theme = Theme.of(context); return Row( @@ -227,22 +429,21 @@ class StorageConfigCard extends StatelessWidget { Icon( icon, size: 16, - color: theme.colorScheme.onSurface.withOpacity(0.6), + color: theme.colorScheme.primary, ), const SizedBox(width: 8), Text( '$label: ', - style: theme.textTheme.bodyMedium?.copyWith( - fontWeight: FontWeight.w500, + style: theme.textTheme.bodySmall?.copyWith( + color: theme.colorScheme.onSurface.withOpacity(0.6), ), ), Expanded( child: Text( value, - style: theme.textTheme.bodyMedium?.copyWith( - color: theme.colorScheme.onSurface.withOpacity(0.8), + style: theme.textTheme.bodySmall?.copyWith( + fontWeight: FontWeight.w500, ), - overflow: TextOverflow.ellipsis, ), ), ], @@ -256,7 +457,18 @@ class StorageConfigCard extends StatelessWidget { case 'ftp': return Icons.cloud_upload; default: - return Icons.storage; + return Icons.help_outline; + } + } + + Color _getStorageColor(String storageType) { + switch (storageType) { + case 'local': + return Colors.blue; + case 'ftp': + return Colors.green; + default: + return Colors.grey; } } @@ -265,9 +477,9 @@ class StorageConfigCard extends StatelessWidget { case 'local': return 'Local Storage'; case 'ftp': - return 'FTP Storage'; + return 'FTP Server'; default: - return 'Unknown Storage'; + return 'Unknown'; } } } \ 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 9ae900b..6027d94 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 @@ -4,10 +4,12 @@ import '../../../core/api_client.dart'; class StorageConfigFormDialog extends StatefulWidget { final Map? config; + final VoidCallback? onSaved; const StorageConfigFormDialog({ super.key, this.config, + this.onSaved, }); @override @@ -28,6 +30,7 @@ class _StorageConfigFormDialogState extends State { bool _isDefault = false; bool _isActive = true; bool _isLoading = false; + bool _useTls = false; @override void initState() { @@ -49,10 +52,11 @@ class _StorageConfigFormDialogState extends State { _basePathController.text = configData['base_path'] ?? ''; } else if (_selectedStorageType == 'ftp') { _ftpHostController.text = configData['host'] ?? ''; - _ftpPortController.text = configData['port']?.toString() ?? '21'; + _ftpPortController.text = (configData['port'] ?? 21).toString(); _ftpUsernameController.text = configData['username'] ?? ''; _ftpPasswordController.text = configData['password'] ?? ''; - _ftpDirectoryController.text = configData['directory'] ?? ''; + _ftpDirectoryController.text = configData['directory'] ?? '/'; + _useTls = configData['use_tls'] == true; } } @@ -68,27 +72,8 @@ class _StorageConfigFormDialogState extends State { super.dispose(); } - Map _buildConfigData() { - if (_selectedStorageType == 'local') { - return { - 'base_path': _basePathController.text, - }; - } else if (_selectedStorageType == 'ftp') { - return { - 'host': _ftpHostController.text, - 'port': int.tryParse(_ftpPortController.text) ?? 21, - 'username': _ftpUsernameController.text, - 'password': _ftpPasswordController.text, - 'directory': _ftpDirectoryController.text, - }; - } - return {}; - } - Future _saveConfig() async { - if (!_formKey.currentState!.validate()) { - return; - } + if (!_formKey.currentState!.validate()) return; setState(() { _isLoading = true; @@ -96,29 +81,69 @@ class _StorageConfigFormDialogState extends State { try { 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(), - }, - ); + Map configData = {}; - if (response.data != null && response.data['success'] == true) { - if (mounted) { - Navigator.of(context).pop(response.data['data']); - } + if (_selectedStorageType == 'local') { + configData = { + 'base_path': _basePathController.text.trim(), + }; + } else if (_selectedStorageType == 'ftp') { + configData = { + 'host': _ftpHostController.text.trim(), + 'port': int.tryParse(_ftpPortController.text) ?? 21, + 'username': _ftpUsernameController.text.trim(), + 'password': _ftpPasswordController.text, + 'directory': _ftpDirectoryController.text.trim(), + 'use_tls': _useTls, + }; + } + + final requestData = { + 'name': _nameController.text.trim(), + 'storage_type': _selectedStorageType, + 'config_data': configData, + 'is_default': _isDefault, + 'is_active': _isActive, + }; + + if (widget.config != null) { + // Update existing config + await api.put( + '/api/v1/admin/files/storage-configs/${widget.config!['id']}', + data: requestData, + ); } else { - throw Exception(response.data?['message'] ?? 'خطا در ذخیره تنظیمات'); + // Create new config + await api.post( + '/api/v1/admin/files/storage-configs/', + data: requestData, + ); + } + + if (mounted) { + Navigator.of(context).pop(); + + // Only show SnackBar if there's no onSaved callback (parent will handle notification) + if (widget.onSaved == null) { + ScaffoldMessenger.of(context).showSnackBar( + SnackBar( + content: Text( + widget.config != null + ? 'تنظیمات ذخیره‌سازی به‌روزرسانی شد' + : 'تنظیمات ذخیره‌سازی ایجاد شد', + ), + backgroundColor: Colors.green, + ), + ); + } + + widget.onSaved?.call(); } } catch (e) { if (mounted) { ScaffoldMessenger.of(context).showSnackBar( SnackBar( - content: Text('Error: $e'), + content: Text('خطا در ذخیره تنظیمات: $e'), backgroundColor: Colors.red, ), ); @@ -139,211 +164,192 @@ class _StorageConfigFormDialogState extends State { final isEditing = widget.config != null; return Dialog( + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(20), + ), child: Container( - width: MediaQuery.of(context).size.width * 0.8, - constraints: const BoxConstraints(maxWidth: 600), - child: Form( - key: _formKey, + constraints: const BoxConstraints(maxWidth: 600, maxHeight: 700), child: Column( mainAxisSize: MainAxisSize.min, children: [ - Padding( - padding: const EdgeInsets.all(16), + // Header + Container( + padding: const EdgeInsets.all(24), + decoration: BoxDecoration( + color: theme.colorScheme.primary, + borderRadius: const BorderRadius.only( + topLeft: Radius.circular(20), + topRight: Radius.circular(20), + ), + ), child: Row( children: [ Icon( isEditing ? Icons.edit : Icons.add, - color: theme.colorScheme.primary, + color: theme.colorScheme.onPrimary, + size: 28, + ), + const SizedBox(width: 12), + Expanded( + child: Text( + isEditing + ? 'ویرایش پیکربندی ذخیره‌سازی' + : 'ایجاد پیکربندی ذخیره‌سازی', + style: theme.textTheme.headlineSmall?.copyWith( + color: theme.colorScheme.onPrimary, + fontWeight: FontWeight.bold, + ), ), - const SizedBox(width: 8), - Text( - isEditing ? l10n.editStorageConfig : l10n.addStorageConfig, - style: theme.textTheme.titleLarge, - ), - const Spacer(), + ), IconButton( onPressed: () => Navigator.of(context).pop(), icon: const Icon(Icons.close), + color: theme.colorScheme.onPrimary, ), ], ), ), + + // Form Flexible( child: SingleChildScrollView( - padding: const EdgeInsets.symmetric(horizontal: 16), + padding: const EdgeInsets.all(24), + child: Form( + key: _formKey, child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ + // Basic Information + _buildSectionHeader(context, 'اطلاعات پایه'), + const SizedBox(height: 16), + // Name TextFormField( controller: _nameController, decoration: InputDecoration( - labelText: l10n.storageName, - border: const OutlineInputBorder(), + labelText: 'نام', + hintText: 'نام پیکربندی ذخیره‌سازی را وارد کنید', + prefixIcon: const Icon(Icons.label_outline), + border: OutlineInputBorder( + borderRadius: BorderRadius.circular(12), + ), ), validator: (value) { - if (value == null || value.isEmpty) { - return l10n.requiredField; + if (value == null || value.trim().isEmpty) { + return 'لطفاً نام را وارد کنید'; } return null; }, ), + const SizedBox(height: 16), // Storage Type - DropdownButtonFormField( - value: _selectedStorageType, - decoration: InputDecoration( - labelText: l10n.storageType, - border: const OutlineInputBorder(), + Text( + l10n.storageType, + style: theme.textTheme.titleMedium?.copyWith( + fontWeight: FontWeight.bold, ), - items: [ - DropdownMenuItem( + ), + const SizedBox(height: 8), + Row( + children: [ + Expanded( + child: RadioListTile( + title: Row( + children: [ + Icon(Icons.storage, size: 20), + const SizedBox(width: 8), + Text(l10n.localStorage), + ], + ), value: 'local', - child: Text(l10n.localStorage), + groupValue: _selectedStorageType, + onChanged: (value) { + setState(() { + _selectedStorageType = value!; + }); + }, + ), ), - DropdownMenuItem( - value: 'ftp', - child: Text(l10n.ftpStorage), - ), - ], + Expanded( + child: RadioListTile( + title: Row( + children: [ + Icon(Icons.cloud_upload, size: 20), + const SizedBox(width: 8), + Text('سرور FTP'), + ], + ), + value: 'ftp', + groupValue: _selectedStorageType, onChanged: (value) { setState(() { _selectedStorageType = value!; }); }, ), + ), + ], + ), + + const SizedBox(height: 24), + + // Configuration Details + _buildSectionHeader(context, 'جزئیات پیکربندی'), const SizedBox(height: 16), - // Configuration based on storage type if (_selectedStorageType == 'local') ...[ - TextFormField( - controller: _basePathController, - decoration: InputDecoration( - labelText: l10n.basePath, - border: const OutlineInputBorder(), - hintText: '/var/hesabix/files', - ), - validator: (value) { - if (value == null || value.isEmpty) { - return l10n.requiredField; - } - return null; - }, - ), + _buildLocalConfigFields(context), ] else if (_selectedStorageType == 'ftp') ...[ - TextFormField( - controller: _ftpHostController, - decoration: InputDecoration( - labelText: l10n.ftpHost, - border: const OutlineInputBorder(), - hintText: 'ftp.example.com', - ), - validator: (value) { - if (value == null || value.isEmpty) { - return l10n.requiredField; - } - return null; - }, - ), - const SizedBox(height: 16), - TextFormField( - controller: _ftpPortController, - decoration: InputDecoration( - labelText: l10n.ftpPort, - border: const OutlineInputBorder(), - hintText: '21', - ), - keyboardType: TextInputType.number, - validator: (value) { - if (value == null || value.isEmpty) { - return l10n.requiredField; - } - if (int.tryParse(value) == null) { - return 'Invalid port number'; - } - return null; - }, - ), - const SizedBox(height: 16), - TextFormField( - controller: _ftpUsernameController, - decoration: InputDecoration( - labelText: l10n.ftpUsername, - border: const OutlineInputBorder(), - hintText: 'username', - ), - validator: (value) { - if (value == null || value.isEmpty) { - return l10n.requiredField; - } - return null; - }, - ), - const SizedBox(height: 16), - TextFormField( - controller: _ftpPasswordController, - decoration: InputDecoration( - labelText: l10n.ftpPassword, - border: const OutlineInputBorder(), - hintText: 'password', - ), - obscureText: true, - validator: (value) { - if (value == null || value.isEmpty) { - return l10n.requiredField; - } - return null; - }, - ), - const SizedBox(height: 16), - TextFormField( - controller: _ftpDirectoryController, - decoration: InputDecoration( - labelText: l10n.ftpDirectory, - border: const OutlineInputBorder(), - hintText: '/hesabix/files', - ), - validator: (value) { - if (value == null || value.isEmpty) { - return l10n.requiredField; - } - return null; - }, - ), + _buildFtpConfigFields(context), ], + + const SizedBox(height: 24), + + // Options + _buildSectionHeader(context, 'گزینه‌ها'), const SizedBox(height: 16), - // Options - Row( - children: [ - Checkbox( + SwitchListTile( + title: Text('تنظیم به عنوان پیش‌فرض'), + subtitle: Text('این پیکربندی به عنوان پیش‌فرض تنظیم شود'), value: _isDefault, onChanged: (value) { setState(() { - _isDefault = value ?? false; + _isDefault = value; }); }, - ), - Text(l10n.isDefault), - const SizedBox(width: 24), - Checkbox( + secondary: const Icon(Icons.star), + ), + + SwitchListTile( + title: Text('فعال'), + subtitle: Text('این پیکربندی فعال باشد'), value: _isActive, onChanged: (value) { setState(() { - _isActive = value ?? false; + _isActive = value; }); }, - ), - Text(l10n.isActive), - ], + secondary: const Icon(Icons.power), ), ], ), ), ), - Padding( - padding: const EdgeInsets.all(16), + ), + + // Actions + Container( + padding: const EdgeInsets.all(24), + decoration: BoxDecoration( + color: theme.colorScheme.surfaceVariant.withOpacity(0.3), + borderRadius: const BorderRadius.only( + bottomLeft: Radius.circular(20), + bottomRight: Radius.circular(20), + ), + ), child: Row( mainAxisAlignment: MainAxisAlignment.end, children: [ @@ -351,16 +357,26 @@ class _StorageConfigFormDialogState extends State { onPressed: _isLoading ? null : () => Navigator.of(context).pop(), child: Text(l10n.cancel), ), - const SizedBox(width: 8), - ElevatedButton( + const SizedBox(width: 12), + ElevatedButton.icon( onPressed: _isLoading ? null : _saveConfig, - child: _isLoading + icon: _isLoading ? const SizedBox( width: 16, height: 16, child: CircularProgressIndicator(strokeWidth: 2), ) - : Text(l10n.save), + : Icon(isEditing ? Icons.save : Icons.add), + label: Text( + _isLoading + ? 'در حال ذخیره...' + : (isEditing ? 'به‌روزرسانی' : 'ایجاد'), + ), + style: ElevatedButton.styleFrom( + backgroundColor: theme.colorScheme.primary, + foregroundColor: theme.colorScheme.onPrimary, + padding: const EdgeInsets.symmetric(horizontal: 24, vertical: 12), + ), ), ], ), @@ -368,7 +384,175 @@ class _StorageConfigFormDialogState extends State { ], ), ), - ), ); } -} + + Widget _buildSectionHeader(BuildContext context, String title) { + final theme = Theme.of(context); + + return Row( + children: [ + Container( + width: 4, + height: 20, + decoration: BoxDecoration( + color: theme.colorScheme.primary, + borderRadius: BorderRadius.circular(2), + ), + ), + const SizedBox(width: 12), + Text( + title, + style: theme.textTheme.titleLarge?.copyWith( + fontWeight: FontWeight.bold, + color: theme.colorScheme.onSurface, + ), + ), + ], + ); + } + + Widget _buildLocalConfigFields(BuildContext context) { + + return Column( + children: [ + TextFormField( + controller: _basePathController, + decoration: InputDecoration( + labelText: 'مسیر پایه', + hintText: 'مسیر پایه را وارد کنید', + prefixIcon: const Icon(Icons.folder_outlined), + border: OutlineInputBorder( + borderRadius: BorderRadius.circular(12), + ), + ), + validator: (value) { + if (value == null || value.trim().isEmpty) { + return 'لطفاً مسیر پایه را وارد کنید'; + } + return null; + }, + ), + ], + ); + } + + Widget _buildFtpConfigFields(BuildContext context) { + + return Column( + children: [ + TextFormField( + controller: _ftpHostController, + decoration: InputDecoration( + labelText: 'میزبان', + hintText: 'آدرس میزبان را وارد کنید', + prefixIcon: const Icon(Icons.dns), + border: OutlineInputBorder( + borderRadius: BorderRadius.circular(12), + ), + ), + validator: (value) { + if (value == null || value.trim().isEmpty) { + return 'لطفاً میزبان را وارد کنید'; + } + return null; + }, + ), + + const SizedBox(height: 16), + + TextFormField( + controller: _ftpPortController, + decoration: InputDecoration( + labelText: 'پورت', + hintText: '21', + prefixIcon: const Icon(Icons.settings_ethernet), + border: OutlineInputBorder( + borderRadius: BorderRadius.circular(12), + ), + ), + keyboardType: TextInputType.number, + validator: (value) { + if (value == null || value.trim().isEmpty) { + return 'لطفاً پورت را وارد کنید'; + } + final port = int.tryParse(value); + if (port == null || port < 1 || port > 65535) { + return 'لطفاً پورت معتبر وارد کنید'; + } + return null; + }, + ), + + const SizedBox(height: 16), + + TextFormField( + controller: _ftpUsernameController, + decoration: InputDecoration( + labelText: 'نام کاربری', + hintText: 'نام کاربری را وارد کنید', + prefixIcon: const Icon(Icons.person), + border: OutlineInputBorder( + borderRadius: BorderRadius.circular(12), + ), + ), + validator: (value) { + if (value == null || value.trim().isEmpty) { + return 'لطفاً نام کاربری را وارد کنید'; + } + return null; + }, + ), + + const SizedBox(height: 16), + + TextFormField( + controller: _ftpPasswordController, + decoration: InputDecoration( + labelText: 'رمز عبور', + hintText: 'رمز عبور را وارد کنید', + prefixIcon: const Icon(Icons.lock), + border: OutlineInputBorder( + borderRadius: BorderRadius.circular(12), + ), + ), + obscureText: true, + validator: (value) { + if (value == null || value.trim().isEmpty) { + return 'لطفاً رمز عبور را وارد کنید'; + } + return null; + }, + ), + + const SizedBox(height: 16), + + TextFormField( + controller: _ftpDirectoryController, + decoration: InputDecoration( + labelText: 'دایرکتوری', + hintText: '/', + prefixIcon: const Icon(Icons.folder), + border: OutlineInputBorder( + borderRadius: BorderRadius.circular(12), + ), + ), + ), + + const SizedBox(height: 16), + + SwitchListTile( + title: Text('استفاده از TLS'), + subtitle: Text('اتصال امن با TLS فعال شود'), + value: _useTls, + onChanged: (value) { + setState(() { + _useTls = value; + }); + }, + secondary: const Icon(Icons.security), + ), + ], + ); + } +} \ No newline at end of file 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 b1186b7..3feea46 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 @@ -1,17 +1,21 @@ 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}); + final VoidCallback? onRefresh; + + const StorageConfigListWidget({ + super.key, + this.onRefresh, + }); @override - State createState() => _StorageConfigListWidgetState(); + State createState() => StorageConfigListWidgetState(); } -class _StorageConfigListWidgetState extends State { +class StorageConfigListWidgetState extends State { List> _storageConfigs = []; bool _isLoading = true; String? _error; @@ -19,10 +23,10 @@ class _StorageConfigListWidgetState extends State { @override void initState() { super.initState(); - _loadStorageConfigs(); + loadStorageConfigs(); } - Future _loadStorageConfigs() async { + Future loadStorageConfigs() async { setState(() { _isLoading = true; _error = null; @@ -49,55 +53,7 @@ class _StorageConfigListWidgetState extends State { } } - - Future _addStorageConfig() async { - final result = await showDialog>( - context: context, - builder: (context) => const StorageConfigFormDialog(), - ); - - if (result != null) { - _loadStorageConfigs(); - } - } - - Future _editStorageConfig(Map config) async { - final result = await showDialog>( - context: context, - builder: (context) => StorageConfigFormDialog(config: config), - ); - - if (result != null) { - _loadStorageConfigs(); - } - } - - Future _setAsDefault(String configId) async { - final l10n = AppLocalizations.of(context); - try { - // TODO: Call API to set as default - await Future.delayed(const Duration(seconds: 1)); // Simulate API call - - ScaffoldMessenger.of(context).showSnackBar( - SnackBar( - content: Text(l10n.setAsDefault), - backgroundColor: Colors.green, - ), - ); - - _loadStorageConfigs(); - } catch (e) { - ScaffoldMessenger.of(context).showSnackBar( - SnackBar( - content: Text('Error: $e'), - backgroundColor: Colors.red, - ), - ); - } - } - Future _testConnection(String configId) async { - final l10n = AppLocalizations.of(context); try { final api = ApiClient(); final response = await api.post('/api/v1/admin/files/storage-configs/$configId/test'); @@ -107,14 +63,14 @@ class _StorageConfigListWidgetState extends State { if (testResult['success'] == true) { ScaffoldMessenger.of(context).showSnackBar( SnackBar( - content: Text(l10n.connectionSuccessful), + content: Text('اتصال موفقیت‌آمیز بود'), backgroundColor: Colors.green, ), ); } else { ScaffoldMessenger.of(context).showSnackBar( SnackBar( - content: Text('${l10n.connectionFailed}: ${testResult['error']}'), + content: Text('اتصال ناموفق: ${testResult['error']}'), backgroundColor: Colors.red, ), ); @@ -125,29 +81,27 @@ class _StorageConfigListWidgetState extends State { } catch (e) { ScaffoldMessenger.of(context).showSnackBar( SnackBar( - content: Text('${l10n.connectionFailed}: $e'), + content: Text('اتصال ناموفق: $e'), backgroundColor: Colors.red, ), ); } } - Future _deleteConfig(String configId) async { - final l10n = AppLocalizations.of(context); final confirmed = await showDialog( context: context, builder: (context) => AlertDialog( - title: Text(l10n.deleteConfirm), - content: Text(l10n.deleteConfirmMessage), + title: Text('تأیید حذف'), + content: Text('آیا از حذف این پیکربندی اطمینان دارید؟'), actions: [ TextButton( onPressed: () => Navigator.of(context).pop(false), - child: Text(l10n.cancel), + child: Text('لغو'), ), TextButton( onPressed: () => Navigator.of(context).pop(true), - child: Text(l10n.delete), + child: Text('حذف'), ), ], ), @@ -155,36 +109,116 @@ class _StorageConfigListWidgetState extends State { if (confirmed == true) { try { - // TODO: Call API to delete config - await Future.delayed(const Duration(seconds: 1)); // Simulate API call + final api = ApiClient(); + final response = await api.delete('/api/v1/admin/files/storage-configs/$configId'); - ScaffoldMessenger.of(context).showSnackBar( - SnackBar( - content: Text(l10n.fileDeleted), - backgroundColor: Colors.green, - ), - ); - - _loadStorageConfigs(); + if (response.data != null && response.data['success'] == true) { + ScaffoldMessenger.of(context).showSnackBar( + SnackBar( + content: Text('فایل حذف شد'), + backgroundColor: Colors.green, + ), + ); + + // Refresh the list + loadStorageConfigs(); + } else { + final errorMessage = response.data?['error']?['message'] ?? + response.data?['message'] ?? + 'خطا در حذف تنظیمات'; + throw Exception(errorMessage); + } } catch (e) { + String errorMessage = 'خطا در حذف تنظیمات'; + + // بررسی نوع خطا + if (e.toString().contains('STORAGE_CONFIG_HAS_FILES')) { + errorMessage = 'این تنظیمات ذخیره‌سازی دارای فایل است و قابل حذف نیست'; + } else if (e.toString().contains('STORAGE_CONFIG_NOT_FOUND')) { + errorMessage = 'تنظیمات ذخیره‌سازی یافت نشد'; + } else if (e.toString().contains('FORBIDDEN')) { + errorMessage = 'دسترسی غیرمجاز'; + } else { + errorMessage = e.toString().replaceFirst('Exception: ', ''); + } + ScaffoldMessenger.of(context).showSnackBar( SnackBar( - content: Text('Error: $e'), + content: Text(errorMessage), backgroundColor: Colors.red, + duration: const Duration(seconds: 5), ), ); } } } + Future _setAsDefault(String configId) async { + try { + final api = ApiClient(); + final response = await api.put('/api/v1/admin/files/storage-configs/$configId/set-default'); + + if (response.data != null && response.data['success'] == true) { + ScaffoldMessenger.of(context).showSnackBar( + SnackBar( + content: Text('تنظیمات به عنوان پیش‌فرض تنظیم شد'), + backgroundColor: Colors.green, + ), + ); + + // Refresh the list + loadStorageConfigs(); + } else { + throw Exception(response.data?['message'] ?? 'خطا در تنظیم به عنوان پیش‌فرض'); + } + } catch (e) { + ScaffoldMessenger.of(context).showSnackBar( + SnackBar( + content: Text('خطا در تنظیم به عنوان پیش‌فرض: $e'), + backgroundColor: Colors.red, + ), + ); + } + } + + void _editStorageConfig(Map config) { + showDialog( + context: context, + builder: (context) => StorageConfigFormDialog( + config: config, + onSaved: () { + loadStorageConfigs(); + // Show success message + ScaffoldMessenger.of(context).showSnackBar( + const SnackBar( + content: Text('تنظیمات ذخیره‌سازی به‌روزرسانی شد'), + backgroundColor: Colors.green, + ), + ); + }, + ), + ); + } + @override Widget build(BuildContext context) { - final l10n = AppLocalizations.of(context); final theme = Theme.of(context); if (_isLoading) { - return const Center( - child: CircularProgressIndicator(), + return Center( + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + CircularProgressIndicator( + color: theme.colorScheme.primary, + ), + const SizedBox(height: 16), + Text( + 'در حال بارگذاری...', + style: theme.textTheme.bodyLarge, + ), + ], + ), ); } @@ -199,85 +233,125 @@ class _StorageConfigListWidgetState extends State { color: theme.colorScheme.error, ), const SizedBox(height: 16), + Text( + 'خطا', + style: theme.textTheme.headlineSmall?.copyWith( + color: theme.colorScheme.error, + ), + ), + const SizedBox(height: 8), Text( _error!, - style: theme.textTheme.bodyLarge, + style: theme.textTheme.bodyMedium, textAlign: TextAlign.center, ), const SizedBox(height: 16), - ElevatedButton( - onPressed: _loadStorageConfigs, - child: Text(l10n.retry), + ElevatedButton.icon( + onPressed: loadStorageConfigs, + icon: const Icon(Icons.refresh), + label: Text('تلاش مجدد'), ), ], ), ); } - return Column( - children: [ - Padding( - padding: const EdgeInsets.all(16.0), - child: Row( - children: [ - Expanded( - child: Text( - l10n.storageConfigurations, - style: theme.textTheme.headlineSmall, - ), + if (_storageConfigs.isEmpty) { + return Center( + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Icon( + Icons.storage_outlined, + size: 64, + color: theme.colorScheme.primary.withOpacity(0.5), + ), + const SizedBox(height: 16), + Text( + 'هیچ پیکربندی ذخیره‌سازی وجود ندارد', + style: theme.textTheme.headlineSmall?.copyWith( + color: theme.colorScheme.onSurface.withOpacity(0.6), ), - ElevatedButton.icon( - onPressed: _addStorageConfig, - icon: const Icon(Icons.add), - label: Text(l10n.addStorageConfig), + ), + const SizedBox(height: 8), + Text( + 'اولین پیکربندی ذخیره‌سازی را ایجاد کنید', + style: theme.textTheme.bodyMedium?.copyWith( + color: theme.colorScheme.onSurface.withOpacity(0.6), ), - ], - ), + textAlign: TextAlign.center, + ), + const SizedBox(height: 24), + Text( + 'از دکمه + در پایین صفحه استفاده کنید', + style: theme.textTheme.bodySmall?.copyWith( + color: theme.colorScheme.onSurface.withOpacity(0.5), + fontStyle: FontStyle.italic, + ), + ), + ], ), - Expanded( - child: _storageConfigs.isEmpty - ? Center( - child: Column( - mainAxisAlignment: MainAxisAlignment.center, - children: [ - Icon( - Icons.storage_outlined, - size: 64, - color: theme.colorScheme.onSurface.withOpacity(0.5), - ), - const SizedBox(height: 16), - Text( - l10n.noFilesFound, - style: theme.textTheme.bodyLarge?.copyWith( - color: theme.colorScheme.onSurface.withOpacity(0.7), - ), - ), - ], + ); + } + + return RefreshIndicator( + onRefresh: loadStorageConfigs, + child: Padding( + padding: const EdgeInsets.all(16.0), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + // Header + Row( + children: [ + Icon( + Icons.storage, + color: theme.colorScheme.primary, + size: 28, + ), + const SizedBox(width: 12), + Text( + 'پیکربندی‌های ذخیره‌سازی', + style: theme.textTheme.headlineSmall?.copyWith( + fontWeight: FontWeight.bold, + color: theme.colorScheme.onSurface, ), - ) - : ListView.builder( - padding: const EdgeInsets.symmetric(horizontal: 16), - itemCount: _storageConfigs.length, - itemBuilder: (context, index) { - final config = _storageConfigs[index]; - return Padding( - padding: const EdgeInsets.only(bottom: 8), - child: StorageConfigCard( - config: config, - onEdit: () => _editStorageConfig(config), - onSetDefault: config['is_default'] == false - ? () => _setAsDefault(config['id']) - : null, - onTestConnection: () => _testConnection(config['id']), - onDelete: config['is_default'] == false - ? () => _deleteConfig(config['id']) - : null, - ), - ); - }, ), + const Spacer(), + Text( + '${_storageConfigs.length} پیکربندی', + style: theme.textTheme.bodyMedium?.copyWith( + color: theme.colorScheme.onSurface.withOpacity(0.6), + ), + ), + ], + ), + const SizedBox(height: 24), + + // Storage Configs List + Expanded( + child: ListView.builder( + itemCount: _storageConfigs.length, + itemBuilder: (context, index) { + final config = _storageConfigs[index]; + return Padding( + padding: const EdgeInsets.only(bottom: 16.0), + child: StorageConfigCard( + config: config, + onEdit: () => _editStorageConfig(config), + onSetDefault: config['is_default'] == false + ? () => _setAsDefault(config['id']) + : null, + onTestConnection: () => _testConnection(config['id']), + onDelete: () => _deleteConfig(config['id']), + ), + ); + }, + ), + ), + ], ), - ], + ), ); } -} +} \ No newline at end of file diff --git a/hesabixUI/hesabix_ui/lib/widgets/admin/file_storage/storage_management_page.dart b/hesabixUI/hesabix_ui/lib/widgets/admin/file_storage/storage_management_page.dart new file mode 100644 index 0000000..4a56e22 --- /dev/null +++ b/hesabixUI/hesabix_ui/lib/widgets/admin/file_storage/storage_management_page.dart @@ -0,0 +1,76 @@ +import 'package:flutter/material.dart'; +import 'storage_config_list_widget.dart'; +import 'storage_config_form_dialog.dart'; + +class StorageManagementPage extends StatefulWidget { + const StorageManagementPage({super.key}); + + @override + State createState() => _StorageManagementPageState(); +} + +class _StorageManagementPageState extends State { + final GlobalKey _listKey = GlobalKey(); + + @override + Widget build(BuildContext context) { + final theme = Theme.of(context); + + return Scaffold( + appBar: AppBar( + title: Text( + 'پیکربندی‌های ذخیره‌سازی', + style: theme.textTheme.headlineSmall?.copyWith( + fontWeight: FontWeight.bold, + color: theme.colorScheme.onPrimary, + ), + ), + backgroundColor: theme.colorScheme.primary, + foregroundColor: theme.colorScheme.onPrimary, + elevation: 0, + ), + body: Container( + decoration: BoxDecoration( + gradient: LinearGradient( + begin: Alignment.topCenter, + end: Alignment.bottomCenter, + colors: [ + theme.colorScheme.primary.withOpacity(0.1), + theme.colorScheme.surface, + ], + ), + ), + child: StorageConfigListWidget( + key: _listKey, + onRefresh: () => _listKey.currentState?.loadStorageConfigs(), + ), + ), + floatingActionButton: FloatingActionButton.extended( + onPressed: () => _showCreateDialog(context), + icon: const Icon(Icons.add), + label: const Text('ایجاد پیکربندی ذخیره‌سازی'), + backgroundColor: theme.colorScheme.primary, + foregroundColor: theme.colorScheme.onPrimary, + ), + ); + } + + void _showCreateDialog(BuildContext context) { + showDialog( + context: context, + builder: (context) => StorageConfigFormDialog( + onSaved: () { + // Refresh the list + _listKey.currentState?.loadStorageConfigs(); + // Show success message + ScaffoldMessenger.of(context).showSnackBar( + const SnackBar( + content: Text('تنظیمات ذخیره‌سازی ایجاد شد'), + backgroundColor: Colors.green, + ), + ); + }, + ), + ); + } +} diff --git a/hesabixUI/hesabix_ui/lib/widgets/data_table/data_table_widget.dart b/hesabixUI/hesabix_ui/lib/widgets/data_table/data_table_widget.dart index b085eb1..2038829 100644 --- a/hesabixUI/hesabix_ui/lib/widgets/data_table/data_table_widget.dart +++ b/hesabixUI/hesabix_ui/lib/widgets/data_table/data_table_widget.dart @@ -1,6 +1,6 @@ import 'dart:async'; import 'dart:typed_data'; -import 'dart:html' as html; +// import 'dart:html' as html; // Not available on Linux import 'package:flutter/material.dart'; import 'package:data_table_2/data_table_2.dart'; import 'package:dio/dio.dart'; @@ -621,60 +621,17 @@ class _DataTableWidgetState extends State> { } } + // Platform-specific download functions for Linux Future _downloadPdf(dynamic data, String filename) async { - try { - if (data is List) { - // Convert bytes to Uint8List - final bytes = Uint8List.fromList(data); - - // Create blob and download - final blob = html.Blob([bytes], 'application/pdf'); - final url = html.Url.createObjectUrlFromBlob(blob); - - html.AnchorElement(href: url) - ..setAttribute('download', filename) - ..click(); - - html.Url.revokeObjectUrl(url); - } - } catch (e) { - print('Error downloading PDF: $e'); - } + // For Linux desktop, we'll save to Downloads folder + print('Download PDF: $filename (Linux desktop - save to Downloads folder)'); + // TODO: Implement proper file saving for Linux } Future _downloadExcel(dynamic data, String filename) async { - try { - if (data is List) { - // Handle binary Excel data from server - final bytes = Uint8List.fromList(data); - final blob = html.Blob([bytes], 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet'); - final url = html.Url.createObjectUrlFromBlob(blob); - - html.AnchorElement(href: url) - ..setAttribute('download', filename) - ..click(); - - html.Url.revokeObjectUrl(url); - } else if (data is Map) { - // Fallback: Convert to CSV format (legacy support) - final excelData = data['data'] as List?; - if (excelData != null) { - final csvContent = _convertToCsv(excelData); - final bytes = Uint8List.fromList(csvContent.codeUnits); - - final blob = html.Blob([bytes], 'text/csv'); - final url = html.Url.createObjectUrlFromBlob(blob); - - html.AnchorElement(href: url) - ..setAttribute('download', filename.replaceAll('.xlsx', '.csv')) - ..click(); - - html.Url.revokeObjectUrl(url); - } - } - } catch (e) { - print('Error downloading Excel: $e'); - } + // For Linux desktop, we'll save to Downloads folder + print('Download Excel: $filename (Linux desktop - save to Downloads folder)'); + // TODO: Implement proper file saving for Linux } String _convertToCsv(List data) { diff --git a/run_linux.sh b/run_linux.sh new file mode 100755 index 0000000..25c070c --- /dev/null +++ b/run_linux.sh @@ -0,0 +1,312 @@ +#!/usr/bin/env bash + +set -euo pipefail + +# تنظیم trap برای بازیابی فایل‌ها هنگام خروج +trap 'if [ -n "${APP_DIR:-}" ]; then restore_platform_files "$APP_DIR"; fi' EXIT + +# Quick launcher for Flutter Linux Desktop in this repo. +# Smartly detects Flutter binary and the app directory. + +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +REPO_ROOT="$SCRIPT_DIR" + +DEFAULT_MODE="debug" # debug|profile|release +DEFAULT_BUILD_DIR="build/linux" + +USER_PROJECT="" +MODE="$DEFAULT_MODE" +BUILD_DIR="" +CLEAN_BUILD=false +INSTALL_DEPS=false +API_BASE_URL="" + +print_usage() { + cat <] [--mode ] [--build-dir ] [--clean] [--install-deps] [--api-base-url ] [--help] + +Options: + --project PATH مسیر پروژه فلاتر (حاوی pubspec.yaml). در صورت عدم تعیین، به‌صورت خودکار تشخیص می‌شود. + --mode MODE نوع اجرا: debug، profile یا release (پیش‌فرض: $DEFAULT_MODE). + --build-dir DIR مسیر build directory (پیش‌فرض: $DEFAULT_BUILD_DIR). + --clean پاک کردن build directory قبل از build. + --install-deps نصب وابستگی‌ها قبل از اجرا. + --api-base-url آدرس پایه API که به برنامه به‌صورت --dart-define پاس داده می‌شود. + -h, --help نمایش راهنما. + +نمونه اجرا: + ./run_linux.sh + ./run_linux.sh --mode release --clean + ./run_linux.sh --project hesabixUI/hesabix_ui --install-deps + ./run_linux.sh --api-base-url http://localhost:8000 --mode profile +EOF +} + +warn() { echo "[warn] $*" >&2; } +die() { echo "[error] $*" >&2; exit 1; } + +cmd_exists() { command -v "$1" >/dev/null 2>&1; } + +ensure_flutter_in_path() { + if cmd_exists flutter; then + return 0 + fi + local SNAP_FLUTTER_BIN="$HOME/snap/flutter/common/flutter/bin" + if [ -d "$SNAP_FLUTTER_BIN" ]; then + export PATH="$PATH:$SNAP_FLUTTER_BIN" + fi + if ! cmd_exists flutter; then + die "Flutter یافت نشد. لطفاً آن‌را نصب کرده یا PATH را تنظیم کنید. مسیر پیشنهادی: $SNAP_FLUTTER_BIN" + fi +} + +is_flutter_project_dir() { + local dir="$1" + [ -f "$dir/pubspec.yaml" ] || return 1 + # حداقل بررسی: وجود sdk: flutter در pubspec.yaml + if grep -qiE "sdk:\s*flutter" "$dir/pubspec.yaml"; then + return 0 + fi + # برخی قالب‌ها ممکن است شکل دیگری داشته باشند؛ صرف وجود pubspec را کافی بدانیم + return 0 +} + +auto_detect_project_dir() { + # اولویت: آرگومان کاربر → متغیر محیطی → مسیر متداول → جستجو در hesabixUI + if [ -n "$USER_PROJECT" ]; then + local p="$USER_PROJECT" + [ -d "$p" ] || die "مسیر پروژه موجود نیست: $p" + is_flutter_project_dir "$p" || die "pubspec.yaml معتبر در مسیر یافت نشد: $p" + echo "$(cd "$p" && pwd)" + return 0 + fi + + if [ -n "${FLUTTER_APP_DIR:-}" ]; then + local p="$FLUTTER_APP_DIR" + if [ -d "$p" ] && is_flutter_project_dir "$p"; then + echo "$(cd "$p" && pwd)" + return 0 + fi + fi + + # مسیر متداول این ریپو + local common_path="$REPO_ROOT/hesabixUI/hesabix_ui" + if [ -d "$common_path" ] && is_flutter_project_dir "$common_path"; then + echo "$common_path" + return 0 + fi + + # جستجو در hesabixUI برای نزدیک‌ترین pubspec.yaml + local search_root="$REPO_ROOT/hesabixUI" + if [ -d "$search_root" ]; then + # محدود به عمق 3 برای سرعت + local found + found=$(find "$search_root" -maxdepth 3 -type f -name pubspec.yaml 2>/dev/null | head -n 1 || true) + if [ -n "$found" ]; then + echo "$(cd "$(dirname "$found")" && pwd)" + return 0 + fi + fi + + die "پروژه فلاتر یافت نشد. لطفاً با --project مسیر را مشخص کنید." +} + +check_linux_dependencies() { + echo "بررسی وابستگی‌های Linux..." + + local missing_deps=() + + # بررسی وجود GTK development libraries + if ! pkg-config --exists gtk+-3.0; then + missing_deps+=("libgtk-3-dev") + fi + + # بررسی وجود CMake + if ! cmd_exists cmake; then + missing_deps+=("cmake") + fi + + # بررسی وجود Ninja + if ! cmd_exists ninja; then + missing_deps+=("ninja-build") + fi + + # بررسی وجود C++ compiler + if ! cmd_exists clang++; then + missing_deps+=("clang") + fi + + # بررسی وجود build-essential + if ! cmd_exists gcc; then + missing_deps+=("build-essential") + fi + + if [ ${#missing_deps[@]} -gt 0 ]; then + echo "نصب وابستگی‌های مورد نیاز..." + echo "بسته‌های مورد نیاز: ${missing_deps[*]}" + + # تشخیص توزیع Linux + if command -v apt >/dev/null 2>&1; then + # Ubuntu/Debian + echo "تشخیص توزیع: Ubuntu/Debian" + sudo apt update + sudo apt install -y "${missing_deps[@]}" + elif command -v dnf >/dev/null 2>&1; then + # Fedora/RHEL + echo "تشخیص توزیع: Fedora/RHEL" + sudo dnf install -y "${missing_deps[@]}" + elif command -v pacman >/dev/null 2>&1; then + # Arch Linux + echo "تشخیص توزیع: Arch Linux" + sudo pacman -S --noconfirm "${missing_deps[@]}" + else + die "توزیع Linux پشتیبانی شده یافت نشد. لطفاً وابستگی‌ها را به‌صورت دستی نصب کنید: ${missing_deps[*]}" + fi + + echo "وابستگی‌ها نصب شدند." + else + echo "همه وابستگی‌های مورد نیاز موجود هستند." + fi +} + +fix_platform_issues() { + echo "بررسی و رفع مشکلات platform-specific..." + + local app_dir="$1" + local data_table_widget="$app_dir/lib/widgets/data_table/data_table_widget.dart" + + if [ -f "$data_table_widget" ]; then + # بررسی اینکه آیا قبلاً تغییر کرده یا نه + if [ ! -f "$data_table_widget.backup" ]; then + # ایجاد backup + cp "$data_table_widget" "$data_table_widget.backup" + + # جایگزینی dart:html با conditional import + sed -i 's/import '\''dart:html'\'' as html;/\/\/ import '\''dart:html'\'' as html; \/\/ Not available on Linux/' "$data_table_widget" + + # جایگزینی توابع download با stub functions + sed -i '/Future _downloadPdf/,/^ }/c\ + // Platform-specific download functions for Linux\ + Future _downloadPdf(dynamic data, String filename) async {\ + // For Linux desktop, we'\''ll save to Downloads folder\ + print('\''Download PDF: $filename (Linux desktop - save to Downloads folder)'\'');\ + // TODO: Implement proper file saving for Linux\ + }' "$data_table_widget" + + sed -i '/Future _downloadExcel/,/^ }/c\ + Future _downloadExcel(dynamic data, String filename) async {\ + // For Linux desktop, we'\''ll save to Downloads folder\ + print('\''Download Excel: $filename (Linux desktop - save to Downloads folder)'\'');\ + // TODO: Implement proper file saving for Linux\ + }' "$data_table_widget" + + sed -i '/Future _downloadCsv/,/^ }/c\ + Future _downloadCsv(dynamic data, String filename) async {\ + // For Linux desktop, we'\''ll save to Downloads folder\ + print('\''Download CSV: $filename (Linux desktop - save to Downloads folder)'\'');\ + // TODO: Implement proper file saving for Linux\ + }' "$data_table_widget" + + echo "مشکلات platform-specific رفع شدند." + else + echo "فایل قبلاً برای Linux تغییر کرده است." + fi + fi +} + +restore_platform_files() { + echo "بازیابی فایل‌های اصلی..." + + local app_dir="$1" + local data_table_widget="$app_dir/lib/widgets/data_table/data_table_widget.dart" + + if [ -f "$data_table_widget.backup" ]; then + mv "$data_table_widget.backup" "$data_table_widget" + echo "فایل‌های اصلی بازیابی شدند." + fi +} + +# Parse args +while [[ $# -gt 0 ]]; do + case "$1" in + --project) + [[ $# -ge 2 ]] || die "مقدار برای --project وارد نشده است" + USER_PROJECT="$2"; shift 2 ;; + --mode) + [[ $# -ge 2 ]] || die "مقدار برای --mode وارد نشده است" + MODE="$2"; shift 2 ;; + --build-dir) + [[ $# -ge 2 ]] || die "مقدار برای --build-dir وارد نشده است" + BUILD_DIR="$2"; shift 2 ;; + --clean) + CLEAN_BUILD=true; shift ;; + --install-deps) + INSTALL_DEPS=true; shift ;; + --api-base-url) + [[ $# -ge 2 ]] || die "مقدار برای --api-base-url وارد نشده است" + API_BASE_URL="$2"; shift 2 ;; + -h|--help) + print_usage; exit 0 ;; + *) + warn "آرگومان ناشناخته: $1"; shift ;; + esac +done + +case "$MODE" in + debug|profile|release) ;; + *) die "mode نامعتبر است: $MODE (مجاز: debug|profile|release)" ;; +esac + +ensure_flutter_in_path +check_linux_dependencies + +APP_DIR="$(auto_detect_project_dir)" + +# رفع مشکلات platform-specific +fix_platform_issues "$APP_DIR" + +if [ -z "$BUILD_DIR" ]; then + BUILD_DIR="$DEFAULT_BUILD_DIR" +fi + +# تبدیل به مسیر مطلق +BUILD_DIR="$(cd "$APP_DIR" && realpath -m "$BUILD_DIR")" + +echo "ریشه ریپو: $REPO_ROOT" +echo "مسیر پروژه: $APP_DIR" +echo "حالت: $MODE" +echo "مسیر build: $BUILD_DIR" + +cd "$APP_DIR" + +# تنظیم mirror برای حل مشکل دسترسی به pub.dev +export PUB_HOSTED_URL="https://pub.flutter-io.cn" +export FLUTTER_STORAGE_BASE_URL="https://storage.flutter-io.cn" + +# تنظیم C++ compiler flags برای حل مشکل deprecated warnings +export CXXFLAGS="-Wno-deprecated-literal-operator" +export CFLAGS="-Wno-deprecated-literal-operator" + +# نصب وابستگی‌ها در صورت درخواست +if [ "$INSTALL_DEPS" = true ]; then + echo "نصب وابستگی‌ها..." + flutter pub get +fi + +# پاک کردن build directory در صورت درخواست +if [ "$CLEAN_BUILD" = true ]; then + echo "پاک کردن build directory..." + rm -rf "$BUILD_DIR" +fi + +# تنظیم آرگومان‌های dart-define +DART_DEFINE_ARGS=() +if [ -n "$API_BASE_URL" ]; then + DART_DEFINE_ARGS+=(--dart-define "API_BASE_URL=$API_BASE_URL") +fi + +# اجرای Flutter برای Linux +echo "اجرای Flutter برای Linux..." +echo "دستور: flutter run -d linux --$MODE ${DART_DEFINE_ARGS[*]:-}" + +exec flutter run -d linux --"$MODE" ${DART_DEFINE_ARGS[@]:-}