diff --git a/docs/EMAIL_SERVICE_README.md b/docs/EMAIL_SERVICE_README.md
new file mode 100644
index 0000000..db3213b
--- /dev/null
+++ b/docs/EMAIL_SERVICE_README.md
@@ -0,0 +1,210 @@
+# سرویس ایمیل حسابیکس
+
+## نمای کلی
+
+سرویس ایمیل حسابیکس یک سیستم داخلی برای ارسال ایمیل است که توسعهدهندگان میتوانند به راحتی از آن استفاده کنند. این سرویس از SMTP استفاده میکند و تنظیمات اتصال در دیتابیس ذخیره میشود.
+
+## ویژگیها
+
+- ✅ ارسال ایمیل با SMTP
+- ✅ پشتیبانی از TLS و SSL
+- ✅ ذخیره تنظیمات در دیتابیس
+- ✅ مدیریت چندین پیکربندی
+- ✅ تست اتصال
+- ✅ رابط کاربری برای مدیریت
+- ✅ پشتیبانی از چندزبانه (فارسی/انگلیسی)
+- ✅ امنیت و رمزگذاری
+
+## ساختار فایلها
+
+### Backend
+
+```
+hesabixAPI/
+├── adapters/db/models/email_config.py # مدل دیتابیس
+├── adapters/db/repositories/email_config_repository.py # Repository
+├── adapters/api/v1/schema_models/email.py # Schema models
+├── adapters/api/v1/admin/email_config.py # API endpoints
+├── app/services/email_service.py # سرویس اصلی
+└── locales/
+ ├── fa/LC_MESSAGES/messages.po # ترجمههای فارسی
+ └── en/LC_MESSAGES/messages.po # ترجمههای انگلیسی
+```
+
+### Frontend
+
+```
+hesabixUI/hesabix_ui/lib/
+├── models/email_models.dart # مدلهای Flutter
+├── services/email_service.dart # سرویس Flutter
+├── pages/admin/email_settings_page.dart # صفحه مدیریت
+└── l10n/
+ ├── app_fa.arb # ترجمههای فارسی
+ └── app_en.arb # ترجمههای انگلیسی
+```
+
+## استفاده برای توسعهدهندگان
+
+### 1. ارسال ایمیل ساده
+
+```dart
+import 'package:hesabix_ui/services/email_service.dart';
+
+final emailService = EmailService();
+
+// ارسال ایمیل سفارشی
+await emailService.sendCustomEmail(
+ to: 'user@example.com',
+ subject: 'عنوان ایمیل',
+ body: 'متن ایمیل',
+ htmlBody: '
عنوان
متن
',
+);
+```
+
+### 2. ارسال ایمیل خوشآمدگویی
+
+```dart
+await emailService.sendWelcomeEmail(
+ 'user@example.com',
+ 'نام کاربر',
+);
+```
+
+### 3. ارسال ایمیل بازیابی رمز عبور
+
+```dart
+await emailService.sendPasswordResetEmail(
+ 'user@example.com',
+ 'https://example.com/reset?token=abc123',
+);
+```
+
+### 4. ارسال ایمیل اطلاعرسانی
+
+```dart
+await emailService.sendNotificationEmail(
+ 'user@example.com',
+ 'عنوان اطلاعرسانی',
+ 'پیام اطلاعرسانی',
+);
+```
+
+## مدیریت تنظیمات
+
+### 1. دسترسی به صفحه تنظیمات
+
+1. وارد بخش "تنظیمات سیستم" شوید
+2. روی "تنظیمات ایمیل" کلیک کنید
+
+### 2. افزودن پیکربندی جدید
+
+1. فرم را پر کنید:
+ - **نام پیکربندی**: نام منحصر به فرد
+ - **میزبان SMTP**: آدرس سرور SMTP
+ - **پورت SMTP**: پورت سرور (معمولاً 587 یا 465)
+ - **نام کاربری**: نام کاربری SMTP
+ - **رمز عبور**: رمز عبور SMTP
+ - **ایمیل فرستنده**: آدرس ایمیل فرستنده
+ - **نام فرستنده**: نام نمایشی فرستنده
+ - **TLS/SSL**: نوع رمزگذاری
+
+2. روی "ذخیره پیکربندی" کلیک کنید
+
+### 3. تست اتصال
+
+1. پیکربندی مورد نظر را انتخاب کنید
+2. روی "تست اتصال" کلیک کنید
+3. وضعیت اتصال نمایش داده میشود
+
+### 4. ارسال ایمیل تست
+
+1. پیکربندی مورد نظر را انتخاب کنید
+2. روی "ارسال ایمیل تست" کلیک کنید
+3. ایمیل تست به آدرس "ایمیل فرستنده" ارسال میشود
+
+## API Endpoints
+
+### مدیریت پیکربندیها
+
+- `GET /api/v1/admin/email/configs` - دریافت لیست پیکربندیها
+- `GET /api/v1/admin/email/configs/{id}` - دریافت پیکربندی خاص
+- `POST /api/v1/admin/email/configs` - ایجاد پیکربندی جدید
+- `PUT /api/v1/admin/email/configs/{id}` - بروزرسانی پیکربندی
+- `DELETE /api/v1/admin/email/configs/{id}` - حذف پیکربندی
+
+### تست و ارسال
+
+- `POST /api/v1/admin/email/configs/{id}/test` - تست اتصال
+- `POST /api/v1/admin/email/configs/{id}/activate` - فعالسازی پیکربندی
+- `POST /api/v1/admin/email/send` - ارسال ایمیل
+
+## امنیت
+
+- رمزهای عبور SMTP در دیتابیس ذخیره میشوند (باید رمزگذاری شوند)
+- تمام endpoint ها نیاز به احراز هویت دارند
+- تست اتصال قبل از فعالسازی انجام میشود
+
+## چندزبانه
+
+### فارسی
+- تمام متنها به فارسی ترجمه شدهاند
+- پشتیبانی از RTL
+- فرمت تاریخ شمسی
+
+### انگلیسی
+- پشتیبانی کامل از انگلیسی
+- فرمت تاریخ میلادی
+
+## عیبیابی
+
+### مشکلات رایج
+
+1. **خطا در اتصال SMTP**
+ - بررسی صحت آدرس میزبان و پورت
+ - بررسی نام کاربری و رمز عبور
+ - بررسی تنظیمات TLS/SSL
+
+2. **ایمیل ارسال نمیشود**
+ - بررسی پیکربندی فعال
+ - تست اتصال
+ - بررسی لاگهای سرور
+
+3. **خطا در رابط کاربری**
+ - بررسی اتصال به API
+ - بررسی مجوزهای کاربر
+ - بررسی ترجمهها
+
+### لاگها
+
+- لاگهای ارسال ایمیل در console نمایش داده میشوند
+- خطاهای SMTP در response API نمایش داده میشوند
+
+## توسعه آینده
+
+### ویژگیهای پیشنهادی
+
+- [ ] سیستم قالبهای ایمیل
+- [ ] صف ارسال ایمیل
+- [ ] آمار ارسال
+- [ ] لاگگیری کامل
+- [ ] رمزگذاری رمزهای عبور
+- [ ] پشتیبانی از چندین ارائهدهنده SMTP
+- [ ] تست خودکار اتصال
+
+### بهبودهای فنی
+
+- [ ] Cache کردن پیکربندیها
+- [ ] Connection pooling
+- [ ] Retry mechanism
+- [ ] Rate limiting
+- [ ] Monitoring و alerting
+
+## پشتیبانی
+
+برای گزارش مشکلات یا درخواست ویژگیهای جدید، لطفاً با تیم توسعه تماس بگیرید.
+
+---
+
+**نسخه**: 1.0.0
+**تاریخ**: 2025-01-17
+**نویسنده**: تیم توسعه حسابیکس
diff --git a/hesabixAPI/adapters/api/v1/admin/email_config.py b/hesabixAPI/adapters/api/v1/admin/email_config.py
new file mode 100644
index 0000000..fe79901
--- /dev/null
+++ b/hesabixAPI/adapters/api/v1/admin/email_config.py
@@ -0,0 +1,349 @@
+from fastapi import APIRouter, Depends, HTTPException, Request
+from sqlalchemy.orm import Session
+from typing import List
+
+from adapters.db.session import get_db
+from adapters.db.models.email_config import EmailConfig
+from adapters.db.repositories.email_config_repository import EmailConfigRepository
+from adapters.api.v1.schema_models.email import (
+ EmailConfigCreate,
+ EmailConfigUpdate,
+ EmailConfigResponse,
+ SendEmailRequest,
+ TestConnectionRequest
+)
+from adapters.api.v1.schemas import SuccessResponse
+from app.core.responses import success_response, format_datetime_fields
+from app.core.permissions import require_app_permission
+from app.core.auth_dependency import get_current_user, AuthContext
+from app.core.i18n import gettext, negotiate_locale
+
+router = APIRouter(prefix="/admin/email", tags=["Email Configuration"])
+
+
+@router.get("/configs", response_model=SuccessResponse)
+@require_app_permission("superadmin")
+async def get_email_configs(
+ request: Request,
+ db: Session = Depends(get_db),
+ ctx: AuthContext = Depends(get_current_user)
+):
+ """Get all email configurations"""
+ try:
+ email_repo = EmailConfigRepository(db)
+ configs = email_repo.get_all_configs()
+
+ config_responses = [
+ EmailConfigResponse.model_validate(config) for config in configs
+ ]
+
+ # Format datetime fields based on calendar type
+ formatted_data = format_datetime_fields(config_responses, request)
+
+ return success_response(
+ data=formatted_data,
+ request=request
+ )
+ except Exception as e:
+ raise HTTPException(status_code=500, detail=str(e))
+
+
+@router.get("/configs/{config_id}", response_model=SuccessResponse)
+@require_app_permission("superadmin")
+async def get_email_config(
+ config_id: int,
+ request: Request,
+ db: Session = Depends(get_db),
+ ctx: AuthContext = Depends(get_current_user)
+):
+ """Get specific email configuration"""
+ try:
+ email_repo = EmailConfigRepository(db)
+ config = email_repo.get_by_id(config_id)
+
+ if not config:
+ locale = negotiate_locale(request.headers.get("Accept-Language"))
+ raise HTTPException(status_code=404, detail=gettext("Email configuration not found", locale))
+
+ config_response = EmailConfigResponse.model_validate(config)
+
+ # Format datetime fields based on calendar type
+ formatted_data = format_datetime_fields(config_response.model_dump(), request)
+
+ return success_response(
+ data=formatted_data,
+ request=request
+ )
+ except HTTPException:
+ raise
+ except Exception as e:
+ raise HTTPException(status_code=500, detail=str(e))
+
+
+@router.post("/configs", response_model=SuccessResponse)
+@require_app_permission("superadmin")
+async def create_email_config(
+ request_data: EmailConfigCreate,
+ request: Request,
+ db: Session = Depends(get_db),
+ ctx: AuthContext = Depends(get_current_user)
+):
+ """Create new email configuration"""
+ try:
+ email_repo = EmailConfigRepository(db)
+
+ # Get locale from request
+ locale = negotiate_locale(request.headers.get("Accept-Language"))
+
+ # Check if name already exists
+ existing_config = email_repo.get_by_name(request_data.name)
+ if existing_config:
+ raise HTTPException(status_code=400, detail=gettext("Configuration name already exists", locale))
+
+ # Create new config
+ config = EmailConfig(**request_data.model_dump())
+ email_repo.db.add(config)
+ email_repo.db.commit()
+ email_repo.db.refresh(config)
+
+ # If this is the first config, set it as default
+ if not email_repo.get_default_config():
+ email_repo.set_default_config(config.id)
+
+ config_response = EmailConfigResponse.model_validate(config)
+
+ # Format datetime fields based on calendar type
+ formatted_data = format_datetime_fields(config_response.model_dump(), request)
+
+ return success_response(
+ data=formatted_data,
+ request=request
+ )
+ except HTTPException:
+ raise
+ except Exception as e:
+ raise HTTPException(status_code=500, detail=str(e))
+
+
+@router.put("/configs/{config_id}", response_model=SuccessResponse)
+@require_app_permission("superadmin")
+async def update_email_config(
+ config_id: int,
+ request_data: EmailConfigUpdate,
+ request: Request,
+ db: Session = Depends(get_db),
+ ctx: AuthContext = Depends(get_current_user)
+):
+ """Update email configuration"""
+ try:
+ email_repo = EmailConfigRepository(db)
+ config = email_repo.get_by_id(config_id)
+
+ # Get locale from request
+ locale = negotiate_locale(request.headers.get("Accept-Language"))
+
+ if not config:
+ raise HTTPException(status_code=404, detail=gettext("Email configuration not found", locale))
+
+ # Check name uniqueness if name is being updated
+ if request_data.name and request_data.name != config.name:
+ existing_config = email_repo.get_by_name(request_data.name)
+ if existing_config:
+ raise HTTPException(status_code=400, detail=gettext("Configuration name already exists", locale))
+
+ # Update config
+ update_data = request_data.model_dump(exclude_unset=True)
+
+ # Prevent changing is_default through update - use set-default endpoint instead
+ if 'is_default' in update_data:
+ del update_data['is_default']
+
+ for field, value in update_data.items():
+ setattr(config, field, value)
+
+ email_repo.update(config)
+
+ config_response = EmailConfigResponse.model_validate(config)
+
+ # Format datetime fields based on calendar type
+ formatted_data = format_datetime_fields(config_response.model_dump(), request)
+
+ return success_response(
+ data=formatted_data,
+ request=request
+ )
+ except HTTPException:
+ raise
+ except Exception as e:
+ raise HTTPException(status_code=500, detail=str(e))
+
+
+@router.delete("/configs/{config_id}", response_model=SuccessResponse)
+@require_app_permission("superadmin")
+async def delete_email_config(
+ config_id: int,
+ request: Request,
+ db: Session = Depends(get_db),
+ ctx: AuthContext = Depends(get_current_user)
+):
+ """Delete email configuration"""
+ try:
+ email_repo = EmailConfigRepository(db)
+ config = email_repo.get_by_id(config_id)
+
+ # Get locale from request
+ locale = negotiate_locale(request.headers.get("Accept-Language"))
+
+ if not config:
+ raise HTTPException(status_code=404, detail=gettext("Email configuration not found", locale))
+
+ # Prevent deletion of default config
+ if config.is_default:
+ raise HTTPException(status_code=400, detail=gettext("Cannot delete default configuration", locale))
+
+ email_repo.delete(config)
+
+ return success_response(
+ data=None,
+ request=request
+ )
+ except HTTPException:
+ raise
+ except Exception as e:
+ raise HTTPException(status_code=500, detail=str(e))
+
+
+@router.post("/configs/{config_id}/test", response_model=SuccessResponse)
+@require_app_permission("superadmin")
+async def test_email_config(
+ config_id: int,
+ request: Request,
+ db: Session = Depends(get_db),
+ ctx: AuthContext = Depends(get_current_user)
+):
+ """Test email configuration connection"""
+ try:
+ email_repo = EmailConfigRepository(db)
+ config = email_repo.get_by_id(config_id)
+
+ # Get locale from request
+ locale = negotiate_locale(request.headers.get("Accept-Language"))
+
+ if not config:
+ raise HTTPException(status_code=404, detail=gettext("Email configuration not found", locale))
+
+ is_connected = email_repo.test_connection(config)
+
+ return success_response(
+ data={"connected": is_connected},
+ request=request
+ )
+ except HTTPException:
+ raise
+ except Exception as e:
+ raise HTTPException(status_code=500, detail=str(e))
+
+
+@router.post("/configs/{config_id}/activate", response_model=SuccessResponse)
+@require_app_permission("superadmin")
+async def activate_email_config(
+ config_id: int,
+ request: Request,
+ db: Session = Depends(get_db),
+ ctx: AuthContext = Depends(get_current_user)
+):
+ """Activate email configuration"""
+ try:
+ email_repo = EmailConfigRepository(db)
+ config = email_repo.get_by_id(config_id)
+
+ # Get locale from request
+ locale = negotiate_locale(request.headers.get("Accept-Language"))
+
+ if not config:
+ raise HTTPException(status_code=404, detail=gettext("Email configuration not found", locale))
+
+ success = email_repo.set_active_config(config_id)
+
+ if not success:
+ raise HTTPException(status_code=500, detail=gettext("Failed to activate configuration", locale))
+
+ return success_response(
+ data=None,
+ request=request
+ )
+ except HTTPException:
+ raise
+ except Exception as e:
+ raise HTTPException(status_code=500, detail=str(e))
+
+
+@router.post("/configs/{config_id}/set-default", response_model=SuccessResponse)
+@require_app_permission("superadmin")
+async def set_default_email_config(
+ config_id: int,
+ request: Request,
+ db: Session = Depends(get_db),
+ ctx: AuthContext = Depends(get_current_user)
+):
+ """Set email configuration as default"""
+ try:
+ email_repo = EmailConfigRepository(db)
+ config = email_repo.get_by_id(config_id)
+
+ # Get locale from request
+ locale = negotiate_locale(request.headers.get("Accept-Language"))
+
+ if not config:
+ raise HTTPException(status_code=404, detail=gettext("Email configuration not found", locale))
+
+ success = email_repo.set_default_config(config_id)
+
+ if not success:
+ raise HTTPException(status_code=500, detail=gettext("Failed to set default configuration", locale))
+
+ return success_response(
+ data=None,
+ request=request
+ )
+ except HTTPException:
+ raise
+ except Exception as e:
+ raise HTTPException(status_code=500, detail=str(e))
+
+
+@router.post("/send", response_model=SuccessResponse)
+@require_app_permission("superadmin")
+async def send_email(
+ request_data: SendEmailRequest,
+ request: Request,
+ db: Session = Depends(get_db),
+ ctx: AuthContext = Depends(get_current_user)
+):
+ """Send email using configured SMTP"""
+ try:
+ from app.services.email_service import EmailService
+
+ email_service = EmailService(db)
+ success = email_service.send_email(
+ to=request_data.to,
+ subject=request_data.subject,
+ body=request_data.body,
+ html_body=request_data.html_body,
+ config_id=request_data.config_id
+ )
+
+ # Get locale from request
+ locale = negotiate_locale(request.headers.get("Accept-Language"))
+
+ if not success:
+ raise HTTPException(status_code=500, detail=gettext("Failed to send email", locale))
+
+ return success_response(
+ data={"sent": True},
+ request=request
+ )
+ except HTTPException:
+ raise
+ except Exception as e:
+ raise HTTPException(status_code=500, detail=str(e))
diff --git a/hesabixAPI/adapters/api/v1/admin/file_storage.py b/hesabixAPI/adapters/api/v1/admin/file_storage.py
index 462df1e..906242c 100644
--- a/hesabixAPI/adapters/api/v1/admin/file_storage.py
+++ b/hesabixAPI/adapters/api/v1/admin/file_storage.py
@@ -6,14 +6,14 @@ from sqlalchemy import and_
from adapters.db.session import get_db
from app.core.auth_dependency import get_current_user, AuthContext
-from app.core.permissions import require_permission
+from app.core.permissions import require_app_permission
from app.core.responses import success_response
from app.core.responses import ApiError
from app.core.i18n import locale_dependency
from app.services.file_storage_service import FileStorageService
from adapters.db.repositories.file_storage_repository import StorageConfigRepository, FileStorageRepository
from adapters.db.models.user import User
-from adapters.db.models.file_storage import StorageConfig
+from adapters.db.models.file_storage import StorageConfig, FileStorage
from adapters.api.v1.schema_models.file_storage import (
StorageConfigCreateRequest,
StorageConfigUpdateRequest,
@@ -30,6 +30,7 @@ router = APIRouter(prefix="/admin/files", tags=["Admin File Management"])
@router.get("/", response_model=dict)
+@require_app_permission("superadmin")
async def list_all_files(
request: Request,
page: int = Query(1, ge=1),
@@ -43,15 +44,6 @@ async def list_all_files(
):
"""لیست تمام فایلها با فیلتر"""
try:
- # Check permission
- if not current_user.has_app_permission("admin.file.view"):
- raise ApiError(
- code="FORBIDDEN",
- message=translator.t("FORBIDDEN", "دسترسی غیرمجاز"),
- http_status=403,
- translator=translator
- )
-
file_repo = FileStorageRepository(db)
# محاسبه offset برای pagination
@@ -130,6 +122,7 @@ async def list_all_files(
@router.get("/unverified", response_model=dict)
+@require_app_permission("superadmin")
async def get_unverified_files(
request: Request,
db: Session = Depends(get_db),
@@ -167,6 +160,7 @@ async def get_unverified_files(
@router.post("/cleanup-temporary", response_model=dict)
+@require_app_permission("superadmin")
async def cleanup_temporary_files(
request: Request,
db: Session = Depends(get_db),
@@ -194,6 +188,7 @@ async def cleanup_temporary_files(
@router.delete("/{file_id}", response_model=dict)
+@require_app_permission("superadmin")
async def force_delete_file(
file_id: UUID,
request: Request,
@@ -228,6 +223,7 @@ async def force_delete_file(
@router.put("/{file_id}/restore", response_model=dict)
+@require_app_permission("superadmin")
async def restore_file(
file_id: UUID,
request: Request,
@@ -262,6 +258,7 @@ async def restore_file(
@router.get("/statistics", response_model=dict)
+@require_app_permission("superadmin")
async def get_file_statistics(
request: Request,
db: Session = Depends(get_db),
@@ -285,6 +282,7 @@ async def get_file_statistics(
# Storage Configuration Management
@router.get("/storage-configs/", response_model=dict)
+@require_app_permission("superadmin")
async def get_storage_configs(
request: Request,
db: Session = Depends(get_db),
@@ -293,15 +291,6 @@ async def get_storage_configs(
):
"""لیست تنظیمات ذخیرهسازی"""
try:
- # Check permission
- if not current_user.has_app_permission("admin.storage.view"):
- raise ApiError(
- code="FORBIDDEN",
- message=translator.t("FORBIDDEN", "دسترسی غیرمجاز"),
- http_status=403,
- translator=translator
- )
-
config_repo = StorageConfigRepository(db)
configs = config_repo.get_all_configs()
@@ -331,6 +320,7 @@ async def get_storage_configs(
@router.post("/storage-configs/", response_model=dict)
+@require_app_permission("superadmin")
async def create_storage_config(
request: Request,
config_request: StorageConfigCreateRequest,
@@ -367,6 +357,7 @@ async def create_storage_config(
@router.put("/storage-configs/{config_id}", response_model=dict)
+@require_app_permission("superadmin")
async def update_storage_config(
config_id: UUID,
request: Request,
@@ -392,6 +383,7 @@ async def update_storage_config(
@router.put("/storage-configs/{config_id}/set-default", response_model=dict)
+@require_app_permission("superadmin")
async def set_default_storage_config(
config_id: UUID,
request: Request,
@@ -426,6 +418,7 @@ async def set_default_storage_config(
@router.delete("/storage-configs/{config_id}", response_model=dict)
+@require_app_permission("superadmin")
async def delete_storage_config(
config_id: str,
request: Request,
@@ -435,15 +428,6 @@ async def delete_storage_config(
):
"""حذف تنظیمات ذخیرهسازی"""
try:
- # Check permission
- if not current_user.has_app_permission("admin.storage.delete"):
- raise ApiError(
- code="FORBIDDEN",
- message=translator.t("FORBIDDEN", "دسترسی غیرمجاز"),
- http_status=403,
- translator=translator
- )
-
config_repo = StorageConfigRepository(db)
# بررسی وجود فایلها قبل از حذف
@@ -480,6 +464,7 @@ async def delete_storage_config(
@router.post("/storage-configs/{config_id}/test", response_model=dict)
+@require_app_permission("superadmin")
async def test_storage_config(
config_id: str,
request: Request,
diff --git a/hesabixAPI/adapters/api/v1/business_dashboard.py b/hesabixAPI/adapters/api/v1/business_dashboard.py
new file mode 100644
index 0000000..ebd0ed9
--- /dev/null
+++ b/hesabixAPI/adapters/api/v1/business_dashboard.py
@@ -0,0 +1,194 @@
+# Removed __future__ annotations to fix OpenAPI schema generation
+
+from fastapi import APIRouter, Depends, Request, HTTPException
+from sqlalchemy.orm import Session
+
+from adapters.db.session import get_db
+from adapters.api.v1.schemas import SuccessResponse
+from app.core.responses import success_response, format_datetime_fields
+from app.core.auth_dependency import get_current_user, AuthContext
+from app.core.permissions import require_business_access
+from app.services.business_dashboard_service import (
+ get_business_dashboard_data, get_business_members, get_business_statistics
+)
+
+router = APIRouter(prefix="/business", tags=["business-dashboard"])
+
+
+@router.post("/{business_id}/dashboard",
+ summary="دریافت داشبورد کسب و کار",
+ description="دریافت اطلاعات کلی و آمار کسب و کار",
+ response_model=SuccessResponse,
+ responses={
+ 200: {
+ "description": "داشبورد کسب و کار با موفقیت دریافت شد",
+ "content": {
+ "application/json": {
+ "example": {
+ "success": True,
+ "message": "داشبورد کسب و کار دریافت شد",
+ "data": {
+ "business_info": {
+ "id": 1,
+ "name": "شرکت نمونه",
+ "business_type": "شرکت",
+ "business_field": "تولیدی",
+ "owner_id": 1,
+ "created_at": "1403/01/01 00:00:00",
+ "member_count": 5
+ },
+ "statistics": {
+ "total_sales": 1000000.0,
+ "total_purchases": 500000.0,
+ "active_members": 5,
+ "recent_transactions": 25
+ },
+ "recent_activities": [
+ {
+ "id": 1,
+ "title": "فروش جدید",
+ "description": "فروش محصول A به مبلغ 100,000 تومان",
+ "icon": "sell",
+ "time_ago": "2 ساعت پیش"
+ }
+ ]
+ }
+ }
+ }
+ }
+ },
+ 401: {
+ "description": "کاربر احراز هویت نشده است"
+ },
+ 403: {
+ "description": "دسترسی غیرمجاز به کسب و کار"
+ },
+ 404: {
+ "description": "کسب و کار یافت نشد"
+ }
+ }
+)
+@require_business_access("business_id")
+def get_business_dashboard(
+ request: Request,
+ business_id: int,
+ ctx: AuthContext = Depends(get_current_user),
+ db: Session = Depends(get_db)
+) -> dict:
+ """دریافت داشبورد کسب و کار"""
+ dashboard_data = get_business_dashboard_data(db, business_id, ctx)
+ formatted_data = format_datetime_fields(dashboard_data, request)
+ return success_response(formatted_data, request)
+
+
+@router.post("/{business_id}/members",
+ summary="لیست اعضای کسب و کار",
+ description="دریافت لیست اعضای کسب و کار",
+ response_model=SuccessResponse,
+ responses={
+ 200: {
+ "description": "لیست اعضا با موفقیت دریافت شد",
+ "content": {
+ "application/json": {
+ "example": {
+ "success": True,
+ "message": "لیست اعضا دریافت شد",
+ "data": {
+ "items": [
+ {
+ "id": 1,
+ "user_id": 2,
+ "first_name": "احمد",
+ "last_name": "احمدی",
+ "email": "ahmad@example.com",
+ "role": "مدیر فروش",
+ "permissions": {
+ "sales": {"write": True, "delete": True},
+ "reports": {"export": True}
+ },
+ "joined_at": "1403/01/01 00:00:00"
+ }
+ ],
+ "pagination": {
+ "total": 1,
+ "page": 1,
+ "per_page": 10,
+ "total_pages": 1,
+ "has_next": False,
+ "has_prev": False
+ }
+ }
+ }
+ }
+ }
+ },
+ 401: {
+ "description": "کاربر احراز هویت نشده است"
+ },
+ 403: {
+ "description": "دسترسی غیرمجاز به کسب و کار"
+ }
+ }
+)
+@require_business_access("business_id")
+def get_business_members(
+ request: Request,
+ business_id: int,
+ ctx: AuthContext = Depends(get_current_user),
+ db: Session = Depends(get_db)
+) -> dict:
+ """لیست اعضای کسب و کار"""
+ members_data = get_business_members(db, business_id, ctx)
+ formatted_data = format_datetime_fields(members_data, request)
+ return success_response(formatted_data, request)
+
+
+@router.post("/{business_id}/statistics",
+ summary="آمار کسب و کار",
+ description="دریافت آمار تفصیلی کسب و کار",
+ response_model=SuccessResponse,
+ responses={
+ 200: {
+ "description": "آمار با موفقیت دریافت شد",
+ "content": {
+ "application/json": {
+ "example": {
+ "success": True,
+ "message": "آمار دریافت شد",
+ "data": {
+ "sales_by_month": [
+ {"month": "1403/01", "amount": 500000},
+ {"month": "1403/02", "amount": 750000}
+ ],
+ "top_products": [
+ {"name": "محصول A", "sales_count": 100, "revenue": 500000}
+ ],
+ "member_activity": {
+ "active_today": 3,
+ "active_this_week": 5,
+ "total_members": 8
+ }
+ }
+ }
+ }
+ }
+ },
+ 401: {
+ "description": "کاربر احراز هویت نشده است"
+ },
+ 403: {
+ "description": "دسترسی غیرمجاز به کسب و کار"
+ }
+ }
+)
+@require_business_access("business_id")
+def get_business_statistics(
+ request: Request,
+ business_id: int,
+ ctx: AuthContext = Depends(get_current_user),
+ db: Session = Depends(get_db)
+) -> dict:
+ """آمار کسب و کار"""
+ stats_data = get_business_statistics(db, business_id, ctx)
+ formatted_data = format_datetime_fields(stats_data, request)
+ return success_response(formatted_data, request)
diff --git a/hesabixAPI/adapters/api/v1/businesses.py b/hesabixAPI/adapters/api/v1/businesses.py
index d6c3f23..d186a15 100644
--- a/hesabixAPI/adapters/api/v1/businesses.py
+++ b/hesabixAPI/adapters/api/v1/businesses.py
@@ -6,8 +6,7 @@ from sqlalchemy.orm import Session
from adapters.db.session import get_db
from adapters.api.v1.schemas import (
BusinessCreateRequest, BusinessUpdateRequest, BusinessResponse,
- BusinessListResponse, BusinessSummaryResponse, SuccessResponse,
- QueryInfo
+ BusinessListResponse, BusinessSummaryResponse, SuccessResponse
)
from app.core.responses import success_response, format_datetime_fields
from app.core.auth_dependency import get_current_user, AuthContext
@@ -63,10 +62,10 @@ def create_new_business(
owner_id = ctx.get_user_id()
business = create_business(db, business_data, owner_id)
formatted_data = format_datetime_fields(business, request)
- return success_response(formatted_data, request, "کسب و کار با موفقیت ایجاد شد")
+ return success_response(formatted_data, request)
-@router.get("",
+@router.post("/list",
summary="لیست کسب و کارهای کاربر",
description="دریافت لیست کسب و کارهای کاربر جاری با قابلیت فیلتر و جستجو",
response_model=SuccessResponse,
@@ -86,7 +85,7 @@ def create_new_business(
"business_type": "شرکت",
"business_field": "تولیدی",
"owner_id": 1,
- "created_at": "2024-01-01T00:00:00Z"
+ "created_at": "1403/01/01 00:00:00"
}
],
"pagination": {
@@ -109,19 +108,54 @@ def create_new_business(
)
def list_user_businesses(
request: Request,
- query_info: QueryInfo,
ctx: AuthContext = Depends(get_current_user),
- db: Session = Depends(get_db)
+ db: Session = Depends(get_db),
+ take: int = 10,
+ skip: int = 0,
+ sort_by: str = "created_at",
+ sort_desc: bool = True,
+ search: str = None
) -> dict:
"""لیست کسب و کارهای کاربر"""
owner_id = ctx.get_user_id()
- query_dict = query_info.dict()
+ query_dict = {
+ "take": take,
+ "skip": skip,
+ "sort_by": sort_by,
+ "sort_desc": sort_desc,
+ "search": search
+ }
businesses = get_businesses_by_owner(db, owner_id, query_dict)
formatted_data = format_datetime_fields(businesses, request)
- return success_response(formatted_data, request, "لیست کسب و کارها دریافت شد")
+
+ # اگر formatted_data یک dict با کلید items است، آن را استخراج کنیم
+ if isinstance(formatted_data, dict) and 'items' in formatted_data:
+ items = formatted_data['items']
+ else:
+ items = formatted_data
+
+ # برای حالا total را برابر با تعداد items قرار میدهیم
+ # در آینده میتوان total را از service دریافت کرد
+ total = len(items)
+ page = (skip // take) + 1
+ total_pages = (total + take - 1) // take
+
+ response_data = {
+ "items": items,
+ "pagination": {
+ "total": total,
+ "page": page,
+ "per_page": take,
+ "total_pages": total_pages,
+ "has_next": page < total_pages,
+ "has_prev": page > 1,
+ },
+ "query_info": query_dict
+ }
+ return success_response(response_data, request)
-@router.get("/{business_id}",
+@router.post("/{business_id}/details",
summary="جزئیات کسب و کار",
description="دریافت جزئیات یک کسب و کار خاص",
response_model=SuccessResponse,
@@ -141,7 +175,7 @@ def list_user_businesses(
"owner_id": 1,
"address": "تهران، خیابان ولیعصر",
"phone": "02112345678",
- "created_at": "2024-01-01T00:00:00Z"
+ "created_at": "1403/01/01 00:00:00"
}
}
}
@@ -169,7 +203,7 @@ def get_business(
raise HTTPException(status_code=404, detail="کسب و کار یافت نشد")
formatted_data = format_datetime_fields(business, request)
- return success_response(formatted_data, request, "جزئیات کسب و کار دریافت شد")
+ return success_response(formatted_data, request)
@router.put("/{business_id}",
@@ -266,7 +300,7 @@ def delete_business_info(
return success_response({"ok": True}, request, "کسب و کار با موفقیت حذف شد")
-@router.get("/summary/stats",
+@router.post("/stats",
summary="آمار کسب و کارها",
description="دریافت آمار کلی کسب و کارهای کاربر",
response_model=SuccessResponse,
@@ -307,4 +341,4 @@ def get_business_stats(
"""آمار کسب و کارها"""
owner_id = ctx.get_user_id()
stats = get_business_summary(db, owner_id)
- return success_response(stats, request, "آمار کسب و کارها دریافت شد")
+ return success_response(stats, request)
diff --git a/hesabixAPI/adapters/api/v1/schema_models/email.py b/hesabixAPI/adapters/api/v1/schema_models/email.py
new file mode 100644
index 0000000..633f6e7
--- /dev/null
+++ b/hesabixAPI/adapters/api/v1/schema_models/email.py
@@ -0,0 +1,59 @@
+from pydantic import BaseModel, EmailStr, Field
+from typing import Optional
+from datetime import datetime
+
+
+class EmailConfigBase(BaseModel):
+ name: str = Field(..., min_length=1, max_length=100, description="Configuration name")
+ smtp_host: str = Field(..., min_length=1, max_length=255, description="SMTP host")
+ smtp_port: int = Field(..., ge=1, le=65535, description="SMTP port")
+ smtp_username: str = Field(..., min_length=1, max_length=255, description="SMTP username")
+ smtp_password: str = Field(..., min_length=1, max_length=255, description="SMTP password")
+ use_tls: bool = Field(default=True, description="Use TLS encryption")
+ use_ssl: bool = Field(default=False, description="Use SSL encryption")
+ from_email: EmailStr = Field(..., description="From email address")
+ from_name: str = Field(..., min_length=1, max_length=100, description="From name")
+ is_active: bool = Field(default=True, description="Is this configuration active")
+ is_default: bool = Field(default=False, description="Is this the default configuration")
+
+
+class EmailConfigCreate(EmailConfigBase):
+ pass
+
+
+class EmailConfigUpdate(BaseModel):
+ name: Optional[str] = Field(None, min_length=1, max_length=100)
+ smtp_host: Optional[str] = Field(None, min_length=1, max_length=255)
+ smtp_port: Optional[int] = Field(None, ge=1, le=65535)
+ smtp_username: Optional[str] = Field(None, min_length=1, max_length=255)
+ smtp_password: Optional[str] = Field(None, min_length=1, max_length=255)
+ use_tls: Optional[bool] = None
+ use_ssl: Optional[bool] = None
+ from_email: Optional[EmailStr] = None
+ from_name: Optional[str] = Field(None, min_length=1, max_length=100)
+ is_active: Optional[bool] = None
+ is_default: Optional[bool] = None
+
+
+class EmailConfigResponse(EmailConfigBase):
+ id: int
+ created_at: datetime
+ updated_at: datetime
+
+ class Config:
+ from_attributes = True
+
+
+class SendEmailRequest(BaseModel):
+ to: EmailStr = Field(..., description="Recipient email address")
+ subject: str = Field(..., min_length=1, max_length=255, description="Email subject")
+ body: str = Field(..., min_length=1, description="Email body (plain text)")
+ html_body: Optional[str] = Field(None, description="Email body (HTML)")
+ config_id: Optional[int] = Field(None, description="Specific config ID to use")
+
+
+class TestConnectionRequest(BaseModel):
+ config_id: int = Field(..., description="Configuration ID to test")
+
+
+# These response models are no longer needed as we use SuccessResponse from schemas.py
diff --git a/hesabixAPI/adapters/db/models/__init__.py b/hesabixAPI/adapters/db/models/__init__.py
index bbb2d13..7e44cb1 100644
--- a/hesabixAPI/adapters/db/models/__init__.py
+++ b/hesabixAPI/adapters/db/models/__init__.py
@@ -12,6 +12,9 @@ from .business_permission import BusinessPermission # noqa: F401
from .support import * # noqa: F401, F403
# Import file storage models
-from .file_storage import * # noqa: F401, F403
+from .file_storage import *
+
+# Import email config models
+from .email_config import EmailConfig # noqa: F401, F403
diff --git a/hesabixAPI/adapters/db/models/email_config.py b/hesabixAPI/adapters/db/models/email_config.py
new file mode 100644
index 0000000..17aa98f
--- /dev/null
+++ b/hesabixAPI/adapters/db/models/email_config.py
@@ -0,0 +1,27 @@
+from __future__ import annotations
+
+from datetime import datetime
+
+from sqlalchemy import String, DateTime, Boolean, Integer, Text
+from sqlalchemy.orm import Mapped, mapped_column
+
+from adapters.db.session import Base
+
+
+class EmailConfig(Base):
+ __tablename__ = "email_configs"
+
+ id: Mapped[int] = mapped_column(primary_key=True, autoincrement=True)
+ name: Mapped[str] = mapped_column(String(100), nullable=False, index=True)
+ smtp_host: Mapped[str] = mapped_column(String(255), nullable=False)
+ smtp_port: Mapped[int] = mapped_column(Integer, nullable=False)
+ smtp_username: Mapped[str] = mapped_column(String(255), nullable=False)
+ smtp_password: Mapped[str] = mapped_column(String(255), nullable=False) # Should be encrypted
+ use_tls: Mapped[bool] = mapped_column(Boolean, default=True, nullable=False)
+ use_ssl: Mapped[bool] = mapped_column(Boolean, default=False, nullable=False)
+ from_email: Mapped[str] = mapped_column(String(255), nullable=False)
+ from_name: Mapped[str] = mapped_column(String(100), nullable=False)
+ is_active: Mapped[bool] = mapped_column(Boolean, default=True, nullable=False)
+ is_default: Mapped[bool] = mapped_column(Boolean, default=False, nullable=False)
+ created_at: Mapped[datetime] = mapped_column(DateTime, default=datetime.utcnow, nullable=False)
+ updated_at: Mapped[datetime] = mapped_column(DateTime, default=datetime.utcnow, onupdate=datetime.utcnow, nullable=False)
diff --git a/hesabixAPI/adapters/db/repositories/base_repo.py b/hesabixAPI/adapters/db/repositories/base_repo.py
index 2b6109f..ffb52b6 100644
--- a/hesabixAPI/adapters/db/repositories/base_repo.py
+++ b/hesabixAPI/adapters/db/repositories/base_repo.py
@@ -53,3 +53,12 @@ class BaseRepository(Generic[T]):
stmt = stmt.where(column == value)
return self.db.execute(stmt).scalars().first() is not None
+
+ def delete(self, obj: T) -> None:
+ """حذف رکورد از دیتابیس"""
+ self.db.delete(obj)
+ self.db.commit()
+
+ def update(self, obj: T) -> None:
+ """بروزرسانی رکورد در دیتابیس"""
+ self.db.commit()
\ No newline at end of file
diff --git a/hesabixAPI/adapters/db/repositories/email_config_repository.py b/hesabixAPI/adapters/db/repositories/email_config_repository.py
new file mode 100644
index 0000000..834c467
--- /dev/null
+++ b/hesabixAPI/adapters/db/repositories/email_config_repository.py
@@ -0,0 +1,85 @@
+from typing import Optional, List
+from sqlalchemy.orm import Session
+from sqlalchemy import and_
+
+from adapters.db.models.email_config import EmailConfig
+from adapters.db.repositories.base_repo import BaseRepository
+
+
+class EmailConfigRepository(BaseRepository[EmailConfig]):
+ def __init__(self, db: Session):
+ super().__init__(db, EmailConfig)
+
+ def get_active_config(self) -> Optional[EmailConfig]:
+ """Get the currently active email configuration"""
+ return self.db.query(self.model_class).filter(self.model_class.is_active == True).first()
+
+ def get_default_config(self) -> Optional[EmailConfig]:
+ """Get the default email configuration"""
+ return self.db.query(self.model_class).filter(self.model_class.is_default == True).first()
+
+ def set_default_config(self, config_id: int) -> bool:
+ """Set a configuration as default (removes default from others)"""
+ try:
+ # First check if the config exists
+ config = self.get_by_id(config_id)
+ if not config:
+ return False
+
+ # Remove default from all configs
+ self.db.query(self.model_class).update({self.model_class.is_default: False})
+
+ # Set the specified config as default
+ config.is_default = True
+ self.db.commit()
+ return True
+ except Exception as e:
+ self.db.rollback()
+ print(f"Error in set_default_config: {e}") # Debug log
+ return False
+
+ def get_by_name(self, name: str) -> Optional[EmailConfig]:
+ """Get email configuration by name"""
+ return self.db.query(self.model_class).filter(self.model_class.name == name).first()
+
+ def get_all_configs(self) -> List[EmailConfig]:
+ """Get all email configurations"""
+ return self.db.query(self.model_class).order_by(self.model_class.created_at.desc()).all()
+
+ def set_active_config(self, config_id: int) -> bool:
+ """Set a specific configuration as active and deactivate others"""
+ try:
+ # Deactivate all configs
+ self.db.query(self.model_class).update({self.model_class.is_active: False})
+
+ # Activate the specified config
+ config = self.get_by_id(config_id)
+ if config:
+ config.is_active = True
+ self.db.commit()
+ return True
+ return False
+ except Exception:
+ self.db.rollback()
+ return False
+
+ def test_connection(self, config: EmailConfig) -> bool:
+ """Test SMTP connection for a configuration"""
+ try:
+ import smtplib
+ from email.mime.text import MIMEText
+
+ # Create SMTP connection
+ if config.use_ssl:
+ server = smtplib.SMTP_SSL(config.smtp_host, config.smtp_port)
+ else:
+ server = smtplib.SMTP(config.smtp_host, config.smtp_port)
+ if config.use_tls:
+ server.starttls()
+
+ # Login
+ server.login(config.smtp_username, config.smtp_password)
+ server.quit()
+ return True
+ except Exception:
+ return False
diff --git a/hesabixAPI/app/core/auth_dependency.py b/hesabixAPI/app/core/auth_dependency.py
index cd91eda..37c205f 100644
--- a/hesabixAPI/app/core/auth_dependency.py
+++ b/hesabixAPI/app/core/auth_dependency.py
@@ -249,14 +249,16 @@ class AuthContext:
def get_current_user(
request: Request,
- authorization: Optional[str] = Header(default=None),
db: Session = Depends(get_db)
) -> AuthContext:
"""دریافت اطلاعات کامل کاربر کنونی و تنظیمات از درخواست"""
- if not authorization or not authorization.startswith("ApiKey "):
+ # Get authorization from request headers
+ auth_header = request.headers.get("Authorization")
+
+ if not auth_header or not auth_header.startswith("ApiKey "):
raise ApiError("UNAUTHORIZED", "Missing or invalid API key", http_status=401)
- api_key = authorization[len("ApiKey ") :].strip()
+ api_key = auth_header[len("ApiKey ") :].strip()
key_hash = hash_api_key(api_key)
repo = ApiKeyRepository(db)
obj = repo.get_by_hash(key_hash)
diff --git a/hesabixAPI/app/core/i18n.py b/hesabixAPI/app/core/i18n.py
index 54d59ce..574ca12 100644
--- a/hesabixAPI/app/core/i18n.py
+++ b/hesabixAPI/app/core/i18n.py
@@ -51,3 +51,9 @@ def get_translator(locale: str = "fa") -> Translator:
return Translator(locale)
+def gettext(key: str, locale: str = "fa") -> str:
+ """Get translation for a key using gettext"""
+ translator = get_translator(locale)
+ return translator.t(key)
+
+
diff --git a/hesabixAPI/app/core/permissions.py b/hesabixAPI/app/core/permissions.py
index 1750002..f00d098 100644
--- a/hesabixAPI/app/core/permissions.py
+++ b/hesabixAPI/app/core/permissions.py
@@ -74,7 +74,23 @@ def require_business_access(business_id_param: str = "business_id"):
def decorator(func: Callable) -> Callable:
@wraps(func)
def wrapper(*args, **kwargs) -> Any:
- ctx = get_current_user()
+ # Find request in args or kwargs
+ request = None
+ for arg in args:
+ if hasattr(arg, 'headers'): # Check if it's a Request object
+ request = arg
+ break
+
+ if not request and 'request' in kwargs:
+ request = kwargs['request']
+
+ if not request:
+ raise ApiError("INTERNAL_ERROR", "Request not found", http_status=500)
+
+ # Get database session
+ from adapters.db.session import get_db
+ db = next(get_db())
+ ctx = get_current_user(request, db)
business_id = kwargs.get(business_id_param)
if business_id and not ctx.can_access_business(business_id):
raise ApiError("FORBIDDEN", f"No access to business {business_id}", http_status=403)
diff --git a/hesabixAPI/app/main.py b/hesabixAPI/app/main.py
index 9b8cb32..83315ca 100644
--- a/hesabixAPI/app/main.py
+++ b/hesabixAPI/app/main.py
@@ -7,12 +7,14 @@ from adapters.api.v1.health import router as health_router
from adapters.api.v1.auth import router as auth_router
from adapters.api.v1.users import router as users_router
from adapters.api.v1.businesses import router as businesses_router
+from adapters.api.v1.business_dashboard import router as business_dashboard_router
from adapters.api.v1.support.tickets import router as support_tickets_router
from adapters.api.v1.support.operator import router as support_operator_router
from adapters.api.v1.support.categories import router as support_categories_router
from adapters.api.v1.support.priorities import router as support_priorities_router
from adapters.api.v1.support.statuses import router as support_statuses_router
from adapters.api.v1.admin.file_storage import router as admin_file_storage_router
+from adapters.api.v1.admin.email_config import router as admin_email_config_router
from app.core.i18n import negotiate_locale, Translator
from app.core.error_handlers import register_error_handlers
from app.core.smart_normalizer import smart_normalize_json, SmartNormalizerConfig
@@ -268,6 +270,7 @@ def create_app() -> FastAPI:
application.include_router(auth_router, prefix=settings.api_v1_prefix)
application.include_router(users_router, prefix=settings.api_v1_prefix)
application.include_router(businesses_router, prefix=settings.api_v1_prefix)
+ application.include_router(business_dashboard_router, prefix=settings.api_v1_prefix)
# Support endpoints
application.include_router(support_tickets_router, prefix=f"{settings.api_v1_prefix}/support")
@@ -278,6 +281,7 @@ def create_app() -> FastAPI:
# Admin endpoints
application.include_router(admin_file_storage_router, prefix=settings.api_v1_prefix)
+ application.include_router(admin_email_config_router, prefix=settings.api_v1_prefix)
register_error_handlers(application)
diff --git a/hesabixAPI/app/services/business_dashboard_service.py b/hesabixAPI/app/services/business_dashboard_service.py
new file mode 100644
index 0000000..dd720a6
--- /dev/null
+++ b/hesabixAPI/app/services/business_dashboard_service.py
@@ -0,0 +1,194 @@
+from __future__ import annotations
+
+from typing import List, Optional, Dict, Any
+from sqlalchemy.orm import Session
+from sqlalchemy import select, and_, func
+from datetime import datetime, timedelta
+
+from adapters.db.repositories.business_repo import BusinessRepository
+from adapters.db.repositories.business_permission_repo import BusinessPermissionRepository
+from adapters.db.repositories.user_repo import UserRepository
+from adapters.db.models.business import Business
+from adapters.db.models.business_permission import BusinessPermission
+from adapters.db.models.user import User
+from app.core.auth_dependency import AuthContext
+
+
+def get_business_dashboard_data(db: Session, business_id: int, ctx: AuthContext) -> Dict[str, Any]:
+ """دریافت دادههای داشبورد کسب و کار"""
+ business_repo = BusinessRepository(db)
+ business = business_repo.get_by_id(business_id)
+
+ if not business:
+ raise ValueError("کسب و کار یافت نشد")
+
+ # بررسی دسترسی کاربر
+ if not ctx.can_access_business(business_id):
+ raise ValueError("دسترسی غیرمجاز")
+
+ # دریافت اطلاعات کسب و کار
+ business_info = _get_business_info(business, db)
+
+ # دریافت آمار
+ statistics = _get_business_statistics(business_id, db)
+
+ # دریافت فعالیتهای اخیر
+ recent_activities = _get_recent_activities(business_id, db)
+
+ return {
+ "business_info": business_info,
+ "statistics": statistics,
+ "recent_activities": recent_activities
+ }
+
+
+def get_business_members(db: Session, business_id: int, ctx: AuthContext) -> Dict[str, Any]:
+ """دریافت لیست اعضای کسب و کار"""
+ if not ctx.can_access_business(business_id):
+ raise ValueError("دسترسی غیرمجاز")
+
+ permission_repo = BusinessPermissionRepository(db)
+ user_repo = UserRepository(db)
+
+ # دریافت دسترسیهای کسب و کار
+ permissions = permission_repo.get_business_users(business_id)
+
+ members = []
+ for permission in permissions:
+ user = user_repo.get_by_id(permission.user_id)
+ if user:
+ members.append({
+ "id": permission.id,
+ "user_id": user.id,
+ "first_name": user.first_name,
+ "last_name": user.last_name,
+ "email": user.email,
+ "mobile": user.mobile,
+ "role": _get_user_role(permission.business_permissions),
+ "permissions": permission.business_permissions or {},
+ "joined_at": permission.created_at.isoformat()
+ })
+
+ return {
+ "items": members,
+ "pagination": {
+ "total": len(members),
+ "page": 1,
+ "per_page": len(members),
+ "total_pages": 1,
+ "has_next": False,
+ "has_prev": False
+ }
+ }
+
+
+def get_business_statistics(db: Session, business_id: int, ctx: AuthContext) -> Dict[str, Any]:
+ """دریافت آمار تفصیلی کسب و کار"""
+ if not ctx.can_access_business(business_id):
+ raise ValueError("دسترسی غیرمجاز")
+
+ # آمار فروش ماهانه (نمونه)
+ sales_by_month = [
+ {"month": "2024-01", "amount": 500000},
+ {"month": "2024-02", "amount": 750000},
+ {"month": "2024-03", "amount": 600000}
+ ]
+
+ # پرفروشترین محصولات (نمونه)
+ top_products = [
+ {"name": "محصول A", "sales_count": 100, "revenue": 500000},
+ {"name": "محصول B", "sales_count": 80, "revenue": 400000},
+ {"name": "محصول C", "sales_count": 60, "revenue": 300000}
+ ]
+
+ # آمار فعالیت اعضا
+ permission_repo = BusinessPermissionRepository(db)
+ members = permission_repo.get_business_users(business_id)
+
+ member_activity = {
+ "active_today": len([m for m in members if m.created_at.date() == datetime.now().date()]),
+ "active_this_week": len([m for m in members if m.created_at >= datetime.now() - timedelta(days=7)]),
+ "total_members": len(members)
+ }
+
+ return {
+ "sales_by_month": sales_by_month,
+ "top_products": top_products,
+ "member_activity": member_activity
+ }
+
+
+def _get_business_info(business: Business, db: Session) -> Dict[str, Any]:
+ """دریافت اطلاعات کسب و کار"""
+ permission_repo = BusinessPermissionRepository(db)
+ member_count = len(permission_repo.get_business_users(business.id))
+
+ return {
+ "id": business.id,
+ "name": business.name,
+ "business_type": business.business_type.value,
+ "business_field": business.business_field.value,
+ "owner_id": business.owner_id,
+ "address": business.address,
+ "phone": business.phone,
+ "mobile": business.mobile,
+ "created_at": business.created_at.isoformat(),
+ "member_count": member_count
+ }
+
+
+def _get_business_statistics(business_id: int, db: Session) -> Dict[str, Any]:
+ """دریافت آمار کلی کسب و کار"""
+ # در اینجا میتوانید آمار واقعی را از جداول مربوطه دریافت کنید
+ # فعلاً دادههای نمونه برمیگردانیم
+ return {
+ "total_sales": 1000000.0,
+ "total_purchases": 500000.0,
+ "active_members": 5,
+ "recent_transactions": 25
+ }
+
+
+def _get_recent_activities(business_id: int, db: Session) -> List[Dict[str, Any]]:
+ """دریافت فعالیتهای اخیر"""
+ # در اینجا میتوانید فعالیتهای واقعی را از جداول مربوطه دریافت کنید
+ # فعلاً دادههای نمونه برمیگردانیم
+ return [
+ {
+ "id": 1,
+ "title": "فروش جدید",
+ "description": "فروش محصول A به مبلغ 100,000 تومان",
+ "icon": "sell",
+ "time_ago": "2 ساعت پیش"
+ },
+ {
+ "id": 2,
+ "title": "عضو جدید",
+ "description": "احمد احمدی به تیم اضافه شد",
+ "icon": "person_add",
+ "time_ago": "5 ساعت پیش"
+ },
+ {
+ "id": 3,
+ "title": "گزارش ماهانه",
+ "description": "گزارش فروش ماه ژانویه تولید شد",
+ "icon": "assessment",
+ "time_ago": "1 روز پیش"
+ }
+ ]
+
+
+def _get_user_role(permissions: Optional[Dict[str, Any]]) -> str:
+ """تعیین نقش کاربر بر اساس دسترسیها"""
+ if not permissions:
+ return "عضو"
+
+ # بررسی دسترسیهای مختلف برای تعیین نقش
+ if permissions.get("settings", {}).get("manage_users"):
+ return "مدیر"
+ elif permissions.get("sales", {}).get("write"):
+ return "مدیر فروش"
+ elif permissions.get("accounting", {}).get("write"):
+ return "حسابدار"
+ else:
+ return "عضو"
diff --git a/hesabixAPI/app/services/business_service.py b/hesabixAPI/app/services/business_service.py
index 75a11f3..e70d2d3 100644
--- a/hesabixAPI/app/services/business_service.py
+++ b/hesabixAPI/app/services/business_service.py
@@ -181,6 +181,6 @@ def _business_to_dict(business: Business) -> Dict[str, Any]:
"province": business.province,
"city": business.city,
"postal_code": business.postal_code,
- "created_at": business.created_at.isoformat(),
- "updated_at": business.updated_at.isoformat()
+ "created_at": business.created_at, # datetime object بماند
+ "updated_at": business.updated_at # datetime object بماند
}
diff --git a/hesabixAPI/app/services/email_service.py b/hesabixAPI/app/services/email_service.py
new file mode 100644
index 0000000..f1a2399
--- /dev/null
+++ b/hesabixAPI/app/services/email_service.py
@@ -0,0 +1,143 @@
+import smtplib
+from email.mime.text import MIMEText
+from email.mime.multipart import MIMEMultipart
+from typing import Optional, List
+from sqlalchemy.orm import Session
+
+from adapters.db.models.email_config import EmailConfig
+from adapters.db.repositories.email_config_repository import EmailConfigRepository
+
+
+class EmailService:
+ def __init__(self, db: Session):
+ self.db = db
+ self.email_repo = EmailConfigRepository(db)
+
+ def send_email(
+ self,
+ to: str,
+ subject: str,
+ body: str,
+ html_body: Optional[str] = None,
+ config_id: Optional[int] = None
+ ) -> bool:
+ """
+ Send email using SMTP configuration
+
+ Args:
+ to: Recipient email address
+ subject: Email subject
+ body: Plain text body
+ html_body: HTML body (optional)
+ config_id: Specific config ID to use (optional)
+
+ Returns:
+ bool: True if email sent successfully, False otherwise
+ """
+ try:
+ # Get email configuration - prioritize default config
+ if config_id:
+ config = self.email_repo.get_by_id(config_id)
+ else:
+ # First try to get default config
+ config = self.email_repo.get_default_config()
+ if not config:
+ # Fallback to active config
+ config = self.email_repo.get_active_config()
+
+ if not config:
+ return False
+
+ # Create message
+ msg = MIMEMultipart('alternative')
+ msg['From'] = f"{config.from_name} <{config.from_email}>"
+ msg['To'] = to
+ msg['Subject'] = subject
+
+ # Add plain text part
+ text_part = MIMEText(body, 'plain', 'utf-8')
+ msg.attach(text_part)
+
+ # Add HTML part if provided
+ if html_body:
+ html_part = MIMEText(html_body, 'html', 'utf-8')
+ msg.attach(html_part)
+
+ # Send email
+ return self._send_smtp_email(config, msg)
+
+ except Exception as e:
+ print(f"Error sending email: {e}")
+ return False
+
+ def send_template_email(
+ self,
+ template_name: str,
+ to: str,
+ context: dict,
+ config_id: Optional[int] = None
+ ) -> bool:
+ """
+ Send email using a template (placeholder for future template system)
+
+ Args:
+ template_name: Name of the template
+ to: Recipient email address
+ context: Template context variables
+ config_id: Specific config ID to use (optional)
+
+ Returns:
+ bool: True if email sent successfully, False otherwise
+ """
+ # For now, just use basic template substitution
+ # This can be extended with a proper template engine later
+ subject = context.get('subject', 'Email from Hesabix')
+ body = context.get('body', '')
+ html_body = context.get('html_body')
+
+ return self.send_email(to, subject, body, html_body, config_id)
+
+ def test_connection(self, config_id: int) -> bool:
+ """
+ Test SMTP connection for a specific configuration
+
+ Args:
+ config_id: Configuration ID to test
+
+ Returns:
+ bool: True if connection successful, False otherwise
+ """
+ config = self.email_repo.get_by_id(config_id)
+ if not config:
+ return False
+
+ return self.email_repo.test_connection(config)
+
+ def get_active_config(self) -> Optional[EmailConfig]:
+ """Get the currently active email configuration"""
+ return self.email_repo.get_active_config()
+
+ def get_all_configs(self) -> List[EmailConfig]:
+ """Get all email configurations"""
+ return self.email_repo.get_all_configs()
+
+ def _send_smtp_email(self, config: EmailConfig, msg: MIMEMultipart) -> bool:
+ """Internal method to send email via SMTP"""
+ try:
+ # Create SMTP connection
+ if config.use_ssl:
+ server = smtplib.SMTP_SSL(config.smtp_host, config.smtp_port)
+ else:
+ server = smtplib.SMTP(config.smtp_host, config.smtp_port)
+ if config.use_tls:
+ server.starttls()
+
+ # Login and send
+ server.login(config.smtp_username, config.smtp_password)
+ server.send_message(msg)
+ server.quit()
+
+ return True
+ except Exception as e:
+ print(f"SMTP error: {e}")
+ return False
diff --git a/hesabixAPI/hesabix_api.egg-info/SOURCES.txt b/hesabixAPI/hesabix_api.egg-info/SOURCES.txt
index f9b73d8..9f623f1 100644
--- a/hesabixAPI/hesabix_api.egg-info/SOURCES.txt
+++ b/hesabixAPI/hesabix_api.egg-info/SOURCES.txt
@@ -8,8 +8,10 @@ adapters/api/v1/businesses.py
adapters/api/v1/health.py
adapters/api/v1/schemas.py
adapters/api/v1/users.py
+adapters/api/v1/admin/email_config.py
adapters/api/v1/admin/file_storage.py
adapters/api/v1/schema_models/__init__.py
+adapters/api/v1/schema_models/email.py
adapters/api/v1/schema_models/file_storage.py
adapters/api/v1/support/__init__.py
adapters/api/v1/support/categories.py
@@ -25,6 +27,7 @@ adapters/db/models/api_key.py
adapters/db/models/business.py
adapters/db/models/business_permission.py
adapters/db/models/captcha.py
+adapters/db/models/email_config.py
adapters/db/models/file_storage.py
adapters/db/models/password_reset.py
adapters/db/models/user.py
@@ -38,6 +41,7 @@ adapters/db/repositories/api_key_repo.py
adapters/db/repositories/base_repo.py
adapters/db/repositories/business_permission_repo.py
adapters/db/repositories/business_repo.py
+adapters/db/repositories/email_config_repository.py
adapters/db/repositories/file_storage_repository.py
adapters/db/repositories/password_reset_repo.py
adapters/db/repositories/user_repo.py
@@ -66,6 +70,7 @@ app/services/api_key_service.py
app/services/auth_service.py
app/services/business_service.py
app/services/captcha_service.py
+app/services/email_service.py
app/services/file_storage_service.py
app/services/query_service.py
app/services/pdf/__init__.py
@@ -84,6 +89,8 @@ migrations/versions/20250117_000004_add_business_contact_fields.py
migrations/versions/20250117_000005_add_business_geographic_fields.py
migrations/versions/20250117_000006_add_app_permissions_to_users.py
migrations/versions/20250117_000007_create_business_permissions_table.py
+migrations/versions/20250117_000008_add_email_config_table.py
+migrations/versions/20250117_000009_add_is_default_to_email_config.py
migrations/versions/20250915_000001_init_auth_tables.py
migrations/versions/20250916_000002_add_referral_fields.py
migrations/versions/5553f8745c6e_add_support_tables.py
diff --git a/hesabixAPI/locales/en/LC_MESSAGES/messages.mo b/hesabixAPI/locales/en/LC_MESSAGES/messages.mo
index 4beb188..be05735 100644
Binary files a/hesabixAPI/locales/en/LC_MESSAGES/messages.mo and b/hesabixAPI/locales/en/LC_MESSAGES/messages.mo differ
diff --git a/hesabixAPI/locales/en/LC_MESSAGES/messages.po b/hesabixAPI/locales/en/LC_MESSAGES/messages.po
index 7760e9c..db10484 100644
--- a/hesabixAPI/locales/en/LC_MESSAGES/messages.po
+++ b/hesabixAPI/locales/en/LC_MESSAGES/messages.po
@@ -33,6 +33,47 @@ msgstr "Field is required"
msgid "INVALID_EMAIL"
msgstr "Invalid email address"
+# Email Configuration
+msgid "Email configurations retrieved successfully"
+msgstr "Email configurations retrieved successfully"
+
+msgid "Email configuration retrieved successfully"
+msgstr "Email configuration retrieved successfully"
+
+msgid "Email configuration created successfully"
+msgstr "Email configuration created successfully"
+
+msgid "Email configuration updated successfully"
+msgstr "Email configuration updated successfully"
+
+msgid "Email configuration deleted successfully"
+msgstr "Email configuration deleted successfully"
+
+msgid "Email configuration not found"
+msgstr "Email configuration not found"
+
+msgid "Configuration name already exists"
+msgstr "Configuration name already exists"
+
+msgid "Connection test completed"
+msgstr "Connection test completed"
+
+msgid "Email configuration activated successfully"
+msgstr "Email configuration activated successfully"
+
+msgid "Failed to activate configuration"
+msgstr "Failed to activate configuration"
+
+msgid "Email sent successfully"
+msgstr "Email sent successfully"
+msgid "Cannot delete default configuration"
+msgstr "Cannot delete default configuration"
+msgid "Failed to set default configuration"
+msgstr "Failed to set default configuration"
+
+msgid "Failed to send email"
+msgstr "Failed to send email"
+
# Auth
msgid "INVALID_CAPTCHA"
msgstr "Invalid captcha code."
diff --git a/hesabixAPI/locales/fa/LC_MESSAGES/messages.mo b/hesabixAPI/locales/fa/LC_MESSAGES/messages.mo
index bad1332..6e1b411 100644
Binary files a/hesabixAPI/locales/fa/LC_MESSAGES/messages.mo and b/hesabixAPI/locales/fa/LC_MESSAGES/messages.mo differ
diff --git a/hesabixAPI/locales/fa/LC_MESSAGES/messages.po b/hesabixAPI/locales/fa/LC_MESSAGES/messages.po
index f47a8b4..ec270ea 100644
--- a/hesabixAPI/locales/fa/LC_MESSAGES/messages.po
+++ b/hesabixAPI/locales/fa/LC_MESSAGES/messages.po
@@ -33,6 +33,47 @@ msgstr "فیلد الزامی است"
msgid "INVALID_EMAIL"
msgstr "ایمیل نامعتبر است"
+# Email Configuration
+msgid "Email configurations retrieved successfully"
+msgstr "پیکربندیهای ایمیل با موفقیت دریافت شد"
+
+msgid "Email configuration retrieved successfully"
+msgstr "پیکربندی ایمیل با موفقیت دریافت شد"
+
+msgid "Email configuration created successfully"
+msgstr "پیکربندی ایمیل با موفقیت ایجاد شد"
+
+msgid "Email configuration updated successfully"
+msgstr "پیکربندی ایمیل با موفقیت بروزرسانی شد"
+
+msgid "Email configuration deleted successfully"
+msgstr "پیکربندی ایمیل با موفقیت حذف شد"
+
+msgid "Email configuration not found"
+msgstr "پیکربندی ایمیل یافت نشد"
+
+msgid "Configuration name already exists"
+msgstr "نام پیکربندی قبلاً استفاده شده است"
+
+msgid "Connection test completed"
+msgstr "تست اتصال تکمیل شد"
+
+msgid "Email configuration activated successfully"
+msgstr "پیکربندی ایمیل با موفقیت فعال شد"
+
+msgid "Failed to activate configuration"
+msgstr "فعالسازی پیکربندی ناموفق بود"
+
+msgid "Email sent successfully"
+msgstr "ایمیل با موفقیت ارسال شد"
+msgid "Cannot delete default configuration"
+msgstr "نمیتوان پیکربندی پیشفرض را حذف کرد"
+msgid "Failed to set default configuration"
+msgstr "تنظیم پیکربندی پیشفرض ناموفق بود"
+
+msgid "Failed to send email"
+msgstr "ارسال ایمیل ناموفق بود"
+
# Auth
msgid "INVALID_CAPTCHA"
msgstr "کد امنیتی نامعتبر است."
diff --git a/hesabixAPI/migrations/versions/20250117_000008_add_email_config_table.py b/hesabixAPI/migrations/versions/20250117_000008_add_email_config_table.py
new file mode 100644
index 0000000..e04e8a8
--- /dev/null
+++ b/hesabixAPI/migrations/versions/20250117_000008_add_email_config_table.py
@@ -0,0 +1,45 @@
+"""add_email_config_table
+
+Revision ID: 20250117_000008
+Revises: 5553f8745c6e
+Create Date: 2025-01-17 12:00:00.000000
+
+"""
+from alembic import op
+import sqlalchemy as sa
+
+
+# revision identifiers, used by Alembic.
+revision = '20250117_000008'
+down_revision = '5553f8745c6e'
+branch_labels = None
+depends_on = None
+
+
+def upgrade() -> None:
+ # ### commands auto generated by Alembic - please adjust! ###
+ op.create_table('email_configs',
+ sa.Column('id', sa.Integer(), autoincrement=True, nullable=False),
+ sa.Column('name', sa.String(length=100), nullable=False),
+ sa.Column('smtp_host', sa.String(length=255), nullable=False),
+ sa.Column('smtp_port', sa.Integer(), nullable=False),
+ sa.Column('smtp_username', sa.String(length=255), nullable=False),
+ sa.Column('smtp_password', sa.String(length=255), nullable=False),
+ sa.Column('use_tls', sa.Boolean(), nullable=False),
+ sa.Column('use_ssl', sa.Boolean(), nullable=False),
+ sa.Column('from_email', sa.String(length=255), nullable=False),
+ sa.Column('from_name', sa.String(length=100), nullable=False),
+ sa.Column('is_active', sa.Boolean(), nullable=False),
+ sa.Column('created_at', sa.DateTime(), nullable=False),
+ sa.Column('updated_at', sa.DateTime(), nullable=False),
+ sa.PrimaryKeyConstraint('id')
+ )
+ op.create_index(op.f('ix_email_configs_name'), 'email_configs', ['name'], unique=False)
+ # ### end Alembic commands ###
+
+
+def downgrade() -> None:
+ # ### commands auto generated by Alembic - please adjust! ###
+ op.drop_index(op.f('ix_email_configs_name'), table_name='email_configs')
+ op.drop_table('email_configs')
+ # ### end Alembic commands ###
diff --git a/hesabixAPI/migrations/versions/20250117_000009_add_is_default_to_email_config.py b/hesabixAPI/migrations/versions/20250117_000009_add_is_default_to_email_config.py
new file mode 100644
index 0000000..c604f48
--- /dev/null
+++ b/hesabixAPI/migrations/versions/20250117_000009_add_is_default_to_email_config.py
@@ -0,0 +1,26 @@
+"""add is_default to email_config
+
+Revision ID: 20250117_000009
+Revises: 20250117_000008
+Create Date: 2025-01-17 12:00:00.000000
+
+"""
+from alembic import op
+import sqlalchemy as sa
+
+
+# revision identifiers, used by Alembic.
+revision = '20250117_000009'
+down_revision = '20250117_000008'
+branch_labels = None
+depends_on = None
+
+
+def upgrade():
+ # Add is_default column to email_configs table
+ op.add_column('email_configs', sa.Column('is_default', sa.Boolean(), nullable=False, server_default='0'))
+
+
+def downgrade():
+ # Remove is_default column from email_configs table
+ op.drop_column('email_configs', 'is_default')
diff --git a/hesabixUI/hesabix_ui/lib/core/referral_store.dart b/hesabixUI/hesabix_ui/lib/core/referral_store.dart
index 1aff516..5050e91 100644
--- a/hesabixUI/hesabix_ui/lib/core/referral_store.dart
+++ b/hesabixUI/hesabix_ui/lib/core/referral_store.dart
@@ -1,6 +1,5 @@
import 'dart:async';
-import 'package:flutter/foundation.dart';
import 'package:shared_preferences/shared_preferences.dart';
class ReferralStore {
diff --git a/hesabixUI/hesabix_ui/lib/core/splash_controller.dart b/hesabixUI/hesabix_ui/lib/core/splash_controller.dart
new file mode 100644
index 0000000..55d7b8f
--- /dev/null
+++ b/hesabixUI/hesabix_ui/lib/core/splash_controller.dart
@@ -0,0 +1,76 @@
+import 'dart:async';
+import 'package:flutter/material.dart';
+
+class SplashController extends ChangeNotifier {
+ static const Duration _minimumSplashDuration = Duration(seconds: 2);
+
+ bool _isLoading = true;
+ DateTime? _startTime;
+ Timer? _minimumDurationTimer;
+ Completer? _loadingCompleter;
+
+ bool get isLoading => _isLoading;
+
+ SplashController() {
+ _startTime = DateTime.now();
+ }
+
+ /// شروع loading با حداقل زمان نمایش
+ Future startLoading() async {
+ _isLoading = true;
+ _loadingCompleter = Completer();
+ notifyListeners();
+
+ // شروع تایمر برای حداقل زمان نمایش
+ _minimumDurationTimer = Timer(_minimumSplashDuration, () {
+ if (_loadingCompleter != null && !_loadingCompleter!.isCompleted) {
+ _loadingCompleter!.complete();
+ }
+ });
+
+ return _loadingCompleter!.future;
+ }
+
+ /// اتمام loading (فقط اگر حداقل زمان گذشته باشد)
+ void finishLoading() {
+ if (_minimumDurationTimer != null && _minimumDurationTimer!.isActive) {
+ // اگر هنوز حداقل زمان نگذشته، منتظر بمان
+ _minimumDurationTimer!.cancel();
+ _minimumDurationTimer = Timer(
+ _minimumSplashDuration - DateTime.now().difference(_startTime!),
+ () {
+ _completeLoading();
+ },
+ );
+ } else {
+ _completeLoading();
+ }
+ }
+
+ void _completeLoading() {
+ if (_isLoading) {
+ _isLoading = false;
+ notifyListeners();
+ }
+ }
+
+ /// بررسی اینکه آیا حداقل زمان گذشته یا نه
+ bool get hasMinimumTimePassed {
+ if (_startTime == null) return true;
+ return DateTime.now().difference(_startTime!) >= _minimumSplashDuration;
+ }
+
+ /// دریافت زمان باقیمانده تا اتمام حداقل زمان
+ Duration get remainingTime {
+ if (_startTime == null) return Duration.zero;
+ final elapsed = DateTime.now().difference(_startTime!);
+ final remaining = _minimumSplashDuration - elapsed;
+ return remaining.isNegative ? Duration.zero : remaining;
+ }
+
+ @override
+ void dispose() {
+ _minimumDurationTimer?.cancel();
+ super.dispose();
+ }
+}
diff --git a/hesabixUI/hesabix_ui/lib/l10n/app_en.arb b/hesabixUI/hesabix_ui/lib/l10n/app_en.arb
index a0623f8..aa584d8 100644
--- a/hesabixUI/hesabix_ui/lib/l10n/app_en.arb
+++ b/hesabixUI/hesabix_ui/lib/l10n/app_en.arb
@@ -48,6 +48,30 @@
,
"systemSettings": "System Settings",
"adminTools": "Admin Tools",
+ "emailSettings": "Email Settings",
+ "emailSettingsDescription": "Configure SMTP settings for email sending",
+ "emailConfigurations": "Email Configurations",
+ "noEmailConfigurations": "No email configurations found",
+ "addEmailConfiguration": "Add Email Configuration",
+ "configurationName": "Configuration Name",
+ "smtpHost": "SMTP Host",
+ "smtpPort": "SMTP Port",
+ "smtpUsername": "SMTP Username",
+ "smtpPassword": "SMTP Password",
+ "fromEmail": "From Email",
+ "fromName": "From Name",
+ "useTls": "Use TLS",
+ "useSsl": "Use SSL",
+ "isActive": "Active",
+ "active": "Active",
+ "testConnection": "Test Connection",
+ "sendTestEmail": "Send Test Email",
+ "saveConfiguration": "Save Configuration",
+ "deleteConfiguration": "Delete Configuration",
+ "deleteConfigurationConfirm": "Are you sure you want to delete this configuration?",
+ "delete": "Delete",
+ "invalidPort": "Invalid port",
+ "invalidEmail": "Invalid email",
"ok": "OK",
"cancel": "Cancel",
"columnSettings": "Column Settings",
@@ -202,6 +226,8 @@
"trading": "Trading",
"service": "Service",
"other": "Other",
+ "owner": "Owner",
+ "member": "Member",
"support": "Support",
"newTicket": "New Ticket",
"ticketTitle": "Ticket Title",
@@ -340,6 +366,32 @@
"connectionSuccessful": "Connection Successful",
"connectionFailed": "Connection Failed",
"setAsDefault": "Set as Default",
+ "defaultConfiguration": "Default Configuration",
+ "setDefaultConfirm": "Are you sure you want to set this configuration as default?",
+ "defaultSetSuccessfully": "Default configuration set successfully",
+ "defaultSetFailed": "Failed to set default configuration",
+ "cannotDeleteDefault": "Cannot delete default configuration",
+ "defaultConfigurationNote": "Default configuration is used for sending emails and cannot be deleted",
+ "setAsDefaultEmail": "Set as Default Email",
+ "defaultEmailServer": "Default Email Server",
+ "changeDefaultEmail": "Change Default Email",
+ "currentDefault": "Current Default",
+ "makeDefault": "Make Default",
+ "defaultEmailNote": "Emails are sent from the default server",
+ "noDefaultSet": "No default email server is set",
+ "selectDefaultServer": "Select Default Server",
+ "defaultServerChanged": "Default server changed",
+ "defaultServerChangeFailed": "Failed to change default server",
+ "emailConfigSavedSuccessfully": "Email configuration saved successfully",
+ "emailConfigUpdatedSuccessfully": "Email configuration updated successfully",
+ "editEmailConfiguration": "Edit Email Configuration",
+ "updateConfiguration": "Update Configuration",
+ "testEmailSubject": "Test Email",
+ "testEmailBody": "This is a test email.",
+ "testEmailSentSuccessfully": "Test email sent successfully",
+ "emailConfigDeletedSuccessfully": "Configuration deleted successfully",
+ "confirm": "Confirm",
+ "cancel": "Cancel",
"fileStatistics": "File Statistics",
"totalFiles": "Total Files",
"totalSize": "Total Size",
@@ -413,6 +465,24 @@
"systemAdministration": "System Administration",
"generalSettings": "General Settings",
"securitySettings": "Security Settings",
- "maintenanceSettings": "Maintenance Settings"
+ "maintenanceSettings": "Maintenance Settings",
+ "initializing": "Initializing...",
+ "loadingLanguageSettings": "Loading language settings...",
+ "loadingCalendarSettings": "Loading calendar settings...",
+ "loadingThemeSettings": "Loading theme settings...",
+ "loadingAuthentication": "Loading authentication...",
+ "businessManagementPlatform": "Business Management Platform",
+ "businessDashboard": "Business Dashboard",
+ "businessStatistics": "Business Statistics",
+ "recentActivities": "Recent Activities",
+ "sales": "Sales",
+ "accounting": "Accounting",
+ "inventory": "Inventory",
+ "reports": "Reports",
+ "members": "Members",
+ "backToProfile": "Back to Profile",
+ "noBusinessesFound": "No businesses found",
+ "createFirstBusiness": "Create your first business",
+ "accessDenied": "Access denied"
}
diff --git a/hesabixUI/hesabix_ui/lib/l10n/app_fa.arb b/hesabixUI/hesabix_ui/lib/l10n/app_fa.arb
index d84148b..d44225b 100644
--- a/hesabixUI/hesabix_ui/lib/l10n/app_fa.arb
+++ b/hesabixUI/hesabix_ui/lib/l10n/app_fa.arb
@@ -53,6 +53,30 @@
"marketing": "بازاریابی",
"systemSettings": "تنظیمات سیستم",
"adminTools": "ابزارهای مدیریتی",
+ "emailSettings": "تنظیمات ایمیل",
+ "emailSettingsDescription": "پیکربندی تنظیمات SMTP برای ارسال ایمیل",
+ "emailConfigurations": "پیکربندیهای ایمیل",
+ "noEmailConfigurations": "هیچ پیکربندی ایمیلی وجود ندارد",
+ "addEmailConfiguration": "افزودن پیکربندی ایمیل",
+ "configurationName": "نام پیکربندی",
+ "smtpHost": "میزبان SMTP",
+ "smtpPort": "پورت SMTP",
+ "smtpUsername": "نام کاربری SMTP",
+ "smtpPassword": "رمز عبور SMTP",
+ "fromEmail": "ایمیل فرستنده",
+ "fromName": "نام فرستنده",
+ "useTls": "استفاده از TLS",
+ "useSsl": "استفاده از SSL",
+ "isActive": "فعال",
+ "active": "فعال",
+ "testConnection": "تست اتصال",
+ "sendTestEmail": "ارسال ایمیل تست",
+ "saveConfiguration": "ذخیره پیکربندی",
+ "deleteConfiguration": "حذف پیکربندی",
+ "deleteConfigurationConfirm": "آیا مطمئن هستید که میخواهید این پیکربندی را حذف کنید؟",
+ "delete": "حذف",
+ "invalidPort": "پورت نامعتبر است",
+ "invalidEmail": "ایمیل نامعتبر است",
"ok": "تایید",
"cancel": "انصراف",
"columnSettings": "تنظیمات ستونها",
@@ -201,6 +225,8 @@
"trading": "بازرگانی",
"service": "خدماتی",
"other": "سایر",
+ "owner": "مالک",
+ "member": "عضو",
"support": "پشتیبانی",
"newTicket": "تیکت جدید",
"ticketTitle": "عنوان تیکت",
@@ -339,6 +365,32 @@
"connectionSuccessful": "اتصال موفقیتآمیز",
"connectionFailed": "اتصال ناموفق",
"setAsDefault": "تنظیم به عنوان پیشفرض",
+ "defaultConfiguration": "پیکربندی پیشفرض",
+ "setDefaultConfirm": "آیا مطمئن هستید که میخواهید این پیکربندی را به عنوان پیشفرض تنظیم کنید؟",
+ "defaultSetSuccessfully": "پیکربندی پیشفرض با موفقیت تنظیم شد",
+ "defaultSetFailed": "تنظیم پیکربندی پیشفرض ناموفق بود",
+ "cannotDeleteDefault": "نمیتوان پیکربندی پیشفرض را حذف کرد",
+ "defaultConfigurationNote": "پیکربندی پیشفرض برای ارسال ایمیلها استفاده میشود و قابل حذف نیست",
+ "setAsDefaultEmail": "تنظیم به عنوان ایمیل پیشفرض",
+ "defaultEmailServer": "سرور ایمیل پیشفرض",
+ "changeDefaultEmail": "تغییر ایمیل پیشفرض",
+ "currentDefault": "پیشفرض فعلی",
+ "makeDefault": "پیشفرض کردن",
+ "defaultEmailNote": "ایمیلها از سرور پیشفرض ارسال میشوند",
+ "noDefaultSet": "هیچ سرور ایمیل پیشفرضی تنظیم نشده است",
+ "selectDefaultServer": "انتخاب سرور پیشفرض",
+ "defaultServerChanged": "سرور پیشفرض تغییر کرد",
+ "defaultServerChangeFailed": "تغییر سرور پیشفرض ناموفق بود",
+ "emailConfigSavedSuccessfully": "تنظیمات ایمیل با موفقیت ذخیره شد",
+ "emailConfigUpdatedSuccessfully": "تنظیمات ایمیل با موفقیت بهروزرسانی شد",
+ "editEmailConfiguration": "ویرایش تنظیمات ایمیل",
+ "updateConfiguration": "بهروزرسانی تنظیمات",
+ "testEmailSubject": "تست ایمیل",
+ "testEmailBody": "این یک ایمیل تست است.",
+ "testEmailSentSuccessfully": "ایمیل تست با موفقیت ارسال شد",
+ "emailConfigDeletedSuccessfully": "تنظیمات حذف شد",
+ "confirm": "تایید",
+ "cancel": "لغو",
"fileStatistics": "آمار فایلها",
"totalFiles": "کل فایلها",
"totalSize": "حجم کل",
@@ -412,6 +464,24 @@
"systemAdministration": "مدیریت سیستم",
"generalSettings": "تنظیمات عمومی",
"securitySettings": "تنظیمات امنیتی",
- "maintenanceSettings": "تنظیمات نگهداری"
+ "maintenanceSettings": "تنظیمات نگهداری",
+ "initializing": "در حال راهاندازی...",
+ "loadingLanguageSettings": "در حال بارگذاری تنظیمات زبان...",
+ "loadingCalendarSettings": "در حال بارگذاری تنظیمات تقویم...",
+ "loadingThemeSettings": "در حال بارگذاری تنظیمات تم...",
+ "loadingAuthentication": "در حال بارگذاری احراز هویت...",
+ "businessManagementPlatform": "پلتفرم مدیریت کسبوکار",
+ "businessDashboard": "داشبورد کسب و کار",
+ "businessStatistics": "آمار کسب و کار",
+ "recentActivities": "فعالیتهای اخیر",
+ "sales": "فروش",
+ "accounting": "حسابداری",
+ "inventory": "موجودی",
+ "reports": "گزارشها",
+ "members": "اعضا",
+ "backToProfile": "بازگشت به پروفایل",
+ "noBusinessesFound": "هیچ کسب و کاری یافت نشد",
+ "createFirstBusiness": "اولین کسب و کار خود را ایجاد کنید",
+ "accessDenied": "دسترسی غیرمجاز"
}
diff --git a/hesabixUI/hesabix_ui/lib/l10n/app_localizations.dart b/hesabixUI/hesabix_ui/lib/l10n/app_localizations.dart
index b8e92a5..c4951bc 100644
--- a/hesabixUI/hesabix_ui/lib/l10n/app_localizations.dart
+++ b/hesabixUI/hesabix_ui/lib/l10n/app_localizations.dart
@@ -350,6 +350,150 @@ abstract class AppLocalizations {
/// **'Admin Tools'**
String get adminTools;
+ /// No description provided for @emailSettings.
+ ///
+ /// In en, this message translates to:
+ /// **'Email Settings'**
+ String get emailSettings;
+
+ /// No description provided for @emailSettingsDescription.
+ ///
+ /// In en, this message translates to:
+ /// **'Configure SMTP settings for email sending'**
+ String get emailSettingsDescription;
+
+ /// No description provided for @emailConfigurations.
+ ///
+ /// In en, this message translates to:
+ /// **'Email Configurations'**
+ String get emailConfigurations;
+
+ /// No description provided for @noEmailConfigurations.
+ ///
+ /// In en, this message translates to:
+ /// **'No email configurations found'**
+ String get noEmailConfigurations;
+
+ /// No description provided for @addEmailConfiguration.
+ ///
+ /// In en, this message translates to:
+ /// **'Add Email Configuration'**
+ String get addEmailConfiguration;
+
+ /// No description provided for @configurationName.
+ ///
+ /// In en, this message translates to:
+ /// **'Configuration Name'**
+ String get configurationName;
+
+ /// No description provided for @smtpHost.
+ ///
+ /// In en, this message translates to:
+ /// **'SMTP Host'**
+ String get smtpHost;
+
+ /// No description provided for @smtpPort.
+ ///
+ /// In en, this message translates to:
+ /// **'SMTP Port'**
+ String get smtpPort;
+
+ /// No description provided for @smtpUsername.
+ ///
+ /// In en, this message translates to:
+ /// **'SMTP Username'**
+ String get smtpUsername;
+
+ /// No description provided for @smtpPassword.
+ ///
+ /// In en, this message translates to:
+ /// **'SMTP Password'**
+ String get smtpPassword;
+
+ /// No description provided for @fromEmail.
+ ///
+ /// In en, this message translates to:
+ /// **'From Email'**
+ String get fromEmail;
+
+ /// No description provided for @fromName.
+ ///
+ /// In en, this message translates to:
+ /// **'From Name'**
+ String get fromName;
+
+ /// No description provided for @useTls.
+ ///
+ /// In en, this message translates to:
+ /// **'Use TLS'**
+ String get useTls;
+
+ /// No description provided for @useSsl.
+ ///
+ /// In en, this message translates to:
+ /// **'Use SSL'**
+ String get useSsl;
+
+ /// No description provided for @isActive.
+ ///
+ /// In en, this message translates to:
+ /// **'Active'**
+ String get isActive;
+
+ /// No description provided for @active.
+ ///
+ /// In en, this message translates to:
+ /// **'Active'**
+ String get active;
+
+ /// No description provided for @testConnection.
+ ///
+ /// In en, this message translates to:
+ /// **'Test Connection'**
+ String get testConnection;
+
+ /// No description provided for @sendTestEmail.
+ ///
+ /// In en, this message translates to:
+ /// **'Send Test Email'**
+ String get sendTestEmail;
+
+ /// No description provided for @saveConfiguration.
+ ///
+ /// In en, this message translates to:
+ /// **'Save Configuration'**
+ String get saveConfiguration;
+
+ /// No description provided for @deleteConfiguration.
+ ///
+ /// In en, this message translates to:
+ /// **'Delete Configuration'**
+ String get deleteConfiguration;
+
+ /// No description provided for @deleteConfigurationConfirm.
+ ///
+ /// In en, this message translates to:
+ /// **'Are you sure you want to delete this configuration?'**
+ String get deleteConfigurationConfirm;
+
+ /// No description provided for @delete.
+ ///
+ /// In en, this message translates to:
+ /// **'Delete'**
+ String get delete;
+
+ /// No description provided for @invalidPort.
+ ///
+ /// In en, this message translates to:
+ /// **'Invalid port'**
+ String get invalidPort;
+
+ /// No description provided for @invalidEmail.
+ ///
+ /// In en, this message translates to:
+ /// **'Invalid email'**
+ String get invalidEmail;
+
/// No description provided for @ok.
///
/// In en, this message translates to:
@@ -1208,6 +1352,18 @@ abstract class AppLocalizations {
/// **'Other'**
String get other;
+ /// No description provided for @owner.
+ ///
+ /// In en, this message translates to:
+ /// **'Owner'**
+ String get owner;
+
+ /// No description provided for @member.
+ ///
+ /// In en, this message translates to:
+ /// **'Member'**
+ String get member;
+
/// No description provided for @newTicket.
///
/// In en, this message translates to:
@@ -1844,12 +2000,6 @@ abstract class AppLocalizations {
/// **'Default'**
String get isDefault;
- /// No description provided for @isActive.
- ///
- /// In en, this message translates to:
- /// **'Active'**
- String get isActive;
-
/// No description provided for @configData.
///
/// In en, this message translates to:
@@ -1892,12 +2042,6 @@ abstract class AppLocalizations {
/// **'FTP Directory'**
String get ftpDirectory;
- /// No description provided for @testConnection.
- ///
- /// In en, this message translates to:
- /// **'Test Connection'**
- String get testConnection;
-
/// No description provided for @connectionSuccessful.
///
/// In en, this message translates to:
@@ -1916,6 +2060,156 @@ abstract class AppLocalizations {
/// **'Set as Default'**
String get setAsDefault;
+ /// No description provided for @defaultConfiguration.
+ ///
+ /// In en, this message translates to:
+ /// **'Default Configuration'**
+ String get defaultConfiguration;
+
+ /// No description provided for @setDefaultConfirm.
+ ///
+ /// In en, this message translates to:
+ /// **'Are you sure you want to set this configuration as default?'**
+ String get setDefaultConfirm;
+
+ /// No description provided for @defaultSetSuccessfully.
+ ///
+ /// In en, this message translates to:
+ /// **'Default configuration set successfully'**
+ String get defaultSetSuccessfully;
+
+ /// No description provided for @defaultSetFailed.
+ ///
+ /// In en, this message translates to:
+ /// **'Failed to set default configuration'**
+ String get defaultSetFailed;
+
+ /// No description provided for @cannotDeleteDefault.
+ ///
+ /// In en, this message translates to:
+ /// **'Cannot delete default configuration'**
+ String get cannotDeleteDefault;
+
+ /// No description provided for @defaultConfigurationNote.
+ ///
+ /// In en, this message translates to:
+ /// **'Default configuration is used for sending emails and cannot be deleted'**
+ String get defaultConfigurationNote;
+
+ /// No description provided for @setAsDefaultEmail.
+ ///
+ /// In en, this message translates to:
+ /// **'Set as Default Email'**
+ String get setAsDefaultEmail;
+
+ /// No description provided for @defaultEmailServer.
+ ///
+ /// In en, this message translates to:
+ /// **'Default Email Server'**
+ String get defaultEmailServer;
+
+ /// No description provided for @changeDefaultEmail.
+ ///
+ /// In en, this message translates to:
+ /// **'Change Default Email'**
+ String get changeDefaultEmail;
+
+ /// No description provided for @currentDefault.
+ ///
+ /// In en, this message translates to:
+ /// **'Current Default'**
+ String get currentDefault;
+
+ /// No description provided for @makeDefault.
+ ///
+ /// In en, this message translates to:
+ /// **'Make Default'**
+ String get makeDefault;
+
+ /// No description provided for @defaultEmailNote.
+ ///
+ /// In en, this message translates to:
+ /// **'Emails are sent from the default server'**
+ String get defaultEmailNote;
+
+ /// No description provided for @noDefaultSet.
+ ///
+ /// In en, this message translates to:
+ /// **'No default email server is set'**
+ String get noDefaultSet;
+
+ /// No description provided for @selectDefaultServer.
+ ///
+ /// In en, this message translates to:
+ /// **'Select Default Server'**
+ String get selectDefaultServer;
+
+ /// No description provided for @defaultServerChanged.
+ ///
+ /// In en, this message translates to:
+ /// **'Default server changed'**
+ String get defaultServerChanged;
+
+ /// No description provided for @defaultServerChangeFailed.
+ ///
+ /// In en, this message translates to:
+ /// **'Failed to change default server'**
+ String get defaultServerChangeFailed;
+
+ /// No description provided for @emailConfigSavedSuccessfully.
+ ///
+ /// In en, this message translates to:
+ /// **'Email configuration saved successfully'**
+ String get emailConfigSavedSuccessfully;
+
+ /// No description provided for @emailConfigUpdatedSuccessfully.
+ ///
+ /// In en, this message translates to:
+ /// **'Email configuration updated successfully'**
+ String get emailConfigUpdatedSuccessfully;
+
+ /// No description provided for @editEmailConfiguration.
+ ///
+ /// In en, this message translates to:
+ /// **'Edit Email Configuration'**
+ String get editEmailConfiguration;
+
+ /// No description provided for @updateConfiguration.
+ ///
+ /// In en, this message translates to:
+ /// **'Update Configuration'**
+ String get updateConfiguration;
+
+ /// No description provided for @testEmailSubject.
+ ///
+ /// In en, this message translates to:
+ /// **'Test Email'**
+ String get testEmailSubject;
+
+ /// No description provided for @testEmailBody.
+ ///
+ /// In en, this message translates to:
+ /// **'This is a test email.'**
+ String get testEmailBody;
+
+ /// No description provided for @testEmailSentSuccessfully.
+ ///
+ /// In en, this message translates to:
+ /// **'Test email sent successfully'**
+ String get testEmailSentSuccessfully;
+
+ /// No description provided for @emailConfigDeletedSuccessfully.
+ ///
+ /// In en, this message translates to:
+ /// **'Configuration deleted successfully'**
+ String get emailConfigDeletedSuccessfully;
+
+ /// No description provided for @confirm.
+ ///
+ /// In en, this message translates to:
+ /// **'Confirm'**
+ String get confirm;
+
/// No description provided for @fileStatistics.
///
/// In en, this message translates to:
@@ -2162,12 +2456,6 @@ abstract class AppLocalizations {
/// **'Edit'**
String get edit;
- /// No description provided for @delete.
- ///
- /// In en, this message translates to:
- /// **'Delete'**
- String get delete;
-
/// No description provided for @actions.
///
/// In en, this message translates to:
@@ -2317,6 +2605,114 @@ abstract class AppLocalizations {
/// In en, this message translates to:
/// **'Maintenance Settings'**
String get maintenanceSettings;
+
+ /// No description provided for @initializing.
+ ///
+ /// In en, this message translates to:
+ /// **'Initializing...'**
+ String get initializing;
+
+ /// No description provided for @loadingLanguageSettings.
+ ///
+ /// In en, this message translates to:
+ /// **'Loading language settings...'**
+ String get loadingLanguageSettings;
+
+ /// No description provided for @loadingCalendarSettings.
+ ///
+ /// In en, this message translates to:
+ /// **'Loading calendar settings...'**
+ String get loadingCalendarSettings;
+
+ /// No description provided for @loadingThemeSettings.
+ ///
+ /// In en, this message translates to:
+ /// **'Loading theme settings...'**
+ String get loadingThemeSettings;
+
+ /// No description provided for @loadingAuthentication.
+ ///
+ /// In en, this message translates to:
+ /// **'Loading authentication...'**
+ String get loadingAuthentication;
+
+ /// No description provided for @businessManagementPlatform.
+ ///
+ /// In en, this message translates to:
+ /// **'Business Management Platform'**
+ String get businessManagementPlatform;
+
+ /// No description provided for @businessDashboard.
+ ///
+ /// In en, this message translates to:
+ /// **'Business Dashboard'**
+ String get businessDashboard;
+
+ /// No description provided for @businessStatistics.
+ ///
+ /// In en, this message translates to:
+ /// **'Business Statistics'**
+ String get businessStatistics;
+
+ /// No description provided for @recentActivities.
+ ///
+ /// In en, this message translates to:
+ /// **'Recent Activities'**
+ String get recentActivities;
+
+ /// No description provided for @sales.
+ ///
+ /// In en, this message translates to:
+ /// **'Sales'**
+ String get sales;
+
+ /// No description provided for @accounting.
+ ///
+ /// In en, this message translates to:
+ /// **'Accounting'**
+ String get accounting;
+
+ /// No description provided for @inventory.
+ ///
+ /// In en, this message translates to:
+ /// **'Inventory'**
+ String get inventory;
+
+ /// No description provided for @reports.
+ ///
+ /// In en, this message translates to:
+ /// **'Reports'**
+ String get reports;
+
+ /// No description provided for @members.
+ ///
+ /// In en, this message translates to:
+ /// **'Members'**
+ String get members;
+
+ /// No description provided for @backToProfile.
+ ///
+ /// In en, this message translates to:
+ /// **'Back to Profile'**
+ String get backToProfile;
+
+ /// No description provided for @noBusinessesFound.
+ ///
+ /// In en, this message translates to:
+ /// **'No businesses found'**
+ String get noBusinessesFound;
+
+ /// No description provided for @createFirstBusiness.
+ ///
+ /// In en, this message translates to:
+ /// **'Create your first business'**
+ String get createFirstBusiness;
+
+ /// No description provided for @accessDenied.
+ ///
+ /// In en, this message translates to:
+ /// **'Access denied'**
+ String get accessDenied;
}
class _AppLocalizationsDelegate
diff --git a/hesabixUI/hesabix_ui/lib/l10n/app_localizations_en.dart b/hesabixUI/hesabix_ui/lib/l10n/app_localizations_en.dart
index 620e769..5087499 100644
--- a/hesabixUI/hesabix_ui/lib/l10n/app_localizations_en.dart
+++ b/hesabixUI/hesabix_ui/lib/l10n/app_localizations_en.dart
@@ -136,6 +136,80 @@ class AppLocalizationsEn extends AppLocalizations {
@override
String get adminTools => 'Admin Tools';
+ @override
+ String get emailSettings => 'Email Settings';
+
+ @override
+ String get emailSettingsDescription =>
+ 'Configure SMTP settings for email sending';
+
+ @override
+ String get emailConfigurations => 'Email Configurations';
+
+ @override
+ String get noEmailConfigurations => 'No email configurations found';
+
+ @override
+ String get addEmailConfiguration => 'Add Email Configuration';
+
+ @override
+ String get configurationName => 'Configuration Name';
+
+ @override
+ String get smtpHost => 'SMTP Host';
+
+ @override
+ String get smtpPort => 'SMTP Port';
+
+ @override
+ String get smtpUsername => 'SMTP Username';
+
+ @override
+ String get smtpPassword => 'SMTP Password';
+
+ @override
+ String get fromEmail => 'From Email';
+
+ @override
+ String get fromName => 'From Name';
+
+ @override
+ String get useTls => 'Use TLS';
+
+ @override
+ String get useSsl => 'Use SSL';
+
+ @override
+ String get isActive => 'Active';
+
+ @override
+ String get active => 'Active';
+
+ @override
+ String get testConnection => 'Test Connection';
+
+ @override
+ String get sendTestEmail => 'Send Test Email';
+
+ @override
+ String get saveConfiguration => 'Save Configuration';
+
+ @override
+ String get deleteConfiguration => 'Delete Configuration';
+
+ @override
+ String get deleteConfigurationConfirm =>
+ 'Are you sure you want to delete this configuration?';
+
+ @override
+ String get delete => 'Delete';
+
+ @override
+ String get invalidPort => 'Invalid port';
+
+ @override
+ String get invalidEmail => 'Invalid email';
+
@override
String get ok => 'OK';
@@ -577,6 +651,12 @@ class AppLocalizationsEn extends AppLocalizations {
@override
String get other => 'Other';
+ @override
+ String get owner => 'Owner';
+
+ @override
+ String get member => 'Member';
+
@override
String get newTicket => 'New Ticket';
@@ -908,9 +988,6 @@ class AppLocalizationsEn extends AppLocalizations {
@override
String get isDefault => 'Default';
- @override
- String get isActive => 'Active';
-
@override
String get configData => 'Configuration Data';
@@ -932,9 +1009,6 @@ class AppLocalizationsEn extends AppLocalizations {
@override
String get ftpDirectory => 'FTP Directory';
- @override
- String get testConnection => 'Test Connection';
-
@override
String get connectionSuccessful => 'Connection Successful';
@@ -944,6 +1018,86 @@ class AppLocalizationsEn extends AppLocalizations {
@override
String get setAsDefault => 'Set as Default';
+ @override
+ String get defaultConfiguration => 'Default Configuration';
+
+ @override
+ String get setDefaultConfirm =>
+ 'Are you sure you want to set this configuration as default?';
+
+ @override
+ String get defaultSetSuccessfully => 'Default configuration set successfully';
+
+ @override
+ String get defaultSetFailed => 'Failed to set default configuration';
+
+ @override
+ String get cannotDeleteDefault => 'Cannot delete default configuration';
+
+ @override
+ String get defaultConfigurationNote =>
+ 'Default configuration is used for sending emails and cannot be deleted';
+
+ @override
+ String get setAsDefaultEmail => 'Set as Default Email';
+
+ @override
+ String get defaultEmailServer => 'Default Email Server';
+
+ @override
+ String get changeDefaultEmail => 'Change Default Email';
+
+ @override
+ String get currentDefault => 'Current Default';
+
+ @override
+ String get makeDefault => 'Make Default';
+
+ @override
+ String get defaultEmailNote => 'Emails are sent from the default server';
+
+ @override
+ String get noDefaultSet => 'No default email server is set';
+
+ @override
+ String get selectDefaultServer => 'Select Default Server';
+
+ @override
+ String get defaultServerChanged => 'Default server changed';
+
+ @override
+ String get defaultServerChangeFailed => 'Failed to change default server';
+
+ @override
+ String get emailConfigSavedSuccessfully =>
+ 'Email configuration saved successfully';
+
+ @override
+ String get emailConfigUpdatedSuccessfully =>
+ 'Email configuration updated successfully';
+
+ @override
+ String get editEmailConfiguration => 'Edit Email Configuration';
+
+ @override
+ String get updateConfiguration => 'Update Configuration';
+
+ @override
+ String get testEmailSubject => 'Test Email';
+
+ @override
+ String get testEmailBody => 'This is a test email.';
+
+ @override
+ String get testEmailSentSuccessfully => 'Test email sent successfully';
+
+ @override
+ String get emailConfigDeletedSuccessfully =>
+ 'Configuration deleted successfully';
+
+ @override
+ String get confirm => 'Confirm';
+
@override
String get fileStatistics => 'File Statistics';
@@ -1071,9 +1225,6 @@ class AppLocalizationsEn extends AppLocalizations {
@override
String get edit => 'Edit';
- @override
- String get delete => 'Delete';
-
@override
String get actions => 'Actions';
@@ -1151,4 +1302,58 @@ class AppLocalizationsEn extends AppLocalizations {
@override
String get maintenanceSettings => 'Maintenance Settings';
+
+ @override
+ String get initializing => 'Initializing...';
+
+ @override
+ String get loadingLanguageSettings => 'Loading language settings...';
+
+ @override
+ String get loadingCalendarSettings => 'Loading calendar settings...';
+
+ @override
+ String get loadingThemeSettings => 'Loading theme settings...';
+
+ @override
+ String get loadingAuthentication => 'Loading authentication...';
+
+ @override
+ String get businessManagementPlatform => 'Business Management Platform';
+
+ @override
+ String get businessDashboard => 'Business Dashboard';
+
+ @override
+ String get businessStatistics => 'Business Statistics';
+
+ @override
+ String get recentActivities => 'Recent Activities';
+
+ @override
+ String get sales => 'Sales';
+
+ @override
+ String get accounting => 'Accounting';
+
+ @override
+ String get inventory => 'Inventory';
+
+ @override
+ String get reports => 'Reports';
+
+ @override
+ String get members => 'Members';
+
+ @override
+ String get backToProfile => 'Back to Profile';
+
+ @override
+ String get noBusinessesFound => 'No businesses found';
+
+ @override
+ String get createFirstBusiness => 'Create your first business';
+
+ @override
+ String get accessDenied => 'Access denied';
}
diff --git a/hesabixUI/hesabix_ui/lib/l10n/app_localizations_fa.dart b/hesabixUI/hesabix_ui/lib/l10n/app_localizations_fa.dart
index 80b4154..7bb21b6 100644
--- a/hesabixUI/hesabix_ui/lib/l10n/app_localizations_fa.dart
+++ b/hesabixUI/hesabix_ui/lib/l10n/app_localizations_fa.dart
@@ -136,6 +136,80 @@ class AppLocalizationsFa extends AppLocalizations {
@override
String get adminTools => 'ابزارهای مدیریتی';
+ @override
+ String get emailSettings => 'تنظیمات ایمیل';
+
+ @override
+ String get emailSettingsDescription =>
+ 'پیکربندی تنظیمات SMTP برای ارسال ایمیل';
+
+ @override
+ String get emailConfigurations => 'پیکربندیهای ایمیل';
+
+ @override
+ String get noEmailConfigurations => 'هیچ پیکربندی ایمیلی وجود ندارد';
+
+ @override
+ String get addEmailConfiguration => 'افزودن پیکربندی ایمیل';
+
+ @override
+ String get configurationName => 'نام پیکربندی';
+
+ @override
+ String get smtpHost => 'میزبان SMTP';
+
+ @override
+ String get smtpPort => 'پورت SMTP';
+
+ @override
+ String get smtpUsername => 'نام کاربری SMTP';
+
+ @override
+ String get smtpPassword => 'رمز عبور SMTP';
+
+ @override
+ String get fromEmail => 'ایمیل فرستنده';
+
+ @override
+ String get fromName => 'نام فرستنده';
+
+ @override
+ String get useTls => 'استفاده از TLS';
+
+ @override
+ String get useSsl => 'استفاده از SSL';
+
+ @override
+ String get isActive => 'فعال';
+
+ @override
+ String get active => 'فعال';
+
+ @override
+ String get testConnection => 'تست اتصال';
+
+ @override
+ String get sendTestEmail => 'ارسال ایمیل تست';
+
+ @override
+ String get saveConfiguration => 'ذخیره پیکربندی';
+
+ @override
+ String get deleteConfiguration => 'حذف پیکربندی';
+
+ @override
+ String get deleteConfigurationConfirm =>
+ 'آیا مطمئن هستید که میخواهید این پیکربندی را حذف کنید؟';
+
+ @override
+ String get delete => 'حذف';
+
+ @override
+ String get invalidPort => 'پورت نامعتبر است';
+
+ @override
+ String get invalidEmail => 'ایمیل نامعتبر است';
+
@override
String get ok => 'تایید';
@@ -575,6 +649,12 @@ class AppLocalizationsFa extends AppLocalizations {
@override
String get other => 'سایر';
+ @override
+ String get owner => 'مالک';
+
+ @override
+ String get member => 'عضو';
+
@override
String get newTicket => 'تیکت جدید';
@@ -904,9 +984,6 @@ class AppLocalizationsFa extends AppLocalizations {
@override
String get isDefault => 'پیشفرض';
- @override
- String get isActive => 'فعال';
-
@override
String get configData => 'دادههای پیکربندی';
@@ -928,9 +1005,6 @@ class AppLocalizationsFa extends AppLocalizations {
@override
String get ftpDirectory => 'پوشه FTP';
- @override
- String get testConnection => 'تست اتصال';
-
@override
String get connectionSuccessful => 'اتصال موفقیتآمیز';
@@ -940,6 +1014,84 @@ class AppLocalizationsFa extends AppLocalizations {
@override
String get setAsDefault => 'تنظیم به عنوان پیشفرض';
+ @override
+ String get defaultConfiguration => 'پیکربندی پیشفرض';
+
+ @override
+ String get setDefaultConfirm =>
+ 'آیا مطمئن هستید که میخواهید این پیکربندی را به عنوان پیشفرض تنظیم کنید؟';
+
+ @override
+ String get defaultSetSuccessfully => 'پیکربندی پیشفرض با موفقیت تنظیم شد';
+
+ @override
+ String get defaultSetFailed => 'تنظیم پیکربندی پیشفرض ناموفق بود';
+
+ @override
+ String get cannotDeleteDefault => 'نمیتوان پیکربندی پیشفرض را حذف کرد';
+
+ @override
+ String get defaultConfigurationNote =>
+ 'پیکربندی پیشفرض برای ارسال ایمیلها استفاده میشود و قابل حذف نیست';
+
+ @override
+ String get setAsDefaultEmail => 'تنظیم به عنوان ایمیل پیشفرض';
+
+ @override
+ String get defaultEmailServer => 'سرور ایمیل پیشفرض';
+
+ @override
+ String get changeDefaultEmail => 'تغییر ایمیل پیشفرض';
+
+ @override
+ String get currentDefault => 'پیشفرض فعلی';
+
+ @override
+ String get makeDefault => 'پیشفرض کردن';
+
+ @override
+ String get defaultEmailNote => 'ایمیلها از سرور پیشفرض ارسال میشوند';
+
+ @override
+ String get noDefaultSet => 'هیچ سرور ایمیل پیشفرضی تنظیم نشده است';
+
+ @override
+ String get selectDefaultServer => 'انتخاب سرور پیشفرض';
+
+ @override
+ String get defaultServerChanged => 'سرور پیشفرض تغییر کرد';
+
+ @override
+ String get defaultServerChangeFailed => 'تغییر سرور پیشفرض ناموفق بود';
+
+ @override
+ String get emailConfigSavedSuccessfully => 'تنظیمات ایمیل با موفقیت ذخیره شد';
+
+ @override
+ String get emailConfigUpdatedSuccessfully =>
+ 'تنظیمات ایمیل با موفقیت بهروزرسانی شد';
+
+ @override
+ String get editEmailConfiguration => 'ویرایش تنظیمات ایمیل';
+
+ @override
+ String get updateConfiguration => 'بهروزرسانی تنظیمات';
+
+ @override
+ String get testEmailSubject => 'تست ایمیل';
+
+ @override
+ String get testEmailBody => 'این یک ایمیل تست است.';
+
+ @override
+ String get testEmailSentSuccessfully => 'ایمیل تست با موفقیت ارسال شد';
+
+ @override
+ String get emailConfigDeletedSuccessfully => 'تنظیمات حذف شد';
+
+ @override
+ String get confirm => 'تایید';
+
@override
String get fileStatistics => 'آمار فایلها';
@@ -1065,9 +1217,6 @@ class AppLocalizationsFa extends AppLocalizations {
@override
String get edit => 'ویرایش';
- @override
- String get delete => 'حذف';
-
@override
String get actions => 'عملیات';
@@ -1143,4 +1292,58 @@ class AppLocalizationsFa extends AppLocalizations {
@override
String get maintenanceSettings => 'تنظیمات نگهداری';
+
+ @override
+ String get initializing => 'در حال راهاندازی...';
+
+ @override
+ String get loadingLanguageSettings => 'در حال بارگذاری تنظیمات زبان...';
+
+ @override
+ String get loadingCalendarSettings => 'در حال بارگذاری تنظیمات تقویم...';
+
+ @override
+ String get loadingThemeSettings => 'در حال بارگذاری تنظیمات تم...';
+
+ @override
+ String get loadingAuthentication => 'در حال بارگذاری احراز هویت...';
+
+ @override
+ String get businessManagementPlatform => 'پلتفرم مدیریت کسبوکار';
+
+ @override
+ String get businessDashboard => 'داشبورد کسب و کار';
+
+ @override
+ String get businessStatistics => 'آمار کسب و کار';
+
+ @override
+ String get recentActivities => 'فعالیتهای اخیر';
+
+ @override
+ String get sales => 'فروش';
+
+ @override
+ String get accounting => 'حسابداری';
+
+ @override
+ String get inventory => 'موجودی';
+
+ @override
+ String get reports => 'گزارشها';
+
+ @override
+ String get members => 'اعضا';
+
+ @override
+ String get backToProfile => 'بازگشت به پروفایل';
+
+ @override
+ String get noBusinessesFound => 'هیچ کسب و کاری یافت نشد';
+
+ @override
+ String get createFirstBusiness => 'اولین کسب و کار خود را ایجاد کنید';
+
+ @override
+ String get accessDenied => 'دسترسی غیرمجاز';
}
diff --git a/hesabixUI/hesabix_ui/lib/main.dart b/hesabixUI/hesabix_ui/lib/main.dart
index 2f87463..5cb3947 100644
--- a/hesabixUI/hesabix_ui/lib/main.dart
+++ b/hesabixUI/hesabix_ui/lib/main.dart
@@ -17,6 +17,9 @@ 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 'pages/admin/email_settings_page.dart';
+import 'pages/business/business_shell.dart';
+import 'pages/business/dashboard/business_dashboard_page.dart';
import 'package:hesabix_ui/l10n/app_localizations.dart';
import 'core/locale_controller.dart';
import 'core/calendar_controller.dart';
@@ -25,6 +28,7 @@ import 'theme/theme_controller.dart';
import 'theme/app_theme.dart';
import 'core/auth_store.dart';
import 'core/permission_guard.dart';
+import 'widgets/simple_splash_screen.dart';
void main() {
// Use path-based routing instead of hash routing
@@ -44,216 +48,131 @@ class _MyAppState extends State {
CalendarController? _calendarController;
ThemeController? _themeController;
AuthStore? _authStore;
+ bool _isLoading = true;
+ DateTime? _loadStartTime;
@override
void initState() {
super.initState();
- LocaleController.load().then((c) {
- setState(() {
- _controller = c
- ..addListener(() {
- // Update ApiClient language header on change
- ApiClient.setCurrentLocale(c.locale);
- setState(() {});
- });
- ApiClient.setCurrentLocale(c.locale);
- });
- });
+ _loadStartTime = DateTime.now();
+ _loadControllers();
+ }
- CalendarController.load().then((cc) {
- setState(() {
- _calendarController = cc
- ..addListener(() {
- setState(() {});
- });
- ApiClient.bindCalendarController(cc);
- });
+ Future _loadControllers() async {
+ // بارگذاری تمام کنترلرها
+ final localeController = await LocaleController.load();
+ final calendarController = await CalendarController.load();
+ final themeController = ThemeController();
+ await themeController.load();
+ final authStore = AuthStore();
+ await authStore.load();
+
+ // تنظیم کنترلرها
+ setState(() {
+ _controller = localeController;
+ _calendarController = calendarController;
+ _themeController = themeController;
+ _authStore = authStore;
});
-
- final tc = ThemeController();
- tc.load().then((_) {
- setState(() {
- _themeController = tc
- ..addListener(() {
- setState(() {});
- });
- });
+
+ // اضافه کردن listeners
+ _controller!.addListener(() {
+ ApiClient.setCurrentLocale(_controller!.locale);
+ setState(() {});
});
-
- final store = AuthStore();
- store.load().then((_) {
- setState(() {
- _authStore = store
- ..addListener(() {
- setState(() {});
- });
- ApiClient.bindAuthStore(store);
- });
+
+ _calendarController!.addListener(() {
+ setState(() {});
});
+
+ _themeController!.addListener(() {
+ setState(() {});
+ });
+
+ _authStore!.addListener(() {
+ setState(() {});
+ });
+
+ // تنظیم API Client
+ ApiClient.setCurrentLocale(_controller!.locale);
+ ApiClient.bindCalendarController(_calendarController!);
+ ApiClient.bindAuthStore(_authStore!);
+
+ // اطمینان از حداقل 4 ثانیه نمایش splash screen
+ final elapsed = DateTime.now().difference(_loadStartTime!);
+ const minimumDuration = Duration(seconds: 4);
+ if (elapsed < minimumDuration) {
+ await Future.delayed(minimumDuration - elapsed);
+ }
+
+ // اتمام loading
+ if (mounted) {
+ setState(() {
+ _isLoading = false;
+ });
+ }
}
// Root of application with GoRouter
@override
Widget build(BuildContext context) {
- // اگر هنوز loading است، یک router ساده با loading page بساز
- if (_controller == null || _calendarController == null || _themeController == null || _authStore == null) {
+ // اگر هنوز loading است، splash screen نمایش بده
+ if (_isLoading ||
+ _controller == null ||
+ _calendarController == null ||
+ _themeController == null ||
+ _authStore == null) {
final loadingRouter = GoRouter(
redirect: (context, state) {
// در حین loading، هیچ redirect نکن - URL را حفظ کن
return null;
},
routes: [
- GoRoute(
- path: '/',
- builder: (context, state) => const Scaffold(
- body: Center(
- child: Column(
- mainAxisAlignment: MainAxisAlignment.center,
- children: [
- CircularProgressIndicator(),
- SizedBox(height: 16),
- Text('Loading...'),
- ],
- ),
- ),
- ),
- ),
- // برای سایر مسیرها هم loading page نمایش بده
- GoRoute(
- path: '/login',
- builder: (context, state) => const Scaffold(
- body: Center(
- child: Column(
- mainAxisAlignment: MainAxisAlignment.center,
- children: [
- CircularProgressIndicator(),
- SizedBox(height: 16),
- Text('Loading...'),
- ],
- ),
- ),
- ),
- ),
- GoRoute(
- path: '/user/profile/dashboard',
- builder: (context, state) => const Scaffold(
- body: Center(
- child: Column(
- mainAxisAlignment: MainAxisAlignment.center,
- children: [
- CircularProgressIndicator(),
- SizedBox(height: 16),
- Text('Loading...'),
- ],
- ),
- ),
- ),
- ),
- GoRoute(
- path: '/user/profile/marketing',
- builder: (context, state) => const Scaffold(
- body: Center(
- child: Column(
- mainAxisAlignment: MainAxisAlignment.center,
- children: [
- CircularProgressIndicator(),
- SizedBox(height: 16),
- Text('Loading...'),
- ],
- ),
- ),
- ),
- ),
- GoRoute(
- path: '/user/profile/new-business',
- builder: (context, state) => const Scaffold(
- body: Center(
- child: Column(
- mainAxisAlignment: MainAxisAlignment.center,
- children: [
- CircularProgressIndicator(),
- SizedBox(height: 16),
- Text('Loading...'),
- ],
- ),
- ),
- ),
- ),
- GoRoute(
- path: '/user/profile/businesses',
- builder: (context, state) => const Scaffold(
- body: Center(
- child: Column(
- mainAxisAlignment: MainAxisAlignment.center,
- children: [
- CircularProgressIndicator(),
- SizedBox(height: 16),
- Text('Loading...'),
- ],
- ),
- ),
- ),
- ),
- GoRoute(
- path: '/user/profile/support',
- builder: (context, state) => const Scaffold(
- body: Center(
- child: Column(
- mainAxisAlignment: MainAxisAlignment.center,
- children: [
- CircularProgressIndicator(),
- SizedBox(height: 16),
- Text('Loading...'),
- ],
- ),
- ),
- ),
- ),
- GoRoute(
- path: '/user/profile/change-password',
- builder: (context, state) => const Scaffold(
- body: Center(
- child: Column(
- mainAxisAlignment: MainAxisAlignment.center,
- children: [
- CircularProgressIndicator(),
- SizedBox(height: 16),
- Text('Loading...'),
- ],
- ),
- ),
- ),
- ),
- GoRoute(
- path: '/user/profile/system-settings',
- builder: (context, state) => const Scaffold(
- body: Center(
- child: Column(
- mainAxisAlignment: MainAxisAlignment.center,
- children: [
- CircularProgressIndicator(),
- SizedBox(height: 16),
- Text('Loading...'),
- ],
- ),
- ),
- ),
- ),
- // Catch-all route برای هر URL دیگر
+ // برای تمام مسیرها splash screen نمایش بده
GoRoute(
path: '/:path(.*)',
- builder: (context, state) => const Scaffold(
- body: Center(
- child: Column(
- mainAxisAlignment: MainAxisAlignment.center,
- children: [
- CircularProgressIndicator(),
- SizedBox(height: 16),
- Text('Loading...'),
- ],
- ),
- ),
- ),
+ builder: (context, state) {
+ // تشخیص نوع loading بر اساس controller های موجود
+ String loadingMessage = 'Initializing...';
+ if (_controller == null) {
+ loadingMessage = 'Loading language settings...';
+ } else if (_calendarController == null) {
+ loadingMessage = 'Loading calendar settings...';
+ } else if (_themeController == null) {
+ loadingMessage = 'Loading theme settings...';
+ } else if (_authStore == null) {
+ loadingMessage = 'Loading authentication...';
+ }
+
+ // اگر controller موجود است، از locale آن استفاده کن
+ if (_controller != null) {
+ final isFa = _controller!.locale.languageCode == 'fa';
+ if (isFa) {
+ if (_controller == null) {
+ loadingMessage = 'در حال بارگذاری تنظیمات زبان...';
+ } else if (_calendarController == null) {
+ loadingMessage = 'در حال بارگذاری تنظیمات تقویم...';
+ } else if (_themeController == null) {
+ loadingMessage = 'در حال بارگذاری تنظیمات تم...';
+ } else if (_authStore == null) {
+ loadingMessage = 'در حال بارگذاری احراز هویت...';
+ } else {
+ loadingMessage = 'در حال راهاندازی...';
+ }
+ }
+ }
+
+ return SimpleSplashScreen(
+ message: loadingMessage,
+ showLogo: true,
+ displayDuration: const Duration(seconds: 4),
+ locale: _controller?.locale,
+ onComplete: () {
+ // این callback زمانی فراخوانی میشود که splash screen تمام شود
+ // اما ما از splash controller استفاده میکنیم
+ },
+ );
+ },
),
],
);
@@ -264,6 +183,7 @@ class _MyAppState extends State {
locale: const Locale('en'),
supportedLocales: const [Locale('en'), Locale('fa')],
localizationsDelegates: const [
+ AppLocalizations.delegate,
GlobalMaterialLocalizations.delegate,
GlobalCupertinoLocalizations.delegate,
GlobalWidgetsLocalizations.delegate,
@@ -431,10 +351,49 @@ class _MyAppState extends State {
return const SystemLogsPage();
},
),
+ GoRoute(
+ path: 'email',
+ name: 'system_settings_email',
+ builder: (context, state) {
+ if (_authStore == null || !_authStore!.isSuperAdmin) {
+ return PermissionGuard.buildAccessDeniedPage();
+ }
+ return const EmailSettingsPage();
+ },
+ ),
],
),
],
),
+ GoRoute(
+ path: '/business/:business_id',
+ name: 'business_shell',
+ builder: (context, state) {
+ final businessId = int.parse(state.pathParameters['business_id']!);
+ return BusinessShell(
+ businessId: businessId,
+ authStore: _authStore!,
+ calendarController: _calendarController!,
+ child: const SizedBox.shrink(), // Will be replaced by child routes
+ );
+ },
+ routes: [
+ GoRoute(
+ path: 'dashboard',
+ name: 'business_dashboard',
+ builder: (context, state) {
+ final businessId = int.parse(state.pathParameters['business_id']!);
+ return BusinessShell(
+ businessId: businessId,
+ authStore: _authStore!,
+ calendarController: _calendarController!,
+ child: BusinessDashboardPage(businessId: businessId),
+ );
+ },
+ ),
+ // TODO: Add other business routes (sales, accounting, etc.)
+ ],
+ ),
],
);
diff --git a/hesabixUI/hesabix_ui/lib/models/business_dashboard_models.dart b/hesabixUI/hesabix_ui/lib/models/business_dashboard_models.dart
new file mode 100644
index 0000000..2e268a5
--- /dev/null
+++ b/hesabixUI/hesabix_ui/lib/models/business_dashboard_models.dart
@@ -0,0 +1,245 @@
+class BusinessInfo {
+ final int id;
+ final String name;
+ final String businessType;
+ final String businessField;
+ final int ownerId;
+ final String? address;
+ final String? phone;
+ final String? mobile;
+ final String createdAt;
+ final int memberCount;
+
+ BusinessInfo({
+ required this.id,
+ required this.name,
+ required this.businessType,
+ required this.businessField,
+ required this.ownerId,
+ this.address,
+ this.phone,
+ this.mobile,
+ required this.createdAt,
+ required this.memberCount,
+ });
+
+ factory BusinessInfo.fromJson(Map json) {
+ // Handle both string and object formats for created_at
+ String createdAt;
+ if (json['created_at'] is String) {
+ createdAt = json['created_at'];
+ } else if (json['created_at'] is Map) {
+ createdAt = json['created_at']['formatted'] ?? json['created_at']['date_only'] ?? '';
+ } else {
+ createdAt = '';
+ }
+
+ return BusinessInfo(
+ id: json['id'],
+ name: json['name'],
+ businessType: json['business_type'],
+ businessField: json['business_field'],
+ ownerId: json['owner_id'],
+ address: json['address'],
+ phone: json['phone'],
+ mobile: json['mobile'],
+ createdAt: createdAt,
+ memberCount: json['member_count'],
+ );
+ }
+}
+
+class BusinessStatistics {
+ final double totalSales;
+ final double totalPurchases;
+ final int activeMembers;
+ final int recentTransactions;
+
+ BusinessStatistics({
+ required this.totalSales,
+ required this.totalPurchases,
+ required this.activeMembers,
+ required this.recentTransactions,
+ });
+
+ factory BusinessStatistics.fromJson(Map json) {
+ return BusinessStatistics(
+ totalSales: (json['total_sales'] ?? 0).toDouble(),
+ totalPurchases: (json['total_purchases'] ?? 0).toDouble(),
+ activeMembers: json['active_members'] ?? 0,
+ recentTransactions: json['recent_transactions'] ?? 0,
+ );
+ }
+}
+
+class Activity {
+ final int id;
+ final String title;
+ final String description;
+ final String icon;
+ final String timeAgo;
+
+ Activity({
+ required this.id,
+ required this.title,
+ required this.description,
+ required this.icon,
+ required this.timeAgo,
+ });
+
+ factory Activity.fromJson(Map json) {
+ return Activity(
+ id: json['id'],
+ title: json['title'],
+ description: json['description'],
+ icon: json['icon'],
+ timeAgo: json['time_ago'],
+ );
+ }
+}
+
+class BusinessDashboardResponse {
+ final BusinessInfo businessInfo;
+ final BusinessStatistics statistics;
+ final List recentActivities;
+
+ BusinessDashboardResponse({
+ required this.businessInfo,
+ required this.statistics,
+ required this.recentActivities,
+ });
+
+ factory BusinessDashboardResponse.fromJson(Map json) {
+ return BusinessDashboardResponse(
+ businessInfo: BusinessInfo.fromJson(json['business_info']),
+ statistics: BusinessStatistics.fromJson(json['statistics']),
+ recentActivities: (json['recent_activities'] as List)
+ .map((activity) => Activity.fromJson(activity))
+ .toList(),
+ );
+ }
+}
+
+class BusinessMember {
+ final int id;
+ final int userId;
+ final String firstName;
+ final String lastName;
+ final String email;
+ final String? mobile;
+ final String role;
+ final Map permissions;
+ final String joinedAt;
+
+ BusinessMember({
+ required this.id,
+ required this.userId,
+ required this.firstName,
+ required this.lastName,
+ required this.email,
+ this.mobile,
+ required this.role,
+ required this.permissions,
+ required this.joinedAt,
+ });
+
+ factory BusinessMember.fromJson(Map json) {
+ // Handle both string and object formats for joined_at
+ String joinedAt;
+ if (json['joined_at'] is String) {
+ joinedAt = json['joined_at'];
+ } else if (json['joined_at'] is Map) {
+ joinedAt = json['joined_at']['formatted'] ?? json['joined_at']['date_only'] ?? '';
+ } else {
+ joinedAt = '';
+ }
+
+ return BusinessMember(
+ id: json['id'],
+ userId: json['user_id'],
+ firstName: json['first_name'] ?? '',
+ lastName: json['last_name'] ?? '',
+ email: json['email'] ?? '',
+ mobile: json['mobile'],
+ role: json['role'] ?? 'عضو',
+ permissions: Map.from(json['permissions'] ?? {}),
+ joinedAt: joinedAt,
+ );
+ }
+}
+
+class BusinessMembersResponse {
+ final List items;
+ final Map pagination;
+
+ BusinessMembersResponse({
+ required this.items,
+ required this.pagination,
+ });
+
+ factory BusinessMembersResponse.fromJson(Map json) {
+ return BusinessMembersResponse(
+ items: (json['items'] as List)
+ .map((member) => BusinessMember.fromJson(member))
+ .toList(),
+ pagination: json['pagination'],
+ );
+ }
+}
+
+class BusinessWithPermission {
+ final int id;
+ final String name;
+ final String businessType;
+ final String businessField;
+ final int ownerId;
+ final String? address;
+ final String? phone;
+ final String? mobile;
+ final String createdAt;
+ final bool isOwner;
+ final String role;
+ final Map permissions;
+
+ BusinessWithPermission({
+ required this.id,
+ required this.name,
+ required this.businessType,
+ required this.businessField,
+ required this.ownerId,
+ this.address,
+ this.phone,
+ this.mobile,
+ required this.createdAt,
+ required this.isOwner,
+ required this.role,
+ required this.permissions,
+ });
+
+ factory BusinessWithPermission.fromJson(Map json) {
+ // Handle both string and object formats for created_at
+ String createdAt;
+ if (json['created_at'] is String) {
+ createdAt = json['created_at'];
+ } else if (json['created_at'] is Map) {
+ createdAt = json['created_at']['formatted'] ?? json['created_at']['date_only'] ?? '';
+ } else {
+ createdAt = '';
+ }
+
+ return BusinessWithPermission(
+ id: json['id'],
+ name: json['name'],
+ businessType: json['business_type'],
+ businessField: json['business_field'],
+ ownerId: json['owner_id'],
+ address: json['address'],
+ phone: json['phone'],
+ mobile: json['mobile'],
+ createdAt: createdAt,
+ isOwner: json['is_owner'] ?? false,
+ role: json['role'] ?? 'عضو',
+ permissions: Map.from(json['permissions'] ?? {}),
+ );
+ }
+}
diff --git a/hesabixUI/hesabix_ui/lib/models/email_models.dart b/hesabixUI/hesabix_ui/lib/models/email_models.dart
new file mode 100644
index 0000000..c99ff2b
--- /dev/null
+++ b/hesabixUI/hesabix_ui/lib/models/email_models.dart
@@ -0,0 +1,306 @@
+class EmailConfig {
+ final int id;
+ final String name;
+ final String smtpHost;
+ final int smtpPort;
+ final String smtpUsername;
+ final bool useTls;
+ final bool useSsl;
+ final String fromEmail;
+ final String fromName;
+ final bool isActive;
+ final bool isDefault;
+ final DateTime createdAt;
+ final DateTime updatedAt;
+
+ EmailConfig({
+ required this.id,
+ required this.name,
+ required this.smtpHost,
+ required this.smtpPort,
+ required this.smtpUsername,
+ required this.useTls,
+ required this.useSsl,
+ required this.fromEmail,
+ required this.fromName,
+ required this.isActive,
+ required this.isDefault,
+ required this.createdAt,
+ required this.updatedAt,
+ });
+
+ factory EmailConfig.fromJson(Map json) {
+ return EmailConfig(
+ id: json['id'] as int,
+ name: json['name'] as String,
+ smtpHost: json['smtp_host'] as String,
+ smtpPort: json['smtp_port'] as int,
+ smtpUsername: json['smtp_username'] as String,
+ useTls: json['use_tls'] as bool,
+ useSsl: json['use_ssl'] as bool,
+ fromEmail: json['from_email'] as String,
+ fromName: json['from_name'] as String,
+ isActive: json['is_active'] as bool,
+ isDefault: json['is_default'] as bool? ?? false,
+ createdAt: _parseDateTime(json['created_at']),
+ updatedAt: _parseDateTime(json['updated_at']),
+ );
+ }
+
+ static DateTime _parseDateTime(dynamic dateValue) {
+ if (dateValue == null) {
+ return DateTime.now();
+ }
+
+ if (dateValue is String) {
+ return DateTime.parse(dateValue);
+ }
+
+ if (dateValue is Map) {
+ // Handle formatted date object
+ final formatted = dateValue['formatted'] as String?;
+ if (formatted != null) {
+ return DateTime.parse(formatted);
+ }
+ }
+
+ return DateTime.now();
+ }
+
+ Map toJson() {
+ return {
+ 'id': id,
+ 'name': name,
+ 'smtp_host': smtpHost,
+ 'smtp_port': smtpPort,
+ 'smtp_username': smtpUsername,
+ 'use_tls': useTls,
+ 'use_ssl': useSsl,
+ 'from_email': fromEmail,
+ 'from_name': fromName,
+ 'is_active': isActive,
+ 'is_default': isDefault,
+ 'created_at': createdAt.toIso8601String(),
+ 'updated_at': updatedAt.toIso8601String(),
+ };
+ }
+
+ EmailConfig copyWith({
+ int? id,
+ String? name,
+ String? smtpHost,
+ int? smtpPort,
+ String? smtpUsername,
+ bool? useTls,
+ bool? useSsl,
+ String? fromEmail,
+ String? fromName,
+ bool? isActive,
+ bool? isDefault,
+ DateTime? createdAt,
+ DateTime? updatedAt,
+ }) {
+ return EmailConfig(
+ id: id ?? this.id,
+ name: name ?? this.name,
+ smtpHost: smtpHost ?? this.smtpHost,
+ smtpPort: smtpPort ?? this.smtpPort,
+ smtpUsername: smtpUsername ?? this.smtpUsername,
+ useTls: useTls ?? this.useTls,
+ useSsl: useSsl ?? this.useSsl,
+ fromEmail: fromEmail ?? this.fromEmail,
+ fromName: fromName ?? this.fromName,
+ isActive: isActive ?? this.isActive,
+ isDefault: isDefault ?? this.isDefault,
+ createdAt: createdAt ?? this.createdAt,
+ updatedAt: updatedAt ?? this.updatedAt,
+ );
+ }
+}
+
+class CreateEmailConfigRequest {
+ final String name;
+ final String smtpHost;
+ final int smtpPort;
+ final String smtpUsername;
+ final String smtpPassword;
+ final bool useTls;
+ final bool useSsl;
+ final String fromEmail;
+ final String fromName;
+ final bool isActive;
+
+ CreateEmailConfigRequest({
+ required this.name,
+ required this.smtpHost,
+ required this.smtpPort,
+ required this.smtpUsername,
+ required this.smtpPassword,
+ required this.useTls,
+ required this.useSsl,
+ required this.fromEmail,
+ required this.fromName,
+ this.isActive = true,
+ });
+
+ Map toJson() {
+ return {
+ 'name': name,
+ 'smtp_host': smtpHost,
+ 'smtp_port': smtpPort,
+ 'smtp_username': smtpUsername,
+ 'smtp_password': smtpPassword,
+ 'use_tls': useTls,
+ 'use_ssl': useSsl,
+ 'from_email': fromEmail,
+ 'from_name': fromName,
+ 'is_active': isActive,
+ };
+ }
+}
+
+class UpdateEmailConfigRequest {
+ final String? name;
+ final String? smtpHost;
+ final int? smtpPort;
+ final String? smtpUsername;
+ final String? smtpPassword;
+ final bool? useTls;
+ final bool? useSsl;
+ final String? fromEmail;
+ final String? fromName;
+ final bool? isActive;
+
+ UpdateEmailConfigRequest({
+ this.name,
+ this.smtpHost,
+ this.smtpPort,
+ this.smtpUsername,
+ this.smtpPassword,
+ this.useTls,
+ this.useSsl,
+ this.fromEmail,
+ this.fromName,
+ this.isActive,
+ });
+
+ Map toJson() {
+ final Map json = {};
+ if (name != null) json['name'] = name;
+ if (smtpHost != null) json['smtp_host'] = smtpHost;
+ if (smtpPort != null) json['smtp_port'] = smtpPort;
+ if (smtpUsername != null) json['smtp_username'] = smtpUsername;
+ if (smtpPassword != null) json['smtp_password'] = smtpPassword;
+ if (useTls != null) json['use_tls'] = useTls;
+ if (useSsl != null) json['use_ssl'] = useSsl;
+ if (fromEmail != null) json['from_email'] = fromEmail;
+ if (fromName != null) json['from_name'] = fromName;
+ if (isActive != null) json['is_active'] = isActive;
+ return json;
+ }
+}
+
+class SendEmailRequest {
+ final String to;
+ final String subject;
+ final String body;
+ final String? htmlBody;
+ final int? configId;
+
+ SendEmailRequest({
+ required this.to,
+ required this.subject,
+ required this.body,
+ this.htmlBody,
+ this.configId,
+ });
+
+ Map toJson() {
+ return {
+ 'to': to,
+ 'subject': subject,
+ 'body': body,
+ if (htmlBody != null) 'html_body': htmlBody,
+ if (configId != null) 'config_id': configId,
+ };
+ }
+}
+
+class EmailConfigListResponse {
+ final bool success;
+ final List data;
+ final String message;
+
+ EmailConfigListResponse({
+ required this.success,
+ required this.data,
+ required this.message,
+ });
+
+ factory EmailConfigListResponse.fromJson(Map json) {
+ return EmailConfigListResponse(
+ success: json['success'] as bool? ?? true,
+ data: (json['data'] as List? ?? [])
+ .map((item) => EmailConfig.fromJson(item as Map))
+ .toList(),
+ message: json['message'] as String? ?? '',
+ );
+ }
+}
+
+class EmailConfigResponse {
+ final bool success;
+ final EmailConfig data;
+ final String message;
+
+ EmailConfigResponse({
+ required this.success,
+ required this.data,
+ required this.message,
+ });
+
+ factory EmailConfigResponse.fromJson(Map json) {
+ return EmailConfigResponse(
+ success: json['success'] as bool? ?? true,
+ data: EmailConfig.fromJson(json['data'] as Map),
+ message: json['message'] as String? ?? '',
+ );
+ }
+}
+
+class SendEmailResponse {
+ final bool success;
+ final String message;
+
+ SendEmailResponse({
+ required this.success,
+ required this.message,
+ });
+
+ factory SendEmailResponse.fromJson(Map json) {
+ return SendEmailResponse(
+ success: json['success'] as bool? ?? true,
+ message: json['message'] as String? ?? '',
+ );
+ }
+}
+
+class TestConnectionResponse {
+ final bool success;
+ final String message;
+ final bool connected;
+
+ TestConnectionResponse({
+ required this.success,
+ required this.message,
+ required this.connected,
+ });
+
+ factory TestConnectionResponse.fromJson(Map json) {
+ return TestConnectionResponse(
+ success: json['success'] as bool? ?? true,
+ message: json['message'] as String? ?? '',
+ connected: json['connected'] as bool? ?? false,
+ );
+ }
+}
diff --git a/hesabixUI/hesabix_ui/lib/pages/admin/email_settings_page.dart b/hesabixUI/hesabix_ui/lib/pages/admin/email_settings_page.dart
new file mode 100644
index 0000000..076e0cd
--- /dev/null
+++ b/hesabixUI/hesabix_ui/lib/pages/admin/email_settings_page.dart
@@ -0,0 +1,644 @@
+import 'package:flutter/material.dart';
+import 'package:hesabix_ui/l10n/app_localizations.dart';
+import 'package:hesabix_ui/models/email_models.dart';
+import 'package:hesabix_ui/services/email_service.dart';
+
+class EmailSettingsPage extends StatefulWidget {
+ const EmailSettingsPage({super.key});
+
+ @override
+ State createState() => _EmailSettingsPageState();
+}
+
+class _EmailSettingsPageState extends State {
+ final EmailService _emailService = EmailService();
+ final _formKey = GlobalKey();
+ bool _isLoading = false;
+ bool _isTesting = false;
+ List _configs = [];
+ EmailConfig? _selectedConfig;
+
+ // Form controllers
+ final _nameController = TextEditingController();
+ final _smtpHostController = TextEditingController();
+ final _smtpPortController = TextEditingController();
+ final _smtpUsernameController = TextEditingController();
+ final _smtpPasswordController = TextEditingController();
+ final _fromEmailController = TextEditingController();
+ final _fromNameController = TextEditingController();
+ bool _useTls = true;
+ bool _useSsl = false;
+ bool _isActive = true;
+ bool _isEditing = false;
+
+ @override
+ void initState() {
+ super.initState();
+ _initializeApiClient();
+ _loadConfigs();
+ }
+
+ void _initializeApiClient() {
+ // Initialize ApiClient - it will get AuthStore from global state
+ // AuthStore should be bound in main.dart or app initialization
+ }
+
+ @override
+ void dispose() {
+ _nameController.dispose();
+ _smtpHostController.dispose();
+ _smtpPortController.dispose();
+ _smtpUsernameController.dispose();
+ _smtpPasswordController.dispose();
+ _fromEmailController.dispose();
+ _fromNameController.dispose();
+ super.dispose();
+ }
+
+ Future _loadConfigs() async {
+ setState(() => _isLoading = true);
+ try {
+ final response = await _emailService.getEmailConfigs();
+ setState(() {
+ _configs = response.data;
+ // Select the default config, or first one if no default
+ _selectedConfig = _configs.where((config) => config.isDefault).isNotEmpty
+ ? _configs.where((config) => config.isDefault).first
+ : (_configs.isNotEmpty ? _configs.first : null);
+ });
+ } catch (e) {
+ _showErrorSnackBar(e.toString());
+ } finally {
+ setState(() => _isLoading = false);
+ }
+ }
+
+ Future _saveConfig() async {
+ if (!_formKey.currentState!.validate()) return;
+
+ setState(() => _isLoading = true);
+ try {
+ final t = AppLocalizations.of(context);
+
+ if (_isEditing && _selectedConfig != null) {
+ // Update existing config
+ final request = UpdateEmailConfigRequest(
+ name: _nameController.text,
+ smtpHost: _smtpHostController.text,
+ smtpPort: int.parse(_smtpPortController.text),
+ smtpUsername: _smtpUsernameController.text,
+ smtpPassword: _smtpPasswordController.text,
+ useTls: _useTls,
+ useSsl: _useSsl,
+ fromEmail: _fromEmailController.text,
+ fromName: _fromNameController.text,
+ isActive: _isActive,
+ );
+
+ await _emailService.updateEmailConfig(_selectedConfig!.id, request);
+ _showSuccessSnackBar(t.emailConfigUpdatedSuccessfully);
+ } else {
+ // Create new config
+ final request = CreateEmailConfigRequest(
+ name: _nameController.text,
+ smtpHost: _smtpHostController.text,
+ smtpPort: int.parse(_smtpPortController.text),
+ smtpUsername: _smtpUsernameController.text,
+ smtpPassword: _smtpPasswordController.text,
+ useTls: _useTls,
+ useSsl: _useSsl,
+ fromEmail: _fromEmailController.text,
+ fromName: _fromNameController.text,
+ isActive: _isActive,
+ );
+
+ await _emailService.createEmailConfig(request);
+ _showSuccessSnackBar(t.emailConfigSavedSuccessfully);
+ }
+
+ if (!mounted) return;
+ _loadConfigs();
+ _clearForm();
+ } catch (e) {
+ _showErrorSnackBar(e.toString());
+ } finally {
+ setState(() => _isLoading = false);
+ }
+ }
+
+ Future _testConnection() async {
+ if (_selectedConfig == null) return;
+ final config = _selectedConfig!;
+
+ setState(() => _isTesting = true);
+ try {
+ final response = await _emailService.testEmailConfig(config.id);
+ if (!mounted) return;
+ final t = AppLocalizations.of(context);
+ if (response.connected) {
+ _showSuccessSnackBar(t.connectionSuccessful);
+ } else {
+ _showErrorSnackBar(t.connectionFailed);
+ }
+ } catch (e) {
+ _showErrorSnackBar(e.toString());
+ } finally {
+ setState(() => _isTesting = false);
+ }
+ }
+
+ Future _setAsDefault(EmailConfig config) async {
+ final t = AppLocalizations.of(context);
+ final confirmed = await showDialog(
+ context: context,
+ builder: (context) => AlertDialog(
+ title: Text(t.setDefaultConfirm),
+ content: Text(t.setDefaultConfirm),
+ actions: [
+ TextButton(
+ onPressed: () => Navigator.of(context).pop(false),
+ child: Text(t.cancel),
+ ),
+ TextButton(
+ onPressed: () => Navigator.of(context).pop(true),
+ child: Text(t.confirm),
+ ),
+ ],
+ ),
+ );
+
+ if (confirmed != true) return;
+
+ setState(() => _isLoading = true);
+ try {
+ await _emailService.setDefaultEmailConfig(config.id);
+ _showSuccessSnackBar(t.defaultSetSuccessfully);
+ // Force refresh the configs and update selected config
+ await _loadConfigs();
+ // Update selected config to the one that was just set as default
+ _selectedConfig = _configs.firstWhere((c) => c.id == config.id);
+ } catch (e) {
+ _showErrorSnackBar(t.defaultSetFailed);
+ } finally {
+ setState(() => _isLoading = false);
+ }
+ }
+
+ Future _sendTestEmail() async {
+ if (_selectedConfig == null) return;
+
+ try {
+ final t = AppLocalizations.of(context);
+ await _emailService.sendCustomEmail(
+ to: _fromEmailController.text,
+ subject: t.testEmailSubject,
+ body: t.testEmailBody,
+ configId: _selectedConfig!.id,
+ );
+ _showSuccessSnackBar(t.testEmailSentSuccessfully);
+ } catch (e) {
+ _showErrorSnackBar(e.toString());
+ }
+ }
+
+ void _clearForm() {
+ _nameController.clear();
+ _smtpHostController.clear();
+ _smtpPortController.clear();
+ _smtpUsernameController.clear();
+ _smtpPasswordController.clear();
+ _fromEmailController.clear();
+ _fromNameController.clear();
+ _useTls = true;
+ _useSsl = false;
+ _isActive = true;
+ _isEditing = false;
+ _selectedConfig = null;
+ }
+
+ void _showErrorSnackBar(String message) {
+ ScaffoldMessenger.of(context).showSnackBar(
+ SnackBar(
+ content: Text(message),
+ backgroundColor: Colors.red,
+ ),
+ );
+ }
+
+ void _showSuccessSnackBar(String message) {
+ ScaffoldMessenger.of(context).showSnackBar(
+ SnackBar(
+ content: Text(message),
+ backgroundColor: Colors.green,
+ ),
+ );
+ }
+
+ @override
+ Widget build(BuildContext context) {
+ final theme = Theme.of(context);
+ final colorScheme = theme.colorScheme;
+ final t = AppLocalizations.of(context);
+
+ return Scaffold(
+ backgroundColor: colorScheme.surface,
+ appBar: AppBar(
+ title: Text(t.emailSettings),
+ backgroundColor: colorScheme.surface,
+ foregroundColor: colorScheme.onSurface,
+ elevation: 0,
+ ),
+ body: _isLoading
+ ? const Center(child: CircularProgressIndicator())
+ : SingleChildScrollView(
+ padding: const EdgeInsets.all(20),
+ child: Column(
+ crossAxisAlignment: CrossAxisAlignment.start,
+ children: [
+ _buildConfigList(theme, colorScheme, t),
+ const SizedBox(height: 24),
+ _buildConfigForm(theme, colorScheme, t),
+ ],
+ ),
+ ),
+ );
+ }
+
+ Widget _buildConfigList(ThemeData theme, ColorScheme colorScheme, AppLocalizations t) {
+ return Card(
+ child: Padding(
+ padding: const EdgeInsets.all(16),
+ child: Column(
+ crossAxisAlignment: CrossAxisAlignment.start,
+ children: [
+ Text(
+ t.emailConfigurations,
+ style: theme.textTheme.titleLarge?.copyWith(
+ fontWeight: FontWeight.bold,
+ ),
+ ),
+ const SizedBox(height: 16),
+ if (_configs.isEmpty)
+ Text(
+ t.noEmailConfigurations,
+ style: theme.textTheme.bodyMedium?.copyWith(
+ color: colorScheme.onSurface.withValues(alpha: 0.6),
+ ),
+ )
+ else
+ ..._configs.map((config) => _buildConfigItem(config, theme, colorScheme, t)),
+ ],
+ ),
+ ),
+ );
+ }
+
+ Widget _buildConfigItem(EmailConfig config, ThemeData theme, ColorScheme colorScheme, AppLocalizations t) {
+ return Card(
+ margin: const EdgeInsets.only(bottom: 8),
+ child: ListTile(
+ leading: Icon(
+ config.isActive ? Icons.email : Icons.email_outlined,
+ color: config.isActive ? Colors.green : colorScheme.onSurface.withValues(alpha: 0.6),
+ ),
+ title: Row(
+ children: [
+ Text(config.name),
+ if (config.isDefault) ...[
+ const SizedBox(width: 8),
+ Container(
+ padding: const EdgeInsets.symmetric(horizontal: 6, vertical: 2),
+ decoration: BoxDecoration(
+ color: Colors.blue.withValues(alpha: 0.1),
+ borderRadius: BorderRadius.circular(8),
+ ),
+ child: Text(
+ t.currentDefault,
+ style: TextStyle(
+ color: Colors.blue,
+ fontSize: 10,
+ fontWeight: FontWeight.w500,
+ ),
+ ),
+ ),
+ ],
+ ],
+ ),
+ subtitle: Text('${config.smtpHost}:${config.smtpPort}'),
+ trailing: Row(
+ mainAxisSize: MainAxisSize.min,
+ children: [
+ if (config.isActive)
+ Container(
+ padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 4),
+ decoration: BoxDecoration(
+ color: Colors.green.withValues(alpha: 0.1),
+ borderRadius: BorderRadius.circular(12),
+ ),
+ child: Text(
+ t.active,
+ style: TextStyle(
+ color: Colors.green,
+ fontSize: 12,
+ fontWeight: FontWeight.w500,
+ ),
+ ),
+ ),
+ const SizedBox(width: 8),
+ IconButton(
+ icon: const Icon(Icons.edit),
+ onPressed: () => _editConfig(config),
+ tooltip: t.edit,
+ ),
+ if (!config.isDefault)
+ IconButton(
+ icon: const Icon(Icons.star_outline),
+ onPressed: () => _setAsDefault(config),
+ tooltip: t.makeDefault,
+ ),
+ IconButton(
+ icon: const Icon(Icons.delete),
+ onPressed: config.isDefault ? null : () => _deleteConfig(config.id),
+ tooltip: config.isDefault ? t.cannotDeleteDefault : t.delete,
+ ),
+ ],
+ ),
+ onTap: () => _selectConfig(config),
+ ),
+ );
+ }
+
+ Widget _buildConfigForm(ThemeData theme, ColorScheme colorScheme, AppLocalizations t) {
+ return Card(
+ child: Padding(
+ padding: const EdgeInsets.all(16),
+ child: Form(
+ key: _formKey,
+ child: Column(
+ crossAxisAlignment: CrossAxisAlignment.start,
+ children: [
+ Text(
+ _isEditing ? t.editEmailConfiguration : t.addEmailConfiguration,
+ style: theme.textTheme.titleLarge?.copyWith(
+ fontWeight: FontWeight.bold,
+ ),
+ ),
+ const SizedBox(height: 16),
+ Row(
+ children: [
+ Expanded(
+ child: TextFormField(
+ controller: _nameController,
+ decoration: InputDecoration(
+ labelText: t.configurationName,
+ border: const OutlineInputBorder(),
+ ),
+ validator: (value) {
+ if (value == null || value.isEmpty) {
+ return t.requiredField;
+ }
+ return null;
+ },
+ ),
+ ),
+ const SizedBox(width: 16),
+ Expanded(
+ child: TextFormField(
+ controller: _smtpHostController,
+ decoration: InputDecoration(
+ labelText: t.smtpHost,
+ border: const OutlineInputBorder(),
+ ),
+ validator: (value) {
+ if (value == null || value.isEmpty) {
+ return t.requiredField;
+ }
+ return null;
+ },
+ ),
+ ),
+ ],
+ ),
+ const SizedBox(height: 16),
+ Row(
+ children: [
+ Expanded(
+ child: TextFormField(
+ controller: _smtpPortController,
+ decoration: InputDecoration(
+ labelText: t.smtpPort,
+ border: const OutlineInputBorder(),
+ ),
+ keyboardType: TextInputType.number,
+ validator: (value) {
+ if (value == null || value.isEmpty) {
+ return t.requiredField;
+ }
+ if (int.tryParse(value) == null) {
+ return t.invalidPort;
+ }
+ return null;
+ },
+ ),
+ ),
+ const SizedBox(width: 16),
+ Expanded(
+ child: TextFormField(
+ controller: _smtpUsernameController,
+ decoration: InputDecoration(
+ labelText: t.smtpUsername,
+ border: const OutlineInputBorder(),
+ ),
+ validator: (value) {
+ if (value == null || value.isEmpty) {
+ return t.requiredField;
+ }
+ return null;
+ },
+ ),
+ ),
+ ],
+ ),
+ const SizedBox(height: 16),
+ TextFormField(
+ controller: _smtpPasswordController,
+ decoration: InputDecoration(
+ labelText: t.smtpPassword,
+ border: const OutlineInputBorder(),
+ ),
+ obscureText: true,
+ validator: (value) {
+ if (value == null || value.isEmpty) {
+ return t.requiredField;
+ }
+ return null;
+ },
+ ),
+ const SizedBox(height: 16),
+ Row(
+ children: [
+ Expanded(
+ child: TextFormField(
+ controller: _fromEmailController,
+ decoration: InputDecoration(
+ labelText: t.fromEmail,
+ border: const OutlineInputBorder(),
+ ),
+ keyboardType: TextInputType.emailAddress,
+ validator: (value) {
+ if (value == null || value.isEmpty) {
+ return t.requiredField;
+ }
+ if (!value.contains('@')) {
+ return t.invalidEmail;
+ }
+ return null;
+ },
+ ),
+ ),
+ const SizedBox(width: 16),
+ Expanded(
+ child: TextFormField(
+ controller: _fromNameController,
+ decoration: InputDecoration(
+ labelText: t.fromName,
+ border: const OutlineInputBorder(),
+ ),
+ validator: (value) {
+ if (value == null || value.isEmpty) {
+ return t.requiredField;
+ }
+ return null;
+ },
+ ),
+ ),
+ ],
+ ),
+ const SizedBox(height: 16),
+ Row(
+ children: [
+ Checkbox(
+ value: _useTls,
+ onChanged: (value) => setState(() => _useTls = value ?? false),
+ ),
+ Text(t.useTls),
+ const SizedBox(width: 24),
+ Checkbox(
+ value: _useSsl,
+ onChanged: (value) => setState(() => _useSsl = value ?? false),
+ ),
+ Text(t.useSsl),
+ const SizedBox(width: 24),
+ Checkbox(
+ value: _isActive,
+ onChanged: (value) => setState(() => _isActive = value ?? false),
+ ),
+ Text(t.isActive),
+ ],
+ ),
+ const SizedBox(height: 24),
+ Row(
+ children: [
+ Expanded(
+ child: ElevatedButton(
+ onPressed: _isLoading ? null : _saveConfig,
+ child: _isLoading
+ ? const SizedBox(
+ width: 20,
+ height: 20,
+ child: CircularProgressIndicator(strokeWidth: 2),
+ )
+ : Text(_isEditing ? t.updateConfiguration : t.saveConfiguration),
+ ),
+ ),
+ const SizedBox(width: 16),
+ if (_selectedConfig != null) ...[
+ Expanded(
+ child: ElevatedButton(
+ onPressed: _isTesting ? null : _testConnection,
+ style: ElevatedButton.styleFrom(
+ backgroundColor: Colors.blue,
+ ),
+ child: _isTesting
+ ? const SizedBox(
+ width: 20,
+ height: 20,
+ child: CircularProgressIndicator(strokeWidth: 2),
+ )
+ : Text(t.testConnection),
+ ),
+ ),
+ const SizedBox(width: 16),
+ Expanded(
+ child: ElevatedButton(
+ onPressed: _sendTestEmail,
+ style: ElevatedButton.styleFrom(
+ backgroundColor: Colors.green,
+ ),
+ child: Text(t.sendTestEmail),
+ ),
+ ),
+ ],
+ ],
+ ),
+ ],
+ ),
+ ),
+ ),
+ );
+ }
+
+ Future _selectConfig(EmailConfig config) async {
+ setState(() => _selectedConfig = config);
+ }
+
+ Future _editConfig(EmailConfig config) async {
+ setState(() {
+ _selectedConfig = config;
+ _isEditing = true;
+ _nameController.text = config.name;
+ _smtpHostController.text = config.smtpHost;
+ _smtpPortController.text = config.smtpPort.toString();
+ _smtpUsernameController.text = config.smtpUsername;
+ _smtpPasswordController.clear(); // Password is not returned for security
+ _fromEmailController.text = config.fromEmail;
+ _fromNameController.text = config.fromName;
+ _useTls = config.useTls;
+ _useSsl = config.useSsl;
+ _isActive = config.isActive;
+ });
+ }
+
+ Future _deleteConfig(int configId) async {
+ final t = AppLocalizations.of(context);
+ final confirmed = await showDialog(
+ context: context,
+ builder: (context) => AlertDialog(
+ title: Text(t.deleteConfiguration),
+ content: Text(t.deleteConfigurationConfirm),
+ actions: [
+ TextButton(
+ onPressed: () => Navigator.of(context).pop(false),
+ child: Text(t.cancel),
+ ),
+ TextButton(
+ onPressed: () => Navigator.of(context).pop(true),
+ child: Text(t.delete),
+ ),
+ ],
+ ),
+ );
+
+ if (confirmed == true) {
+ try {
+ await _emailService.deleteEmailConfig(configId);
+ if (!mounted) return;
+ final t = AppLocalizations.of(context);
+ _showSuccessSnackBar(t.emailConfigDeletedSuccessfully);
+ _loadConfigs();
+ } catch (e) {
+ _showErrorSnackBar(e.toString());
+ }
+ }
+ }
+}
diff --git a/hesabixUI/hesabix_ui/lib/pages/admin/file_storage_settings_page.dart b/hesabixUI/hesabix_ui/lib/pages/admin/file_storage_settings_page.dart
index 2396c82..1c7d70b 100644
--- a/hesabixUI/hesabix_ui/lib/pages/admin/file_storage_settings_page.dart
+++ b/hesabixUI/hesabix_ui/lib/pages/admin/file_storage_settings_page.dart
@@ -1,5 +1,4 @@
import 'package:flutter/material.dart';
-import 'package:hesabix_ui/l10n/app_localizations.dart';
import 'package:hesabix_ui/widgets/admin/file_storage/storage_management_page.dart';
class FileStorageSettingsPage extends StatelessWidget {
diff --git a/hesabixUI/hesabix_ui/lib/pages/admin/storage_management_page.dart b/hesabixUI/hesabix_ui/lib/pages/admin/storage_management_page.dart
index 1c18c7a..5dda982 100644
--- a/hesabixUI/hesabix_ui/lib/pages/admin/storage_management_page.dart
+++ b/hesabixUI/hesabix_ui/lib/pages/admin/storage_management_page.dart
@@ -42,7 +42,7 @@ class _AdminStorageManagementPageState extends State
begin: Alignment.topCenter,
end: Alignment.bottomCenter,
colors: [
- theme.colorScheme.primary.withOpacity(0.1),
+ theme.colorScheme.primary.withValues(alpha: 0.1),
theme.colorScheme.surface,
],
),
diff --git a/hesabixUI/hesabix_ui/lib/pages/admin/system_configuration_page.dart b/hesabixUI/hesabix_ui/lib/pages/admin/system_configuration_page.dart
index 246ea9e..fc5180d 100644
--- a/hesabixUI/hesabix_ui/lib/pages/admin/system_configuration_page.dart
+++ b/hesabixUI/hesabix_ui/lib/pages/admin/system_configuration_page.dart
@@ -65,7 +65,7 @@ class _SystemConfigurationPageState extends State {
begin: Alignment.topCenter,
end: Alignment.bottomCenter,
colors: [
- theme.colorScheme.primary.withOpacity(0.1),
+ theme.colorScheme.primary.withValues(alpha: 0.1),
theme.colorScheme.surface,
],
),
@@ -232,7 +232,7 @@ class _SystemConfigurationPageState extends State {
return Padding(
padding: const EdgeInsets.only(bottom: 16),
child: DropdownButtonFormField(
- value: value,
+ initialValue: value,
decoration: InputDecoration(
labelText: label,
border: const OutlineInputBorder(),
diff --git a/hesabixUI/hesabix_ui/lib/pages/admin/system_logs_page.dart b/hesabixUI/hesabix_ui/lib/pages/admin/system_logs_page.dart
index 765ebf1..35275eb 100644
--- a/hesabixUI/hesabix_ui/lib/pages/admin/system_logs_page.dart
+++ b/hesabixUI/hesabix_ui/lib/pages/admin/system_logs_page.dart
@@ -115,7 +115,7 @@ class _SystemLogsPageState extends State {
begin: Alignment.topCenter,
end: Alignment.bottomCenter,
colors: [
- theme.colorScheme.primary.withOpacity(0.1),
+ theme.colorScheme.primary.withValues(alpha: 0.1),
theme.colorScheme.surface,
],
),
@@ -162,7 +162,7 @@ class _SystemLogsPageState extends State {
children: [
Expanded(
child: DropdownButtonFormField(
- value: _selectedLevel,
+ initialValue: _selectedLevel,
decoration: const InputDecoration(
labelText: 'Log Level',
border: OutlineInputBorder(),
@@ -180,7 +180,7 @@ class _SystemLogsPageState extends State {
const SizedBox(width: 16),
Expanded(
child: DropdownButtonFormField(
- value: _selectedDateRange,
+ initialValue: _selectedDateRange,
decoration: const InputDecoration(
labelText: 'Date Range',
border: OutlineInputBorder(),
@@ -212,13 +212,13 @@ class _SystemLogsPageState extends State {
Icon(
Icons.analytics_outlined,
size: 64,
- color: theme.colorScheme.onSurface.withOpacity(0.5),
+ color: theme.colorScheme.onSurface.withValues(alpha: 0.5),
),
const SizedBox(height: 16),
Text(
'No logs found',
style: theme.textTheme.titleLarge?.copyWith(
- color: theme.colorScheme.onSurface.withOpacity(0.7),
+ color: theme.colorScheme.onSurface.withValues(alpha: 0.7),
),
),
],
@@ -248,7 +248,7 @@ class _SystemLogsPageState extends State {
subtitle: Text(
'${log['timestamp']} • ${log['module']}',
style: TextStyle(
- color: theme.colorScheme.onSurface.withOpacity(0.7),
+ color: theme.colorScheme.onSurface.withValues(alpha: 0.7),
fontSize: 12,
),
),
@@ -270,7 +270,7 @@ class _SystemLogsPageState extends State {
width: double.infinity,
padding: const EdgeInsets.all(12),
decoration: BoxDecoration(
- color: theme.colorScheme.surfaceVariant.withOpacity(0.5),
+ color: theme.colorScheme.surfaceContainerHighest.withValues(alpha: 0.5),
borderRadius: BorderRadius.circular(8),
),
child: Text(
diff --git a/hesabixUI/hesabix_ui/lib/pages/admin/user_management_page.dart b/hesabixUI/hesabix_ui/lib/pages/admin/user_management_page.dart
index 7450e32..7161347 100644
--- a/hesabixUI/hesabix_ui/lib/pages/admin/user_management_page.dart
+++ b/hesabixUI/hesabix_ui/lib/pages/admin/user_management_page.dart
@@ -96,7 +96,7 @@ class _UserManagementPageState extends State {
begin: Alignment.topCenter,
end: Alignment.bottomCenter,
colors: [
- theme.colorScheme.primary.withOpacity(0.1),
+ theme.colorScheme.primary.withValues(alpha: 0.1),
theme.colorScheme.surface,
],
),
@@ -150,7 +150,7 @@ class _UserManagementPageState extends State {
children: [
Expanded(
child: DropdownButtonFormField(
- value: _selectedFilter,
+ initialValue: _selectedFilter,
decoration: const InputDecoration(
labelText: 'Filter',
border: OutlineInputBorder(),
@@ -190,13 +190,13 @@ class _UserManagementPageState extends State {
Icon(
Icons.people_outline,
size: 64,
- color: theme.colorScheme.onSurface.withOpacity(0.5),
+ color: theme.colorScheme.onSurface.withValues(alpha: 0.5),
),
const SizedBox(height: 16),
Text(
'No users found',
style: theme.textTheme.titleLarge?.copyWith(
- color: theme.colorScheme.onSurface.withOpacity(0.7),
+ color: theme.colorScheme.onSurface.withValues(alpha: 0.7),
),
),
],
@@ -301,7 +301,7 @@ class _UserManagementPageState extends State {
role.toUpperCase(),
style: const TextStyle(fontSize: 10, fontWeight: FontWeight.bold),
),
- backgroundColor: _getRoleColor(role).withOpacity(0.2),
+ backgroundColor: _getRoleColor(role).withValues(alpha: 0.2),
labelStyle: TextStyle(color: _getRoleColor(role)),
);
}
diff --git a/hesabixUI/hesabix_ui/lib/pages/business/business_shell.dart b/hesabixUI/hesabix_ui/lib/pages/business/business_shell.dart
new file mode 100644
index 0000000..a08f94b
--- /dev/null
+++ b/hesabixUI/hesabix_ui/lib/pages/business/business_shell.dart
@@ -0,0 +1,173 @@
+import 'package:flutter/material.dart';
+import 'package:go_router/go_router.dart';
+import 'package:hesabix_ui/l10n/app_localizations.dart';
+import '../../core/auth_store.dart';
+import '../../core/calendar_controller.dart';
+
+class BusinessShell extends StatefulWidget {
+ final int businessId;
+ final AuthStore authStore;
+ final CalendarController calendarController;
+ final Widget child;
+
+ const BusinessShell({
+ super.key,
+ required this.businessId,
+ required this.authStore,
+ required this.calendarController,
+ required this.child,
+ });
+
+ @override
+ State createState() => _BusinessShellState();
+}
+
+class _BusinessShellState extends State {
+
+ @override
+ void initState() {
+ super.initState();
+ // اضافه کردن listener برای AuthStore
+ widget.authStore.addListener(() {
+ if (mounted) {
+ setState(() {});
+ }
+ });
+ }
+
+ @override
+ Widget build(BuildContext context) {
+ final width = MediaQuery.of(context).size.width;
+ final bool useRail = width >= 700;
+ final bool railExtended = width >= 1100;
+ final ColorScheme scheme = Theme.of(context).colorScheme;
+ final String location = GoRouterState.of(context).uri.toString();
+ final bool isDark = Theme.of(context).brightness == Brightness.dark;
+ final String logoAsset = isDark
+ ? 'assets/images/logo-light.png'
+ : 'assets/images/logo-light.png';
+
+ final t = AppLocalizations.of(context);
+ final destinations = <_Dest>[
+ _Dest(t.businessDashboard, Icons.dashboard_outlined, Icons.dashboard, '/business/${widget.businessId}/dashboard'),
+ _Dest(t.sales, Icons.sell, Icons.sell, '/business/${widget.businessId}/sales'),
+ _Dest(t.accounting, Icons.account_balance, Icons.account_balance, '/business/${widget.businessId}/accounting'),
+ _Dest(t.inventory, Icons.inventory, Icons.inventory, '/business/${widget.businessId}/inventory'),
+ _Dest(t.reports, Icons.assessment, Icons.assessment, '/business/${widget.businessId}/reports'),
+ _Dest(t.members, Icons.people, Icons.people, '/business/${widget.businessId}/members'),
+ _Dest(t.settings, Icons.settings, Icons.settings, '/business/${widget.businessId}/settings'),
+ ];
+
+ int selectedIndex = 0;
+ for (int i = 0; i < destinations.length; i++) {
+ if (location.startsWith(destinations[i].path)) {
+ selectedIndex = i;
+ break;
+ }
+ }
+
+ Future onSelect(int index) async {
+ final path = destinations[index].path;
+ if (GoRouterState.of(context).uri.toString() != path) {
+ context.go(path);
+ }
+ }
+
+ Future onBackToProfile() async {
+ context.go('/user/profile/businesses');
+ }
+
+ // Brand top bar with contrast color
+ final Color appBarBg = Theme.of(context).brightness == Brightness.dark
+ ? scheme.surfaceContainerHighest
+ : scheme.primary;
+ final Color appBarFg = Theme.of(context).brightness == Brightness.dark
+ ? scheme.onSurfaceVariant
+ : scheme.onPrimary;
+
+ final appBar = AppBar(
+ backgroundColor: appBarBg,
+ foregroundColor: appBarFg,
+ elevation: 0,
+ title: Row(
+ children: [
+ Image.asset(
+ logoAsset,
+ height: 32,
+ width: 32,
+ errorBuilder: (context, error, stackTrace) => Icon(
+ Icons.business,
+ color: appBarFg,
+ size: 32,
+ ),
+ ),
+ const SizedBox(width: 12),
+ Text(
+ 'Hesabix',
+ style: TextStyle(
+ color: appBarFg,
+ fontWeight: FontWeight.bold,
+ fontSize: 20,
+ ),
+ ),
+ ],
+ ),
+ actions: [
+ IconButton(
+ icon: Icon(Icons.arrow_back, color: appBarFg),
+ onPressed: onBackToProfile,
+ tooltip: t.backToProfile,
+ ),
+ const SizedBox(width: 8),
+ ],
+ );
+
+ if (useRail) {
+ return Scaffold(
+ appBar: appBar,
+ body: Row(
+ children: [
+ NavigationRail(
+ selectedIndex: selectedIndex,
+ onDestinationSelected: onSelect,
+ labelType: railExtended ? NavigationRailLabelType.selected : NavigationRailLabelType.all,
+ extended: railExtended,
+ destinations: destinations.map((dest) => NavigationRailDestination(
+ icon: Icon(dest.icon),
+ selectedIcon: Icon(dest.selectedIcon),
+ label: Text(dest.label),
+ )).toList(),
+ ),
+ const VerticalDivider(thickness: 1, width: 1),
+ Expanded(
+ child: widget.child,
+ ),
+ ],
+ ),
+ );
+ } else {
+ return Scaffold(
+ appBar: appBar,
+ body: widget.child,
+ bottomNavigationBar: NavigationBar(
+ selectedIndex: selectedIndex,
+ onDestinationSelected: onSelect,
+ destinations: destinations.map((dest) => NavigationDestination(
+ icon: Icon(dest.icon),
+ selectedIcon: Icon(dest.selectedIcon),
+ label: dest.label,
+ )).toList(),
+ ),
+ );
+ }
+ }
+}
+
+class _Dest {
+ final String label;
+ final IconData icon;
+ final IconData selectedIcon;
+ final String path;
+
+ const _Dest(this.label, this.icon, this.selectedIcon, this.path);
+}
diff --git a/hesabixUI/hesabix_ui/lib/pages/business/dashboard/business_dashboard_page.dart b/hesabixUI/hesabix_ui/lib/pages/business/dashboard/business_dashboard_page.dart
new file mode 100644
index 0000000..8542ae7
--- /dev/null
+++ b/hesabixUI/hesabix_ui/lib/pages/business/dashboard/business_dashboard_page.dart
@@ -0,0 +1,438 @@
+import 'package:flutter/material.dart';
+import 'package:hesabix_ui/l10n/app_localizations.dart';
+import '../../../services/business_dashboard_service.dart';
+import '../../../core/api_client.dart';
+import '../../../models/business_dashboard_models.dart';
+
+class BusinessDashboardPage extends StatefulWidget {
+ final int businessId;
+
+ const BusinessDashboardPage({super.key, required this.businessId});
+
+ @override
+ State createState() => _BusinessDashboardPageState();
+}
+
+class _BusinessDashboardPageState extends State {
+ final BusinessDashboardService _service = BusinessDashboardService(ApiClient());
+ BusinessDashboardResponse? _dashboardData;
+ bool _loading = true;
+ String? _error;
+
+ @override
+ void initState() {
+ super.initState();
+ _loadDashboard();
+ }
+
+ Future _loadDashboard() async {
+ try {
+ setState(() {
+ _loading = true;
+ _error = null;
+ });
+
+ final data = await _service.getDashboard(widget.businessId);
+
+ if (mounted) {
+ setState(() {
+ _dashboardData = data;
+ _loading = false;
+ });
+ }
+ } catch (e) {
+ if (mounted) {
+ setState(() {
+ _error = e.toString();
+ _loading = false;
+ });
+ }
+ }
+ }
+
+ @override
+ Widget build(BuildContext context) {
+ final t = AppLocalizations.of(context);
+
+ if (_loading) {
+ return const Center(child: CircularProgressIndicator());
+ }
+
+ if (_error != null) {
+ return Center(
+ child: Column(
+ mainAxisAlignment: MainAxisAlignment.center,
+ children: [
+ Icon(Icons.error, size: 64, color: Colors.red),
+ const SizedBox(height: 16),
+ Text(
+ _error!,
+ textAlign: TextAlign.center,
+ style: Theme.of(context).textTheme.bodyLarge,
+ ),
+ const SizedBox(height: 16),
+ ElevatedButton(
+ onPressed: _loadDashboard,
+ child: Text(t.retry),
+ ),
+ ],
+ ),
+ );
+ }
+
+ return Padding(
+ padding: const EdgeInsets.all(16.0),
+ child: Column(
+ crossAxisAlignment: CrossAxisAlignment.start,
+ children: [
+ Text(
+ t.businessDashboard,
+ style: Theme.of(context).textTheme.headlineMedium,
+ ),
+ const SizedBox(height: 16),
+ if (_dashboardData != null) ...[
+ _buildBusinessInfo(_dashboardData!.businessInfo),
+ const SizedBox(height: 24),
+ _buildStatistics(_dashboardData!.statistics),
+ const SizedBox(height: 24),
+ _buildRecentActivities(_dashboardData!.recentActivities),
+ ],
+ ],
+ ),
+ );
+ }
+
+ Widget _buildBusinessInfo(BusinessInfo info) {
+ return Card(
+ child: Padding(
+ padding: const EdgeInsets.all(16.0),
+ child: Column(
+ crossAxisAlignment: CrossAxisAlignment.start,
+ children: [
+ Row(
+ children: [
+ Icon(Icons.business, size: 32, color: Theme.of(context).colorScheme.primary),
+ const SizedBox(width: 12),
+ Expanded(
+ child: Column(
+ crossAxisAlignment: CrossAxisAlignment.start,
+ children: [
+ Text(
+ info.name,
+ style: Theme.of(context).textTheme.headlineSmall,
+ ),
+ const SizedBox(height: 4),
+ Text(
+ '${info.businessType} - ${info.businessField}',
+ style: Theme.of(context).textTheme.bodyMedium?.copyWith(
+ color: Theme.of(context).colorScheme.onSurfaceVariant,
+ ),
+ ),
+ ],
+ ),
+ ),
+ ],
+ ),
+ const SizedBox(height: 16),
+ Row(
+ children: [
+ _buildInfoChip(
+ Icons.people,
+ '${info.memberCount} عضو',
+ Theme.of(context).colorScheme.primary,
+ ),
+ const SizedBox(width: 8),
+ _buildInfoChip(
+ Icons.calendar_today,
+ 'تأسیس: ${_formatDate(info.createdAt)}',
+ Theme.of(context).colorScheme.secondary,
+ ),
+ ],
+ ),
+ if (info.address != null) ...[
+ const SizedBox(height: 12),
+ Row(
+ children: [
+ Icon(Icons.location_on, size: 16, color: Theme.of(context).colorScheme.onSurfaceVariant),
+ const SizedBox(width: 4),
+ Expanded(
+ child: Text(
+ info.address!,
+ style: Theme.of(context).textTheme.bodySmall?.copyWith(
+ color: Theme.of(context).colorScheme.onSurfaceVariant,
+ ),
+ ),
+ ),
+ ],
+ ),
+ ],
+ if (info.phone != null || info.mobile != null) ...[
+ const SizedBox(height: 8),
+ Row(
+ children: [
+ Icon(Icons.phone, size: 16, color: Theme.of(context).colorScheme.onSurfaceVariant),
+ const SizedBox(width: 4),
+ Text(
+ info.phone ?? info.mobile!,
+ style: Theme.of(context).textTheme.bodySmall?.copyWith(
+ color: Theme.of(context).colorScheme.onSurfaceVariant,
+ ),
+ ),
+ ],
+ ),
+ ],
+ ],
+ ),
+ ),
+ );
+ }
+
+ Widget _buildInfoChip(IconData icon, String text, Color color) {
+ return Container(
+ padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 6),
+ decoration: BoxDecoration(
+ color: color.withValues(alpha: 0.1),
+ borderRadius: BorderRadius.circular(16),
+ border: Border.all(color: color.withValues(alpha: 0.3)),
+ ),
+ child: Row(
+ mainAxisSize: MainAxisSize.min,
+ children: [
+ Icon(icon, size: 16, color: color),
+ const SizedBox(width: 4),
+ Text(
+ text,
+ style: TextStyle(
+ color: color,
+ fontSize: 12,
+ fontWeight: FontWeight.w500,
+ ),
+ ),
+ ],
+ ),
+ );
+ }
+
+ Widget _buildStatistics(BusinessStatistics stats) {
+ return Card(
+ child: Padding(
+ padding: const EdgeInsets.all(16.0),
+ child: Column(
+ crossAxisAlignment: CrossAxisAlignment.start,
+ children: [
+ Text(
+ AppLocalizations.of(context).businessStatistics,
+ style: Theme.of(context).textTheme.titleLarge,
+ ),
+ const SizedBox(height: 16),
+ Row(
+ children: [
+ Expanded(
+ child: _buildStatCard(
+ 'فروش کل',
+ _formatCurrency(stats.totalSales),
+ Icons.trending_up,
+ Colors.green,
+ ),
+ ),
+ const SizedBox(width: 16),
+ Expanded(
+ child: _buildStatCard(
+ 'خرید کل',
+ _formatCurrency(stats.totalPurchases),
+ Icons.trending_down,
+ Colors.orange,
+ ),
+ ),
+ ],
+ ),
+ const SizedBox(height: 16),
+ Row(
+ children: [
+ Expanded(
+ child: _buildStatCard(
+ 'اعضای فعال',
+ stats.activeMembers.toString(),
+ Icons.people,
+ Colors.blue,
+ ),
+ ),
+ const SizedBox(width: 16),
+ Expanded(
+ child: _buildStatCard(
+ 'تراکنشهای اخیر',
+ stats.recentTransactions.toString(),
+ Icons.receipt,
+ Colors.purple,
+ ),
+ ),
+ ],
+ ),
+ ],
+ ),
+ ),
+ );
+ }
+
+ Widget _buildStatCard(String title, String value, IconData icon, Color color) {
+ return Container(
+ padding: const EdgeInsets.all(16),
+ decoration: BoxDecoration(
+ color: color.withValues(alpha: 0.1),
+ borderRadius: BorderRadius.circular(12),
+ border: Border.all(color: color.withValues(alpha: 0.2)),
+ ),
+ child: Column(
+ crossAxisAlignment: CrossAxisAlignment.start,
+ children: [
+ Row(
+ children: [
+ Icon(icon, color: color, size: 20),
+ const SizedBox(width: 8),
+ Expanded(
+ child: Text(
+ title,
+ style: Theme.of(context).textTheme.bodyMedium?.copyWith(
+ color: Theme.of(context).colorScheme.onSurfaceVariant,
+ ),
+ ),
+ ),
+ ],
+ ),
+ const SizedBox(height: 8),
+ Text(
+ value,
+ style: Theme.of(context).textTheme.headlineSmall?.copyWith(
+ color: color,
+ fontWeight: FontWeight.bold,
+ ),
+ ),
+ ],
+ ),
+ );
+ }
+
+ Widget _buildRecentActivities(List activities) {
+ return Card(
+ child: Padding(
+ padding: const EdgeInsets.all(16.0),
+ child: Column(
+ crossAxisAlignment: CrossAxisAlignment.start,
+ children: [
+ Text(
+ AppLocalizations.of(context).recentActivities,
+ style: Theme.of(context).textTheme.titleLarge,
+ ),
+ const SizedBox(height: 16),
+ if (activities.isEmpty)
+ Center(
+ child: Column(
+ children: [
+ Icon(
+ Icons.inbox,
+ size: 48,
+ color: Theme.of(context).colorScheme.onSurfaceVariant,
+ ),
+ const SizedBox(height: 8),
+ Text(
+ 'هیچ فعالیتی یافت نشد',
+ style: Theme.of(context).textTheme.bodyMedium?.copyWith(
+ color: Theme.of(context).colorScheme.onSurfaceVariant,
+ ),
+ ),
+ ],
+ ),
+ )
+ else
+ ...activities.map((activity) => _buildActivityItem(activity)),
+ ],
+ ),
+ ),
+ );
+ }
+
+ Widget _buildActivityItem(Activity activity) {
+ IconData activityIcon;
+ Color iconColor;
+
+ switch (activity.icon) {
+ case 'sell':
+ activityIcon = Icons.sell;
+ iconColor = Colors.green;
+ break;
+ case 'person_add':
+ activityIcon = Icons.person_add;
+ iconColor = Colors.blue;
+ break;
+ case 'assessment':
+ activityIcon = Icons.assessment;
+ iconColor = Colors.orange;
+ break;
+ default:
+ activityIcon = Icons.info;
+ iconColor = Colors.grey;
+ }
+
+ return Padding(
+ padding: const EdgeInsets.only(bottom: 12),
+ child: Row(
+ children: [
+ Container(
+ padding: const EdgeInsets.all(8),
+ decoration: BoxDecoration(
+ color: iconColor.withValues(alpha: 0.1),
+ borderRadius: BorderRadius.circular(8),
+ ),
+ child: Icon(activityIcon, color: iconColor, size: 20),
+ ),
+ const SizedBox(width: 12),
+ Expanded(
+ child: Column(
+ crossAxisAlignment: CrossAxisAlignment.start,
+ children: [
+ Text(
+ activity.title,
+ style: Theme.of(context).textTheme.bodyMedium?.copyWith(
+ fontWeight: FontWeight.w500,
+ ),
+ ),
+ const SizedBox(height: 2),
+ Text(
+ activity.description,
+ style: Theme.of(context).textTheme.bodySmall?.copyWith(
+ color: Theme.of(context).colorScheme.onSurfaceVariant,
+ ),
+ ),
+ ],
+ ),
+ ),
+ Text(
+ activity.timeAgo,
+ style: Theme.of(context).textTheme.bodySmall?.copyWith(
+ color: Theme.of(context).colorScheme.onSurfaceVariant,
+ ),
+ ),
+ ],
+ ),
+ );
+ }
+
+ String _formatCurrency(double amount) {
+ if (amount >= 1000000) {
+ return '${(amount / 1000000).toStringAsFixed(1)}M تومان';
+ } else if (amount >= 1000) {
+ return '${(amount / 1000).toStringAsFixed(1)}K تومان';
+ } else {
+ return '${amount.toStringAsFixed(0)} تومان';
+ }
+ }
+
+ String _formatDate(String dateString) {
+ try {
+ final date = DateTime.parse(dateString);
+ return '${date.year}/${date.month.toString().padLeft(2, '0')}/${date.day.toString().padLeft(2, '0')}';
+ } catch (e) {
+ return dateString;
+ }
+ }
+}
diff --git a/hesabixUI/hesabix_ui/lib/pages/home_page.dart b/hesabixUI/hesabix_ui/lib/pages/home_page.dart
index 05945af..9c442a4 100644
--- a/hesabixUI/hesabix_ui/lib/pages/home_page.dart
+++ b/hesabixUI/hesabix_ui/lib/pages/home_page.dart
@@ -43,7 +43,6 @@ class _ThemeMenu extends StatelessWidget {
Widget build(BuildContext context) {
return PopupMenuButton(
icon: const Icon(Icons.color_lens_outlined),
- initialValue: controller.mode,
onSelected: (mode) => controller.setMode(mode),
itemBuilder: (context) => const [
PopupMenuItem(value: ThemeMode.system, child: Text('System')),
diff --git a/hesabixUI/hesabix_ui/lib/pages/login_page.dart b/hesabixUI/hesabix_ui/lib/pages/login_page.dart
index 368bb6c..371a06d 100644
--- a/hesabixUI/hesabix_ui/lib/pages/login_page.dart
+++ b/hesabixUI/hesabix_ui/lib/pages/login_page.dart
@@ -1,6 +1,5 @@
import 'dart:async';
import 'dart:convert';
-import 'dart:typed_data';
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:go_router/go_router.dart';
@@ -352,7 +351,9 @@ class _LoginPageState extends State with SingleTickerProviderStateMix
_showSnack(t.registerSuccess);
// پاکسازی کد معرف پس از ثبتنام موفق
unawaited(ReferralStore.clearReferrer());
- context.go('/user/profile/dashboard');
+ if (mounted) {
+ context.go('/user/profile/dashboard');
+ }
} catch (e) {
if (!mounted) return;
final msg = _extractErrorMessage(e, AppLocalizations.of(context));
diff --git a/hesabixUI/hesabix_ui/lib/pages/profile/businesses_page.dart b/hesabixUI/hesabix_ui/lib/pages/profile/businesses_page.dart
index 10a6557..47586b8 100644
--- a/hesabixUI/hesabix_ui/lib/pages/profile/businesses_page.dart
+++ b/hesabixUI/hesabix_ui/lib/pages/profile/businesses_page.dart
@@ -1,24 +1,447 @@
import 'package:flutter/material.dart';
+import 'package:go_router/go_router.dart';
import 'package:hesabix_ui/l10n/app_localizations.dart';
+import '../../services/business_dashboard_service.dart';
+import '../../core/api_client.dart';
+import '../../models/business_dashboard_models.dart';
-class BusinessesPage extends StatelessWidget {
+class BusinessesPage extends StatefulWidget {
const BusinessesPage({super.key});
+ @override
+ State createState() => _BusinessesPageState();
+}
+
+class _BusinessesPageState extends State {
+ final BusinessDashboardService _service = BusinessDashboardService(ApiClient());
+ List _businesses = [];
+ bool _loading = true;
+ String? _error;
+
+ @override
+ void initState() {
+ super.initState();
+ _loadBusinesses();
+ }
+
+ Future _loadBusinesses() async {
+ try {
+ setState(() {
+ _loading = true;
+ _error = null;
+ });
+
+ final businesses = await _service.getUserBusinesses();
+
+ if (mounted) {
+ setState(() {
+ _businesses = businesses;
+ _loading = false;
+ });
+ }
+ } catch (e) {
+ if (mounted) {
+ setState(() {
+ _loading = false;
+ _error = e.toString();
+ });
+ ScaffoldMessenger.of(context).showSnackBar(
+ SnackBar(content: Text('خطا در بارگذاری کسب و کارها: $e')),
+ );
+ }
+ }
+ }
+
+ void _navigateToBusiness(int businessId) {
+ context.go('/business/$businessId/dashboard');
+ }
+
@override
Widget build(BuildContext context) {
final t = AppLocalizations.of(context);
+
return Padding(
padding: const EdgeInsets.all(16.0),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
- Text(t.businesses, style: Theme.of(context).textTheme.titleLarge),
- const SizedBox(height: 8),
- Text('${t.businesses} - sample page'),
+ Row(
+ mainAxisAlignment: MainAxisAlignment.spaceBetween,
+ children: [
+ Text(
+ t.businesses,
+ style: Theme.of(context).textTheme.headlineMedium,
+ ),
+ ElevatedButton.icon(
+ onPressed: () => context.go('/user/profile/new-business'),
+ icon: const Icon(Icons.add),
+ label: Text(t.newBusiness),
+ ),
+ ],
+ ),
+ const SizedBox(height: 16),
+ if (_loading)
+ const Center(child: CircularProgressIndicator())
+ else if (_error != null)
+ Center(
+ child: Column(
+ children: [
+ Icon(Icons.error, size: 64, color: Colors.red),
+ const SizedBox(height: 16),
+ Text(_error!),
+ const SizedBox(height: 16),
+ ElevatedButton(
+ onPressed: _loadBusinesses,
+ child: Text(t.retry),
+ ),
+ ],
+ ),
+ )
+ else if (_businesses.isEmpty)
+ Center(
+ child: Column(
+ children: [
+ Icon(Icons.business, size: 64, color: Colors.grey),
+ const SizedBox(height: 16),
+ Text(t.noBusinessesFound),
+ const SizedBox(height: 16),
+ ElevatedButton(
+ onPressed: () => context.go('/user/profile/new-business'),
+ child: Text(t.createFirstBusiness),
+ ),
+ ],
+ ),
+ )
+ else
+ Expanded(
+ child: LayoutBuilder(
+ builder: (context, constraints) {
+ // Responsive grid based on screen width
+ int crossAxisCount;
+ if (constraints.maxWidth > 1200) {
+ crossAxisCount = 4;
+ } else if (constraints.maxWidth > 900) {
+ crossAxisCount = 3;
+ } else if (constraints.maxWidth > 600) {
+ crossAxisCount = 2;
+ } else {
+ crossAxisCount = 1;
+ }
+
+ return GridView.builder(
+ gridDelegate: SliverGridDelegateWithFixedCrossAxisCount(
+ crossAxisCount: crossAxisCount,
+ childAspectRatio: crossAxisCount == 1 ? 4.0 : 1.3,
+ crossAxisSpacing: 12,
+ mainAxisSpacing: 12,
+ ),
+ itemCount: _businesses.length,
+ itemBuilder: (context, index) {
+ final business = _businesses[index];
+ return _BusinessCard(
+ business: business,
+ onTap: () => _navigateToBusiness(business.id),
+ isCompact: crossAxisCount > 1,
+ );
+ },
+ );
+ },
+ ),
+ ),
],
),
);
}
}
+class _BusinessCard extends StatelessWidget {
+ final BusinessWithPermission business;
+ final VoidCallback onTap;
+ final bool isCompact;
+
+ const _BusinessCard({
+ required this.business,
+ required this.onTap,
+ this.isCompact = true,
+ });
+
+ @override
+ Widget build(BuildContext context) {
+ if (isCompact) {
+ return _buildCompactCard(context);
+ } else {
+ return _buildWideCard(context);
+ }
+ }
+
+ Widget _buildCompactCard(BuildContext context) {
+ return Card(
+ elevation: 1,
+ margin: EdgeInsets.zero,
+ child: InkWell(
+ onTap: onTap,
+ borderRadius: BorderRadius.circular(8),
+ child: Container(
+ padding: const EdgeInsets.all(8.0),
+ child: Column(
+ crossAxisAlignment: CrossAxisAlignment.start,
+ mainAxisAlignment: MainAxisAlignment.spaceBetween,
+ children: [
+ // Header with icon and role badge
+ Row(
+ children: [
+ Container(
+ padding: const EdgeInsets.all(8),
+ decoration: BoxDecoration(
+ color: business.isOwner
+ ? Theme.of(context).colorScheme.primary.withValues(alpha: 0.1)
+ : Theme.of(context).colorScheme.secondary.withValues(alpha: 0.1),
+ borderRadius: BorderRadius.circular(8),
+ ),
+ child: Icon(
+ business.isOwner ? Icons.business : Icons.business_outlined,
+ color: business.isOwner
+ ? Theme.of(context).colorScheme.primary
+ : Theme.of(context).colorScheme.secondary,
+ size: 20,
+ ),
+ ),
+ const SizedBox(width: 8),
+ Expanded(
+ child: Container(
+ padding: const EdgeInsets.symmetric(horizontal: 6, vertical: 2),
+ decoration: BoxDecoration(
+ color: business.isOwner
+ ? Theme.of(context).colorScheme.primary.withValues(alpha: 0.1)
+ : Theme.of(context).colorScheme.secondary.withValues(alpha: 0.1),
+ borderRadius: BorderRadius.circular(10),
+ ),
+ child: Text(
+ business.isOwner ? AppLocalizations.of(context).owner : AppLocalizations.of(context).member,
+ style: TextStyle(
+ color: business.isOwner
+ ? Theme.of(context).colorScheme.primary
+ : Theme.of(context).colorScheme.secondary,
+ fontSize: 10,
+ fontWeight: FontWeight.w600,
+ ),
+ ),
+ ),
+ ),
+ ],
+ ),
+
+ const SizedBox(height: 6),
+
+ // Business name
+ Text(
+ business.name,
+ style: Theme.of(context).textTheme.titleSmall?.copyWith(
+ fontWeight: FontWeight.w600,
+ fontSize: 14,
+ ),
+ maxLines: 2,
+ overflow: TextOverflow.ellipsis,
+ ),
+
+ const SizedBox(height: 3),
+
+ // Business type and field
+ Text(
+ '${_translateBusinessType(business.businessType, context)} • ${_translateBusinessField(business.businessField, context)}',
+ style: Theme.of(context).textTheme.bodySmall?.copyWith(
+ color: Theme.of(context).colorScheme.onSurfaceVariant,
+ fontSize: 11,
+ ),
+ maxLines: 1,
+ overflow: TextOverflow.ellipsis,
+ ),
+
+ const SizedBox(height: 6),
+
+ // Footer with date and arrow
+ Row(
+ mainAxisAlignment: MainAxisAlignment.spaceBetween,
+ children: [
+ Expanded(
+ child: Text(
+ _formatDate(business.createdAt),
+ style: Theme.of(context).textTheme.bodySmall?.copyWith(
+ color: Theme.of(context).colorScheme.onSurfaceVariant,
+ fontSize: 10,
+ ),
+ maxLines: 1,
+ overflow: TextOverflow.ellipsis,
+ ),
+ ),
+ Icon(
+ Icons.arrow_forward_ios,
+ size: 12,
+ color: Theme.of(context).colorScheme.onSurfaceVariant,
+ ),
+ ],
+ ),
+ ],
+ ),
+ ),
+ ),
+ );
+ }
+
+ Widget _buildWideCard(BuildContext context) {
+ return Card(
+ elevation: 1,
+ margin: EdgeInsets.zero,
+ child: InkWell(
+ onTap: onTap,
+ borderRadius: BorderRadius.circular(8),
+ child: Container(
+ padding: const EdgeInsets.all(16.0),
+ child: Row(
+ children: [
+ // Icon
+ Container(
+ padding: const EdgeInsets.all(12),
+ decoration: BoxDecoration(
+ color: business.isOwner
+ ? Theme.of(context).colorScheme.primary.withValues(alpha: 0.1)
+ : Theme.of(context).colorScheme.secondary.withValues(alpha: 0.1),
+ borderRadius: BorderRadius.circular(12),
+ ),
+ child: Icon(
+ business.isOwner ? Icons.business : Icons.business_outlined,
+ color: business.isOwner
+ ? Theme.of(context).colorScheme.primary
+ : Theme.of(context).colorScheme.secondary,
+ size: 24,
+ ),
+ ),
+
+ const SizedBox(width: 16),
+
+ // Content
+ Expanded(
+ child: Column(
+ crossAxisAlignment: CrossAxisAlignment.start,
+ children: [
+ Row(
+ children: [
+ Expanded(
+ child: Text(
+ business.name,
+ style: Theme.of(context).textTheme.titleMedium?.copyWith(
+ fontWeight: FontWeight.w600,
+ ),
+ maxLines: 1,
+ overflow: TextOverflow.ellipsis,
+ ),
+ ),
+ Container(
+ padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 4),
+ decoration: BoxDecoration(
+ color: business.isOwner
+ ? Theme.of(context).colorScheme.primary.withValues(alpha: 0.1)
+ : Theme.of(context).colorScheme.secondary.withValues(alpha: 0.1),
+ borderRadius: BorderRadius.circular(12),
+ ),
+ child: Text(
+ business.isOwner ? AppLocalizations.of(context).owner : AppLocalizations.of(context).member,
+ style: TextStyle(
+ color: business.isOwner
+ ? Theme.of(context).colorScheme.primary
+ : Theme.of(context).colorScheme.secondary,
+ fontSize: 12,
+ fontWeight: FontWeight.w600,
+ ),
+ ),
+ ),
+ ],
+ ),
+
+ const SizedBox(height: 4),
+
+ Text(
+ '${_translateBusinessType(business.businessType, context)} • ${_translateBusinessField(business.businessField, context)}',
+ style: Theme.of(context).textTheme.bodyMedium?.copyWith(
+ color: Theme.of(context).colorScheme.onSurfaceVariant,
+ ),
+ maxLines: 1,
+ overflow: TextOverflow.ellipsis,
+ ),
+
+ const SizedBox(height: 8),
+
+ Text(
+ 'تأسیس: ${_formatDate(business.createdAt)}',
+ style: Theme.of(context).textTheme.bodySmall?.copyWith(
+ color: Theme.of(context).colorScheme.onSurfaceVariant,
+ ),
+ ),
+ ],
+ ),
+ ),
+
+ const SizedBox(width: 16),
+
+ // Arrow
+ Icon(
+ Icons.arrow_forward_ios,
+ size: 16,
+ color: Theme.of(context).colorScheme.onSurfaceVariant,
+ ),
+ ],
+ ),
+ ),
+ ),
+ );
+ }
+
+ String _formatDate(String dateString) {
+ try {
+ final date = DateTime.parse(dateString);
+ return '${date.year}/${date.month}/${date.day}';
+ } catch (e) {
+ return dateString;
+ }
+ }
+
+ String _translateBusinessType(String type, BuildContext context) {
+ final l10n = AppLocalizations.of(context);
+ switch (type) {
+ case 'شرکت':
+ return l10n.company;
+ case 'مغازه':
+ return l10n.shop;
+ case 'فروشگاه':
+ return l10n.store;
+ case 'اتحادیه':
+ return l10n.union;
+ case 'باشگاه':
+ return l10n.club;
+ case 'موسسه':
+ return l10n.institute;
+ case 'شخصی':
+ return l10n.individual;
+ default:
+ return type;
+ }
+ }
+
+ String _translateBusinessField(String field, BuildContext context) {
+ final l10n = AppLocalizations.of(context);
+ switch (field) {
+ case 'تولیدی':
+ return l10n.manufacturing;
+ case 'بازرگانی':
+ return l10n.trading;
+ case 'خدماتی':
+ return l10n.service;
+ case 'سایر':
+ return l10n.other;
+ default:
+ return field;
+ }
+ }
+}
+
diff --git a/hesabixUI/hesabix_ui/lib/pages/profile/create_ticket_page.dart b/hesabixUI/hesabix_ui/lib/pages/profile/create_ticket_page.dart
index 78fee57..428ef13 100644
--- a/hesabixUI/hesabix_ui/lib/pages/profile/create_ticket_page.dart
+++ b/hesabixUI/hesabix_ui/lib/pages/profile/create_ticket_page.dart
@@ -59,7 +59,7 @@ class _CreateTicketPageState extends State {
borderRadius: BorderRadius.circular(12),
boxShadow: [
BoxShadow(
- color: Colors.black.withOpacity(0.2),
+ color: Colors.black.withValues(alpha: 0.2),
blurRadius: 8,
offset: const Offset(0, 4),
),
@@ -189,8 +189,8 @@ class _CreateTicketPageState extends State {
decoration: BoxDecoration(
gradient: LinearGradient(
colors: [
- theme.colorScheme.primary.withOpacity(0.1),
- theme.colorScheme.primary.withOpacity(0.05),
+ theme.colorScheme.primary.withValues(alpha: 0.1),
+ theme.colorScheme.primary.withValues(alpha: 0.05),
],
),
borderRadius: const BorderRadius.only(
@@ -228,7 +228,7 @@ class _CreateTicketPageState extends State {
Text(
t.descriptionHint,
style: theme.textTheme.bodyMedium?.copyWith(
- color: theme.colorScheme.onSurface.withOpacity(0.7),
+ color: theme.colorScheme.onSurface.withValues(alpha: 0.7),
),
),
],
@@ -268,7 +268,7 @@ class _CreateTicketPageState extends State {
Text(
t.loadingData,
style: theme.textTheme.bodyLarge?.copyWith(
- color: theme.colorScheme.onSurface.withOpacity(0.7),
+ color: theme.colorScheme.onSurface.withValues(alpha: 0.7),
),
),
],
@@ -286,13 +286,13 @@ class _CreateTicketPageState extends State {
Container(
padding: const EdgeInsets.all(24),
decoration: BoxDecoration(
- color: Colors.red.withOpacity(0.1),
+ color: Colors.red.withValues(alpha: 0.1),
shape: BoxShape.circle,
),
child: Icon(
Icons.error_outline,
size: 48,
- color: Colors.red.withOpacity(0.8),
+ color: Colors.red.withValues(alpha: 0.8),
),
),
const SizedBox(height: 24),
@@ -397,7 +397,7 @@ class _CreateTicketPageState extends State {
enabledBorder: OutlineInputBorder(
borderRadius: BorderRadius.circular(12),
borderSide: BorderSide(
- color: theme.colorScheme.outline.withOpacity(0.3),
+ color: theme.colorScheme.outline.withValues(alpha: 0.3),
),
),
focusedBorder: OutlineInputBorder(
@@ -437,7 +437,7 @@ class _CreateTicketPageState extends State {
),
const SizedBox(height: 8),
DropdownButtonFormField(
- value: _selectedCategory,
+ initialValue: _selectedCategory,
decoration: InputDecoration(
border: OutlineInputBorder(
borderRadius: BorderRadius.circular(12),
@@ -445,7 +445,7 @@ class _CreateTicketPageState extends State {
enabledBorder: OutlineInputBorder(
borderRadius: BorderRadius.circular(12),
borderSide: BorderSide(
- color: theme.colorScheme.outline.withOpacity(0.3),
+ color: theme.colorScheme.outline.withValues(alpha: 0.3),
),
),
focusedBorder: OutlineInputBorder(
@@ -493,7 +493,7 @@ class _CreateTicketPageState extends State {
),
const SizedBox(height: 8),
DropdownButtonFormField(
- value: _selectedPriority,
+ initialValue: _selectedPriority,
decoration: InputDecoration(
border: OutlineInputBorder(
borderRadius: BorderRadius.circular(12),
@@ -501,7 +501,7 @@ class _CreateTicketPageState extends State {
enabledBorder: OutlineInputBorder(
borderRadius: BorderRadius.circular(12),
borderSide: BorderSide(
- color: theme.colorScheme.outline.withOpacity(0.3),
+ color: theme.colorScheme.outline.withValues(alpha: 0.3),
),
),
focusedBorder: OutlineInputBorder(
@@ -573,7 +573,7 @@ class _CreateTicketPageState extends State {
enabledBorder: OutlineInputBorder(
borderRadius: BorderRadius.circular(12),
borderSide: BorderSide(
- color: theme.colorScheme.outline.withOpacity(0.3),
+ color: theme.colorScheme.outline.withValues(alpha: 0.3),
),
),
focusedBorder: OutlineInputBorder(
@@ -610,7 +610,7 @@ class _CreateTicketPageState extends State {
color: theme.colorScheme.surface,
border: Border(
top: BorderSide(
- color: theme.colorScheme.outline.withOpacity(0.2),
+ color: theme.colorScheme.outline.withValues(alpha: 0.2),
width: 1,
),
),
@@ -636,7 +636,7 @@ class _CreateTicketPageState extends State {
foregroundColor: Colors.white,
padding: const EdgeInsets.symmetric(vertical: 16, horizontal: 24),
elevation: 4,
- shadowColor: theme.colorScheme.primary.withOpacity(0.3),
+ shadowColor: theme.colorScheme.primary.withValues(alpha: 0.3),
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(12),
),
diff --git a/hesabixUI/hesabix_ui/lib/pages/profile/marketing_page.dart b/hesabixUI/hesabix_ui/lib/pages/profile/marketing_page.dart
index 27f74f4..afaa626 100644
--- a/hesabixUI/hesabix_ui/lib/pages/profile/marketing_page.dart
+++ b/hesabixUI/hesabix_ui/lib/pages/profile/marketing_page.dart
@@ -189,9 +189,9 @@ class _MarketingPageState extends State {
const SizedBox(width: 8),
FilledButton.icon(
onPressed: () async {
+ final messenger = ScaffoldMessenger.of(context);
await Clipboard.setData(ClipboardData(text: inviteLink));
if (!mounted) return;
- final messenger = ScaffoldMessenger.of(context);
messenger
..hideCurrentSnackBar()
..showSnackBar(
diff --git a/hesabixUI/hesabix_ui/lib/pages/profile/new_business_page.dart b/hesabixUI/hesabix_ui/lib/pages/profile/new_business_page.dart
index 674f45c..1427457 100644
--- a/hesabixUI/hesabix_ui/lib/pages/profile/new_business_page.dart
+++ b/hesabixUI/hesabix_ui/lib/pages/profile/new_business_page.dart
@@ -633,7 +633,7 @@ class _NewBusinessPageState extends State {
),
),
),
- value: _businessData.businessType,
+ initialValue: _businessData.businessType,
items: BusinessType.values.map((type) {
return DropdownMenuItem(
value: type,
@@ -675,7 +675,7 @@ class _NewBusinessPageState extends State {
),
),
),
- value: _businessData.businessField,
+ initialValue: _businessData.businessField,
items: BusinessField.values.map((field) {
return DropdownMenuItem(
value: field,
diff --git a/hesabixUI/hesabix_ui/lib/pages/profile/profile_dashboard_page.dart b/hesabixUI/hesabix_ui/lib/pages/profile/profile_dashboard_page.dart
index 9ae4387..c335f04 100644
--- a/hesabixUI/hesabix_ui/lib/pages/profile/profile_dashboard_page.dart
+++ b/hesabixUI/hesabix_ui/lib/pages/profile/profile_dashboard_page.dart
@@ -5,16 +5,13 @@ class ProfileDashboardPage extends StatelessWidget {
@override
Widget build(BuildContext context) {
- return Padding(
- padding: const EdgeInsets.all(16.0),
- child: Column(
- crossAxisAlignment: CrossAxisAlignment.start,
- children: const [
- Text('User Profile Dashboard', style: TextStyle(fontSize: 18, fontWeight: FontWeight.w600)),
- SizedBox(height: 12),
- Text('Summary and quick actions will appear here.'),
- ],
- ),
+ return Column(
+ crossAxisAlignment: CrossAxisAlignment.start,
+ children: const [
+ Text('User Profile Dashboard', style: TextStyle(fontSize: 18, fontWeight: FontWeight.w600)),
+ SizedBox(height: 12),
+ Text('Summary and quick actions will appear here.'),
+ ],
);
}
}
diff --git a/hesabixUI/hesabix_ui/lib/pages/profile/profile_shell.dart b/hesabixUI/hesabix_ui/lib/pages/profile/profile_shell.dart
index d4dab00..858ea42 100644
--- a/hesabixUI/hesabix_ui/lib/pages/profile/profile_shell.dart
+++ b/hesabixUI/hesabix_ui/lib/pages/profile/profile_shell.dart
@@ -152,10 +152,7 @@ class _ProfileShellState extends State {
final content = Container(
color: scheme.surface,
child: SafeArea(
- child: Padding(
- padding: const EdgeInsets.all(16),
- child: widget.child,
- ),
+ child: widget.child,
),
);
diff --git a/hesabixUI/hesabix_ui/lib/pages/system_settings_page.dart b/hesabixUI/hesabix_ui/lib/pages/system_settings_page.dart
index 5c5f83b..0011243 100644
--- a/hesabixUI/hesabix_ui/lib/pages/system_settings_page.dart
+++ b/hesabixUI/hesabix_ui/lib/pages/system_settings_page.dart
@@ -44,6 +44,13 @@ class _SystemSettingsPageState extends State {
color: const Color(0xFF9C27B0),
route: '/user/profile/system-settings/logs',
),
+ SettingsItem(
+ title: 'emailSettings',
+ description: 'emailSettingsDescription',
+ icon: Icons.email_outlined,
+ color: const Color(0xFFE91E63),
+ route: '/user/profile/system-settings/email',
+ ),
];
}
@@ -55,25 +62,6 @@ class _SystemSettingsPageState extends State {
return Scaffold(
backgroundColor: colorScheme.surface,
- appBar: AppBar(
- title: Text(
- t.systemSettingsWelcome,
- style: const TextStyle(
- fontWeight: FontWeight.w600,
- fontSize: 20,
- ),
- ),
- backgroundColor: Colors.transparent,
- elevation: 0,
- centerTitle: true,
- actions: [
- IconButton(
- onPressed: () => _showHelpDialog(context),
- icon: const Icon(Icons.help_outline),
- tooltip: t.systemSettingsWelcome,
- ),
- ],
- ),
body: SingleChildScrollView(
padding: const EdgeInsets.all(20),
child: Column(
@@ -97,13 +85,13 @@ class _SystemSettingsPageState extends State {
begin: Alignment.topLeft,
end: Alignment.bottomRight,
colors: [
- colorScheme.primary.withOpacity(0.1),
- colorScheme.primaryContainer.withOpacity(0.3),
+ colorScheme.primary.withValues(alpha: 0.1),
+ colorScheme.primaryContainer.withValues(alpha: 0.3),
],
),
borderRadius: BorderRadius.circular(16),
border: Border.all(
- color: colorScheme.primary.withOpacity(0.2),
+ color: colorScheme.primary.withValues(alpha: 0.2),
width: 1,
),
),
@@ -117,13 +105,13 @@ class _SystemSettingsPageState extends State {
end: Alignment.bottomRight,
colors: [
colorScheme.primary,
- colorScheme.primary.withOpacity(0.8),
+ colorScheme.primary.withValues(alpha: 0.8),
],
),
borderRadius: BorderRadius.circular(12),
boxShadow: [
BoxShadow(
- color: colorScheme.primary.withOpacity(0.3),
+ color: colorScheme.primary.withValues(alpha: 0.3),
blurRadius: 8,
offset: const Offset(0, 2),
),
@@ -152,7 +140,7 @@ class _SystemSettingsPageState extends State {
Text(
t.systemSettingsDescription,
style: theme.textTheme.bodyMedium?.copyWith(
- color: colorScheme.onSurface.withOpacity(0.7),
+ color: colorScheme.onSurface.withValues(alpha: 0.7),
fontSize: 14,
),
),
@@ -162,7 +150,7 @@ class _SystemSettingsPageState extends State {
Container(
padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 6),
decoration: BoxDecoration(
- color: colorScheme.primary.withOpacity(0.1),
+ color: colorScheme.primary.withValues(alpha: 0.1),
borderRadius: BorderRadius.circular(20),
),
child: Text(
@@ -196,7 +184,7 @@ class _SystemSettingsPageState extends State {
Container(
padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 4),
decoration: BoxDecoration(
- color: colorScheme.primary.withOpacity(0.1),
+ color: colorScheme.primary.withValues(alpha: 0.1),
borderRadius: BorderRadius.circular(12),
),
child: Text(
@@ -246,7 +234,7 @@ class _SystemSettingsPageState extends State {
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(12),
side: BorderSide(
- color: colorScheme.outline.withOpacity(0.2),
+ color: colorScheme.outline.withValues(alpha: 0.2),
width: 1,
),
),
@@ -255,8 +243,8 @@ class _SystemSettingsPageState extends State {
child: InkWell(
onTap: () => context.go(item.route!),
borderRadius: BorderRadius.circular(12),
- hoverColor: item.color.withOpacity(0.05),
- splashColor: item.color.withOpacity(0.1),
+ hoverColor: item.color.withValues(alpha: 0.05),
+ splashColor: item.color.withValues(alpha: 0.1),
child: AnimatedContainer(
duration: const Duration(milliseconds: 200),
padding: const EdgeInsets.all(16),
@@ -267,11 +255,11 @@ class _SystemSettingsPageState extends State {
duration: const Duration(milliseconds: 200),
padding: const EdgeInsets.all(12),
decoration: BoxDecoration(
- color: item.color.withOpacity(0.1),
+ color: item.color.withValues(alpha: 0.1),
borderRadius: BorderRadius.circular(8),
boxShadow: [
BoxShadow(
- color: item.color.withOpacity(0.1),
+ color: item.color.withValues(alpha: 0.1),
blurRadius: 4,
offset: const Offset(0, 2),
),
@@ -299,7 +287,7 @@ class _SystemSettingsPageState extends State {
Text(
_getLocalizedText(t, item.description),
style: theme.textTheme.bodySmall?.copyWith(
- color: colorScheme.onSurface.withOpacity(0.6),
+ color: colorScheme.onSurface.withValues(alpha: 0.6),
fontSize: 11,
),
textAlign: TextAlign.center,
@@ -311,7 +299,7 @@ class _SystemSettingsPageState extends State {
duration: const Duration(milliseconds: 200),
padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 4),
decoration: BoxDecoration(
- color: item.color.withOpacity(0.1),
+ color: item.color.withValues(alpha: 0.1),
borderRadius: BorderRadius.circular(12),
),
child: Icon(
@@ -346,41 +334,15 @@ class _SystemSettingsPageState extends State {
return t.systemLogs;
case 'systemLogsDescription':
return t.systemLogsDescription;
+ case 'emailSettings':
+ return t.emailSettings;
+ case 'emailSettingsDescription':
+ return t.emailSettingsDescription;
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 {
diff --git a/hesabixUI/hesabix_ui/lib/services/business_dashboard_service.dart b/hesabixUI/hesabix_ui/lib/services/business_dashboard_service.dart
new file mode 100644
index 0000000..c46cd2d
--- /dev/null
+++ b/hesabixUI/hesabix_ui/lib/services/business_dashboard_service.dart
@@ -0,0 +1,139 @@
+import 'package:dio/dio.dart';
+import '../core/api_client.dart';
+import '../models/business_dashboard_models.dart';
+
+class BusinessDashboardService {
+ final ApiClient _apiClient;
+
+ BusinessDashboardService(this._apiClient);
+
+ /// دریافت داشبورد کسب و کار
+ Future getDashboard(int businessId) async {
+ try {
+ final response = await _apiClient.post