progress in email servie and business dashboard

This commit is contained in:
Hesabix 2025-09-22 21:21:46 +03:30
parent dcada33b89
commit 798dd63627
73 changed files with 6369 additions and 558 deletions

View file

@ -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: '<h1>عنوان</h1><p>متن</p>',
);
```
### 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
**نویسنده**: تیم توسعه حسابیکس

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -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()

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -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 "عضو"

View file

@ -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 بماند
}

View file

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

View file

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

View file

@ -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."

View file

@ -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 "کد امنیتی نامعتبر است."

View file

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

View file

@ -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')

View file

@ -1,6 +1,5 @@
import 'dart:async';
import 'package:flutter/foundation.dart';
import 'package:shared_preferences/shared_preferences.dart';
class ReferralStore {

View file

@ -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<void>? _loadingCompleter;
bool get isLoading => _isLoading;
SplashController() {
_startTime = DateTime.now();
}
/// شروع loading با حداقل زمان نمایش
Future<void> startLoading() async {
_isLoading = true;
_loadingCompleter = Completer<void>();
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();
}
}

View file

@ -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"
}

View file

@ -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": "دسترسی غیرمجاز"
}

View file

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

View file

@ -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';
}

View file

@ -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 => 'دسترسی غیرمجاز';
}

View file

@ -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<MyApp> {
CalendarController? _calendarController;
ThemeController? _themeController;
AuthStore? _authStore;
bool _isLoading = true;
DateTime? _loadStartTime;
@override
void initState() {
super.initState();
LocaleController.load().then((c) {
_loadStartTime = DateTime.now();
_loadControllers();
}
Future<void> _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 = c
..addListener(() {
// Update ApiClient language header on change
ApiClient.setCurrentLocale(c.locale);
setState(() {});
});
ApiClient.setCurrentLocale(c.locale);
});
_controller = localeController;
_calendarController = calendarController;
_themeController = themeController;
_authStore = authStore;
});
CalendarController.load().then((cc) {
setState(() {
_calendarController = cc
..addListener(() {
// اضافه کردن listeners
_controller!.addListener(() {
ApiClient.setCurrentLocale(_controller!.locale);
setState(() {});
});
ApiClient.bindCalendarController(cc);
});
});
final tc = ThemeController();
tc.load().then((_) {
setState(() {
_themeController = tc
..addListener(() {
_calendarController!.addListener(() {
setState(() {});
});
});
});
final store = AuthStore();
store.load().then((_) {
setState(() {
_authStore = store
..addListener(() {
_themeController!.addListener(() {
setState(() {});
});
ApiClient.bindAuthStore(store);
_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: <RouteBase>[
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<MyApp> {
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<MyApp> {
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.)
],
),
],
);

View file

@ -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<String, dynamic> 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<String, dynamic>) {
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<String, dynamic> 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<String, dynamic> 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<Activity> recentActivities;
BusinessDashboardResponse({
required this.businessInfo,
required this.statistics,
required this.recentActivities,
});
factory BusinessDashboardResponse.fromJson(Map<String, dynamic> json) {
return BusinessDashboardResponse(
businessInfo: BusinessInfo.fromJson(json['business_info']),
statistics: BusinessStatistics.fromJson(json['statistics']),
recentActivities: (json['recent_activities'] as List<dynamic>)
.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<String, dynamic> 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<String, dynamic> 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<String, dynamic>) {
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<String, dynamic>.from(json['permissions'] ?? {}),
joinedAt: joinedAt,
);
}
}
class BusinessMembersResponse {
final List<BusinessMember> items;
final Map<String, dynamic> pagination;
BusinessMembersResponse({
required this.items,
required this.pagination,
});
factory BusinessMembersResponse.fromJson(Map<String, dynamic> json) {
return BusinessMembersResponse(
items: (json['items'] as List<dynamic>)
.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<String, dynamic> 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<String, dynamic> 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<String, dynamic>) {
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<String, dynamic>.from(json['permissions'] ?? {}),
);
}
}

View file

@ -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<String, dynamic> 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<String, dynamic>) {
// Handle formatted date object
final formatted = dateValue['formatted'] as String?;
if (formatted != null) {
return DateTime.parse(formatted);
}
}
return DateTime.now();
}
Map<String, dynamic> 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<String, dynamic> 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<String, dynamic> toJson() {
final Map<String, dynamic> 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<String, dynamic> 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<EmailConfig> data;
final String message;
EmailConfigListResponse({
required this.success,
required this.data,
required this.message,
});
factory EmailConfigListResponse.fromJson(Map<String, dynamic> json) {
return EmailConfigListResponse(
success: json['success'] as bool? ?? true,
data: (json['data'] as List? ?? [])
.map((item) => EmailConfig.fromJson(item as Map<String, dynamic>))
.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<String, dynamic> json) {
return EmailConfigResponse(
success: json['success'] as bool? ?? true,
data: EmailConfig.fromJson(json['data'] as Map<String, dynamic>),
message: json['message'] as String? ?? '',
);
}
}
class SendEmailResponse {
final bool success;
final String message;
SendEmailResponse({
required this.success,
required this.message,
});
factory SendEmailResponse.fromJson(Map<String, dynamic> 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<String, dynamic> json) {
return TestConnectionResponse(
success: json['success'] as bool? ?? true,
message: json['message'] as String? ?? '',
connected: json['connected'] as bool? ?? false,
);
}
}

View file

@ -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<EmailSettingsPage> createState() => _EmailSettingsPageState();
}
class _EmailSettingsPageState extends State<EmailSettingsPage> {
final EmailService _emailService = EmailService();
final _formKey = GlobalKey<FormState>();
bool _isLoading = false;
bool _isTesting = false;
List<EmailConfig> _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<void> _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<void> _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<void> _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<void> _setAsDefault(EmailConfig config) async {
final t = AppLocalizations.of(context);
final confirmed = await showDialog<bool>(
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<void> _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<void> _selectConfig(EmailConfig config) async {
setState(() => _selectedConfig = config);
}
Future<void> _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<void> _deleteConfig(int configId) async {
final t = AppLocalizations.of(context);
final confirmed = await showDialog<bool>(
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());
}
}
}
}

View file

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

View file

@ -42,7 +42,7 @@ class _AdminStorageManagementPageState extends State<AdminStorageManagementPage>
begin: Alignment.topCenter,
end: Alignment.bottomCenter,
colors: [
theme.colorScheme.primary.withOpacity(0.1),
theme.colorScheme.primary.withValues(alpha: 0.1),
theme.colorScheme.surface,
],
),

View file

@ -65,7 +65,7 @@ class _SystemConfigurationPageState extends State<SystemConfigurationPage> {
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<SystemConfigurationPage> {
return Padding(
padding: const EdgeInsets.only(bottom: 16),
child: DropdownButtonFormField<String>(
value: value,
initialValue: value,
decoration: InputDecoration(
labelText: label,
border: const OutlineInputBorder(),

View file

@ -115,7 +115,7 @@ class _SystemLogsPageState extends State<SystemLogsPage> {
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<SystemLogsPage> {
children: [
Expanded(
child: DropdownButtonFormField<String>(
value: _selectedLevel,
initialValue: _selectedLevel,
decoration: const InputDecoration(
labelText: 'Log Level',
border: OutlineInputBorder(),
@ -180,7 +180,7 @@ class _SystemLogsPageState extends State<SystemLogsPage> {
const SizedBox(width: 16),
Expanded(
child: DropdownButtonFormField<String>(
value: _selectedDateRange,
initialValue: _selectedDateRange,
decoration: const InputDecoration(
labelText: 'Date Range',
border: OutlineInputBorder(),
@ -212,13 +212,13 @@ class _SystemLogsPageState extends State<SystemLogsPage> {
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<SystemLogsPage> {
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<SystemLogsPage> {
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(

View file

@ -96,7 +96,7 @@ class _UserManagementPageState extends State<UserManagementPage> {
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<UserManagementPage> {
children: [
Expanded(
child: DropdownButtonFormField<String>(
value: _selectedFilter,
initialValue: _selectedFilter,
decoration: const InputDecoration(
labelText: 'Filter',
border: OutlineInputBorder(),
@ -190,13 +190,13 @@ class _UserManagementPageState extends State<UserManagementPage> {
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<UserManagementPage> {
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)),
);
}

View file

@ -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<BusinessShell> createState() => _BusinessShellState();
}
class _BusinessShellState extends State<BusinessShell> {
@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<void> onSelect(int index) async {
final path = destinations[index].path;
if (GoRouterState.of(context).uri.toString() != path) {
context.go(path);
}
}
Future<void> 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);
}

View file

@ -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<BusinessDashboardPage> createState() => _BusinessDashboardPageState();
}
class _BusinessDashboardPageState extends State<BusinessDashboardPage> {
final BusinessDashboardService _service = BusinessDashboardService(ApiClient());
BusinessDashboardResponse? _dashboardData;
bool _loading = true;
String? _error;
@override
void initState() {
super.initState();
_loadDashboard();
}
Future<void> _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<Activity> 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;
}
}
}

View file

@ -43,7 +43,6 @@ class _ThemeMenu extends StatelessWidget {
Widget build(BuildContext context) {
return PopupMenuButton<ThemeMode>(
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')),

View file

@ -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<LoginPage> with SingleTickerProviderStateMix
_showSnack(t.registerSuccess);
// پاکسازی کد معرف پس از ثبتنام موفق
unawaited(ReferralStore.clearReferrer());
if (mounted) {
context.go('/user/profile/dashboard');
}
} catch (e) {
if (!mounted) return;
final msg = _extractErrorMessage(e, AppLocalizations.of(context));

View file

@ -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<BusinessesPage> createState() => _BusinessesPageState();
}
class _BusinessesPageState extends State<BusinessesPage> {
final BusinessDashboardService _service = BusinessDashboardService(ApiClient());
List<BusinessWithPermission> _businesses = [];
bool _loading = true;
String? _error;
@override
void initState() {
super.initState();
_loadBusinesses();
}
Future<void> _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;
}
}
}

View file

@ -59,7 +59,7 @@ class _CreateTicketPageState extends State<CreateTicketPage> {
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<CreateTicketPage> {
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<CreateTicketPage> {
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<CreateTicketPage> {
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<CreateTicketPage> {
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<CreateTicketPage> {
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<CreateTicketPage> {
),
const SizedBox(height: 8),
DropdownButtonFormField<SupportCategory>(
value: _selectedCategory,
initialValue: _selectedCategory,
decoration: InputDecoration(
border: OutlineInputBorder(
borderRadius: BorderRadius.circular(12),
@ -445,7 +445,7 @@ class _CreateTicketPageState extends State<CreateTicketPage> {
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<CreateTicketPage> {
),
const SizedBox(height: 8),
DropdownButtonFormField<SupportPriority>(
value: _selectedPriority,
initialValue: _selectedPriority,
decoration: InputDecoration(
border: OutlineInputBorder(
borderRadius: BorderRadius.circular(12),
@ -501,7 +501,7 @@ class _CreateTicketPageState extends State<CreateTicketPage> {
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<CreateTicketPage> {
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<CreateTicketPage> {
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<CreateTicketPage> {
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),
),

View file

@ -189,9 +189,9 @@ class _MarketingPageState extends State<MarketingPage> {
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(

View file

@ -633,7 +633,7 @@ class _NewBusinessPageState extends State<NewBusinessPage> {
),
),
),
value: _businessData.businessType,
initialValue: _businessData.businessType,
items: BusinessType.values.map((type) {
return DropdownMenuItem(
value: type,
@ -675,7 +675,7 @@ class _NewBusinessPageState extends State<NewBusinessPage> {
),
),
),
value: _businessData.businessField,
initialValue: _businessData.businessField,
items: BusinessField.values.map((field) {
return DropdownMenuItem(
value: field,

View file

@ -5,16 +5,13 @@ class ProfileDashboardPage extends StatelessWidget {
@override
Widget build(BuildContext context) {
return Padding(
padding: const EdgeInsets.all(16.0),
child: Column(
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.'),
],
),
);
}
}

View file

@ -152,11 +152,8 @@ class _ProfileShellState extends State<ProfileShell> {
final content = Container(
color: scheme.surface,
child: SafeArea(
child: Padding(
padding: const EdgeInsets.all(16),
child: widget.child,
),
),
);
// Side colors and styles

View file

@ -44,6 +44,13 @@ class _SystemSettingsPageState extends State<SystemSettingsPage> {
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<SystemSettingsPage> {
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<SystemSettingsPage> {
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<SystemSettingsPage> {
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<SystemSettingsPage> {
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<SystemSettingsPage> {
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<SystemSettingsPage> {
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<SystemSettingsPage> {
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<SystemSettingsPage> {
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<SystemSettingsPage> {
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<SystemSettingsPage> {
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<SystemSettingsPage> {
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<SystemSettingsPage> {
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 {

View file

@ -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<BusinessDashboardResponse> getDashboard(int businessId) async {
try {
final response = await _apiClient.post<Map<String, dynamic>>(
'/api/v1/business/$businessId/dashboard',
);
if (response.data?['success'] == true) {
return BusinessDashboardResponse.fromJson(response.data!['data']);
} else {
throw Exception('Failed to load dashboard: ${response.data?['message']}');
}
} on DioException catch (e) {
if (e.response?.statusCode == 403) {
throw Exception('دسترسی غیرمجاز به این کسب و کار');
} else if (e.response?.statusCode == 404) {
throw Exception('کسب و کار یافت نشد');
} else {
throw Exception('خطا در بارگذاری داشبورد: ${e.message}');
}
} catch (e) {
throw Exception('خطا در بارگذاری داشبورد: $e');
}
}
/// دریافت لیست اعضای کسب و کار
Future<BusinessMembersResponse> getMembers(int businessId) async {
try {
final response = await _apiClient.post<Map<String, dynamic>>(
'/api/v1/business/$businessId/members',
);
if (response.data?['success'] == true) {
return BusinessMembersResponse.fromJson(response.data!['data']);
} else {
throw Exception('Failed to load members: ${response.data?['message']}');
}
} on DioException catch (e) {
if (e.response?.statusCode == 403) {
throw Exception('دسترسی غیرمجاز به این کسب و کار');
} else if (e.response?.statusCode == 404) {
throw Exception('کسب و کار یافت نشد');
} else {
throw Exception('خطا در بارگذاری اعضا: ${e.message}');
}
} catch (e) {
throw Exception('خطا در بارگذاری اعضا: $e');
}
}
/// دریافت آمار کسب و کار
Future<Map<String, dynamic>> getStatistics(int businessId) async {
try {
final response = await _apiClient.post<Map<String, dynamic>>(
'/api/v1/business/$businessId/statistics',
);
if (response.data?['success'] == true) {
return response.data!['data'];
} else {
throw Exception('Failed to load statistics: ${response.data?['message']}');
}
} on DioException catch (e) {
if (e.response?.statusCode == 403) {
throw Exception('دسترسی غیرمجاز به این کسب و کار');
} else if (e.response?.statusCode == 404) {
throw Exception('کسب و کار یافت نشد');
} else {
throw Exception('خطا در بارگذاری آمار: ${e.message}');
}
} catch (e) {
throw Exception('خطا در بارگذاری آمار: $e');
}
}
/// دریافت لیست کسب و کارهای کاربر (مالک + عضو)
Future<List<BusinessWithPermission>> getUserBusinesses() async {
try {
// دریافت کسب و کارهای مالک با POST request
final ownedResponse = await _apiClient.post<Map<String, dynamic>>(
'/api/v1/businesses/list',
data: {
'take': 100,
'skip': 0,
'sort_by': 'created_at',
'sort_desc': true,
'search': null,
},
);
List<BusinessWithPermission> businesses = [];
if (ownedResponse.data?['success'] == true) {
final ownedItems = ownedResponse.data!['data']['items'] as List<dynamic>;
businesses.addAll(
ownedItems.map((item) {
final business = BusinessWithPermission.fromJson(item);
return BusinessWithPermission(
id: business.id,
name: business.name,
businessType: business.businessType,
businessField: business.businessField,
ownerId: business.ownerId,
address: business.address,
phone: business.phone,
mobile: business.mobile,
createdAt: business.createdAt,
isOwner: true,
role: 'مالک',
permissions: {},
);
}),
);
}
// TODO: در آینده میتوان کسب و کارهای عضو را نیز اضافه کرد
// از API endpoint جدید برای کسب و کارهای عضو
return businesses;
} on DioException catch (e) {
if (e.response?.statusCode == 401) {
throw Exception('لطفاً ابتدا وارد شوید');
} else {
throw Exception('خطا در بارگذاری کسب و کارها: ${e.message}');
}
} catch (e) {
throw Exception('خطا در بارگذاری کسب و کارها: $e');
}
}
}

View file

@ -0,0 +1,248 @@
import 'package:dio/dio.dart';
import '../core/api_client.dart';
import '../models/email_models.dart';
class EmailService {
static final EmailService _instance = EmailService._internal();
factory EmailService() => _instance;
EmailService._internal();
late final ApiClient _apiClient;
void _initializeApiClient() {
_apiClient = ApiClient();
}
void _ensureApiClientInitialized() {
try {
_apiClient;
} catch (e) {
_initializeApiClient();
}
}
/// Send email using configured SMTP
Future<SendEmailResponse> sendEmail(SendEmailRequest request) async {
_ensureApiClientInitialized();
try {
final response = await _apiClient.post<Map<String, dynamic>>(
'/api/v1/admin/email/send',
data: request.toJson(),
);
return SendEmailResponse.fromJson(response.data!);
} on DioException catch (e) {
throw _handleError(e);
}
}
/// Send welcome email to new user
Future<SendEmailResponse> sendWelcomeEmail(String userEmail, String userName) async {
_ensureApiClientInitialized();
return sendEmail(SendEmailRequest(
to: userEmail,
subject: 'خوش آمدید به حسابیکس',
body: 'سلام $userName،\n\nبه حسابیکس خوش آمدید! امیدواریم تجربه خوبی داشته باشید.\n\nبا احترام\nتیم حسابیکس',
htmlBody: '''
<h2>خوش آمدید به حسابیکس</h2>
<p>سلام $userName،</p>
<p>به حسابیکس خوش آمدید! امیدواریم تجربه خوبی داشته باشید.</p>
<p>با احترام<br>تیم حسابیکس</p>
''',
));
}
/// Send password reset email
Future<SendEmailResponse> sendPasswordResetEmail(String userEmail, String resetLink) async {
_ensureApiClientInitialized();
return sendEmail(SendEmailRequest(
to: userEmail,
subject: 'بازیابی رمز عبور',
body: 'برای بازیابی رمز عبور روی لینک زیر کلیک کنید:\n\n$resetLink\n\nاین لینک تا 1 ساعت معتبر است.',
htmlBody: '''
<h2>بازیابی رمز عبور</h2>
<p>برای بازیابی رمز عبور روی لینک زیر کلیک کنید:</p>
<p><a href="$resetLink" style="background-color: #007bff; color: white; padding: 10px 20px; text-decoration: none; border-radius: 5px;">بازیابی رمز عبور</a></p>
<p>این لینک تا 1 ساعت معتبر است.</p>
''',
));
}
/// Send notification email
Future<SendEmailResponse> sendNotificationEmail(String userEmail, String title, String message) async {
_ensureApiClientInitialized();
return sendEmail(SendEmailRequest(
to: userEmail,
subject: title,
body: message,
htmlBody: '''
<h2>$title</h2>
<p>$message</p>
''',
));
}
/// Send custom email
Future<SendEmailResponse> sendCustomEmail({
required String to,
required String subject,
required String body,
String? htmlBody,
int? configId,
}) async {
_ensureApiClientInitialized();
return sendEmail(SendEmailRequest(
to: to,
subject: subject,
body: body,
htmlBody: htmlBody,
configId: configId,
));
}
/// Get all email configurations
Future<EmailConfigListResponse> getEmailConfigs() async {
_ensureApiClientInitialized();
try {
final response = await _apiClient.get<Map<String, dynamic>>(
'/api/v1/admin/email/configs',
);
final data = response.data!;
return EmailConfigListResponse(
success: data['success'] as bool? ?? true,
data: (data['data'] as List? ?? [])
.map((item) => EmailConfig.fromJson(item as Map<String, dynamic>))
.toList(),
message: data['message'] as String? ?? '',
);
} on DioException catch (e) {
throw _handleError(e);
}
}
/// Get specific email configuration
Future<EmailConfigResponse> getEmailConfig(int configId) async {
_ensureApiClientInitialized();
try {
final response = await _apiClient.get<Map<String, dynamic>>(
'/api/v1/admin/email/configs/$configId',
);
final data = response.data!;
return EmailConfigResponse(
success: data['success'] as bool? ?? true,
data: EmailConfig.fromJson(data['data'] as Map<String, dynamic>),
message: data['message'] as String? ?? '',
);
} on DioException catch (e) {
throw _handleError(e);
}
}
/// Create new email configuration
Future<EmailConfigResponse> createEmailConfig(CreateEmailConfigRequest request) async {
_ensureApiClientInitialized();
try {
final response = await _apiClient.post<Map<String, dynamic>>(
'/api/v1/admin/email/configs',
data: request.toJson(),
);
final data = response.data!;
return EmailConfigResponse(
success: data['success'] as bool? ?? true,
data: EmailConfig.fromJson(data['data'] as Map<String, dynamic>),
message: data['message'] as String? ?? '',
);
} on DioException catch (e) {
throw _handleError(e);
}
}
/// Update email configuration
Future<EmailConfigResponse> updateEmailConfig(int configId, UpdateEmailConfigRequest request) async {
_ensureApiClientInitialized();
try {
final response = await _apiClient.put<Map<String, dynamic>>(
'/api/v1/admin/email/configs/$configId',
data: request.toJson(),
);
final data = response.data!;
return EmailConfigResponse(
success: data['success'] as bool? ?? true,
data: EmailConfig.fromJson(data['data'] as Map<String, dynamic>),
message: data['message'] as String? ?? '',
);
} on DioException catch (e) {
throw _handleError(e);
}
}
/// Delete email configuration
Future<void> deleteEmailConfig(int configId) async {
_ensureApiClientInitialized();
try {
await _apiClient.delete('/api/v1/admin/email/configs/$configId');
} on DioException catch (e) {
throw _handleError(e);
}
}
/// Test email configuration connection
Future<TestConnectionResponse> testEmailConfig(int configId) async {
_ensureApiClientInitialized();
try {
final response = await _apiClient.post<Map<String, dynamic>>(
'/api/v1/admin/email/configs/$configId/test',
);
final data = response.data!;
return TestConnectionResponse(
success: data['success'] as bool? ?? true,
message: data['message'] as String? ?? '',
connected: data['data']?['connected'] as bool? ?? false,
);
} on DioException catch (e) {
throw _handleError(e);
}
}
/// Activate email configuration
Future<void> activateEmailConfig(int configId) async {
_ensureApiClientInitialized();
try {
await _apiClient.post('/api/v1/admin/email/configs/$configId/activate');
} on DioException catch (e) {
throw _handleError(e);
}
}
/// Set email configuration as default
Future<void> setDefaultEmailConfig(int configId) async {
_ensureApiClientInitialized();
try {
await _apiClient.post('/api/v1/admin/email/configs/$configId/set-default');
} on DioException catch (e) {
throw _handleError(e);
}
}
/// Handle API errors
String _handleError(DioException e) {
if (e.response != null) {
final data = e.response!.data;
if (data is Map<String, dynamic> && data.containsKey('detail')) {
return data['detail'] as String;
}
return 'خطا در ارتباط با سرور: ${e.response!.statusCode}';
} else if (e.type == DioExceptionType.connectionTimeout) {
return 'خطا در اتصال به سرور - لطفاً اتصال اینترنت خود را بررسی کنید';
} else if (e.type == DioExceptionType.receiveTimeout) {
return 'زمان دریافت پاسخ از سرور به پایان رسید';
} else {
return 'خطای نامشخص: ${e.message}';
}
}
}

View file

@ -36,7 +36,7 @@ class ThemeController extends ChangeNotifier {
_seed = c;
notifyListeners();
final p = await SharedPreferences.getInstance();
await p.setInt(_seedKey, c.value);
await p.setInt(_seedKey, c.toARGB32());
}
}

View file

@ -41,8 +41,8 @@ class StorageConfigCard extends StatelessWidget {
begin: Alignment.topLeft,
end: Alignment.bottomRight,
colors: [
theme.colorScheme.primary.withOpacity(0.05),
theme.colorScheme.primary.withOpacity(0.02),
theme.colorScheme.primary.withValues(alpha: 0.05),
theme.colorScheme.primary.withValues(alpha: 0.02),
],
)
: null,
@ -58,7 +58,7 @@ class StorageConfigCard extends StatelessWidget {
Container(
padding: const EdgeInsets.all(12),
decoration: BoxDecoration(
color: _getStorageColor(storageType).withOpacity(0.1),
color: _getStorageColor(storageType).withValues(alpha: 0.1),
borderRadius: BorderRadius.circular(12),
),
child: Icon(
@ -255,13 +255,13 @@ class StorageConfigCard extends StatelessWidget {
return Container(
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(
'نوع ذخیره‌سازی نامشخص',
style: theme.textTheme.bodyMedium?.copyWith(
color: theme.colorScheme.onSurface.withOpacity(0.6),
color: theme.colorScheme.onSurface.withValues(alpha: 0.6),
),
),
);
@ -276,10 +276,10 @@ class StorageConfigCard extends StatelessWidget {
return Container(
padding: const EdgeInsets.all(16),
decoration: BoxDecoration(
color: theme.colorScheme.surfaceVariant.withOpacity(0.3),
color: theme.colorScheme.surfaceContainerHighest.withValues(alpha: 0.3),
borderRadius: BorderRadius.circular(12),
border: Border.all(
color: theme.colorScheme.outline.withOpacity(0.2),
color: theme.colorScheme.outline.withValues(alpha: 0.2),
),
),
child: Column(
@ -306,7 +306,7 @@ class StorageConfigCard extends StatelessWidget {
Text(
l10n.basePath,
style: theme.textTheme.bodySmall?.copyWith(
color: theme.colorScheme.onSurface.withOpacity(0.6),
color: theme.colorScheme.onSurface.withValues(alpha: 0.6),
),
),
const SizedBox(height: 4),
@ -316,7 +316,7 @@ class StorageConfigCard extends StatelessWidget {
color: theme.colorScheme.surface,
borderRadius: BorderRadius.circular(6),
border: Border.all(
color: theme.colorScheme.outline.withOpacity(0.3),
color: theme.colorScheme.outline.withValues(alpha: 0.3),
),
),
child: Row(
@ -355,10 +355,10 @@ class StorageConfigCard extends StatelessWidget {
return Container(
padding: const EdgeInsets.all(16),
decoration: BoxDecoration(
color: theme.colorScheme.surfaceVariant.withOpacity(0.3),
color: theme.colorScheme.surfaceContainerHighest.withValues(alpha: 0.3),
borderRadius: BorderRadius.circular(12),
border: Border.all(
color: theme.colorScheme.outline.withOpacity(0.2),
color: theme.colorScheme.outline.withValues(alpha: 0.2),
),
),
child: Column(
@ -435,7 +435,7 @@ class StorageConfigCard extends StatelessWidget {
Text(
'$label: ',
style: theme.textTheme.bodySmall?.copyWith(
color: theme.colorScheme.onSurface.withOpacity(0.6),
color: theme.colorScheme.onSurface.withValues(alpha: 0.6),
),
),
Expanded(

View file

@ -252,43 +252,33 @@ class _StorageConfigFormDialogState extends State<StorageConfigFormDialog> {
),
),
const SizedBox(height: 8),
Row(
Column(
children: [
Expanded(
child: RadioListTile<String>(
title: Row(
children: [
Icon(Icons.storage, size: 20),
const SizedBox(width: 8),
Text(l10n.localStorage),
],
),
ListTile(
leading: Radio<String>(
value: 'local',
// ignore: deprecated_member_use
groupValue: _selectedStorageType,
onChanged: (value) {
setState(() {
_selectedStorageType = value!;
});
},
// ignore: deprecated_member_use
onChanged: (value) => setState(() => _selectedStorageType = value!),
),
title: Text(l10n.localStorage),
trailing: Icon(Icons.storage, size: 20),
onTap: () => setState(() => _selectedStorageType = 'local'),
contentPadding: EdgeInsets.zero,
),
Expanded(
child: RadioListTile<String>(
title: Row(
children: [
Icon(Icons.cloud_upload, size: 20),
const SizedBox(width: 8),
Text('سرور FTP'),
],
),
ListTile(
leading: Radio<String>(
value: 'ftp',
// ignore: deprecated_member_use
groupValue: _selectedStorageType,
onChanged: (value) {
setState(() {
_selectedStorageType = value!;
});
},
// ignore: deprecated_member_use
onChanged: (value) => setState(() => _selectedStorageType = value!),
),
title: Text('سرور FTP'),
trailing: Icon(Icons.cloud_upload, size: 20),
onTap: () => setState(() => _selectedStorageType = 'ftp'),
contentPadding: EdgeInsets.zero,
),
],
),
@ -344,7 +334,7 @@ class _StorageConfigFormDialogState extends State<StorageConfigFormDialog> {
Container(
padding: const EdgeInsets.all(24),
decoration: BoxDecoration(
color: theme.colorScheme.surfaceVariant.withOpacity(0.3),
color: theme.colorScheme.surfaceContainerHighest.withValues(alpha: 0.3),
borderRadius: const BorderRadius.only(
bottomLeft: Radius.circular(20),
bottomRight: Radius.circular(20),

View file

@ -58,6 +58,7 @@ class StorageConfigListWidgetState extends State<StorageConfigListWidget> {
final api = ApiClient();
final response = await api.post('/api/v1/admin/files/storage-configs/$configId/test');
if (!mounted) return;
if (response.data != null && response.data['success'] == true) {
final testResult = response.data['data']['test_result'];
if (testResult['success'] == true) {
@ -79,6 +80,7 @@ class StorageConfigListWidgetState extends State<StorageConfigListWidget> {
throw Exception(response.data?['message'] ?? 'خطا در تست اتصال');
}
} catch (e) {
if (!mounted) return;
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Text('اتصال ناموفق: $e'),
@ -112,6 +114,7 @@ class StorageConfigListWidgetState extends State<StorageConfigListWidget> {
final api = ApiClient();
final response = await api.delete('/api/v1/admin/files/storage-configs/$configId');
if (!mounted) return;
if (response.data != null && response.data['success'] == true) {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
@ -142,6 +145,7 @@ class StorageConfigListWidgetState extends State<StorageConfigListWidget> {
errorMessage = e.toString().replaceFirst('Exception: ', '');
}
if (!mounted) return;
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Text(errorMessage),
@ -158,6 +162,7 @@ class StorageConfigListWidgetState extends State<StorageConfigListWidget> {
final api = ApiClient();
final response = await api.put('/api/v1/admin/files/storage-configs/$configId/set-default');
if (!mounted) return;
if (response.data != null && response.data['success'] == true) {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
@ -172,6 +177,7 @@ class StorageConfigListWidgetState extends State<StorageConfigListWidget> {
throw Exception(response.data?['message'] ?? 'خطا در تنظیم به عنوان پیش‌فرض');
}
} catch (e) {
if (!mounted) return;
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Text('خطا در تنظیم به عنوان پیش‌فرض: $e'),
@ -264,20 +270,20 @@ class StorageConfigListWidgetState extends State<StorageConfigListWidget> {
Icon(
Icons.storage_outlined,
size: 64,
color: theme.colorScheme.primary.withOpacity(0.5),
color: theme.colorScheme.primary.withValues(alpha: 0.5),
),
const SizedBox(height: 16),
Text(
'هیچ پیکربندی ذخیره‌سازی وجود ندارد',
style: theme.textTheme.headlineSmall?.copyWith(
color: theme.colorScheme.onSurface.withOpacity(0.6),
color: theme.colorScheme.onSurface.withValues(alpha: 0.6),
),
),
const SizedBox(height: 8),
Text(
'اولین پیکربندی ذخیره‌سازی را ایجاد کنید',
style: theme.textTheme.bodyMedium?.copyWith(
color: theme.colorScheme.onSurface.withOpacity(0.6),
color: theme.colorScheme.onSurface.withValues(alpha: 0.6),
),
textAlign: TextAlign.center,
),
@ -285,7 +291,7 @@ class StorageConfigListWidgetState extends State<StorageConfigListWidget> {
Text(
'از دکمه + در پایین صفحه استفاده کنید',
style: theme.textTheme.bodySmall?.copyWith(
color: theme.colorScheme.onSurface.withOpacity(0.5),
color: theme.colorScheme.onSurface.withValues(alpha: 0.5),
fontStyle: FontStyle.italic,
),
),
@ -321,7 +327,7 @@ class StorageConfigListWidgetState extends State<StorageConfigListWidget> {
Text(
'${_storageConfigs.length} پیکربندی',
style: theme.textTheme.bodyMedium?.copyWith(
color: theme.colorScheme.onSurface.withOpacity(0.6),
color: theme.colorScheme.onSurface.withValues(alpha: 0.6),
),
),
],

View file

@ -35,7 +35,7 @@ class _StorageManagementPageState extends State<StorageManagementPage> {
begin: Alignment.topCenter,
end: Alignment.bottomCenter,
colors: [
theme.colorScheme.primary.withOpacity(0.1),
theme.colorScheme.primary.withValues(alpha: 0.1),
theme.colorScheme.surface,
],
),

View file

@ -43,7 +43,7 @@ class DataTableSearchDialog extends StatefulWidget {
class _DataTableSearchDialogState extends State<DataTableSearchDialog> {
late TextEditingController _controller;
late String _selectedType;
Set<String> _selectedValues = <String>{};
final Set<String> _selectedValues = <String>{};
DateTime? _fromDate;
DateTime? _toDate;
@ -118,7 +118,7 @@ class _DataTableSearchDialogState extends State<DataTableSearchDialog> {
return [
// Search type dropdown
DropdownButtonFormField<String>(
value: _selectedType,
initialValue: _selectedType,
decoration: InputDecoration(
labelText: t.searchType,
border: const OutlineInputBorder(),
@ -163,27 +163,7 @@ class _DataTableSearchDialogState extends State<DataTableSearchDialog> {
subtitle: Text(_fromDate != null
? HesabixDateUtils.formatForDisplay(_fromDate!, isJalali)
: t.selectDate),
onTap: () async {
final date = isJalali
? await showJalaliDatePicker(
context: context,
initialDate: _fromDate ?? DateTime.now(),
firstDate: DateTime(2000),
lastDate: DateTime.now().add(const Duration(days: 365)),
helpText: t.dateFrom,
)
: await showDatePicker(
context: context,
initialDate: _fromDate ?? DateTime.now(),
firstDate: DateTime(2000),
lastDate: DateTime.now().add(const Duration(days: 365)),
);
if (date != null) {
setState(() {
_fromDate = date;
});
}
},
onTap: () => _selectFromDate(t, isJalali),
),
// To date
ListTile(
@ -192,27 +172,7 @@ class _DataTableSearchDialogState extends State<DataTableSearchDialog> {
subtitle: Text(_toDate != null
? HesabixDateUtils.formatForDisplay(_toDate!, isJalali)
: t.selectDate),
onTap: () async {
final date = isJalali
? await showJalaliDatePicker(
context: context,
initialDate: _toDate ?? _fromDate ?? DateTime.now(),
firstDate: _fromDate ?? DateTime(2000),
lastDate: DateTime.now().add(const Duration(days: 365)),
helpText: t.dateTo,
)
: await showDatePicker(
context: context,
initialDate: _toDate ?? _fromDate ?? DateTime.now(),
firstDate: _fromDate ?? DateTime(2000),
lastDate: DateTime.now().add(const Duration(days: 365)),
);
if (date != null) {
setState(() {
_toDate = date;
});
}
},
onTap: () => _selectToDate(t, isJalali),
),
];
}
@ -344,6 +304,56 @@ class _DataTableSearchDialogState extends State<DataTableSearchDialog> {
}
Navigator.of(context).pop();
}
Future<void> _selectFromDate(AppLocalizations t, bool isJalali) async {
final currentContext = context;
final date = isJalali
? await showJalaliDatePicker(
// ignore: use_build_context_synchronously
context: currentContext,
initialDate: _fromDate ?? DateTime.now(),
firstDate: DateTime(2000),
lastDate: DateTime.now().add(const Duration(days: 365)),
helpText: t.dateFrom,
)
: await showDatePicker(
// ignore: use_build_context_synchronously
context: currentContext,
initialDate: _fromDate ?? DateTime.now(),
firstDate: DateTime(2000),
lastDate: DateTime.now().add(const Duration(days: 365)),
);
if (date != null && mounted) {
setState(() {
_fromDate = date;
});
}
}
Future<void> _selectToDate(AppLocalizations t, bool isJalali) async {
final currentContext = context;
final date = isJalali
? await showJalaliDatePicker(
// ignore: use_build_context_synchronously
context: currentContext,
initialDate: _toDate ?? _fromDate ?? DateTime.now(),
firstDate: _fromDate ?? DateTime(2000),
lastDate: DateTime.now().add(const Duration(days: 365)),
helpText: t.dateTo,
)
: await showDatePicker(
// ignore: use_build_context_synchronously
context: currentContext,
initialDate: _toDate ?? _fromDate ?? DateTime.now(),
firstDate: _fromDate ?? DateTime(2000),
lastDate: DateTime.now().add(const Duration(days: 365)),
);
if (date != null && mounted) {
setState(() {
_toDate = date;
});
}
}
}
/// Dialog for date range filter

View file

@ -1,5 +1,4 @@
import 'dart:async';
import 'dart:typed_data';
// import 'dart:html' as html; // Not available on Linux
import 'package:flutter/material.dart';
import 'package:data_table_2/data_table_2.dart';
@ -63,7 +62,7 @@ class _DataTableWidgetState<T> extends State<DataTableWidget<T>> {
bool _sortDesc = false;
// Row selection state
Set<int> _selectedRows = <int>{};
final Set<int> _selectedRows = <int>{};
bool _isExporting = false;
// Column settings state
@ -144,7 +143,7 @@ class _DataTableWidgetState<T> extends State<DataTableWidget<T>> {
_visibleColumns = _getVisibleColumnsFromSettings(effectiveSettings);
});
} catch (e) {
print('Error loading column settings: $e');
debugPrint('Error loading column settings: $e');
setState(() {
_visibleColumns = List.from(widget.config.columns);
});
@ -466,7 +465,7 @@ class _DataTableWidgetState<T> extends State<DataTableWidget<T>> {
widget.config.onColumnSettingsChanged!(validatedSettings);
}
} catch (e) {
print('Error saving column settings: $e');
debugPrint('Error saving column settings: $e');
if (mounted) {
final t = Localizations.of<AppLocalizations>(context, AppLocalizations)!;
final messenger = ScaffoldMessenger.of(context);
@ -624,43 +623,16 @@ class _DataTableWidgetState<T> extends State<DataTableWidget<T>> {
// Platform-specific download functions for Linux
Future<void> _downloadPdf(dynamic data, String filename) async {
// For Linux desktop, we'll save to Downloads folder
print('Download PDF: $filename (Linux desktop - save to Downloads folder)');
debugPrint('Download PDF: $filename (Linux desktop - save to Downloads folder)');
// TODO: Implement proper file saving for Linux
}
Future<void> _downloadExcel(dynamic data, String filename) async {
// For Linux desktop, we'll save to Downloads folder
print('Download Excel: $filename (Linux desktop - save to Downloads folder)');
debugPrint('Download Excel: $filename (Linux desktop - save to Downloads folder)');
// TODO: Implement proper file saving for Linux
}
String _convertToCsv(List<dynamic> data) {
if (data.isEmpty) return '';
// Get headers from first item
final firstItem = data.first as Map<String, dynamic>;
final headers = firstItem.keys.toList();
// Create CSV content
final csvLines = <String>[];
// Add headers
csvLines.add(headers.join(','));
// Add data rows
for (final item in data) {
final row = <String>[];
for (final header in headers) {
final value = item[header]?.toString() ?? '';
// Escape commas and quotes
final escapedValue = value.replaceAll('"', '""');
row.add('"$escapedValue"');
}
csvLines.add(row.join(','));
}
return csvLines.join('\n');
}
@override
Widget build(BuildContext context) {
@ -1451,7 +1423,7 @@ class _ColumnHeaderWithSearch extends StatelessWidget {
onTap: enabled ? () => onSort(sortBy) : null,
child: Padding(
padding: const EdgeInsets.symmetric(vertical: 8.0),
child: Container(
child: SizedBox(
width: double.infinity,
child: Row(
mainAxisSize: MainAxisSize.max,

View file

@ -21,7 +21,7 @@ class DataTableExampleUsage {
DataTableAction(
icon: Icons.edit,
label: 'ویرایش',
onTap: (user) => print('Edit user: ${user.id}'),
onTap: (user) => debugPrint('Edit user: ${user.id}'),
),
]),
],
@ -51,7 +51,7 @@ class DataTableExampleUsage {
DataTableAction(
icon: Icons.visibility,
label: 'مشاهده',
onTap: (order) => print('View order: ${order.id}'),
onTap: (order) => debugPrint('View order: ${order.id}'),
),
]),
],

View file

@ -1,4 +1,5 @@
import 'dart:convert';
import 'package:flutter/foundation.dart';
import 'package:shared_preferences/shared_preferences.dart';
/// Column settings for a specific table
@ -58,7 +59,7 @@ class ColumnSettingsService {
final json = jsonDecode(jsonString) as Map<String, dynamic>;
return ColumnSettings.fromJson(json);
} catch (e) {
print('Error loading column settings: $e');
debugPrint('Error loading column settings: $e');
return null;
}
}
@ -71,7 +72,7 @@ class ColumnSettingsService {
final jsonString = jsonEncode(settings.toJson());
await prefs.setString(key, jsonString);
} catch (e) {
print('Error saving column settings: $e');
debugPrint('Error saving column settings: $e');
}
}
@ -82,7 +83,7 @@ class ColumnSettingsService {
final key = '$_keyPrefix$tableId';
await prefs.remove(key);
} catch (e) {
print('Error clearing column settings: $e');
debugPrint('Error clearing column settings: $e');
}
}

View file

@ -19,7 +19,7 @@ class ErrorNotice extends StatelessWidget {
decoration: BoxDecoration(
color: bg,
borderRadius: BorderRadius.circular(8),
border: Border.all(color: cs.error.withOpacity(0.4)),
border: Border.all(color: cs.error.withValues(alpha: 0.4)),
),
padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 10),
child: Row(

View file

@ -0,0 +1,296 @@
import 'package:flutter/material.dart';
import 'package:hesabix_ui/l10n/app_localizations.dart';
import 'dart:async';
class ProgressSplashScreen extends StatefulWidget {
final String? message;
final bool showLogo;
final Color? backgroundColor;
final Color? primaryColor;
final Duration minimumDisplayDuration;
final VoidCallback? onComplete;
const ProgressSplashScreen({
super.key,
this.message,
this.showLogo = true,
this.backgroundColor,
this.primaryColor,
this.minimumDisplayDuration = const Duration(seconds: 2),
this.onComplete,
});
@override
State<ProgressSplashScreen> createState() => _ProgressSplashScreenState();
}
class _ProgressSplashScreenState extends State<ProgressSplashScreen>
with TickerProviderStateMixin {
late AnimationController _fadeController;
late AnimationController _scaleController;
late AnimationController _progressController;
late Animation<double> _fadeAnimation;
late Animation<double> _scaleAnimation;
late Animation<double> _progressAnimation;
Timer? _countdownTimer;
int _remainingSeconds = 0;
@override
void initState() {
super.initState();
_remainingSeconds = widget.minimumDisplayDuration.inSeconds;
_fadeController = AnimationController(
duration: const Duration(milliseconds: 800),
vsync: this,
);
_scaleController = AnimationController(
duration: const Duration(milliseconds: 600),
vsync: this,
);
_progressController = AnimationController(
duration: widget.minimumDisplayDuration,
vsync: this,
);
_fadeAnimation = Tween<double>(
begin: 0.0,
end: 1.0,
).animate(CurvedAnimation(
parent: _fadeController,
curve: Curves.easeInOut,
));
_scaleAnimation = Tween<double>(
begin: 0.8,
end: 1.0,
).animate(CurvedAnimation(
parent: _scaleController,
curve: Curves.elasticOut,
));
_progressAnimation = Tween<double>(
begin: 0.0,
end: 1.0,
).animate(CurvedAnimation(
parent: _progressController,
curve: Curves.easeInOut,
));
// Start animations
_fadeController.forward();
_scaleController.forward();
_progressController.forward();
// Start countdown timer
_startCountdown();
}
void _startCountdown() {
_countdownTimer = Timer.periodic(const Duration(seconds: 1), (timer) {
if (mounted) {
setState(() {
_remainingSeconds = widget.minimumDisplayDuration.inSeconds - timer.tick;
if (_remainingSeconds <= 0) {
timer.cancel();
widget.onComplete?.call();
}
});
}
});
}
@override
void dispose() {
_fadeController.dispose();
_scaleController.dispose();
_progressController.dispose();
_countdownTimer?.cancel();
super.dispose();
}
@override
Widget build(BuildContext context) {
final theme = Theme.of(context);
final colorScheme = theme.colorScheme;
final isDark = theme.brightness == Brightness.dark;
final bgColor = widget.backgroundColor ?? colorScheme.surface;
final primary = widget.primaryColor ?? colorScheme.primary;
return Scaffold(
backgroundColor: bgColor,
body: Container(
width: double.infinity,
height: double.infinity,
decoration: BoxDecoration(
gradient: LinearGradient(
begin: Alignment.topCenter,
end: Alignment.bottomCenter,
colors: isDark
? [
bgColor,
bgColor.withOpacity(0.95),
]
: [
bgColor,
bgColor.withOpacity(0.98),
],
),
),
child: AnimatedBuilder(
animation: Listenable.merge([_fadeAnimation, _scaleAnimation, _progressAnimation]),
builder: (context, child) {
return Transform.scale(
scale: _scaleAnimation.value,
child: Opacity(
opacity: _fadeAnimation.value,
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
// Logo Section
if (widget.showLogo) ...[
Container(
width: 120,
height: 120,
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(20),
boxShadow: [
BoxShadow(
color: primary.withOpacity(0.2),
blurRadius: 20,
spreadRadius: 2,
),
],
),
child: ClipRRect(
borderRadius: BorderRadius.circular(20),
child: Image.asset(
isDark ? 'assets/images/logo-light.png' : 'assets/images/logo-blue.png',
fit: BoxFit.contain,
errorBuilder: (context, error, stackTrace) {
return Container(
decoration: BoxDecoration(
color: primary,
borderRadius: BorderRadius.circular(20),
),
child: Icon(
Icons.account_balance,
size: 60,
color: colorScheme.onPrimary,
),
);
},
),
),
),
const SizedBox(height: 32),
],
// App Name
Text(
'Hesabix',
style: theme.textTheme.headlineMedium?.copyWith(
fontWeight: FontWeight.bold,
color: colorScheme.onSurface,
letterSpacing: 1.2,
),
),
const SizedBox(height: 8),
// Subtitle
Text(
AppLocalizations.of(context)?.businessManagementPlatform ?? 'Business Management Platform',
style: theme.textTheme.bodyLarge?.copyWith(
color: colorScheme.onSurfaceVariant,
fontWeight: FontWeight.w400,
),
),
const SizedBox(height: 48),
// Loading Indicator with Progress
Column(
children: [
SizedBox(
width: 40,
height: 40,
child: CircularProgressIndicator(
strokeWidth: 3,
valueColor: AlwaysStoppedAnimation<Color>(primary),
),
),
const SizedBox(height: 24),
// Progress Bar
Container(
width: 200,
height: 4,
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(2),
color: colorScheme.surfaceContainerHighest,
),
child: AnimatedBuilder(
animation: _progressAnimation,
builder: (context, child) {
return FractionallySizedBox(
alignment: Alignment.centerLeft,
widthFactor: _progressAnimation.value,
child: Container(
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(2),
gradient: LinearGradient(
colors: [primary, primary.withOpacity(0.8)],
),
),
),
);
},
),
),
const SizedBox(height: 16),
// Loading Message
Text(
widget.message ?? AppLocalizations.of(context)?.loading ?? 'Loading...',
style: theme.textTheme.bodyMedium?.copyWith(
color: colorScheme.onSurfaceVariant,
fontWeight: FontWeight.w500,
),
),
const SizedBox(height: 8),
// Countdown Timer
if (_remainingSeconds > 0)
Text(
'${_remainingSeconds}s',
style: theme.textTheme.bodySmall?.copyWith(
color: colorScheme.onSurfaceVariant.withOpacity(0.7),
fontWeight: FontWeight.w400,
),
),
],
),
const SizedBox(height: 80),
// Version Info
Text(
'Version 1.0.0',
style: theme.textTheme.bodySmall?.copyWith(
color: colorScheme.onSurfaceVariant.withOpacity(0.6),
),
),
],
),
),
);
},
),
),
);
}
}

View file

@ -0,0 +1,263 @@
import 'package:flutter/material.dart';
import 'dart:async';
class SimpleSplashScreen extends StatefulWidget {
final String? message;
final bool showLogo;
final Color? backgroundColor;
final Color? primaryColor;
final Duration displayDuration;
final VoidCallback? onComplete;
final Locale? locale;
const SimpleSplashScreen({
super.key,
this.message,
this.showLogo = true,
this.backgroundColor,
this.primaryColor,
this.displayDuration = const Duration(seconds: 4),
this.onComplete,
this.locale,
});
@override
State<SimpleSplashScreen> createState() => _SimpleSplashScreenState();
}
class _SimpleSplashScreenState extends State<SimpleSplashScreen>
with TickerProviderStateMixin {
late AnimationController _fadeController;
late AnimationController _scaleController;
late Animation<double> _fadeAnimation;
late Animation<double> _scaleAnimation;
Timer? _displayTimer;
@override
void initState() {
super.initState();
_fadeController = AnimationController(
duration: const Duration(milliseconds: 800),
vsync: this,
);
_scaleController = AnimationController(
duration: const Duration(milliseconds: 600),
vsync: this,
);
_fadeAnimation = Tween<double>(
begin: 0.0,
end: 1.0,
).animate(CurvedAnimation(
parent: _fadeController,
curve: Curves.easeInOut,
));
_scaleAnimation = Tween<double>(
begin: 0.8,
end: 1.0,
).animate(CurvedAnimation(
parent: _scaleController,
curve: Curves.elasticOut,
));
// Start animations
_fadeController.forward();
_scaleController.forward();
// Start display timer
_displayTimer = Timer(widget.displayDuration, () {
widget.onComplete?.call();
});
}
@override
void dispose() {
_fadeController.dispose();
_scaleController.dispose();
_displayTimer?.cancel();
super.dispose();
}
String _getAppName() {
if (widget.locale != null && widget.locale!.languageCode == 'fa') {
return 'حسابیکس';
}
return 'Hesabix';
}
String _getLoadingMessage() {
if (widget.locale != null && widget.locale!.languageCode == 'fa') {
return 'در حال بارگذاری...';
}
return 'Loading...';
}
String _getVersionInfo() {
if (widget.locale != null && widget.locale!.languageCode == 'fa') {
return 'نسخه 1.0.0';
}
return 'Version 1.0.0';
}
String _getMotto() {
if (widget.locale != null && widget.locale!.languageCode == 'fa') {
return 'جهان با تعاون زیبا می‌شود';
}
return 'The world becomes beautiful through cooperation';
}
@override
Widget build(BuildContext context) {
final theme = Theme.of(context);
final colorScheme = theme.colorScheme;
final isDark = theme.brightness == Brightness.dark;
final bgColor = widget.backgroundColor ?? colorScheme.surface;
final primary = widget.primaryColor ?? colorScheme.primary;
return Scaffold(
backgroundColor: bgColor,
body: Container(
width: double.infinity,
height: double.infinity,
decoration: BoxDecoration(
gradient: LinearGradient(
begin: Alignment.topCenter,
end: Alignment.bottomCenter,
colors: isDark
? [
bgColor,
bgColor.withValues(alpha: 0.95),
]
: [
bgColor,
bgColor.withValues(alpha: 0.98),
],
),
),
child: AnimatedBuilder(
animation: Listenable.merge([_fadeAnimation, _scaleAnimation]),
builder: (context, child) {
return Transform.scale(
scale: _scaleAnimation.value,
child: Opacity(
opacity: _fadeAnimation.value,
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
// Logo Section - Simple and Clean
if (widget.showLogo) ...[
Container(
width: 100,
height: 100,
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(20),
boxShadow: [
BoxShadow(
color: primary.withValues(alpha: 0.2),
blurRadius: 20,
spreadRadius: 2,
offset: const Offset(0, 5),
),
],
),
child: ClipRRect(
borderRadius: BorderRadius.circular(20),
child: Image.asset(
isDark ? 'assets/images/logo-light.png' : 'assets/images/logo-blue.png',
fit: BoxFit.contain,
errorBuilder: (context, error, stackTrace) {
return Container(
decoration: BoxDecoration(
color: primary,
borderRadius: BorderRadius.circular(20),
),
child: Icon(
Icons.account_balance,
size: 50,
color: colorScheme.onPrimary,
),
);
},
),
),
),
const SizedBox(height: 32),
],
// App Name - Simple and Clean
Text(
_getAppName(),
style: theme.textTheme.headlineLarge?.copyWith(
fontWeight: FontWeight.bold,
color: colorScheme.onSurface,
letterSpacing: 1.5,
),
),
const SizedBox(height: 24),
// Motto/Slogan - Simple Design
Padding(
padding: const EdgeInsets.symmetric(horizontal: 32),
child: Text(
_getMotto(),
style: theme.textTheme.bodyLarge?.copyWith(
color: colorScheme.onSurfaceVariant,
fontWeight: FontWeight.w400,
fontStyle: FontStyle.italic,
letterSpacing: 0.5,
),
textAlign: TextAlign.center,
),
),
const SizedBox(height: 48),
// Loading Section - Simple and Clean
Column(
children: [
// Simple Loading Indicator
SizedBox(
width: 40,
height: 40,
child: CircularProgressIndicator(
strokeWidth: 3,
valueColor: AlwaysStoppedAnimation<Color>(primary),
),
),
const SizedBox(height: 24),
// Simple Loading Message
Text(
widget.message ?? _getLoadingMessage(),
style: theme.textTheme.bodyMedium?.copyWith(
color: colorScheme.onSurfaceVariant,
fontWeight: FontWeight.w500,
),
),
],
),
const SizedBox(height: 60),
// Simple Version Info
Text(
_getVersionInfo(),
style: theme.textTheme.bodySmall?.copyWith(
color: colorScheme.onSurfaceVariant.withValues(alpha: 0.6),
),
),
],
),
),
);
},
),
),
);
}
}

View file

@ -0,0 +1,242 @@
import 'package:flutter/material.dart';
import 'package:hesabix_ui/l10n/app_localizations.dart';
class SplashScreen extends StatelessWidget {
final String? message;
final bool showLogo;
final Color? backgroundColor;
final Color? primaryColor;
const SplashScreen({
super.key,
this.message,
this.showLogo = true,
this.backgroundColor,
this.primaryColor,
});
@override
Widget build(BuildContext context) {
final theme = Theme.of(context);
final colorScheme = theme.colorScheme;
final isDark = theme.brightness == Brightness.dark;
final bgColor = backgroundColor ?? colorScheme.surface;
final primary = primaryColor ?? colorScheme.primary;
return Scaffold(
backgroundColor: bgColor,
body: Container(
width: double.infinity,
height: double.infinity,
decoration: BoxDecoration(
gradient: LinearGradient(
begin: Alignment.topCenter,
end: Alignment.bottomCenter,
colors: isDark
? [
bgColor,
bgColor.withOpacity(0.95),
]
: [
bgColor,
bgColor.withOpacity(0.98),
],
),
),
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
// Logo Section
if (showLogo) ...[
Container(
width: 120,
height: 120,
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(20),
boxShadow: [
BoxShadow(
color: primary.withOpacity(0.2),
blurRadius: 20,
spreadRadius: 2,
),
],
),
child: ClipRRect(
borderRadius: BorderRadius.circular(20),
child: Image.asset(
isDark ? 'assets/images/logo-light.png' : 'assets/images/logo-blue.png',
fit: BoxFit.contain,
errorBuilder: (context, error, stackTrace) {
return Container(
decoration: BoxDecoration(
color: primary,
borderRadius: BorderRadius.circular(20),
),
child: Icon(
Icons.account_balance,
size: 60,
color: colorScheme.onPrimary,
),
);
},
),
),
),
const SizedBox(height: 32),
],
// App Name
Text(
'Hesabix',
style: theme.textTheme.headlineMedium?.copyWith(
fontWeight: FontWeight.bold,
color: colorScheme.onSurface,
letterSpacing: 1.2,
),
),
const SizedBox(height: 8),
// Subtitle
Text(
AppLocalizations.of(context)?.businessManagementPlatform ?? 'Business Management Platform',
style: theme.textTheme.bodyLarge?.copyWith(
color: colorScheme.onSurfaceVariant,
fontWeight: FontWeight.w400,
),
),
const SizedBox(height: 48),
// Loading Indicator
Column(
children: [
SizedBox(
width: 40,
height: 40,
child: CircularProgressIndicator(
strokeWidth: 3,
valueColor: AlwaysStoppedAnimation<Color>(primary),
),
),
const SizedBox(height: 24),
// Loading Message
Text(
message ?? AppLocalizations.of(context)?.loading ?? 'Loading...',
style: theme.textTheme.bodyMedium?.copyWith(
color: colorScheme.onSurfaceVariant,
fontWeight: FontWeight.w500,
),
),
],
),
const SizedBox(height: 80),
// Version Info (Optional)
Text(
'Version 1.0.0',
style: theme.textTheme.bodySmall?.copyWith(
color: colorScheme.onSurfaceVariant.withOpacity(0.6),
),
),
],
),
),
);
}
}
// Animated Splash Screen with fade effects
class AnimatedSplashScreen extends StatefulWidget {
final String? message;
final bool showLogo;
final Color? backgroundColor;
final Color? primaryColor;
final Duration animationDuration;
final Duration minimumDisplayDuration;
const AnimatedSplashScreen({
super.key,
this.message,
this.showLogo = true,
this.backgroundColor,
this.primaryColor,
this.animationDuration = const Duration(milliseconds: 1500),
this.minimumDisplayDuration = const Duration(seconds: 2),
});
@override
State<AnimatedSplashScreen> createState() => _AnimatedSplashScreenState();
}
class _AnimatedSplashScreenState extends State<AnimatedSplashScreen>
with TickerProviderStateMixin {
late AnimationController _fadeController;
late AnimationController _scaleController;
late Animation<double> _fadeAnimation;
late Animation<double> _scaleAnimation;
@override
void initState() {
super.initState();
_fadeController = AnimationController(
duration: widget.animationDuration,
vsync: this,
);
_scaleController = AnimationController(
duration: const Duration(milliseconds: 800),
vsync: this,
);
_fadeAnimation = Tween<double>(
begin: 0.0,
end: 1.0,
).animate(CurvedAnimation(
parent: _fadeController,
curve: Curves.easeInOut,
));
_scaleAnimation = Tween<double>(
begin: 0.8,
end: 1.0,
).animate(CurvedAnimation(
parent: _scaleController,
curve: Curves.elasticOut,
));
// Start animations
_fadeController.forward();
_scaleController.forward();
}
@override
void dispose() {
_fadeController.dispose();
_scaleController.dispose();
super.dispose();
}
@override
Widget build(BuildContext context) {
return AnimatedBuilder(
animation: Listenable.merge([_fadeAnimation, _scaleAnimation]),
builder: (context, child) {
return Transform.scale(
scale: _scaleAnimation.value,
child: Opacity(
opacity: _fadeAnimation.value,
child: SplashScreen(
message: widget.message,
showLogo: widget.showLogo,
backgroundColor: widget.backgroundColor,
primaryColor: widget.primaryColor,
),
),
);
},
);
}
}

View file

@ -141,9 +141,9 @@ class MessageBubble extends StatelessWidget {
Color _getBorderColor(ThemeData theme, bool isUser) {
if (isUser) {
return theme.colorScheme.primary.withOpacity(0.3);
return theme.colorScheme.primary.withValues(alpha: 0.3);
} else {
return theme.colorScheme.outline.withOpacity(0.3);
return theme.colorScheme.outline.withValues(alpha: 0.3);
}
}
@ -157,7 +157,7 @@ class MessageBubble extends StatelessWidget {
Color _getSenderTextColor(ThemeData theme, bool isUser) {
if (isUser) {
return Colors.white.withOpacity(0.8);
return Colors.white.withValues(alpha: 0.8);
} else {
return theme.colorScheme.primary;
}
@ -165,9 +165,9 @@ class MessageBubble extends StatelessWidget {
Color _getTimeColor(ThemeData theme, bool isUser) {
if (isUser) {
return Colors.white.withOpacity(0.7);
return Colors.white.withValues(alpha: 0.7);
} else {
return theme.colorScheme.onSurface.withOpacity(0.6);
return theme.colorScheme.onSurface.withValues(alpha: 0.6);
}
}

View file

@ -35,10 +35,10 @@ class PriorityIndicator extends StatelessWidget {
vertical: isSmall ? 4 : 6,
),
decoration: BoxDecoration(
color: color.withOpacity(0.1),
color: color.withValues(alpha: 0.1),
borderRadius: BorderRadius.circular(isSmall ? 12 : 16),
border: Border.all(
color: color.withOpacity(0.3),
color: color.withValues(alpha: 0.3),
width: 1,
),
),

View file

@ -29,7 +29,7 @@ class TicketCard extends StatelessWidget {
borderRadius: BorderRadius.circular(16),
boxShadow: [
BoxShadow(
color: theme.colorScheme.shadow.withOpacity(0.08),
color: theme.colorScheme.shadow.withValues(alpha: 0.08),
blurRadius: 8,
offset: const Offset(0, 2),
),
@ -46,7 +46,7 @@ class TicketCard extends StatelessWidget {
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(16),
border: Border.all(
color: theme.colorScheme.outline.withOpacity(0.1),
color: theme.colorScheme.outline.withValues(alpha: 0.1),
width: 1,
),
),
@ -78,7 +78,7 @@ class TicketCard extends StatelessWidget {
Text(
ticket.description,
style: theme.textTheme.bodyMedium?.copyWith(
color: theme.colorScheme.onSurface.withOpacity(0.8),
color: theme.colorScheme.onSurface.withValues(alpha: 0.8),
height: 1.4,
),
maxLines: 3,
@ -129,7 +129,7 @@ class TicketCard extends StatelessWidget {
return Container(
padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 4),
decoration: BoxDecoration(
color: theme.colorScheme.surfaceVariant.withOpacity(0.5),
color: theme.colorScheme.surfaceContainerHighest.withValues(alpha: 0.5),
borderRadius: BorderRadius.circular(8),
),
child: Row(
@ -138,13 +138,13 @@ class TicketCard extends StatelessWidget {
Icon(
Icons.access_time,
size: 12,
color: theme.colorScheme.onSurfaceVariant.withOpacity(0.7),
color: theme.colorScheme.onSurfaceVariant.withValues(alpha: 0.7),
),
const SizedBox(width: 4),
Text(
_formatDate(dateTime, l10n),
style: theme.textTheme.bodySmall?.copyWith(
color: theme.colorScheme.onSurfaceVariant.withOpacity(0.8),
color: theme.colorScheme.onSurfaceVariant.withValues(alpha: 0.8),
fontWeight: FontWeight.w500,
),
),
@ -160,14 +160,14 @@ class TicketCard extends StatelessWidget {
return Container(
padding: const EdgeInsets.all(12),
decoration: BoxDecoration(
color: theme.colorScheme.surfaceVariant.withOpacity(0.3),
color: theme.colorScheme.surfaceContainerHighest.withValues(alpha: 0.3),
borderRadius: BorderRadius.circular(12),
),
child: Row(
children: [
CircleAvatar(
radius: 16,
backgroundColor: theme.colorScheme.primary.withOpacity(0.1),
backgroundColor: theme.colorScheme.primary.withValues(alpha: 0.1),
child: Icon(
Icons.person,
size: 16,
@ -182,7 +182,7 @@ class TicketCard extends StatelessWidget {
Text(
l10n.createdBy,
style: theme.textTheme.bodySmall?.copyWith(
color: theme.colorScheme.onSurfaceVariant.withOpacity(0.7),
color: theme.colorScheme.onSurfaceVariant.withValues(alpha: 0.7),
fontWeight: FontWeight.w500,
),
),
@ -202,7 +202,7 @@ class TicketCard extends StatelessWidget {
Container(
padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 4),
decoration: BoxDecoration(
color: theme.colorScheme.secondary.withOpacity(0.1),
color: theme.colorScheme.secondary.withValues(alpha: 0.1),
borderRadius: BorderRadius.circular(8),
),
child: Row(
@ -239,10 +239,10 @@ class TicketCard extends StatelessWidget {
return Container(
padding: const EdgeInsets.symmetric(horizontal: 10, vertical: 6),
decoration: BoxDecoration(
color: color.withOpacity(0.1),
color: color.withValues(alpha: 0.1),
borderRadius: BorderRadius.circular(12),
border: Border.all(
color: color.withOpacity(0.2),
color: color.withValues(alpha: 0.2),
width: 1,
),
),

View file

@ -62,7 +62,7 @@ class _TicketDetailsDialogState extends State<TicketDetailsDialog> {
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),
),
@ -275,7 +275,7 @@ class _TicketDetailsDialogState extends State<TicketDetailsDialog> {
Container(
padding: const EdgeInsets.all(20),
decoration: BoxDecoration(
color: theme.primaryColor.withOpacity(0.1),
color: theme.primaryColor.withValues(alpha: 0.1),
borderRadius: const BorderRadius.only(
topLeft: Radius.circular(20),
topRight: Radius.circular(20),

View file

@ -22,10 +22,10 @@ class TicketStatusChip extends StatelessWidget {
vertical: isSmall ? 4 : 6,
),
decoration: BoxDecoration(
color: color.withOpacity(0.1),
color: color.withValues(alpha: 0.1),
borderRadius: BorderRadius.circular(isSmall ? 12 : 16),
border: Border.all(
color: color.withOpacity(0.3),
color: color.withValues(alpha: 0.3),
width: 1,
),
),

View file

@ -185,7 +185,7 @@ packages:
source: sdk
version: "0.0.0"
flutter_web_plugins:
dependency: transitive
dependency: "direct main"
description: flutter
source: sdk
version: "0.0.0"
@ -193,10 +193,10 @@ packages:
dependency: "direct main"
description:
name: go_router
sha256: eb059dfe59f08546e9787f895bd01652076f996bcbf485a8609ef990419ad227
sha256: b1488741c9ce37b72e026377c69a59c47378493156fc38efb5a54f6def3f92a3
url: "https://pub.flutter-io.cn"
source: hosted
version: "16.2.1"
version: "16.2.2"
http_parser:
dependency: transitive
description:

View file

@ -32,6 +32,8 @@ dependencies:
sdk: flutter
flutter_localizations:
sdk: flutter
flutter_web_plugins:
sdk: flutter
# The following adds the Cupertino Icons font to your application.
# Use with the CupertinoIcons class for iOS style icons.