progress in email servie and business dashboard
This commit is contained in:
parent
dcada33b89
commit
798dd63627
210
docs/EMAIL_SERVICE_README.md
Normal file
210
docs/EMAIL_SERVICE_README.md
Normal 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
|
||||
**نویسنده**: تیم توسعه حسابیکس
|
||||
349
hesabixAPI/adapters/api/v1/admin/email_config.py
Normal file
349
hesabixAPI/adapters/api/v1/admin/email_config.py
Normal 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))
|
||||
|
|
@ -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,
|
||||
|
|
|
|||
194
hesabixAPI/adapters/api/v1/business_dashboard.py
Normal file
194
hesabixAPI/adapters/api/v1/business_dashboard.py
Normal 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)
|
||||
|
|
@ -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)
|
||||
|
|
|
|||
59
hesabixAPI/adapters/api/v1/schema_models/email.py
Normal file
59
hesabixAPI/adapters/api/v1/schema_models/email.py
Normal 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
|
||||
|
|
@ -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
|
||||
|
||||
|
||||
|
|
|
|||
27
hesabixAPI/adapters/db/models/email_config.py
Normal file
27
hesabixAPI/adapters/db/models/email_config.py
Normal 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)
|
||||
|
|
@ -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()
|
||||
|
|
@ -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
|
||||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
||||
|
|
|
|||
194
hesabixAPI/app/services/business_dashboard_service.py
Normal file
194
hesabixAPI/app/services/business_dashboard_service.py
Normal 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 "عضو"
|
||||
|
|
@ -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 بماند
|
||||
}
|
||||
|
|
|
|||
143
hesabixAPI/app/services/email_service.py
Normal file
143
hesabixAPI/app/services/email_service.py
Normal 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
|
||||
|
|
@ -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
|
||||
|
|
|
|||
Binary file not shown.
|
|
@ -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."
|
||||
|
|
|
|||
Binary file not shown.
|
|
@ -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 "کد امنیتی نامعتبر است."
|
||||
|
|
|
|||
|
|
@ -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 ###
|
||||
|
|
@ -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')
|
||||
|
|
@ -1,6 +1,5 @@
|
|||
import 'dart:async';
|
||||
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:shared_preferences/shared_preferences.dart';
|
||||
|
||||
class ReferralStore {
|
||||
|
|
|
|||
76
hesabixUI/hesabix_ui/lib/core/splash_controller.dart
Normal file
76
hesabixUI/hesabix_ui/lib/core/splash_controller.dart
Normal 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();
|
||||
}
|
||||
}
|
||||
|
|
@ -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"
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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": "دسترسی غیرمجاز"
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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';
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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 => 'دسترسی غیرمجاز';
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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) {
|
||||
setState(() {
|
||||
_controller = c
|
||||
..addListener(() {
|
||||
// Update ApiClient language header on change
|
||||
ApiClient.setCurrentLocale(c.locale);
|
||||
setState(() {});
|
||||
});
|
||||
ApiClient.setCurrentLocale(c.locale);
|
||||
});
|
||||
});
|
||||
_loadStartTime = DateTime.now();
|
||||
_loadControllers();
|
||||
}
|
||||
|
||||
CalendarController.load().then((cc) {
|
||||
setState(() {
|
||||
_calendarController = cc
|
||||
..addListener(() {
|
||||
setState(() {});
|
||||
});
|
||||
ApiClient.bindCalendarController(cc);
|
||||
});
|
||||
Future<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 = localeController;
|
||||
_calendarController = calendarController;
|
||||
_themeController = themeController;
|
||||
_authStore = authStore;
|
||||
});
|
||||
|
||||
final tc = ThemeController();
|
||||
tc.load().then((_) {
|
||||
setState(() {
|
||||
_themeController = tc
|
||||
..addListener(() {
|
||||
setState(() {});
|
||||
});
|
||||
});
|
||||
|
||||
// اضافه کردن listeners
|
||||
_controller!.addListener(() {
|
||||
ApiClient.setCurrentLocale(_controller!.locale);
|
||||
setState(() {});
|
||||
});
|
||||
|
||||
final store = AuthStore();
|
||||
store.load().then((_) {
|
||||
setState(() {
|
||||
_authStore = store
|
||||
..addListener(() {
|
||||
setState(() {});
|
||||
});
|
||||
ApiClient.bindAuthStore(store);
|
||||
});
|
||||
|
||||
_calendarController!.addListener(() {
|
||||
setState(() {});
|
||||
});
|
||||
|
||||
_themeController!.addListener(() {
|
||||
setState(() {});
|
||||
});
|
||||
|
||||
_authStore!.addListener(() {
|
||||
setState(() {});
|
||||
});
|
||||
|
||||
// تنظیم API Client
|
||||
ApiClient.setCurrentLocale(_controller!.locale);
|
||||
ApiClient.bindCalendarController(_calendarController!);
|
||||
ApiClient.bindAuthStore(_authStore!);
|
||||
|
||||
// اطمینان از حداقل 4 ثانیه نمایش splash screen
|
||||
final elapsed = DateTime.now().difference(_loadStartTime!);
|
||||
const minimumDuration = Duration(seconds: 4);
|
||||
if (elapsed < minimumDuration) {
|
||||
await Future.delayed(minimumDuration - elapsed);
|
||||
}
|
||||
|
||||
// اتمام loading
|
||||
if (mounted) {
|
||||
setState(() {
|
||||
_isLoading = false;
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// Root of application with GoRouter
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
// اگر هنوز loading است، یک router ساده با loading page بساز
|
||||
if (_controller == null || _calendarController == null || _themeController == null || _authStore == null) {
|
||||
// اگر هنوز loading است، splash screen نمایش بده
|
||||
if (_isLoading ||
|
||||
_controller == null ||
|
||||
_calendarController == null ||
|
||||
_themeController == null ||
|
||||
_authStore == null) {
|
||||
final loadingRouter = GoRouter(
|
||||
redirect: (context, state) {
|
||||
// در حین loading، هیچ redirect نکن - URL را حفظ کن
|
||||
return null;
|
||||
},
|
||||
routes: <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.)
|
||||
],
|
||||
),
|
||||
],
|
||||
);
|
||||
|
||||
|
|
|
|||
245
hesabixUI/hesabix_ui/lib/models/business_dashboard_models.dart
Normal file
245
hesabixUI/hesabix_ui/lib/models/business_dashboard_models.dart
Normal 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'] ?? {}),
|
||||
);
|
||||
}
|
||||
}
|
||||
306
hesabixUI/hesabix_ui/lib/models/email_models.dart
Normal file
306
hesabixUI/hesabix_ui/lib/models/email_models.dart
Normal 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,
|
||||
);
|
||||
}
|
||||
}
|
||||
644
hesabixUI/hesabix_ui/lib/pages/admin/email_settings_page.dart
Normal file
644
hesabixUI/hesabix_ui/lib/pages/admin/email_settings_page.dart
Normal 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());
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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 {
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
],
|
||||
),
|
||||
|
|
|
|||
|
|
@ -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(),
|
||||
|
|
|
|||
|
|
@ -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(
|
||||
|
|
|
|||
|
|
@ -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)),
|
||||
);
|
||||
}
|
||||
|
|
|
|||
173
hesabixUI/hesabix_ui/lib/pages/business/business_shell.dart
Normal file
173
hesabixUI/hesabix_ui/lib/pages/business/business_shell.dart
Normal 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);
|
||||
}
|
||||
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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')),
|
||||
|
|
|
|||
|
|
@ -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());
|
||||
context.go('/user/profile/dashboard');
|
||||
if (mounted) {
|
||||
context.go('/user/profile/dashboard');
|
||||
}
|
||||
} catch (e) {
|
||||
if (!mounted) return;
|
||||
final msg = _extractErrorMessage(e, AppLocalizations.of(context));
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -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),
|
||||
),
|
||||
|
|
|
|||
|
|
@ -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(
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -5,16 +5,13 @@ class ProfileDashboardPage extends StatelessWidget {
|
|||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Padding(
|
||||
padding: const EdgeInsets.all(16.0),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: const [
|
||||
Text('User Profile Dashboard', style: TextStyle(fontSize: 18, fontWeight: FontWeight.w600)),
|
||||
SizedBox(height: 12),
|
||||
Text('Summary and quick actions will appear here.'),
|
||||
],
|
||||
),
|
||||
return Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: const [
|
||||
Text('User Profile Dashboard', style: TextStyle(fontSize: 18, fontWeight: FontWeight.w600)),
|
||||
SizedBox(height: 12),
|
||||
Text('Summary and quick actions will appear here.'),
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -152,10 +152,7 @@ class _ProfileShellState extends State<ProfileShell> {
|
|||
final content = Container(
|
||||
color: scheme.surface,
|
||||
child: SafeArea(
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(16),
|
||||
child: widget.child,
|
||||
),
|
||||
child: widget.child,
|
||||
),
|
||||
);
|
||||
|
||||
|
|
|
|||
|
|
@ -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 {
|
||||
|
|
|
|||
|
|
@ -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');
|
||||
}
|
||||
}
|
||||
}
|
||||
248
hesabixUI/hesabix_ui/lib/services/email_service.dart
Normal file
248
hesabixUI/hesabix_ui/lib/services/email_service.dart
Normal 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}';
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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());
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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(
|
||||
|
|
|
|||
|
|
@ -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),
|
||||
],
|
||||
),
|
||||
value: 'local',
|
||||
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),
|
||||
|
|
|
|||
|
|
@ -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),
|
||||
),
|
||||
),
|
||||
],
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
],
|
||||
),
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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}'),
|
||||
),
|
||||
]),
|
||||
],
|
||||
|
|
|
|||
|
|
@ -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');
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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(
|
||||
|
|
|
|||
296
hesabixUI/hesabix_ui/lib/widgets/progress_splash_screen.dart
Normal file
296
hesabixUI/hesabix_ui/lib/widgets/progress_splash_screen.dart
Normal 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),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
263
hesabixUI/hesabix_ui/lib/widgets/simple_splash_screen.dart
Normal file
263
hesabixUI/hesabix_ui/lib/widgets/simple_splash_screen.dart
Normal 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),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
242
hesabixUI/hesabix_ui/lib/widgets/splash_screen.dart
Normal file
242
hesabixUI/hesabix_ui/lib/widgets/splash_screen.dart
Normal 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,
|
||||
),
|
||||
),
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
),
|
||||
),
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
),
|
||||
),
|
||||
|
|
|
|||
|
|
@ -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),
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
),
|
||||
),
|
||||
|
|
|
|||
|
|
@ -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:
|
||||
|
|
|
|||
|
|
@ -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.
|
||||
|
|
|
|||
Loading…
Reference in a new issue