This commit is contained in:
Hesabix 2025-09-22 11:00:18 +03:30
parent bee18daf4a
commit dcada33b89
24 changed files with 3860 additions and 1530 deletions

170
LINUX_SCRIPTS_README.md Normal file
View file

@ -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`

303
build_linux.sh Executable file
View file

@ -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 <<EOF
Usage: ./build_linux.sh [--project <path>] [--mode <debug|profile|release>] [--build-dir <dir>] [--output-dir <dir>] [--clean] [--install-deps] [--api-base-url <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"

View file

@ -427,7 +427,7 @@ async def set_default_storage_config(
@router.delete("/storage-configs/{config_id}", response_model=dict) @router.delete("/storage-configs/{config_id}", response_model=dict)
async def delete_storage_config( async def delete_storage_config(
config_id: UUID, config_id: str,
request: Request, request: Request,
db: Session = Depends(get_db), db: Session = Depends(get_db),
current_user: AuthContext = Depends(get_current_user), current_user: AuthContext = Depends(get_current_user),
@ -435,8 +435,28 @@ async def delete_storage_config(
): ):
"""حذف تنظیمات ذخیره‌سازی""" """حذف تنظیمات ذخیره‌سازی"""
try: 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) 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: if not success:
raise ApiError( raise ApiError(
@ -590,21 +610,131 @@ async def _test_local_storage(config: StorageConfig) -> dict:
async def _test_ftp_storage(config: StorageConfig) -> dict: async def _test_ftp_storage(config: StorageConfig) -> dict:
"""تست اتصال به FTP storage""" """تست اتصال به FTP storage"""
import ftplib
import tempfile
import os
from datetime import datetime from datetime import datetime
try: 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 { return {
"success": False, "success": False,
"error": "تست FTP هنوز پیاده‌سازی نشده است", "error": "پارامترهای ضروری FTP (host, username, password) موجود نیست",
"storage_type": "ftp", "storage_type": "ftp",
"tested_at": datetime.utcnow().isoformat() "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": 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: except Exception as e:
return { return {
"success": False, "success": False,
"error": f"خطا در تست FTP storage: {str(e)}", "error": f"خطا در تست FTP storage: {str(e)}",
"storage_type": "ftp",
"tested_at": datetime.utcnow().isoformat() "tested_at": datetime.utcnow().isoformat()
} }

View file

@ -236,7 +236,15 @@ class StorageConfigRepository(BaseRepository[StorageConfig]):
self.db.query(StorageConfig).update({"is_default": False}) self.db.query(StorageConfig).update({"is_default": False})
self.db.commit() 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() config = self.db.query(StorageConfig).filter(StorageConfig.id == config_id).first()
if not config: if not config:
return False return False

View file

@ -396,6 +396,23 @@
"previous": "Previous", "previous": "Previous",
"next": "Next", "next": "Next",
"first": "First", "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"
} }

View file

@ -395,6 +395,23 @@
"previous": "قبلی", "previous": "قبلی",
"next": "بعدی", "next": "بعدی",
"first": "اول", "first": "اول",
"last": "آخر" "last": "آخر",
"systemSettingsWelcome": "تنظیمات سیستم",
"systemSettingsDescription": "مدیریت پیکربندی و مدیریت سیستم",
"storageManagement": "مدیریت ذخیره‌سازی",
"storageManagementDescription": "پیکربندی سیستم‌های ذخیره‌سازی فایل و مدیریت فایل‌ها",
"systemConfiguration": "پیکربندی سیستم",
"systemConfigurationDescription": "تنظیمات عمومی سیستم و ترجیحات",
"userManagement": "مدیریت کاربران",
"userManagementDescription": "مدیریت کاربران، نقش‌ها و مجوزها",
"systemLogs": "لاگ‌های سیستم",
"systemLogsDescription": "مشاهده لاگ‌های سیستم و نظارت",
"backToSettings": "بازگشت به تنظیمات",
"settingsOverview": "نمای کلی تنظیمات",
"availableSettings": "تنظیمات موجود",
"systemAdministration": "مدیریت سیستم",
"generalSettings": "تنظیمات عمومی",
"securitySettings": "تنظیمات امنیتی",
"maintenanceSettings": "تنظیمات نگهداری"
} }

View file

@ -2215,6 +2215,108 @@ abstract class AppLocalizations {
/// In en, this message translates to: /// In en, this message translates to:
/// **'Last'** /// **'Last'**
String get 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 class _AppLocalizationsDelegate

View file

@ -1097,4 +1097,58 @@ class AppLocalizationsEn extends AppLocalizations {
@override @override
String get last => 'Last'; 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';
} }

View file

@ -1091,4 +1091,56 @@ class AppLocalizationsFa extends AppLocalizations {
@override @override
String get last => 'آخر'; 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 => 'تنظیمات نگهداری';
} }

View file

@ -13,6 +13,10 @@ import 'pages/profile/change_password_page.dart';
import 'pages/profile/marketing_page.dart'; import 'pages/profile/marketing_page.dart';
import 'pages/profile/operator/operator_tickets_page.dart'; import 'pages/profile/operator/operator_tickets_page.dart';
import 'pages/system_settings_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 'package:hesabix_ui/l10n/app_localizations.dart';
import 'core/locale_controller.dart'; import 'core/locale_controller.dart';
import 'core/calendar_controller.dart'; import 'core/calendar_controller.dart';
@ -386,6 +390,48 @@ class _MyAppState extends State<MyApp> {
} }
return const SystemSettingsPage(); 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();
},
),
],
), ),
], ],
), ),

View file

@ -1,70 +1,12 @@
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:hesabix_ui/l10n/app_localizations.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_management_page.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';
class FileStorageSettingsPage extends StatefulWidget { class FileStorageSettingsPage extends StatelessWidget {
const FileStorageSettingsPage({super.key}); const FileStorageSettingsPage({super.key});
@override
State<FileStorageSettingsPage> createState() => _FileStorageSettingsPageState();
}
class _FileStorageSettingsPageState extends State<FileStorageSettingsPage>
with TickerProviderStateMixin {
late TabController _tabController;
@override
void initState() {
super.initState();
_tabController = TabController(length: 3, vsync: this);
}
@override
void dispose() {
_tabController.dispose();
super.dispose();
}
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
final l10n = AppLocalizations.of(context)!; return const StorageManagementPage();
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(),
],
),
);
} }
} }

View file

@ -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<AdminStorageManagementPage> createState() => _AdminStorageManagementPageState();
}
class _AdminStorageManagementPageState extends State<AdminStorageManagementPage> {
final GlobalKey<StorageConfigListWidgetState> _listKey = GlobalKey<StorageConfigListWidgetState>();
@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,
),
);
},
),
);
}
}

View file

@ -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<SystemConfigurationPage> createState() => _SystemConfigurationPageState();
}
class _SystemConfigurationPageState extends State<SystemConfigurationPage> {
final _formKey = GlobalKey<FormState>();
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<Widget> 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<String> 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<DropdownMenuItem<String>> items,
required ValueChanged<String?> onChanged,
}) {
return Padding(
padding: const EdgeInsets.only(bottom: 16),
child: DropdownButtonFormField<String>(
value: value,
decoration: InputDecoration(
labelText: label,
border: const OutlineInputBorder(),
),
items: items,
onChanged: onChanged,
),
);
}
Widget _buildSwitchField({
required String label,
required bool value,
required ValueChanged<bool> 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<int> 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<void> _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);
}
}
}
}

View file

@ -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<SystemLogsPage> createState() => _SystemLogsPageState();
}
class _SystemLogsPageState extends State<SystemLogsPage> {
final _searchController = TextEditingController();
String _selectedLevel = 'all';
String _selectedDateRange = 'today';
bool _isLoading = false;
// Mock log data
final List<Map<String, dynamic>> _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<Map<String, dynamic>> 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<String>(
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<String>(
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<String, dynamic> 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')),
);
}
}

View file

@ -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<UserManagementPage> createState() => _UserManagementPageState();
}
class _UserManagementPageState extends State<UserManagementPage> {
final _searchController = TextEditingController();
String _selectedFilter = 'all';
bool _isLoading = false;
// Mock data - in real app, this would come from API
final List<Map<String, dynamic>> _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<Map<String, dynamic>> 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<String>(
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<String, dynamic> 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<String, dynamic> 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<String, dynamic> 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<String, dynamic> 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<String, dynamic> 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')),
);
}
}

View file

@ -1,44 +1,139 @@
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:go_router/go_router.dart';
import 'package:hesabix_ui/l10n/app_localizations.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}); const SystemSettingsPage({super.key});
@override
State<SystemSettingsPage> createState() => _SystemSettingsPageState();
}
class _SystemSettingsPageState extends State<SystemSettingsPage> {
late final List<SettingsItem> _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 @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
final t = AppLocalizations.of(context)!;
final theme = Theme.of(context); final theme = Theme.of(context);
final colorScheme = theme.colorScheme; final colorScheme = theme.colorScheme;
final t = AppLocalizations.of(context);
return Scaffold( return Scaffold(
appBar: AppBar(
title: Text(t.systemSettings),
backgroundColor: colorScheme.surface, backgroundColor: colorScheme.surface,
foregroundColor: colorScheme.onSurface, appBar: AppBar(
elevation: 0, title: Text(
t.systemSettingsWelcome,
style: const TextStyle(
fontWeight: FontWeight.w600,
fontSize: 20,
), ),
body: Container( ),
color: colorScheme.surface, backgroundColor: Colors.transparent,
child: SafeArea( elevation: 0,
child: Padding( centerTitle: true,
padding: const EdgeInsets.all(16.0), actions: [
IconButton(
onPressed: () => _showHelpDialog(context),
icon: const Icon(Icons.help_outline),
tooltip: t.systemSettingsWelcome,
),
],
),
body: SingleChildScrollView(
padding: const EdgeInsets.all(20),
child: Column( child: Column(
crossAxisAlignment: CrossAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.start,
children: [ children: [
// Header _buildWelcomeSection(theme, colorScheme, t),
Container( const SizedBox(height: 24),
padding: const EdgeInsets.all(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( decoration: BoxDecoration(
color: colorScheme.primaryContainer, gradient: LinearGradient(
borderRadius: BorderRadius.circular(12), 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( child: Row(
children: [ children: [
Icon( Container(
Icons.admin_panel_settings, padding: const EdgeInsets.all(12),
size: 32, decoration: BoxDecoration(
color: colorScheme.onPrimaryContainer, 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), const SizedBox(width: 16),
Expanded( Expanded(
@ -46,204 +141,260 @@ class SystemSettingsPage extends StatelessWidget {
crossAxisAlignment: CrossAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.start,
children: [ children: [
Text( Text(
t.systemSettings, t.systemAdministration,
style: theme.textTheme.headlineSmall?.copyWith( style: theme.textTheme.titleLarge?.copyWith(
color: colorScheme.onPrimaryContainer,
fontWeight: FontWeight.bold, fontWeight: FontWeight.bold,
color: colorScheme.onSurface,
fontSize: 18,
), ),
), ),
const SizedBox(height: 4), const SizedBox(height: 6),
Text( Text(
'تنظیمات پیشرفته سیستم - فقط برای ادمین‌ها', t.systemSettingsDescription,
style: theme.textTheme.bodyMedium?.copyWith( style: theme.textTheme.bodyMedium?.copyWith(
color: colorScheme.onPrimaryContainer.withOpacity(0.8), color: colorScheme.onSurface.withOpacity(0.7),
fontSize: 14,
), ),
), ),
], ],
), ),
), ),
],
),
),
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( Container(
padding: const EdgeInsets.all(16), padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 6),
decoration: BoxDecoration( decoration: BoxDecoration(
color: Colors.amber.withOpacity(0.1), color: colorScheme.primary.withOpacity(0.1),
border: Border.all(color: Colors.amber.withOpacity(0.3)), borderRadius: BorderRadius.circular(20),
borderRadius: BorderRadius.circular(8),
), ),
child: Row(
children: [
Icon(
Icons.warning_amber,
color: Colors.amber[700],
size: 20,
),
const SizedBox(width: 12),
Expanded(
child: Text( child: Text(
'توجه: این بخش فقط برای ادمین‌های سیستم قابل دسترسی است. تغییرات در این بخش می‌تواند بر عملکرد کل سیستم تأثیر بگذارد.', '${_settingsItems.length}',
style: theme.textTheme.bodySmall?.copyWith( style: TextStyle(
color: Colors.amber[700], color: colorScheme.primary,
fontWeight: FontWeight.bold,
fontSize: 12,
), ),
), ),
), ),
], ],
), ),
),
],
),
),
),
),
); );
} }
Widget _buildSettingCard( Widget _buildSettingsList(ThemeData theme, ColorScheme colorScheme, AppLocalizations t) {
BuildContext context, { return Column(
required IconData icon,
required String title,
required String subtitle,
required Color color,
VoidCallback? onTap,
}) {
final theme = Theme.of(context);
final colorScheme = theme.colorScheme;
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, crossAxisAlignment: CrossAxisAlignment.start,
children: [ children: [
Icon( Row(
icon, children: [
color: color,
size: 32,
),
const SizedBox(height: 12),
Text( Text(
title, t.availableSettings,
style: theme.textTheme.titleMedium?.copyWith( style: theme.textTheme.titleLarge?.copyWith(
fontWeight: FontWeight.bold, fontWeight: FontWeight.bold,
color: colorScheme.onSurface, color: colorScheme.onSurface,
), ),
), ),
const SizedBox(height: 4), const Spacer(),
Expanded( Container(
child: Text( padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 4),
subtitle, decoration: BoxDecoration(
style: theme.textTheme.bodySmall?.copyWith( color: colorScheme.primary.withOpacity(0.1),
color: colorScheme.onSurface.withOpacity(0.7), 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: 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, maxLines: 2,
overflow: TextOverflow.ellipsis, overflow: TextOverflow.ellipsis,
), ),
),
const SizedBox(height: 8), const SizedBox(height: 8),
Row( AnimatedContainer(
mainAxisAlignment: MainAxisAlignment.end, duration: const Duration(milliseconds: 200),
children: [ padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 4),
Icon( decoration: BoxDecoration(
color: item.color.withOpacity(0.1),
borderRadius: BorderRadius.circular(12),
),
child: Icon(
Icons.arrow_forward_ios, Icons.arrow_forward_ios,
size: 16, size: 12,
color: colorScheme.onSurface.withOpacity(0.5), color: item.color,
),
), ),
], ],
), ),
],
), ),
), ),
), ),
); );
} }
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;
}
}
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,
});
} }

View file

@ -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<FileManagementWidget> createState() => _FileManagementWidgetState();
}
class _FileManagementWidgetState extends State<FileManagementWidget>
with TickerProviderStateMixin {
late TabController _tabController;
List<Map<String, dynamic>> _allFiles = [];
List<Map<String, dynamic>> _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<void> _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<dynamic>;
final unverifiedFiles = unverifiedResponse.data != null && unverifiedResponse.data['success'] == true
? unverifiedResponse.data['data']['unverified_files'] as List<dynamic>
: <dynamic>[];
setState(() {
_allFiles = files.cast<Map<String, dynamic>>();
_unverifiedFiles = unverifiedFiles.cast<Map<String, dynamic>>();
_isLoading = false;
});
} else {
throw Exception(response.data?['message'] ?? 'خطا در دریافت فایل‌ها');
}
} catch (e) {
setState(() {
_error = e.toString();
_isLoading = false;
});
}
}
Future<void> _forceDeleteFile(String fileId) async {
final l10n = AppLocalizations.of(context);
final confirmed = await showDialog<bool>(
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<void> _restoreFile(String fileId) async {
final l10n = AppLocalizations.of(context);
final confirmed = await showDialog<bool>(
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<Map<String, dynamic>> 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<String>(
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;
}
}

View file

@ -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<FileStatisticsWidget> createState() => _FileStatisticsWidgetState();
}
class _FileStatisticsWidgetState extends State<FileStatisticsWidget> {
Map<String, dynamic>? _statistics;
bool _isLoading = true;
String? _error;
@override
void initState() {
super.initState();
_loadStatistics();
}
Future<void> _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<void> _cleanupTemporaryFiles() async {
final l10n = AppLocalizations.of(context);
final confirmed = await showDialog<bool>(
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,
),
),
],
),
);
}
}

View file

@ -23,47 +23,67 @@ class StorageConfigCard extends StatelessWidget {
final theme = Theme.of(context); final theme = Theme.of(context);
final isDefault = config['is_default'] == true; final isDefault = config['is_default'] == true;
final isActive = config['is_active'] == true; final isActive = config['is_active'] == true;
final storageType = config['storage_type'] ?? 'unknown';
return Card( 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( child: Padding(
padding: const EdgeInsets.all(16), padding: const EdgeInsets.all(20),
child: Column( child: Column(
crossAxisAlignment: CrossAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.start,
children: [ children: [
// Header // Header
Row( Row(
children: [ children: [
Icon( Container(
_getStorageIcon(config['storage_type']), padding: const EdgeInsets.all(12),
color: theme.colorScheme.primary, decoration: BoxDecoration(
size: 24, color: _getStorageColor(storageType).withOpacity(0.1),
borderRadius: BorderRadius.circular(12),
), ),
const SizedBox(width: 12), child: Icon(
_getStorageIcon(storageType),
color: _getStorageColor(storageType),
size: 28,
),
),
const SizedBox(width: 16),
Expanded( Expanded(
child: Column( child: Column(
crossAxisAlignment: CrossAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.start,
children: [ 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),
),
),
],
),
),
// Status badges
Row( Row(
children: [ children: [
if (isDefault) Expanded(
child: Text(
config['name'] ?? 'Unknown',
style: theme.textTheme.titleLarge?.copyWith(
fontWeight: FontWeight.bold,
color: theme.colorScheme.onSurface,
),
),
),
if (isDefault) ...[
Container( Container(
padding: const EdgeInsets.symmetric( padding: const EdgeInsets.symmetric(
horizontal: 8, horizontal: 8,
@ -73,13 +93,47 @@ class StorageConfigCard extends StatelessWidget {
color: theme.colorScheme.primary, color: theme.colorScheme.primary,
borderRadius: BorderRadius.circular(12), borderRadius: BorderRadius.circular(12),
), ),
child: Text( child: Row(
l10n.isDefault, mainAxisSize: MainAxisSize.min,
children: [
Icon(
Icons.star,
size: 16,
color: theme.colorScheme.onPrimary,
),
const SizedBox(width: 4),
Text(
'پیش‌فرض',
style: theme.textTheme.bodySmall?.copyWith( style: theme.textTheme.bodySmall?.copyWith(
color: theme.colorScheme.onPrimary, color: theme.colorScheme.onPrimary,
fontWeight: FontWeight.bold, 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,
),
),
), ),
const SizedBox(width: 8), const SizedBox(width: 8),
Container( Container(
@ -89,10 +143,10 @@ class StorageConfigCard extends StatelessWidget {
), ),
decoration: BoxDecoration( decoration: BoxDecoration(
color: isActive ? Colors.green : Colors.red, color: isActive ? Colors.green : Colors.red,
borderRadius: BorderRadius.circular(12), borderRadius: BorderRadius.circular(8),
), ),
child: Text( child: Text(
isActive ? l10n.isActive : 'غیرفعال', isActive ? 'فعال' : 'غیرفعال',
style: theme.textTheme.bodySmall?.copyWith( style: theme.textTheme.bodySmall?.copyWith(
color: Colors.white, color: Colors.white,
fontWeight: FontWeight.bold, fontWeight: FontWeight.bold,
@ -103,52 +157,57 @@ class StorageConfigCard extends StatelessWidget {
), ),
], ],
), ),
),
],
),
const SizedBox(height: 16), const SizedBox(height: 20),
// Configuration details // Configuration details
_buildConfigDetails(context, config), _buildConfigDetails(context, config),
const SizedBox(height: 16), const SizedBox(height: 20),
// Actions // Actions
Row( Row(
mainAxisAlignment: MainAxisAlignment.end, mainAxisAlignment: MainAxisAlignment.end,
children: [ children: [
if (onTestConnection != null) if (onTestConnection != null)
TextButton.icon( _buildActionButton(
onPressed: onTestConnection, context: context,
icon: const Icon(Icons.wifi_protected_setup, size: 16), icon: Icons.wifi_protected_setup,
label: Text(l10n.testConnection), label: l10n.testConnection,
onPressed: onTestConnection!,
color: theme.colorScheme.primary,
), ),
if (onEdit != null) ...[ if (onEdit != null) ...[
const SizedBox(width: 8), const SizedBox(width: 8),
TextButton.icon( _buildActionButton(
onPressed: onEdit, context: context,
icon: const Icon(Icons.edit, size: 16), icon: Icons.edit,
label: Text(l10n.edit), label: l10n.edit,
onPressed: onEdit!,
color: theme.colorScheme.secondary,
), ),
], ],
if (onSetDefault != null) ...[ if (onSetDefault != null) ...[
const SizedBox(width: 8), const SizedBox(width: 8),
TextButton.icon( _buildActionButton(
onPressed: onSetDefault, context: context,
icon: const Icon(Icons.star, size: 16), icon: Icons.star,
label: Text(l10n.setAsDefault), label: l10n.setAsDefault,
style: TextButton.styleFrom( onPressed: onSetDefault!,
foregroundColor: theme.colorScheme.primary, color: Colors.orange,
),
), ),
], ],
if (onDelete != null) ...[ if (onDelete != null) ...[
const SizedBox(width: 8), const SizedBox(width: 8),
TextButton.icon( _buildActionButton(
onPressed: onDelete, context: context,
icon: const Icon(Icons.delete, size: 16), icon: Icons.delete,
label: Text(l10n.delete), label: l10n.delete,
style: TextButton.styleFrom( onPressed: onDelete!,
foregroundColor: theme.colorScheme.error, color: theme.colorScheme.error,
),
), ),
], ],
], ],
@ -156,70 +215,213 @@ class StorageConfigCard extends StatelessWidget {
], ],
), ),
), ),
),
);
}
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<String, dynamic> config) { Widget _buildConfigDetails(BuildContext context, Map<String, dynamic> config) {
final l10n = AppLocalizations.of(context); final theme = Theme.of(context);
final configData = config['config_data'] ?? {}; final configData = config['config_data'] ?? {};
final storageType = config['storage_type']; final storageType = config['storage_type'];
if (storageType == 'local') { if (storageType == 'local') {
return Column( return _buildLocalConfigDetails(context, configData);
crossAxisAlignment: CrossAxisAlignment.start,
children: [
_buildDetailRow(
context,
l10n.basePath,
configData['base_path'] ?? 'N/A',
Icons.folder,
),
],
);
} else if (storageType == 'ftp') { } else if (storageType == 'ftp') {
return Column( 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),
),
),
);
}
}
Widget _buildLocalConfigDetails(BuildContext context, Map<String, dynamic> 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, crossAxisAlignment: CrossAxisAlignment.start,
children: [ children: [
_buildDetailRow( Row(
context, children: [
l10n.ftpHost, Icon(
configData['host'] ?? 'N/A', Icons.folder_outlined,
Icons.dns, size: 20,
color: theme.colorScheme.primary,
), ),
const SizedBox(height: 8), const SizedBox(width: 8),
_buildDetailRow( Text(
context, 'پیکربندی ذخیره‌سازی محلی',
l10n.ftpPort, style: theme.textTheme.titleSmall?.copyWith(
configData['port']?.toString() ?? 'N/A', fontWeight: FontWeight.bold,
Icons.settings_ethernet, color: theme.colorScheme.onSurface,
), ),
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,
), ),
], ],
),
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',
),
),
),
],
),
),
],
),
); );
} }
return const SizedBox.shrink(); Widget _buildFtpConfigDetails(BuildContext context, Map<String, dynamic> 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 _buildDetailRow( Widget _buildConfigRow(BuildContext context, IconData icon, String label, String value) {
BuildContext context,
String label,
String value,
IconData icon,
) {
final theme = Theme.of(context); final theme = Theme.of(context);
return Row( return Row(
@ -227,22 +429,21 @@ class StorageConfigCard extends StatelessWidget {
Icon( Icon(
icon, icon,
size: 16, size: 16,
color: theme.colorScheme.onSurface.withOpacity(0.6), color: theme.colorScheme.primary,
), ),
const SizedBox(width: 8), const SizedBox(width: 8),
Text( Text(
'$label: ', '$label: ',
style: theme.textTheme.bodyMedium?.copyWith( style: theme.textTheme.bodySmall?.copyWith(
fontWeight: FontWeight.w500, color: theme.colorScheme.onSurface.withOpacity(0.6),
), ),
), ),
Expanded( Expanded(
child: Text( child: Text(
value, value,
style: theme.textTheme.bodyMedium?.copyWith( style: theme.textTheme.bodySmall?.copyWith(
color: theme.colorScheme.onSurface.withOpacity(0.8), fontWeight: FontWeight.w500,
), ),
overflow: TextOverflow.ellipsis,
), ),
), ),
], ],
@ -256,7 +457,18 @@ class StorageConfigCard extends StatelessWidget {
case 'ftp': case 'ftp':
return Icons.cloud_upload; return Icons.cloud_upload;
default: 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': case 'local':
return 'Local Storage'; return 'Local Storage';
case 'ftp': case 'ftp':
return 'FTP Storage'; return 'FTP Server';
default: default:
return 'Unknown Storage'; return 'Unknown';
} }
} }
} }

View file

@ -4,10 +4,12 @@ import '../../../core/api_client.dart';
class StorageConfigFormDialog extends StatefulWidget { class StorageConfigFormDialog extends StatefulWidget {
final Map<String, dynamic>? config; final Map<String, dynamic>? config;
final VoidCallback? onSaved;
const StorageConfigFormDialog({ const StorageConfigFormDialog({
super.key, super.key,
this.config, this.config,
this.onSaved,
}); });
@override @override
@ -28,6 +30,7 @@ class _StorageConfigFormDialogState extends State<StorageConfigFormDialog> {
bool _isDefault = false; bool _isDefault = false;
bool _isActive = true; bool _isActive = true;
bool _isLoading = false; bool _isLoading = false;
bool _useTls = false;
@override @override
void initState() { void initState() {
@ -49,10 +52,11 @@ class _StorageConfigFormDialogState extends State<StorageConfigFormDialog> {
_basePathController.text = configData['base_path'] ?? ''; _basePathController.text = configData['base_path'] ?? '';
} else if (_selectedStorageType == 'ftp') { } else if (_selectedStorageType == 'ftp') {
_ftpHostController.text = configData['host'] ?? ''; _ftpHostController.text = configData['host'] ?? '';
_ftpPortController.text = configData['port']?.toString() ?? '21'; _ftpPortController.text = (configData['port'] ?? 21).toString();
_ftpUsernameController.text = configData['username'] ?? ''; _ftpUsernameController.text = configData['username'] ?? '';
_ftpPasswordController.text = configData['password'] ?? ''; _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<StorageConfigFormDialog> {
super.dispose(); super.dispose();
} }
Map<String, dynamic> _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<void> _saveConfig() async { Future<void> _saveConfig() async {
if (!_formKey.currentState!.validate()) { if (!_formKey.currentState!.validate()) return;
return;
}
setState(() { setState(() {
_isLoading = true; _isLoading = true;
@ -96,29 +81,69 @@ class _StorageConfigFormDialogState extends State<StorageConfigFormDialog> {
try { try {
final api = ApiClient(); final api = ApiClient();
final response = await api.post( Map<String, dynamic> configData = {};
'/api/v1/admin/files/storage-configs/',
data: { if (_selectedStorageType == 'local') {
'name': _nameController.text, 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, 'storage_type': _selectedStorageType,
'config_data': configData,
'is_default': _isDefault, 'is_default': _isDefault,
'is_active': _isActive, 'is_active': _isActive,
'config_data': _buildConfigData(), };
},
);
if (response.data != null && response.data['success'] == true) { if (widget.config != null) {
if (mounted) { // Update existing config
Navigator.of(context).pop(response.data['data']); await api.put(
} '/api/v1/admin/files/storage-configs/${widget.config!['id']}',
data: requestData,
);
} else { } 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) { } catch (e) {
if (mounted) { if (mounted) {
ScaffoldMessenger.of(context).showSnackBar( ScaffoldMessenger.of(context).showSnackBar(
SnackBar( SnackBar(
content: Text('Error: $e'), content: Text('خطا در ذخیره تنظیمات: $e'),
backgroundColor: Colors.red, backgroundColor: Colors.red,
), ),
); );
@ -139,211 +164,192 @@ class _StorageConfigFormDialogState extends State<StorageConfigFormDialog> {
final isEditing = widget.config != null; final isEditing = widget.config != null;
return Dialog( return Dialog(
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(20),
),
child: Container( child: Container(
width: MediaQuery.of(context).size.width * 0.8, constraints: const BoxConstraints(maxWidth: 600, maxHeight: 700),
constraints: const BoxConstraints(maxWidth: 600),
child: Form(
key: _formKey,
child: Column( child: Column(
mainAxisSize: MainAxisSize.min, mainAxisSize: MainAxisSize.min,
children: [ children: [
Padding( // Header
padding: const EdgeInsets.all(16), 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( child: Row(
children: [ children: [
Icon( Icon(
isEditing ? Icons.edit : Icons.add, 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( IconButton(
onPressed: () => Navigator.of(context).pop(), onPressed: () => Navigator.of(context).pop(),
icon: const Icon(Icons.close), icon: const Icon(Icons.close),
color: theme.colorScheme.onPrimary,
), ),
], ],
), ),
), ),
// Form
Flexible( Flexible(
child: SingleChildScrollView( child: SingleChildScrollView(
padding: const EdgeInsets.symmetric(horizontal: 16), padding: const EdgeInsets.all(24),
child: Form(
key: _formKey,
child: Column( child: Column(
crossAxisAlignment: CrossAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.start,
children: [ children: [
// Basic Information
_buildSectionHeader(context, 'اطلاعات پایه'),
const SizedBox(height: 16),
// Name // Name
TextFormField( TextFormField(
controller: _nameController, controller: _nameController,
decoration: InputDecoration( decoration: InputDecoration(
labelText: l10n.storageName, labelText: 'نام',
border: const OutlineInputBorder(), hintText: 'نام پیکربندی ذخیره‌سازی را وارد کنید',
prefixIcon: const Icon(Icons.label_outline),
border: OutlineInputBorder(
borderRadius: BorderRadius.circular(12),
),
), ),
validator: (value) { validator: (value) {
if (value == null || value.isEmpty) { if (value == null || value.trim().isEmpty) {
return l10n.requiredField; return 'لطفاً نام را وارد کنید';
} }
return null; return null;
}, },
), ),
const SizedBox(height: 16), const SizedBox(height: 16),
// Storage Type // Storage Type
DropdownButtonFormField<String>( Text(
value: _selectedStorageType, l10n.storageType,
decoration: InputDecoration( style: theme.textTheme.titleMedium?.copyWith(
labelText: l10n.storageType, fontWeight: FontWeight.bold,
border: const OutlineInputBorder(),
), ),
items: [
DropdownMenuItem(
value: 'local',
child: Text(l10n.localStorage),
),
DropdownMenuItem(
value: 'ftp',
child: Text(l10n.ftpStorage),
), ),
const SizedBox(height: 8),
Row(
children: [
Expanded(
child: RadioListTile<String>(
title: Row(
children: [
Icon(Icons.storage, size: 20),
const SizedBox(width: 8),
Text(l10n.localStorage),
], ],
),
value: 'local',
groupValue: _selectedStorageType,
onChanged: (value) { onChanged: (value) {
setState(() { setState(() {
_selectedStorageType = value!; _selectedStorageType = value!;
}); });
}, },
), ),
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) { Expanded(
if (value == null || value.isEmpty) { child: RadioListTile<String>(
return l10n.requiredField; title: Row(
} children: [
return null; Icon(Icons.cloud_upload, size: 20),
const SizedBox(width: 8),
Text('سرور FTP'),
],
),
value: 'ftp',
groupValue: _selectedStorageType,
onChanged: (value) {
setState(() {
_selectedStorageType = value!;
});
}, },
), ),
] 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;
},
), ),
], ],
),
const SizedBox(height: 24),
// Configuration Details
_buildSectionHeader(context, 'جزئیات پیکربندی'),
const SizedBox(height: 16), const SizedBox(height: 16),
if (_selectedStorageType == 'local') ...[
_buildLocalConfigFields(context),
] else if (_selectedStorageType == 'ftp') ...[
_buildFtpConfigFields(context),
],
const SizedBox(height: 24),
// Options // Options
Row( _buildSectionHeader(context, 'گزینه‌ها'),
children: [ const SizedBox(height: 16),
Checkbox(
SwitchListTile(
title: Text('تنظیم به عنوان پیش‌فرض'),
subtitle: Text('این پیکربندی به عنوان پیش‌فرض تنظیم شود'),
value: _isDefault, value: _isDefault,
onChanged: (value) { onChanged: (value) {
setState(() { setState(() {
_isDefault = value ?? false; _isDefault = value;
}); });
}, },
secondary: const Icon(Icons.star),
), ),
Text(l10n.isDefault),
const SizedBox(width: 24), SwitchListTile(
Checkbox( title: Text('فعال'),
subtitle: Text('این پیکربندی فعال باشد'),
value: _isActive, value: _isActive,
onChanged: (value) { onChanged: (value) {
setState(() { setState(() {
_isActive = value ?? false; _isActive = value;
}); });
}, },
), secondary: const Icon(Icons.power),
Text(l10n.isActive),
],
), ),
], ],
), ),
), ),
), ),
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( child: Row(
mainAxisAlignment: MainAxisAlignment.end, mainAxisAlignment: MainAxisAlignment.end,
children: [ children: [
@ -351,16 +357,26 @@ class _StorageConfigFormDialogState extends State<StorageConfigFormDialog> {
onPressed: _isLoading ? null : () => Navigator.of(context).pop(), onPressed: _isLoading ? null : () => Navigator.of(context).pop(),
child: Text(l10n.cancel), child: Text(l10n.cancel),
), ),
const SizedBox(width: 8), const SizedBox(width: 12),
ElevatedButton( ElevatedButton.icon(
onPressed: _isLoading ? null : _saveConfig, onPressed: _isLoading ? null : _saveConfig,
child: _isLoading icon: _isLoading
? const SizedBox( ? const SizedBox(
width: 16, width: 16,
height: 16, height: 16,
child: CircularProgressIndicator(strokeWidth: 2), 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<StorageConfigFormDialog> {
], ],
), ),
), ),
), );
}
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),
),
],
); );
} }
} }

View file

@ -1,17 +1,21 @@
import 'package:flutter/material.dart'; 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_form_dialog.dart';
import 'package:hesabix_ui/widgets/admin/file_storage/storage_config_card.dart'; import 'package:hesabix_ui/widgets/admin/file_storage/storage_config_card.dart';
import '../../../core/api_client.dart'; import '../../../core/api_client.dart';
class StorageConfigListWidget extends StatefulWidget { class StorageConfigListWidget extends StatefulWidget {
const StorageConfigListWidget({super.key}); final VoidCallback? onRefresh;
const StorageConfigListWidget({
super.key,
this.onRefresh,
});
@override @override
State<StorageConfigListWidget> createState() => _StorageConfigListWidgetState(); State<StorageConfigListWidget> createState() => StorageConfigListWidgetState();
} }
class _StorageConfigListWidgetState extends State<StorageConfigListWidget> { class StorageConfigListWidgetState extends State<StorageConfigListWidget> {
List<Map<String, dynamic>> _storageConfigs = []; List<Map<String, dynamic>> _storageConfigs = [];
bool _isLoading = true; bool _isLoading = true;
String? _error; String? _error;
@ -19,10 +23,10 @@ class _StorageConfigListWidgetState extends State<StorageConfigListWidget> {
@override @override
void initState() { void initState() {
super.initState(); super.initState();
_loadStorageConfigs(); loadStorageConfigs();
} }
Future<void> _loadStorageConfigs() async { Future<void> loadStorageConfigs() async {
setState(() { setState(() {
_isLoading = true; _isLoading = true;
_error = null; _error = null;
@ -49,55 +53,7 @@ class _StorageConfigListWidgetState extends State<StorageConfigListWidget> {
} }
} }
Future<void> _addStorageConfig() async {
final result = await showDialog<Map<String, dynamic>>(
context: context,
builder: (context) => const StorageConfigFormDialog(),
);
if (result != null) {
_loadStorageConfigs();
}
}
Future<void> _editStorageConfig(Map<String, dynamic> config) async {
final result = await showDialog<Map<String, dynamic>>(
context: context,
builder: (context) => StorageConfigFormDialog(config: config),
);
if (result != null) {
_loadStorageConfigs();
}
}
Future<void> _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<void> _testConnection(String configId) async { Future<void> _testConnection(String configId) async {
final l10n = AppLocalizations.of(context);
try { try {
final api = ApiClient(); final api = ApiClient();
final response = await api.post('/api/v1/admin/files/storage-configs/$configId/test'); final response = await api.post('/api/v1/admin/files/storage-configs/$configId/test');
@ -107,14 +63,14 @@ class _StorageConfigListWidgetState extends State<StorageConfigListWidget> {
if (testResult['success'] == true) { if (testResult['success'] == true) {
ScaffoldMessenger.of(context).showSnackBar( ScaffoldMessenger.of(context).showSnackBar(
SnackBar( SnackBar(
content: Text(l10n.connectionSuccessful), content: Text('اتصال موفقیت‌آمیز بود'),
backgroundColor: Colors.green, backgroundColor: Colors.green,
), ),
); );
} else { } else {
ScaffoldMessenger.of(context).showSnackBar( ScaffoldMessenger.of(context).showSnackBar(
SnackBar( SnackBar(
content: Text('${l10n.connectionFailed}: ${testResult['error']}'), content: Text('اتصال ناموفق: ${testResult['error']}'),
backgroundColor: Colors.red, backgroundColor: Colors.red,
), ),
); );
@ -125,29 +81,27 @@ class _StorageConfigListWidgetState extends State<StorageConfigListWidget> {
} catch (e) { } catch (e) {
ScaffoldMessenger.of(context).showSnackBar( ScaffoldMessenger.of(context).showSnackBar(
SnackBar( SnackBar(
content: Text('${l10n.connectionFailed}: $e'), content: Text('اتصال ناموفق: $e'),
backgroundColor: Colors.red, backgroundColor: Colors.red,
), ),
); );
} }
} }
Future<void> _deleteConfig(String configId) async { Future<void> _deleteConfig(String configId) async {
final l10n = AppLocalizations.of(context);
final confirmed = await showDialog<bool>( final confirmed = await showDialog<bool>(
context: context, context: context,
builder: (context) => AlertDialog( builder: (context) => AlertDialog(
title: Text(l10n.deleteConfirm), title: Text('تأیید حذف'),
content: Text(l10n.deleteConfirmMessage), content: Text('آیا از حذف این پیکربندی اطمینان دارید؟'),
actions: [ actions: [
TextButton( TextButton(
onPressed: () => Navigator.of(context).pop(false), onPressed: () => Navigator.of(context).pop(false),
child: Text(l10n.cancel), child: Text('لغو'),
), ),
TextButton( TextButton(
onPressed: () => Navigator.of(context).pop(true), onPressed: () => Navigator.of(context).pop(true),
child: Text(l10n.delete), child: Text('حذف'),
), ),
], ],
), ),
@ -155,36 +109,116 @@ class _StorageConfigListWidgetState extends State<StorageConfigListWidget> {
if (confirmed == true) { if (confirmed == true) {
try { try {
// TODO: Call API to delete config final api = ApiClient();
await Future.delayed(const Duration(seconds: 1)); // Simulate API call final response = await api.delete('/api/v1/admin/files/storage-configs/$configId');
if (response.data != null && response.data['success'] == true) {
ScaffoldMessenger.of(context).showSnackBar( ScaffoldMessenger.of(context).showSnackBar(
SnackBar( SnackBar(
content: Text(l10n.fileDeleted), content: Text('فایل حذف شد'),
backgroundColor: Colors.green, backgroundColor: Colors.green,
), ),
); );
_loadStorageConfigs(); // 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(errorMessage),
backgroundColor: Colors.red,
duration: const Duration(seconds: 5),
),
);
}
}
}
Future<void> _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) { } catch (e) {
ScaffoldMessenger.of(context).showSnackBar( ScaffoldMessenger.of(context).showSnackBar(
SnackBar( SnackBar(
content: Text('Error: $e'), content: Text('خطا در تنظیم به عنوان پیش‌فرض: $e'),
backgroundColor: Colors.red, backgroundColor: Colors.red,
), ),
); );
} }
} }
void _editStorageConfig(Map<String, dynamic> 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 @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
final l10n = AppLocalizations.of(context);
final theme = Theme.of(context); final theme = Theme.of(context);
if (_isLoading) { if (_isLoading) {
return const Center( return Center(
child: CircularProgressIndicator(), child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
CircularProgressIndicator(
color: theme.colorScheme.primary,
),
const SizedBox(height: 16),
Text(
'در حال بارگذاری...',
style: theme.textTheme.bodyLarge,
),
],
),
); );
} }
@ -199,69 +233,109 @@ class _StorageConfigListWidgetState extends State<StorageConfigListWidget> {
color: theme.colorScheme.error, color: theme.colorScheme.error,
), ),
const SizedBox(height: 16), const SizedBox(height: 16),
Text(
'خطا',
style: theme.textTheme.headlineSmall?.copyWith(
color: theme.colorScheme.error,
),
),
const SizedBox(height: 8),
Text( Text(
_error!, _error!,
style: theme.textTheme.bodyLarge, style: theme.textTheme.bodyMedium,
textAlign: TextAlign.center, textAlign: TextAlign.center,
), ),
const SizedBox(height: 16), const SizedBox(height: 16),
ElevatedButton( ElevatedButton.icon(
onPressed: _loadStorageConfigs, onPressed: loadStorageConfigs,
child: Text(l10n.retry), icon: const Icon(Icons.refresh),
label: Text('تلاش مجدد'),
), ),
], ],
), ),
); );
} }
return Column( if (_storageConfigs.isEmpty) {
children: [ return Center(
Padding(
padding: const EdgeInsets.all(16.0),
child: Row(
children: [
Expanded(
child: Text(
l10n.storageConfigurations,
style: theme.textTheme.headlineSmall,
),
),
ElevatedButton.icon(
onPressed: _addStorageConfig,
icon: const Icon(Icons.add),
label: Text(l10n.addStorageConfig),
),
],
),
),
Expanded(
child: _storageConfigs.isEmpty
? Center(
child: Column( child: Column(
mainAxisAlignment: MainAxisAlignment.center, mainAxisAlignment: MainAxisAlignment.center,
children: [ children: [
Icon( Icon(
Icons.storage_outlined, Icons.storage_outlined,
size: 64, size: 64,
color: theme.colorScheme.onSurface.withOpacity(0.5), color: theme.colorScheme.primary.withOpacity(0.5),
), ),
const SizedBox(height: 16), const SizedBox(height: 16),
Text( Text(
l10n.noFilesFound, 'هیچ پیکربندی ذخیره‌سازی وجود ندارد',
style: theme.textTheme.bodyLarge?.copyWith( style: theme.textTheme.headlineSmall?.copyWith(
color: theme.colorScheme.onSurface.withOpacity(0.7), color: theme.colorScheme.onSurface.withOpacity(0.6),
),
),
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,
), ),
), ),
], ],
), ),
) );
: ListView.builder( }
padding: const EdgeInsets.symmetric(horizontal: 16),
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,
),
),
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, itemCount: _storageConfigs.length,
itemBuilder: (context, index) { itemBuilder: (context, index) {
final config = _storageConfigs[index]; final config = _storageConfigs[index];
return Padding( return Padding(
padding: const EdgeInsets.only(bottom: 8), padding: const EdgeInsets.only(bottom: 16.0),
child: StorageConfigCard( child: StorageConfigCard(
config: config, config: config,
onEdit: () => _editStorageConfig(config), onEdit: () => _editStorageConfig(config),
@ -269,15 +343,15 @@ class _StorageConfigListWidgetState extends State<StorageConfigListWidget> {
? () => _setAsDefault(config['id']) ? () => _setAsDefault(config['id'])
: null, : null,
onTestConnection: () => _testConnection(config['id']), onTestConnection: () => _testConnection(config['id']),
onDelete: config['is_default'] == false onDelete: () => _deleteConfig(config['id']),
? () => _deleteConfig(config['id'])
: null,
), ),
); );
}, },
), ),
), ),
], ],
),
),
); );
} }
} }

View file

@ -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<StorageManagementPage> createState() => _StorageManagementPageState();
}
class _StorageManagementPageState extends State<StorageManagementPage> {
final GlobalKey<StorageConfigListWidgetState> _listKey = GlobalKey<StorageConfigListWidgetState>();
@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,
),
);
},
),
);
}
}

View file

@ -1,6 +1,6 @@
import 'dart:async'; import 'dart:async';
import 'dart:typed_data'; 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:flutter/material.dart';
import 'package:data_table_2/data_table_2.dart'; import 'package:data_table_2/data_table_2.dart';
import 'package:dio/dio.dart'; import 'package:dio/dio.dart';
@ -621,60 +621,17 @@ class _DataTableWidgetState<T> extends State<DataTableWidget<T>> {
} }
} }
// Platform-specific download functions for Linux
Future<void> _downloadPdf(dynamic data, String filename) async { Future<void> _downloadPdf(dynamic data, String filename) async {
try { // For Linux desktop, we'll save to Downloads folder
if (data is List<int>) { print('Download PDF: $filename (Linux desktop - save to Downloads folder)');
// Convert bytes to Uint8List // TODO: Implement proper file saving for Linux
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');
}
} }
Future<void> _downloadExcel(dynamic data, String filename) async { Future<void> _downloadExcel(dynamic data, String filename) async {
try { // For Linux desktop, we'll save to Downloads folder
if (data is List<int>) { print('Download Excel: $filename (Linux desktop - save to Downloads folder)');
// Handle binary Excel data from server // TODO: Implement proper file saving for Linux
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<String, dynamic>) {
// Fallback: Convert to CSV format (legacy support)
final excelData = data['data'] as List<dynamic>?;
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');
}
} }
String _convertToCsv(List<dynamic> data) { String _convertToCsv(List<dynamic> data) {

312
run_linux.sh Executable file
View file

@ -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 <<EOF
Usage: ./run_linux.sh [--project <path>] [--mode <debug|profile|release>] [--build-dir <dir>] [--clean] [--install-deps] [--api-base-url <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<void> _downloadPdf/,/^ }/c\
// Platform-specific download functions for Linux\
Future<void> _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<void> _downloadExcel/,/^ }/c\
Future<void> _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<void> _downloadCsv/,/^ }/c\
Future<void> _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[@]:-}