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 adapters.db.session import get_db
|
||||||
from app.core.auth_dependency import get_current_user, AuthContext
|
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 success_response
|
||||||
from app.core.responses import ApiError
|
from app.core.responses import ApiError
|
||||||
from app.core.i18n import locale_dependency
|
from app.core.i18n import locale_dependency
|
||||||
from app.services.file_storage_service import FileStorageService
|
from app.services.file_storage_service import FileStorageService
|
||||||
from adapters.db.repositories.file_storage_repository import StorageConfigRepository, FileStorageRepository
|
from adapters.db.repositories.file_storage_repository import StorageConfigRepository, FileStorageRepository
|
||||||
from adapters.db.models.user import User
|
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 (
|
from adapters.api.v1.schema_models.file_storage import (
|
||||||
StorageConfigCreateRequest,
|
StorageConfigCreateRequest,
|
||||||
StorageConfigUpdateRequest,
|
StorageConfigUpdateRequest,
|
||||||
|
|
@ -30,6 +30,7 @@ router = APIRouter(prefix="/admin/files", tags=["Admin File Management"])
|
||||||
|
|
||||||
|
|
||||||
@router.get("/", response_model=dict)
|
@router.get("/", response_model=dict)
|
||||||
|
@require_app_permission("superadmin")
|
||||||
async def list_all_files(
|
async def list_all_files(
|
||||||
request: Request,
|
request: Request,
|
||||||
page: int = Query(1, ge=1),
|
page: int = Query(1, ge=1),
|
||||||
|
|
@ -43,15 +44,6 @@ async def list_all_files(
|
||||||
):
|
):
|
||||||
"""لیست تمام فایلها با فیلتر"""
|
"""لیست تمام فایلها با فیلتر"""
|
||||||
try:
|
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)
|
file_repo = FileStorageRepository(db)
|
||||||
|
|
||||||
# محاسبه offset برای pagination
|
# محاسبه offset برای pagination
|
||||||
|
|
@ -130,6 +122,7 @@ async def list_all_files(
|
||||||
|
|
||||||
|
|
||||||
@router.get("/unverified", response_model=dict)
|
@router.get("/unverified", response_model=dict)
|
||||||
|
@require_app_permission("superadmin")
|
||||||
async def get_unverified_files(
|
async def get_unverified_files(
|
||||||
request: Request,
|
request: Request,
|
||||||
db: Session = Depends(get_db),
|
db: Session = Depends(get_db),
|
||||||
|
|
@ -167,6 +160,7 @@ async def get_unverified_files(
|
||||||
|
|
||||||
|
|
||||||
@router.post("/cleanup-temporary", response_model=dict)
|
@router.post("/cleanup-temporary", response_model=dict)
|
||||||
|
@require_app_permission("superadmin")
|
||||||
async def cleanup_temporary_files(
|
async def cleanup_temporary_files(
|
||||||
request: Request,
|
request: Request,
|
||||||
db: Session = Depends(get_db),
|
db: Session = Depends(get_db),
|
||||||
|
|
@ -194,6 +188,7 @@ async def cleanup_temporary_files(
|
||||||
|
|
||||||
|
|
||||||
@router.delete("/{file_id}", response_model=dict)
|
@router.delete("/{file_id}", response_model=dict)
|
||||||
|
@require_app_permission("superadmin")
|
||||||
async def force_delete_file(
|
async def force_delete_file(
|
||||||
file_id: UUID,
|
file_id: UUID,
|
||||||
request: Request,
|
request: Request,
|
||||||
|
|
@ -228,6 +223,7 @@ async def force_delete_file(
|
||||||
|
|
||||||
|
|
||||||
@router.put("/{file_id}/restore", response_model=dict)
|
@router.put("/{file_id}/restore", response_model=dict)
|
||||||
|
@require_app_permission("superadmin")
|
||||||
async def restore_file(
|
async def restore_file(
|
||||||
file_id: UUID,
|
file_id: UUID,
|
||||||
request: Request,
|
request: Request,
|
||||||
|
|
@ -262,6 +258,7 @@ async def restore_file(
|
||||||
|
|
||||||
|
|
||||||
@router.get("/statistics", response_model=dict)
|
@router.get("/statistics", response_model=dict)
|
||||||
|
@require_app_permission("superadmin")
|
||||||
async def get_file_statistics(
|
async def get_file_statistics(
|
||||||
request: Request,
|
request: Request,
|
||||||
db: Session = Depends(get_db),
|
db: Session = Depends(get_db),
|
||||||
|
|
@ -285,6 +282,7 @@ async def get_file_statistics(
|
||||||
|
|
||||||
# Storage Configuration Management
|
# Storage Configuration Management
|
||||||
@router.get("/storage-configs/", response_model=dict)
|
@router.get("/storage-configs/", response_model=dict)
|
||||||
|
@require_app_permission("superadmin")
|
||||||
async def get_storage_configs(
|
async def get_storage_configs(
|
||||||
request: Request,
|
request: Request,
|
||||||
db: Session = Depends(get_db),
|
db: Session = Depends(get_db),
|
||||||
|
|
@ -293,15 +291,6 @@ async def get_storage_configs(
|
||||||
):
|
):
|
||||||
"""لیست تنظیمات ذخیرهسازی"""
|
"""لیست تنظیمات ذخیرهسازی"""
|
||||||
try:
|
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)
|
config_repo = StorageConfigRepository(db)
|
||||||
configs = config_repo.get_all_configs()
|
configs = config_repo.get_all_configs()
|
||||||
|
|
||||||
|
|
@ -331,6 +320,7 @@ async def get_storage_configs(
|
||||||
|
|
||||||
|
|
||||||
@router.post("/storage-configs/", response_model=dict)
|
@router.post("/storage-configs/", response_model=dict)
|
||||||
|
@require_app_permission("superadmin")
|
||||||
async def create_storage_config(
|
async def create_storage_config(
|
||||||
request: Request,
|
request: Request,
|
||||||
config_request: StorageConfigCreateRequest,
|
config_request: StorageConfigCreateRequest,
|
||||||
|
|
@ -367,6 +357,7 @@ async def create_storage_config(
|
||||||
|
|
||||||
|
|
||||||
@router.put("/storage-configs/{config_id}", response_model=dict)
|
@router.put("/storage-configs/{config_id}", response_model=dict)
|
||||||
|
@require_app_permission("superadmin")
|
||||||
async def update_storage_config(
|
async def update_storage_config(
|
||||||
config_id: UUID,
|
config_id: UUID,
|
||||||
request: Request,
|
request: Request,
|
||||||
|
|
@ -392,6 +383,7 @@ async def update_storage_config(
|
||||||
|
|
||||||
|
|
||||||
@router.put("/storage-configs/{config_id}/set-default", response_model=dict)
|
@router.put("/storage-configs/{config_id}/set-default", response_model=dict)
|
||||||
|
@require_app_permission("superadmin")
|
||||||
async def set_default_storage_config(
|
async def set_default_storage_config(
|
||||||
config_id: UUID,
|
config_id: UUID,
|
||||||
request: Request,
|
request: Request,
|
||||||
|
|
@ -426,6 +418,7 @@ async def set_default_storage_config(
|
||||||
|
|
||||||
|
|
||||||
@router.delete("/storage-configs/{config_id}", response_model=dict)
|
@router.delete("/storage-configs/{config_id}", response_model=dict)
|
||||||
|
@require_app_permission("superadmin")
|
||||||
async def delete_storage_config(
|
async def delete_storage_config(
|
||||||
config_id: str,
|
config_id: str,
|
||||||
request: Request,
|
request: Request,
|
||||||
|
|
@ -435,15 +428,6 @@ async def delete_storage_config(
|
||||||
):
|
):
|
||||||
"""حذف تنظیمات ذخیرهسازی"""
|
"""حذف تنظیمات ذخیرهسازی"""
|
||||||
try:
|
try:
|
||||||
# Check permission
|
|
||||||
if not current_user.has_app_permission("admin.storage.delete"):
|
|
||||||
raise ApiError(
|
|
||||||
code="FORBIDDEN",
|
|
||||||
message=translator.t("FORBIDDEN", "دسترسی غیرمجاز"),
|
|
||||||
http_status=403,
|
|
||||||
translator=translator
|
|
||||||
)
|
|
||||||
|
|
||||||
config_repo = StorageConfigRepository(db)
|
config_repo = StorageConfigRepository(db)
|
||||||
|
|
||||||
# بررسی وجود فایلها قبل از حذف
|
# بررسی وجود فایلها قبل از حذف
|
||||||
|
|
@ -480,6 +464,7 @@ async def delete_storage_config(
|
||||||
|
|
||||||
|
|
||||||
@router.post("/storage-configs/{config_id}/test", response_model=dict)
|
@router.post("/storage-configs/{config_id}/test", response_model=dict)
|
||||||
|
@require_app_permission("superadmin")
|
||||||
async def test_storage_config(
|
async def test_storage_config(
|
||||||
config_id: str,
|
config_id: str,
|
||||||
request: Request,
|
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.db.session import get_db
|
||||||
from adapters.api.v1.schemas import (
|
from adapters.api.v1.schemas import (
|
||||||
BusinessCreateRequest, BusinessUpdateRequest, BusinessResponse,
|
BusinessCreateRequest, BusinessUpdateRequest, BusinessResponse,
|
||||||
BusinessListResponse, BusinessSummaryResponse, SuccessResponse,
|
BusinessListResponse, BusinessSummaryResponse, SuccessResponse
|
||||||
QueryInfo
|
|
||||||
)
|
)
|
||||||
from app.core.responses import success_response, format_datetime_fields
|
from app.core.responses import success_response, format_datetime_fields
|
||||||
from app.core.auth_dependency import get_current_user, AuthContext
|
from app.core.auth_dependency import get_current_user, AuthContext
|
||||||
|
|
@ -63,10 +62,10 @@ def create_new_business(
|
||||||
owner_id = ctx.get_user_id()
|
owner_id = ctx.get_user_id()
|
||||||
business = create_business(db, business_data, owner_id)
|
business = create_business(db, business_data, owner_id)
|
||||||
formatted_data = format_datetime_fields(business, request)
|
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="لیست کسب و کارهای کاربر",
|
summary="لیست کسب و کارهای کاربر",
|
||||||
description="دریافت لیست کسب و کارهای کاربر جاری با قابلیت فیلتر و جستجو",
|
description="دریافت لیست کسب و کارهای کاربر جاری با قابلیت فیلتر و جستجو",
|
||||||
response_model=SuccessResponse,
|
response_model=SuccessResponse,
|
||||||
|
|
@ -86,7 +85,7 @@ def create_new_business(
|
||||||
"business_type": "شرکت",
|
"business_type": "شرکت",
|
||||||
"business_field": "تولیدی",
|
"business_field": "تولیدی",
|
||||||
"owner_id": 1,
|
"owner_id": 1,
|
||||||
"created_at": "2024-01-01T00:00:00Z"
|
"created_at": "1403/01/01 00:00:00"
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"pagination": {
|
"pagination": {
|
||||||
|
|
@ -109,19 +108,54 @@ def create_new_business(
|
||||||
)
|
)
|
||||||
def list_user_businesses(
|
def list_user_businesses(
|
||||||
request: Request,
|
request: Request,
|
||||||
query_info: QueryInfo,
|
|
||||||
ctx: AuthContext = Depends(get_current_user),
|
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:
|
) -> dict:
|
||||||
"""لیست کسب و کارهای کاربر"""
|
"""لیست کسب و کارهای کاربر"""
|
||||||
owner_id = ctx.get_user_id()
|
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)
|
businesses = get_businesses_by_owner(db, owner_id, query_dict)
|
||||||
formatted_data = format_datetime_fields(businesses, request)
|
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="جزئیات کسب و کار",
|
summary="جزئیات کسب و کار",
|
||||||
description="دریافت جزئیات یک کسب و کار خاص",
|
description="دریافت جزئیات یک کسب و کار خاص",
|
||||||
response_model=SuccessResponse,
|
response_model=SuccessResponse,
|
||||||
|
|
@ -141,7 +175,7 @@ def list_user_businesses(
|
||||||
"owner_id": 1,
|
"owner_id": 1,
|
||||||
"address": "تهران، خیابان ولیعصر",
|
"address": "تهران، خیابان ولیعصر",
|
||||||
"phone": "02112345678",
|
"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="کسب و کار یافت نشد")
|
raise HTTPException(status_code=404, detail="کسب و کار یافت نشد")
|
||||||
|
|
||||||
formatted_data = format_datetime_fields(business, request)
|
formatted_data = format_datetime_fields(business, request)
|
||||||
return success_response(formatted_data, request, "جزئیات کسب و کار دریافت شد")
|
return success_response(formatted_data, request)
|
||||||
|
|
||||||
|
|
||||||
@router.put("/{business_id}",
|
@router.put("/{business_id}",
|
||||||
|
|
@ -266,7 +300,7 @@ def delete_business_info(
|
||||||
return success_response({"ok": True}, request, "کسب و کار با موفقیت حذف شد")
|
return success_response({"ok": True}, request, "کسب و کار با موفقیت حذف شد")
|
||||||
|
|
||||||
|
|
||||||
@router.get("/summary/stats",
|
@router.post("/stats",
|
||||||
summary="آمار کسب و کارها",
|
summary="آمار کسب و کارها",
|
||||||
description="دریافت آمار کلی کسب و کارهای کاربر",
|
description="دریافت آمار کلی کسب و کارهای کاربر",
|
||||||
response_model=SuccessResponse,
|
response_model=SuccessResponse,
|
||||||
|
|
@ -307,4 +341,4 @@ def get_business_stats(
|
||||||
"""آمار کسب و کارها"""
|
"""آمار کسب و کارها"""
|
||||||
owner_id = ctx.get_user_id()
|
owner_id = ctx.get_user_id()
|
||||||
stats = get_business_summary(db, owner_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
|
from .support import * # noqa: F401, F403
|
||||||
|
|
||||||
# Import file storage models
|
# 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)
|
stmt = stmt.where(column == value)
|
||||||
|
|
||||||
return self.db.execute(stmt).scalars().first() is not None
|
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(
|
def get_current_user(
|
||||||
request: Request,
|
request: Request,
|
||||||
authorization: Optional[str] = Header(default=None),
|
|
||||||
db: Session = Depends(get_db)
|
db: Session = Depends(get_db)
|
||||||
) -> AuthContext:
|
) -> 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)
|
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)
|
key_hash = hash_api_key(api_key)
|
||||||
repo = ApiKeyRepository(db)
|
repo = ApiKeyRepository(db)
|
||||||
obj = repo.get_by_hash(key_hash)
|
obj = repo.get_by_hash(key_hash)
|
||||||
|
|
|
||||||
|
|
@ -51,3 +51,9 @@ def get_translator(locale: str = "fa") -> Translator:
|
||||||
return Translator(locale)
|
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:
|
def decorator(func: Callable) -> Callable:
|
||||||
@wraps(func)
|
@wraps(func)
|
||||||
def wrapper(*args, **kwargs) -> Any:
|
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)
|
business_id = kwargs.get(business_id_param)
|
||||||
if business_id and not ctx.can_access_business(business_id):
|
if business_id and not ctx.can_access_business(business_id):
|
||||||
raise ApiError("FORBIDDEN", f"No access to business {business_id}", http_status=403)
|
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.auth import router as auth_router
|
||||||
from adapters.api.v1.users import router as users_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.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.tickets import router as support_tickets_router
|
||||||
from adapters.api.v1.support.operator import router as support_operator_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.categories import router as support_categories_router
|
||||||
from adapters.api.v1.support.priorities import router as support_priorities_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.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.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.i18n import negotiate_locale, Translator
|
||||||
from app.core.error_handlers import register_error_handlers
|
from app.core.error_handlers import register_error_handlers
|
||||||
from app.core.smart_normalizer import smart_normalize_json, SmartNormalizerConfig
|
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(auth_router, prefix=settings.api_v1_prefix)
|
||||||
application.include_router(users_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(businesses_router, prefix=settings.api_v1_prefix)
|
||||||
|
application.include_router(business_dashboard_router, prefix=settings.api_v1_prefix)
|
||||||
|
|
||||||
# Support endpoints
|
# Support endpoints
|
||||||
application.include_router(support_tickets_router, prefix=f"{settings.api_v1_prefix}/support")
|
application.include_router(support_tickets_router, prefix=f"{settings.api_v1_prefix}/support")
|
||||||
|
|
@ -278,6 +281,7 @@ def create_app() -> FastAPI:
|
||||||
|
|
||||||
# Admin endpoints
|
# Admin endpoints
|
||||||
application.include_router(admin_file_storage_router, prefix=settings.api_v1_prefix)
|
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)
|
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,
|
"province": business.province,
|
||||||
"city": business.city,
|
"city": business.city,
|
||||||
"postal_code": business.postal_code,
|
"postal_code": business.postal_code,
|
||||||
"created_at": business.created_at.isoformat(),
|
"created_at": business.created_at, # datetime object بماند
|
||||||
"updated_at": business.updated_at.isoformat()
|
"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/health.py
|
||||||
adapters/api/v1/schemas.py
|
adapters/api/v1/schemas.py
|
||||||
adapters/api/v1/users.py
|
adapters/api/v1/users.py
|
||||||
|
adapters/api/v1/admin/email_config.py
|
||||||
adapters/api/v1/admin/file_storage.py
|
adapters/api/v1/admin/file_storage.py
|
||||||
adapters/api/v1/schema_models/__init__.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/schema_models/file_storage.py
|
||||||
adapters/api/v1/support/__init__.py
|
adapters/api/v1/support/__init__.py
|
||||||
adapters/api/v1/support/categories.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.py
|
||||||
adapters/db/models/business_permission.py
|
adapters/db/models/business_permission.py
|
||||||
adapters/db/models/captcha.py
|
adapters/db/models/captcha.py
|
||||||
|
adapters/db/models/email_config.py
|
||||||
adapters/db/models/file_storage.py
|
adapters/db/models/file_storage.py
|
||||||
adapters/db/models/password_reset.py
|
adapters/db/models/password_reset.py
|
||||||
adapters/db/models/user.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/base_repo.py
|
||||||
adapters/db/repositories/business_permission_repo.py
|
adapters/db/repositories/business_permission_repo.py
|
||||||
adapters/db/repositories/business_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/file_storage_repository.py
|
||||||
adapters/db/repositories/password_reset_repo.py
|
adapters/db/repositories/password_reset_repo.py
|
||||||
adapters/db/repositories/user_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/auth_service.py
|
||||||
app/services/business_service.py
|
app/services/business_service.py
|
||||||
app/services/captcha_service.py
|
app/services/captcha_service.py
|
||||||
|
app/services/email_service.py
|
||||||
app/services/file_storage_service.py
|
app/services/file_storage_service.py
|
||||||
app/services/query_service.py
|
app/services/query_service.py
|
||||||
app/services/pdf/__init__.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_000005_add_business_geographic_fields.py
|
||||||
migrations/versions/20250117_000006_add_app_permissions_to_users.py
|
migrations/versions/20250117_000006_add_app_permissions_to_users.py
|
||||||
migrations/versions/20250117_000007_create_business_permissions_table.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/20250915_000001_init_auth_tables.py
|
||||||
migrations/versions/20250916_000002_add_referral_fields.py
|
migrations/versions/20250916_000002_add_referral_fields.py
|
||||||
migrations/versions/5553f8745c6e_add_support_tables.py
|
migrations/versions/5553f8745c6e_add_support_tables.py
|
||||||
|
|
|
||||||
Binary file not shown.
|
|
@ -33,6 +33,47 @@ msgstr "Field is required"
|
||||||
msgid "INVALID_EMAIL"
|
msgid "INVALID_EMAIL"
|
||||||
msgstr "Invalid email address"
|
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
|
# Auth
|
||||||
msgid "INVALID_CAPTCHA"
|
msgid "INVALID_CAPTCHA"
|
||||||
msgstr "Invalid captcha code."
|
msgstr "Invalid captcha code."
|
||||||
|
|
|
||||||
Binary file not shown.
|
|
@ -33,6 +33,47 @@ msgstr "فیلد الزامی است"
|
||||||
msgid "INVALID_EMAIL"
|
msgid "INVALID_EMAIL"
|
||||||
msgstr "ایمیل نامعتبر است"
|
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
|
# Auth
|
||||||
msgid "INVALID_CAPTCHA"
|
msgid "INVALID_CAPTCHA"
|
||||||
msgstr "کد امنیتی نامعتبر است."
|
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 'dart:async';
|
||||||
|
|
||||||
import 'package:flutter/foundation.dart';
|
|
||||||
import 'package:shared_preferences/shared_preferences.dart';
|
import 'package:shared_preferences/shared_preferences.dart';
|
||||||
|
|
||||||
class ReferralStore {
|
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",
|
"systemSettings": "System Settings",
|
||||||
"adminTools": "Admin Tools",
|
"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",
|
"ok": "OK",
|
||||||
"cancel": "Cancel",
|
"cancel": "Cancel",
|
||||||
"columnSettings": "Column Settings",
|
"columnSettings": "Column Settings",
|
||||||
|
|
@ -202,6 +226,8 @@
|
||||||
"trading": "Trading",
|
"trading": "Trading",
|
||||||
"service": "Service",
|
"service": "Service",
|
||||||
"other": "Other",
|
"other": "Other",
|
||||||
|
"owner": "Owner",
|
||||||
|
"member": "Member",
|
||||||
"support": "Support",
|
"support": "Support",
|
||||||
"newTicket": "New Ticket",
|
"newTicket": "New Ticket",
|
||||||
"ticketTitle": "Ticket Title",
|
"ticketTitle": "Ticket Title",
|
||||||
|
|
@ -340,6 +366,32 @@
|
||||||
"connectionSuccessful": "Connection Successful",
|
"connectionSuccessful": "Connection Successful",
|
||||||
"connectionFailed": "Connection Failed",
|
"connectionFailed": "Connection Failed",
|
||||||
"setAsDefault": "Set as Default",
|
"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",
|
"fileStatistics": "File Statistics",
|
||||||
"totalFiles": "Total Files",
|
"totalFiles": "Total Files",
|
||||||
"totalSize": "Total Size",
|
"totalSize": "Total Size",
|
||||||
|
|
@ -413,6 +465,24 @@
|
||||||
"systemAdministration": "System Administration",
|
"systemAdministration": "System Administration",
|
||||||
"generalSettings": "General Settings",
|
"generalSettings": "General Settings",
|
||||||
"securitySettings": "Security 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": "بازاریابی",
|
"marketing": "بازاریابی",
|
||||||
"systemSettings": "تنظیمات سیستم",
|
"systemSettings": "تنظیمات سیستم",
|
||||||
"adminTools": "ابزارهای مدیریتی",
|
"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": "تایید",
|
"ok": "تایید",
|
||||||
"cancel": "انصراف",
|
"cancel": "انصراف",
|
||||||
"columnSettings": "تنظیمات ستونها",
|
"columnSettings": "تنظیمات ستونها",
|
||||||
|
|
@ -201,6 +225,8 @@
|
||||||
"trading": "بازرگانی",
|
"trading": "بازرگانی",
|
||||||
"service": "خدماتی",
|
"service": "خدماتی",
|
||||||
"other": "سایر",
|
"other": "سایر",
|
||||||
|
"owner": "مالک",
|
||||||
|
"member": "عضو",
|
||||||
"support": "پشتیبانی",
|
"support": "پشتیبانی",
|
||||||
"newTicket": "تیکت جدید",
|
"newTicket": "تیکت جدید",
|
||||||
"ticketTitle": "عنوان تیکت",
|
"ticketTitle": "عنوان تیکت",
|
||||||
|
|
@ -339,6 +365,32 @@
|
||||||
"connectionSuccessful": "اتصال موفقیتآمیز",
|
"connectionSuccessful": "اتصال موفقیتآمیز",
|
||||||
"connectionFailed": "اتصال ناموفق",
|
"connectionFailed": "اتصال ناموفق",
|
||||||
"setAsDefault": "تنظیم به عنوان پیشفرض",
|
"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": "آمار فایلها",
|
"fileStatistics": "آمار فایلها",
|
||||||
"totalFiles": "کل فایلها",
|
"totalFiles": "کل فایلها",
|
||||||
"totalSize": "حجم کل",
|
"totalSize": "حجم کل",
|
||||||
|
|
@ -412,6 +464,24 @@
|
||||||
"systemAdministration": "مدیریت سیستم",
|
"systemAdministration": "مدیریت سیستم",
|
||||||
"generalSettings": "تنظیمات عمومی",
|
"generalSettings": "تنظیمات عمومی",
|
||||||
"securitySettings": "تنظیمات امنیتی",
|
"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'**
|
/// **'Admin Tools'**
|
||||||
String get adminTools;
|
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.
|
/// No description provided for @ok.
|
||||||
///
|
///
|
||||||
/// In en, this message translates to:
|
/// In en, this message translates to:
|
||||||
|
|
@ -1208,6 +1352,18 @@ abstract class AppLocalizations {
|
||||||
/// **'Other'**
|
/// **'Other'**
|
||||||
String get 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.
|
/// No description provided for @newTicket.
|
||||||
///
|
///
|
||||||
/// In en, this message translates to:
|
/// In en, this message translates to:
|
||||||
|
|
@ -1844,12 +2000,6 @@ abstract class AppLocalizations {
|
||||||
/// **'Default'**
|
/// **'Default'**
|
||||||
String get isDefault;
|
String get isDefault;
|
||||||
|
|
||||||
/// No description provided for @isActive.
|
|
||||||
///
|
|
||||||
/// In en, this message translates to:
|
|
||||||
/// **'Active'**
|
|
||||||
String get isActive;
|
|
||||||
|
|
||||||
/// No description provided for @configData.
|
/// No description provided for @configData.
|
||||||
///
|
///
|
||||||
/// In en, this message translates to:
|
/// In en, this message translates to:
|
||||||
|
|
@ -1892,12 +2042,6 @@ abstract class AppLocalizations {
|
||||||
/// **'FTP Directory'**
|
/// **'FTP Directory'**
|
||||||
String get ftpDirectory;
|
String get ftpDirectory;
|
||||||
|
|
||||||
/// No description provided for @testConnection.
|
|
||||||
///
|
|
||||||
/// In en, this message translates to:
|
|
||||||
/// **'Test Connection'**
|
|
||||||
String get testConnection;
|
|
||||||
|
|
||||||
/// No description provided for @connectionSuccessful.
|
/// No description provided for @connectionSuccessful.
|
||||||
///
|
///
|
||||||
/// In en, this message translates to:
|
/// In en, this message translates to:
|
||||||
|
|
@ -1916,6 +2060,156 @@ abstract class AppLocalizations {
|
||||||
/// **'Set as Default'**
|
/// **'Set as Default'**
|
||||||
String get setAsDefault;
|
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.
|
/// No description provided for @fileStatistics.
|
||||||
///
|
///
|
||||||
/// In en, this message translates to:
|
/// In en, this message translates to:
|
||||||
|
|
@ -2162,12 +2456,6 @@ abstract class AppLocalizations {
|
||||||
/// **'Edit'**
|
/// **'Edit'**
|
||||||
String get edit;
|
String get edit;
|
||||||
|
|
||||||
/// No description provided for @delete.
|
|
||||||
///
|
|
||||||
/// In en, this message translates to:
|
|
||||||
/// **'Delete'**
|
|
||||||
String get delete;
|
|
||||||
|
|
||||||
/// No description provided for @actions.
|
/// No description provided for @actions.
|
||||||
///
|
///
|
||||||
/// In en, this message translates to:
|
/// In en, this message translates to:
|
||||||
|
|
@ -2317,6 +2605,114 @@ abstract class AppLocalizations {
|
||||||
/// In en, this message translates to:
|
/// In en, this message translates to:
|
||||||
/// **'Maintenance Settings'**
|
/// **'Maintenance Settings'**
|
||||||
String get maintenanceSettings;
|
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
|
class _AppLocalizationsDelegate
|
||||||
|
|
|
||||||
|
|
@ -136,6 +136,80 @@ class AppLocalizationsEn extends AppLocalizations {
|
||||||
@override
|
@override
|
||||||
String get adminTools => 'Admin Tools';
|
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
|
@override
|
||||||
String get ok => 'OK';
|
String get ok => 'OK';
|
||||||
|
|
||||||
|
|
@ -577,6 +651,12 @@ class AppLocalizationsEn extends AppLocalizations {
|
||||||
@override
|
@override
|
||||||
String get other => 'Other';
|
String get other => 'Other';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get owner => 'Owner';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get member => 'Member';
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String get newTicket => 'New Ticket';
|
String get newTicket => 'New Ticket';
|
||||||
|
|
||||||
|
|
@ -908,9 +988,6 @@ class AppLocalizationsEn extends AppLocalizations {
|
||||||
@override
|
@override
|
||||||
String get isDefault => 'Default';
|
String get isDefault => 'Default';
|
||||||
|
|
||||||
@override
|
|
||||||
String get isActive => 'Active';
|
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String get configData => 'Configuration Data';
|
String get configData => 'Configuration Data';
|
||||||
|
|
||||||
|
|
@ -932,9 +1009,6 @@ class AppLocalizationsEn extends AppLocalizations {
|
||||||
@override
|
@override
|
||||||
String get ftpDirectory => 'FTP Directory';
|
String get ftpDirectory => 'FTP Directory';
|
||||||
|
|
||||||
@override
|
|
||||||
String get testConnection => 'Test Connection';
|
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String get connectionSuccessful => 'Connection Successful';
|
String get connectionSuccessful => 'Connection Successful';
|
||||||
|
|
||||||
|
|
@ -944,6 +1018,86 @@ class AppLocalizationsEn extends AppLocalizations {
|
||||||
@override
|
@override
|
||||||
String get setAsDefault => 'Set as Default';
|
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
|
@override
|
||||||
String get fileStatistics => 'File Statistics';
|
String get fileStatistics => 'File Statistics';
|
||||||
|
|
||||||
|
|
@ -1071,9 +1225,6 @@ class AppLocalizationsEn extends AppLocalizations {
|
||||||
@override
|
@override
|
||||||
String get edit => 'Edit';
|
String get edit => 'Edit';
|
||||||
|
|
||||||
@override
|
|
||||||
String get delete => 'Delete';
|
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String get actions => 'Actions';
|
String get actions => 'Actions';
|
||||||
|
|
||||||
|
|
@ -1151,4 +1302,58 @@ class AppLocalizationsEn extends AppLocalizations {
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String get maintenanceSettings => 'Maintenance Settings';
|
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
|
@override
|
||||||
String get adminTools => 'ابزارهای مدیریتی';
|
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
|
@override
|
||||||
String get ok => 'تایید';
|
String get ok => 'تایید';
|
||||||
|
|
||||||
|
|
@ -575,6 +649,12 @@ class AppLocalizationsFa extends AppLocalizations {
|
||||||
@override
|
@override
|
||||||
String get other => 'سایر';
|
String get other => 'سایر';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get owner => 'مالک';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get member => 'عضو';
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String get newTicket => 'تیکت جدید';
|
String get newTicket => 'تیکت جدید';
|
||||||
|
|
||||||
|
|
@ -904,9 +984,6 @@ class AppLocalizationsFa extends AppLocalizations {
|
||||||
@override
|
@override
|
||||||
String get isDefault => 'پیشفرض';
|
String get isDefault => 'پیشفرض';
|
||||||
|
|
||||||
@override
|
|
||||||
String get isActive => 'فعال';
|
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String get configData => 'دادههای پیکربندی';
|
String get configData => 'دادههای پیکربندی';
|
||||||
|
|
||||||
|
|
@ -928,9 +1005,6 @@ class AppLocalizationsFa extends AppLocalizations {
|
||||||
@override
|
@override
|
||||||
String get ftpDirectory => 'پوشه FTP';
|
String get ftpDirectory => 'پوشه FTP';
|
||||||
|
|
||||||
@override
|
|
||||||
String get testConnection => 'تست اتصال';
|
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String get connectionSuccessful => 'اتصال موفقیتآمیز';
|
String get connectionSuccessful => 'اتصال موفقیتآمیز';
|
||||||
|
|
||||||
|
|
@ -940,6 +1014,84 @@ class AppLocalizationsFa extends AppLocalizations {
|
||||||
@override
|
@override
|
||||||
String get setAsDefault => 'تنظیم به عنوان پیشفرض';
|
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
|
@override
|
||||||
String get fileStatistics => 'آمار فایلها';
|
String get fileStatistics => 'آمار فایلها';
|
||||||
|
|
||||||
|
|
@ -1065,9 +1217,6 @@ class AppLocalizationsFa extends AppLocalizations {
|
||||||
@override
|
@override
|
||||||
String get edit => 'ویرایش';
|
String get edit => 'ویرایش';
|
||||||
|
|
||||||
@override
|
|
||||||
String get delete => 'حذف';
|
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String get actions => 'عملیات';
|
String get actions => 'عملیات';
|
||||||
|
|
||||||
|
|
@ -1143,4 +1292,58 @@ class AppLocalizationsFa extends AppLocalizations {
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String get maintenanceSettings => 'تنظیمات نگهداری';
|
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/system_configuration_page.dart';
|
||||||
import 'pages/admin/user_management_page.dart';
|
import 'pages/admin/user_management_page.dart';
|
||||||
import 'pages/admin/system_logs_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 'package:hesabix_ui/l10n/app_localizations.dart';
|
||||||
import 'core/locale_controller.dart';
|
import 'core/locale_controller.dart';
|
||||||
import 'core/calendar_controller.dart';
|
import 'core/calendar_controller.dart';
|
||||||
|
|
@ -25,6 +28,7 @@ import 'theme/theme_controller.dart';
|
||||||
import 'theme/app_theme.dart';
|
import 'theme/app_theme.dart';
|
||||||
import 'core/auth_store.dart';
|
import 'core/auth_store.dart';
|
||||||
import 'core/permission_guard.dart';
|
import 'core/permission_guard.dart';
|
||||||
|
import 'widgets/simple_splash_screen.dart';
|
||||||
|
|
||||||
void main() {
|
void main() {
|
||||||
// Use path-based routing instead of hash routing
|
// Use path-based routing instead of hash routing
|
||||||
|
|
@ -44,216 +48,131 @@ class _MyAppState extends State<MyApp> {
|
||||||
CalendarController? _calendarController;
|
CalendarController? _calendarController;
|
||||||
ThemeController? _themeController;
|
ThemeController? _themeController;
|
||||||
AuthStore? _authStore;
|
AuthStore? _authStore;
|
||||||
|
bool _isLoading = true;
|
||||||
|
DateTime? _loadStartTime;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
void initState() {
|
void initState() {
|
||||||
super.initState();
|
super.initState();
|
||||||
LocaleController.load().then((c) {
|
_loadStartTime = DateTime.now();
|
||||||
|
_loadControllers();
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> _loadControllers() async {
|
||||||
|
// بارگذاری تمام کنترلرها
|
||||||
|
final localeController = await LocaleController.load();
|
||||||
|
final calendarController = await CalendarController.load();
|
||||||
|
final themeController = ThemeController();
|
||||||
|
await themeController.load();
|
||||||
|
final authStore = AuthStore();
|
||||||
|
await authStore.load();
|
||||||
|
|
||||||
|
// تنظیم کنترلرها
|
||||||
setState(() {
|
setState(() {
|
||||||
_controller = c
|
_controller = localeController;
|
||||||
..addListener(() {
|
_calendarController = calendarController;
|
||||||
// Update ApiClient language header on change
|
_themeController = themeController;
|
||||||
ApiClient.setCurrentLocale(c.locale);
|
_authStore = authStore;
|
||||||
setState(() {});
|
|
||||||
});
|
|
||||||
ApiClient.setCurrentLocale(c.locale);
|
|
||||||
});
|
|
||||||
});
|
});
|
||||||
|
|
||||||
CalendarController.load().then((cc) {
|
// اضافه کردن listeners
|
||||||
setState(() {
|
_controller!.addListener(() {
|
||||||
_calendarController = cc
|
ApiClient.setCurrentLocale(_controller!.locale);
|
||||||
..addListener(() {
|
|
||||||
setState(() {});
|
setState(() {});
|
||||||
});
|
});
|
||||||
ApiClient.bindCalendarController(cc);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
final tc = ThemeController();
|
_calendarController!.addListener(() {
|
||||||
tc.load().then((_) {
|
|
||||||
setState(() {
|
|
||||||
_themeController = tc
|
|
||||||
..addListener(() {
|
|
||||||
setState(() {});
|
setState(() {});
|
||||||
});
|
});
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
final store = AuthStore();
|
_themeController!.addListener(() {
|
||||||
store.load().then((_) {
|
|
||||||
setState(() {
|
|
||||||
_authStore = store
|
|
||||||
..addListener(() {
|
|
||||||
setState(() {});
|
setState(() {});
|
||||||
});
|
});
|
||||||
ApiClient.bindAuthStore(store);
|
|
||||||
|
_authStore!.addListener(() {
|
||||||
|
setState(() {});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// تنظیم API Client
|
||||||
|
ApiClient.setCurrentLocale(_controller!.locale);
|
||||||
|
ApiClient.bindCalendarController(_calendarController!);
|
||||||
|
ApiClient.bindAuthStore(_authStore!);
|
||||||
|
|
||||||
|
// اطمینان از حداقل 4 ثانیه نمایش splash screen
|
||||||
|
final elapsed = DateTime.now().difference(_loadStartTime!);
|
||||||
|
const minimumDuration = Duration(seconds: 4);
|
||||||
|
if (elapsed < minimumDuration) {
|
||||||
|
await Future.delayed(minimumDuration - elapsed);
|
||||||
|
}
|
||||||
|
|
||||||
|
// اتمام loading
|
||||||
|
if (mounted) {
|
||||||
|
setState(() {
|
||||||
|
_isLoading = false;
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Root of application with GoRouter
|
// Root of application with GoRouter
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
// اگر هنوز loading است، یک router ساده با loading page بساز
|
// اگر هنوز loading است، splash screen نمایش بده
|
||||||
if (_controller == null || _calendarController == null || _themeController == null || _authStore == null) {
|
if (_isLoading ||
|
||||||
|
_controller == null ||
|
||||||
|
_calendarController == null ||
|
||||||
|
_themeController == null ||
|
||||||
|
_authStore == null) {
|
||||||
final loadingRouter = GoRouter(
|
final loadingRouter = GoRouter(
|
||||||
redirect: (context, state) {
|
redirect: (context, state) {
|
||||||
// در حین loading، هیچ redirect نکن - URL را حفظ کن
|
// در حین loading، هیچ redirect نکن - URL را حفظ کن
|
||||||
return null;
|
return null;
|
||||||
},
|
},
|
||||||
routes: <RouteBase>[
|
routes: <RouteBase>[
|
||||||
GoRoute(
|
// برای تمام مسیرها splash screen نمایش بده
|
||||||
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 دیگر
|
|
||||||
GoRoute(
|
GoRoute(
|
||||||
path: '/:path(.*)',
|
path: '/:path(.*)',
|
||||||
builder: (context, state) => const Scaffold(
|
builder: (context, state) {
|
||||||
body: Center(
|
// تشخیص نوع loading بر اساس controller های موجود
|
||||||
child: Column(
|
String loadingMessage = 'Initializing...';
|
||||||
mainAxisAlignment: MainAxisAlignment.center,
|
if (_controller == null) {
|
||||||
children: [
|
loadingMessage = 'Loading language settings...';
|
||||||
CircularProgressIndicator(),
|
} else if (_calendarController == null) {
|
||||||
SizedBox(height: 16),
|
loadingMessage = 'Loading calendar settings...';
|
||||||
Text('Loading...'),
|
} 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'),
|
locale: const Locale('en'),
|
||||||
supportedLocales: const [Locale('en'), Locale('fa')],
|
supportedLocales: const [Locale('en'), Locale('fa')],
|
||||||
localizationsDelegates: const [
|
localizationsDelegates: const [
|
||||||
|
AppLocalizations.delegate,
|
||||||
GlobalMaterialLocalizations.delegate,
|
GlobalMaterialLocalizations.delegate,
|
||||||
GlobalCupertinoLocalizations.delegate,
|
GlobalCupertinoLocalizations.delegate,
|
||||||
GlobalWidgetsLocalizations.delegate,
|
GlobalWidgetsLocalizations.delegate,
|
||||||
|
|
@ -431,10 +351,49 @@ class _MyAppState extends State<MyApp> {
|
||||||
return const SystemLogsPage();
|
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:flutter/material.dart';
|
||||||
import 'package:hesabix_ui/l10n/app_localizations.dart';
|
|
||||||
import 'package:hesabix_ui/widgets/admin/file_storage/storage_management_page.dart';
|
import 'package:hesabix_ui/widgets/admin/file_storage/storage_management_page.dart';
|
||||||
|
|
||||||
class FileStorageSettingsPage extends StatelessWidget {
|
class FileStorageSettingsPage extends StatelessWidget {
|
||||||
|
|
|
||||||
|
|
@ -42,7 +42,7 @@ class _AdminStorageManagementPageState extends State<AdminStorageManagementPage>
|
||||||
begin: Alignment.topCenter,
|
begin: Alignment.topCenter,
|
||||||
end: Alignment.bottomCenter,
|
end: Alignment.bottomCenter,
|
||||||
colors: [
|
colors: [
|
||||||
theme.colorScheme.primary.withOpacity(0.1),
|
theme.colorScheme.primary.withValues(alpha: 0.1),
|
||||||
theme.colorScheme.surface,
|
theme.colorScheme.surface,
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
|
|
|
||||||
|
|
@ -65,7 +65,7 @@ class _SystemConfigurationPageState extends State<SystemConfigurationPage> {
|
||||||
begin: Alignment.topCenter,
|
begin: Alignment.topCenter,
|
||||||
end: Alignment.bottomCenter,
|
end: Alignment.bottomCenter,
|
||||||
colors: [
|
colors: [
|
||||||
theme.colorScheme.primary.withOpacity(0.1),
|
theme.colorScheme.primary.withValues(alpha: 0.1),
|
||||||
theme.colorScheme.surface,
|
theme.colorScheme.surface,
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
|
|
@ -232,7 +232,7 @@ class _SystemConfigurationPageState extends State<SystemConfigurationPage> {
|
||||||
return Padding(
|
return Padding(
|
||||||
padding: const EdgeInsets.only(bottom: 16),
|
padding: const EdgeInsets.only(bottom: 16),
|
||||||
child: DropdownButtonFormField<String>(
|
child: DropdownButtonFormField<String>(
|
||||||
value: value,
|
initialValue: value,
|
||||||
decoration: InputDecoration(
|
decoration: InputDecoration(
|
||||||
labelText: label,
|
labelText: label,
|
||||||
border: const OutlineInputBorder(),
|
border: const OutlineInputBorder(),
|
||||||
|
|
|
||||||
|
|
@ -115,7 +115,7 @@ class _SystemLogsPageState extends State<SystemLogsPage> {
|
||||||
begin: Alignment.topCenter,
|
begin: Alignment.topCenter,
|
||||||
end: Alignment.bottomCenter,
|
end: Alignment.bottomCenter,
|
||||||
colors: [
|
colors: [
|
||||||
theme.colorScheme.primary.withOpacity(0.1),
|
theme.colorScheme.primary.withValues(alpha: 0.1),
|
||||||
theme.colorScheme.surface,
|
theme.colorScheme.surface,
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
|
|
@ -162,7 +162,7 @@ class _SystemLogsPageState extends State<SystemLogsPage> {
|
||||||
children: [
|
children: [
|
||||||
Expanded(
|
Expanded(
|
||||||
child: DropdownButtonFormField<String>(
|
child: DropdownButtonFormField<String>(
|
||||||
value: _selectedLevel,
|
initialValue: _selectedLevel,
|
||||||
decoration: const InputDecoration(
|
decoration: const InputDecoration(
|
||||||
labelText: 'Log Level',
|
labelText: 'Log Level',
|
||||||
border: OutlineInputBorder(),
|
border: OutlineInputBorder(),
|
||||||
|
|
@ -180,7 +180,7 @@ class _SystemLogsPageState extends State<SystemLogsPage> {
|
||||||
const SizedBox(width: 16),
|
const SizedBox(width: 16),
|
||||||
Expanded(
|
Expanded(
|
||||||
child: DropdownButtonFormField<String>(
|
child: DropdownButtonFormField<String>(
|
||||||
value: _selectedDateRange,
|
initialValue: _selectedDateRange,
|
||||||
decoration: const InputDecoration(
|
decoration: const InputDecoration(
|
||||||
labelText: 'Date Range',
|
labelText: 'Date Range',
|
||||||
border: OutlineInputBorder(),
|
border: OutlineInputBorder(),
|
||||||
|
|
@ -212,13 +212,13 @@ class _SystemLogsPageState extends State<SystemLogsPage> {
|
||||||
Icon(
|
Icon(
|
||||||
Icons.analytics_outlined,
|
Icons.analytics_outlined,
|
||||||
size: 64,
|
size: 64,
|
||||||
color: theme.colorScheme.onSurface.withOpacity(0.5),
|
color: theme.colorScheme.onSurface.withValues(alpha: 0.5),
|
||||||
),
|
),
|
||||||
const SizedBox(height: 16),
|
const SizedBox(height: 16),
|
||||||
Text(
|
Text(
|
||||||
'No logs found',
|
'No logs found',
|
||||||
style: theme.textTheme.titleLarge?.copyWith(
|
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(
|
subtitle: Text(
|
||||||
'${log['timestamp']} • ${log['module']}',
|
'${log['timestamp']} • ${log['module']}',
|
||||||
style: TextStyle(
|
style: TextStyle(
|
||||||
color: theme.colorScheme.onSurface.withOpacity(0.7),
|
color: theme.colorScheme.onSurface.withValues(alpha: 0.7),
|
||||||
fontSize: 12,
|
fontSize: 12,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
|
@ -270,7 +270,7 @@ class _SystemLogsPageState extends State<SystemLogsPage> {
|
||||||
width: double.infinity,
|
width: double.infinity,
|
||||||
padding: const EdgeInsets.all(12),
|
padding: const EdgeInsets.all(12),
|
||||||
decoration: BoxDecoration(
|
decoration: BoxDecoration(
|
||||||
color: theme.colorScheme.surfaceVariant.withOpacity(0.5),
|
color: theme.colorScheme.surfaceContainerHighest.withValues(alpha: 0.5),
|
||||||
borderRadius: BorderRadius.circular(8),
|
borderRadius: BorderRadius.circular(8),
|
||||||
),
|
),
|
||||||
child: Text(
|
child: Text(
|
||||||
|
|
|
||||||
|
|
@ -96,7 +96,7 @@ class _UserManagementPageState extends State<UserManagementPage> {
|
||||||
begin: Alignment.topCenter,
|
begin: Alignment.topCenter,
|
||||||
end: Alignment.bottomCenter,
|
end: Alignment.bottomCenter,
|
||||||
colors: [
|
colors: [
|
||||||
theme.colorScheme.primary.withOpacity(0.1),
|
theme.colorScheme.primary.withValues(alpha: 0.1),
|
||||||
theme.colorScheme.surface,
|
theme.colorScheme.surface,
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
|
|
@ -150,7 +150,7 @@ class _UserManagementPageState extends State<UserManagementPage> {
|
||||||
children: [
|
children: [
|
||||||
Expanded(
|
Expanded(
|
||||||
child: DropdownButtonFormField<String>(
|
child: DropdownButtonFormField<String>(
|
||||||
value: _selectedFilter,
|
initialValue: _selectedFilter,
|
||||||
decoration: const InputDecoration(
|
decoration: const InputDecoration(
|
||||||
labelText: 'Filter',
|
labelText: 'Filter',
|
||||||
border: OutlineInputBorder(),
|
border: OutlineInputBorder(),
|
||||||
|
|
@ -190,13 +190,13 @@ class _UserManagementPageState extends State<UserManagementPage> {
|
||||||
Icon(
|
Icon(
|
||||||
Icons.people_outline,
|
Icons.people_outline,
|
||||||
size: 64,
|
size: 64,
|
||||||
color: theme.colorScheme.onSurface.withOpacity(0.5),
|
color: theme.colorScheme.onSurface.withValues(alpha: 0.5),
|
||||||
),
|
),
|
||||||
const SizedBox(height: 16),
|
const SizedBox(height: 16),
|
||||||
Text(
|
Text(
|
||||||
'No users found',
|
'No users found',
|
||||||
style: theme.textTheme.titleLarge?.copyWith(
|
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(),
|
role.toUpperCase(),
|
||||||
style: const TextStyle(fontSize: 10, fontWeight: FontWeight.bold),
|
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)),
|
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) {
|
Widget build(BuildContext context) {
|
||||||
return PopupMenuButton<ThemeMode>(
|
return PopupMenuButton<ThemeMode>(
|
||||||
icon: const Icon(Icons.color_lens_outlined),
|
icon: const Icon(Icons.color_lens_outlined),
|
||||||
initialValue: controller.mode,
|
|
||||||
onSelected: (mode) => controller.setMode(mode),
|
onSelected: (mode) => controller.setMode(mode),
|
||||||
itemBuilder: (context) => const [
|
itemBuilder: (context) => const [
|
||||||
PopupMenuItem(value: ThemeMode.system, child: Text('System')),
|
PopupMenuItem(value: ThemeMode.system, child: Text('System')),
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,5 @@
|
||||||
import 'dart:async';
|
import 'dart:async';
|
||||||
import 'dart:convert';
|
import 'dart:convert';
|
||||||
import 'dart:typed_data';
|
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:flutter/services.dart';
|
import 'package:flutter/services.dart';
|
||||||
import 'package:go_router/go_router.dart';
|
import 'package:go_router/go_router.dart';
|
||||||
|
|
@ -352,7 +351,9 @@ class _LoginPageState extends State<LoginPage> with SingleTickerProviderStateMix
|
||||||
_showSnack(t.registerSuccess);
|
_showSnack(t.registerSuccess);
|
||||||
// پاکسازی کد معرف پس از ثبتنام موفق
|
// پاکسازی کد معرف پس از ثبتنام موفق
|
||||||
unawaited(ReferralStore.clearReferrer());
|
unawaited(ReferralStore.clearReferrer());
|
||||||
|
if (mounted) {
|
||||||
context.go('/user/profile/dashboard');
|
context.go('/user/profile/dashboard');
|
||||||
|
}
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
if (!mounted) return;
|
if (!mounted) return;
|
||||||
final msg = _extractErrorMessage(e, AppLocalizations.of(context));
|
final msg = _extractErrorMessage(e, AppLocalizations.of(context));
|
||||||
|
|
|
||||||
|
|
@ -1,24 +1,447 @@
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:go_router/go_router.dart';
|
||||||
import 'package:hesabix_ui/l10n/app_localizations.dart';
|
import 'package:hesabix_ui/l10n/app_localizations.dart';
|
||||||
|
import '../../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});
|
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
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
final t = AppLocalizations.of(context);
|
final t = AppLocalizations.of(context);
|
||||||
|
|
||||||
return Padding(
|
return Padding(
|
||||||
padding: const EdgeInsets.all(16.0),
|
padding: const EdgeInsets.all(16.0),
|
||||||
child: Column(
|
child: Column(
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
children: [
|
children: [
|
||||||
Text(t.businesses, style: Theme.of(context).textTheme.titleLarge),
|
Row(
|
||||||
const SizedBox(height: 8),
|
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||||
Text('${t.businesses} - sample page'),
|
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),
|
borderRadius: BorderRadius.circular(12),
|
||||||
boxShadow: [
|
boxShadow: [
|
||||||
BoxShadow(
|
BoxShadow(
|
||||||
color: Colors.black.withOpacity(0.2),
|
color: Colors.black.withValues(alpha: 0.2),
|
||||||
blurRadius: 8,
|
blurRadius: 8,
|
||||||
offset: const Offset(0, 4),
|
offset: const Offset(0, 4),
|
||||||
),
|
),
|
||||||
|
|
@ -189,8 +189,8 @@ class _CreateTicketPageState extends State<CreateTicketPage> {
|
||||||
decoration: BoxDecoration(
|
decoration: BoxDecoration(
|
||||||
gradient: LinearGradient(
|
gradient: LinearGradient(
|
||||||
colors: [
|
colors: [
|
||||||
theme.colorScheme.primary.withOpacity(0.1),
|
theme.colorScheme.primary.withValues(alpha: 0.1),
|
||||||
theme.colorScheme.primary.withOpacity(0.05),
|
theme.colorScheme.primary.withValues(alpha: 0.05),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
borderRadius: const BorderRadius.only(
|
borderRadius: const BorderRadius.only(
|
||||||
|
|
@ -228,7 +228,7 @@ class _CreateTicketPageState extends State<CreateTicketPage> {
|
||||||
Text(
|
Text(
|
||||||
t.descriptionHint,
|
t.descriptionHint,
|
||||||
style: theme.textTheme.bodyMedium?.copyWith(
|
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(
|
Text(
|
||||||
t.loadingData,
|
t.loadingData,
|
||||||
style: theme.textTheme.bodyLarge?.copyWith(
|
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(
|
Container(
|
||||||
padding: const EdgeInsets.all(24),
|
padding: const EdgeInsets.all(24),
|
||||||
decoration: BoxDecoration(
|
decoration: BoxDecoration(
|
||||||
color: Colors.red.withOpacity(0.1),
|
color: Colors.red.withValues(alpha: 0.1),
|
||||||
shape: BoxShape.circle,
|
shape: BoxShape.circle,
|
||||||
),
|
),
|
||||||
child: Icon(
|
child: Icon(
|
||||||
Icons.error_outline,
|
Icons.error_outline,
|
||||||
size: 48,
|
size: 48,
|
||||||
color: Colors.red.withOpacity(0.8),
|
color: Colors.red.withValues(alpha: 0.8),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
const SizedBox(height: 24),
|
const SizedBox(height: 24),
|
||||||
|
|
@ -397,7 +397,7 @@ class _CreateTicketPageState extends State<CreateTicketPage> {
|
||||||
enabledBorder: OutlineInputBorder(
|
enabledBorder: OutlineInputBorder(
|
||||||
borderRadius: BorderRadius.circular(12),
|
borderRadius: BorderRadius.circular(12),
|
||||||
borderSide: BorderSide(
|
borderSide: BorderSide(
|
||||||
color: theme.colorScheme.outline.withOpacity(0.3),
|
color: theme.colorScheme.outline.withValues(alpha: 0.3),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
focusedBorder: OutlineInputBorder(
|
focusedBorder: OutlineInputBorder(
|
||||||
|
|
@ -437,7 +437,7 @@ class _CreateTicketPageState extends State<CreateTicketPage> {
|
||||||
),
|
),
|
||||||
const SizedBox(height: 8),
|
const SizedBox(height: 8),
|
||||||
DropdownButtonFormField<SupportCategory>(
|
DropdownButtonFormField<SupportCategory>(
|
||||||
value: _selectedCategory,
|
initialValue: _selectedCategory,
|
||||||
decoration: InputDecoration(
|
decoration: InputDecoration(
|
||||||
border: OutlineInputBorder(
|
border: OutlineInputBorder(
|
||||||
borderRadius: BorderRadius.circular(12),
|
borderRadius: BorderRadius.circular(12),
|
||||||
|
|
@ -445,7 +445,7 @@ class _CreateTicketPageState extends State<CreateTicketPage> {
|
||||||
enabledBorder: OutlineInputBorder(
|
enabledBorder: OutlineInputBorder(
|
||||||
borderRadius: BorderRadius.circular(12),
|
borderRadius: BorderRadius.circular(12),
|
||||||
borderSide: BorderSide(
|
borderSide: BorderSide(
|
||||||
color: theme.colorScheme.outline.withOpacity(0.3),
|
color: theme.colorScheme.outline.withValues(alpha: 0.3),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
focusedBorder: OutlineInputBorder(
|
focusedBorder: OutlineInputBorder(
|
||||||
|
|
@ -493,7 +493,7 @@ class _CreateTicketPageState extends State<CreateTicketPage> {
|
||||||
),
|
),
|
||||||
const SizedBox(height: 8),
|
const SizedBox(height: 8),
|
||||||
DropdownButtonFormField<SupportPriority>(
|
DropdownButtonFormField<SupportPriority>(
|
||||||
value: _selectedPriority,
|
initialValue: _selectedPriority,
|
||||||
decoration: InputDecoration(
|
decoration: InputDecoration(
|
||||||
border: OutlineInputBorder(
|
border: OutlineInputBorder(
|
||||||
borderRadius: BorderRadius.circular(12),
|
borderRadius: BorderRadius.circular(12),
|
||||||
|
|
@ -501,7 +501,7 @@ class _CreateTicketPageState extends State<CreateTicketPage> {
|
||||||
enabledBorder: OutlineInputBorder(
|
enabledBorder: OutlineInputBorder(
|
||||||
borderRadius: BorderRadius.circular(12),
|
borderRadius: BorderRadius.circular(12),
|
||||||
borderSide: BorderSide(
|
borderSide: BorderSide(
|
||||||
color: theme.colorScheme.outline.withOpacity(0.3),
|
color: theme.colorScheme.outline.withValues(alpha: 0.3),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
focusedBorder: OutlineInputBorder(
|
focusedBorder: OutlineInputBorder(
|
||||||
|
|
@ -573,7 +573,7 @@ class _CreateTicketPageState extends State<CreateTicketPage> {
|
||||||
enabledBorder: OutlineInputBorder(
|
enabledBorder: OutlineInputBorder(
|
||||||
borderRadius: BorderRadius.circular(12),
|
borderRadius: BorderRadius.circular(12),
|
||||||
borderSide: BorderSide(
|
borderSide: BorderSide(
|
||||||
color: theme.colorScheme.outline.withOpacity(0.3),
|
color: theme.colorScheme.outline.withValues(alpha: 0.3),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
focusedBorder: OutlineInputBorder(
|
focusedBorder: OutlineInputBorder(
|
||||||
|
|
@ -610,7 +610,7 @@ class _CreateTicketPageState extends State<CreateTicketPage> {
|
||||||
color: theme.colorScheme.surface,
|
color: theme.colorScheme.surface,
|
||||||
border: Border(
|
border: Border(
|
||||||
top: BorderSide(
|
top: BorderSide(
|
||||||
color: theme.colorScheme.outline.withOpacity(0.2),
|
color: theme.colorScheme.outline.withValues(alpha: 0.2),
|
||||||
width: 1,
|
width: 1,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
|
@ -636,7 +636,7 @@ class _CreateTicketPageState extends State<CreateTicketPage> {
|
||||||
foregroundColor: Colors.white,
|
foregroundColor: Colors.white,
|
||||||
padding: const EdgeInsets.symmetric(vertical: 16, horizontal: 24),
|
padding: const EdgeInsets.symmetric(vertical: 16, horizontal: 24),
|
||||||
elevation: 4,
|
elevation: 4,
|
||||||
shadowColor: theme.colorScheme.primary.withOpacity(0.3),
|
shadowColor: theme.colorScheme.primary.withValues(alpha: 0.3),
|
||||||
shape: RoundedRectangleBorder(
|
shape: RoundedRectangleBorder(
|
||||||
borderRadius: BorderRadius.circular(12),
|
borderRadius: BorderRadius.circular(12),
|
||||||
),
|
),
|
||||||
|
|
|
||||||
|
|
@ -189,9 +189,9 @@ class _MarketingPageState extends State<MarketingPage> {
|
||||||
const SizedBox(width: 8),
|
const SizedBox(width: 8),
|
||||||
FilledButton.icon(
|
FilledButton.icon(
|
||||||
onPressed: () async {
|
onPressed: () async {
|
||||||
|
final messenger = ScaffoldMessenger.of(context);
|
||||||
await Clipboard.setData(ClipboardData(text: inviteLink));
|
await Clipboard.setData(ClipboardData(text: inviteLink));
|
||||||
if (!mounted) return;
|
if (!mounted) return;
|
||||||
final messenger = ScaffoldMessenger.of(context);
|
|
||||||
messenger
|
messenger
|
||||||
..hideCurrentSnackBar()
|
..hideCurrentSnackBar()
|
||||||
..showSnackBar(
|
..showSnackBar(
|
||||||
|
|
|
||||||
|
|
@ -633,7 +633,7 @@ class _NewBusinessPageState extends State<NewBusinessPage> {
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
value: _businessData.businessType,
|
initialValue: _businessData.businessType,
|
||||||
items: BusinessType.values.map((type) {
|
items: BusinessType.values.map((type) {
|
||||||
return DropdownMenuItem(
|
return DropdownMenuItem(
|
||||||
value: type,
|
value: type,
|
||||||
|
|
@ -675,7 +675,7 @@ class _NewBusinessPageState extends State<NewBusinessPage> {
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
value: _businessData.businessField,
|
initialValue: _businessData.businessField,
|
||||||
items: BusinessField.values.map((field) {
|
items: BusinessField.values.map((field) {
|
||||||
return DropdownMenuItem(
|
return DropdownMenuItem(
|
||||||
value: field,
|
value: field,
|
||||||
|
|
|
||||||
|
|
@ -5,16 +5,13 @@ class ProfileDashboardPage extends StatelessWidget {
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
return Padding(
|
return Column(
|
||||||
padding: const EdgeInsets.all(16.0),
|
|
||||||
child: Column(
|
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
children: const [
|
children: const [
|
||||||
Text('User Profile Dashboard', style: TextStyle(fontSize: 18, fontWeight: FontWeight.w600)),
|
Text('User Profile Dashboard', style: TextStyle(fontSize: 18, fontWeight: FontWeight.w600)),
|
||||||
SizedBox(height: 12),
|
SizedBox(height: 12),
|
||||||
Text('Summary and quick actions will appear here.'),
|
Text('Summary and quick actions will appear here.'),
|
||||||
],
|
],
|
||||||
),
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -152,11 +152,8 @@ class _ProfileShellState extends State<ProfileShell> {
|
||||||
final content = Container(
|
final content = Container(
|
||||||
color: scheme.surface,
|
color: scheme.surface,
|
||||||
child: SafeArea(
|
child: SafeArea(
|
||||||
child: Padding(
|
|
||||||
padding: const EdgeInsets.all(16),
|
|
||||||
child: widget.child,
|
child: widget.child,
|
||||||
),
|
),
|
||||||
),
|
|
||||||
);
|
);
|
||||||
|
|
||||||
// Side colors and styles
|
// Side colors and styles
|
||||||
|
|
|
||||||
|
|
@ -44,6 +44,13 @@ class _SystemSettingsPageState extends State<SystemSettingsPage> {
|
||||||
color: const Color(0xFF9C27B0),
|
color: const Color(0xFF9C27B0),
|
||||||
route: '/user/profile/system-settings/logs',
|
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(
|
return Scaffold(
|
||||||
backgroundColor: colorScheme.surface,
|
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(
|
body: SingleChildScrollView(
|
||||||
padding: const EdgeInsets.all(20),
|
padding: const EdgeInsets.all(20),
|
||||||
child: Column(
|
child: Column(
|
||||||
|
|
@ -97,13 +85,13 @@ class _SystemSettingsPageState extends State<SystemSettingsPage> {
|
||||||
begin: Alignment.topLeft,
|
begin: Alignment.topLeft,
|
||||||
end: Alignment.bottomRight,
|
end: Alignment.bottomRight,
|
||||||
colors: [
|
colors: [
|
||||||
colorScheme.primary.withOpacity(0.1),
|
colorScheme.primary.withValues(alpha: 0.1),
|
||||||
colorScheme.primaryContainer.withOpacity(0.3),
|
colorScheme.primaryContainer.withValues(alpha: 0.3),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
borderRadius: BorderRadius.circular(16),
|
borderRadius: BorderRadius.circular(16),
|
||||||
border: Border.all(
|
border: Border.all(
|
||||||
color: colorScheme.primary.withOpacity(0.2),
|
color: colorScheme.primary.withValues(alpha: 0.2),
|
||||||
width: 1,
|
width: 1,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
|
@ -117,13 +105,13 @@ class _SystemSettingsPageState extends State<SystemSettingsPage> {
|
||||||
end: Alignment.bottomRight,
|
end: Alignment.bottomRight,
|
||||||
colors: [
|
colors: [
|
||||||
colorScheme.primary,
|
colorScheme.primary,
|
||||||
colorScheme.primary.withOpacity(0.8),
|
colorScheme.primary.withValues(alpha: 0.8),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
borderRadius: BorderRadius.circular(12),
|
borderRadius: BorderRadius.circular(12),
|
||||||
boxShadow: [
|
boxShadow: [
|
||||||
BoxShadow(
|
BoxShadow(
|
||||||
color: colorScheme.primary.withOpacity(0.3),
|
color: colorScheme.primary.withValues(alpha: 0.3),
|
||||||
blurRadius: 8,
|
blurRadius: 8,
|
||||||
offset: const Offset(0, 2),
|
offset: const Offset(0, 2),
|
||||||
),
|
),
|
||||||
|
|
@ -152,7 +140,7 @@ class _SystemSettingsPageState extends State<SystemSettingsPage> {
|
||||||
Text(
|
Text(
|
||||||
t.systemSettingsDescription,
|
t.systemSettingsDescription,
|
||||||
style: theme.textTheme.bodyMedium?.copyWith(
|
style: theme.textTheme.bodyMedium?.copyWith(
|
||||||
color: colorScheme.onSurface.withOpacity(0.7),
|
color: colorScheme.onSurface.withValues(alpha: 0.7),
|
||||||
fontSize: 14,
|
fontSize: 14,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
|
@ -162,7 +150,7 @@ class _SystemSettingsPageState extends State<SystemSettingsPage> {
|
||||||
Container(
|
Container(
|
||||||
padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 6),
|
padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 6),
|
||||||
decoration: BoxDecoration(
|
decoration: BoxDecoration(
|
||||||
color: colorScheme.primary.withOpacity(0.1),
|
color: colorScheme.primary.withValues(alpha: 0.1),
|
||||||
borderRadius: BorderRadius.circular(20),
|
borderRadius: BorderRadius.circular(20),
|
||||||
),
|
),
|
||||||
child: Text(
|
child: Text(
|
||||||
|
|
@ -196,7 +184,7 @@ class _SystemSettingsPageState extends State<SystemSettingsPage> {
|
||||||
Container(
|
Container(
|
||||||
padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 4),
|
padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 4),
|
||||||
decoration: BoxDecoration(
|
decoration: BoxDecoration(
|
||||||
color: colorScheme.primary.withOpacity(0.1),
|
color: colorScheme.primary.withValues(alpha: 0.1),
|
||||||
borderRadius: BorderRadius.circular(12),
|
borderRadius: BorderRadius.circular(12),
|
||||||
),
|
),
|
||||||
child: Text(
|
child: Text(
|
||||||
|
|
@ -246,7 +234,7 @@ class _SystemSettingsPageState extends State<SystemSettingsPage> {
|
||||||
shape: RoundedRectangleBorder(
|
shape: RoundedRectangleBorder(
|
||||||
borderRadius: BorderRadius.circular(12),
|
borderRadius: BorderRadius.circular(12),
|
||||||
side: BorderSide(
|
side: BorderSide(
|
||||||
color: colorScheme.outline.withOpacity(0.2),
|
color: colorScheme.outline.withValues(alpha: 0.2),
|
||||||
width: 1,
|
width: 1,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
|
@ -255,8 +243,8 @@ class _SystemSettingsPageState extends State<SystemSettingsPage> {
|
||||||
child: InkWell(
|
child: InkWell(
|
||||||
onTap: () => context.go(item.route!),
|
onTap: () => context.go(item.route!),
|
||||||
borderRadius: BorderRadius.circular(12),
|
borderRadius: BorderRadius.circular(12),
|
||||||
hoverColor: item.color.withOpacity(0.05),
|
hoverColor: item.color.withValues(alpha: 0.05),
|
||||||
splashColor: item.color.withOpacity(0.1),
|
splashColor: item.color.withValues(alpha: 0.1),
|
||||||
child: AnimatedContainer(
|
child: AnimatedContainer(
|
||||||
duration: const Duration(milliseconds: 200),
|
duration: const Duration(milliseconds: 200),
|
||||||
padding: const EdgeInsets.all(16),
|
padding: const EdgeInsets.all(16),
|
||||||
|
|
@ -267,11 +255,11 @@ class _SystemSettingsPageState extends State<SystemSettingsPage> {
|
||||||
duration: const Duration(milliseconds: 200),
|
duration: const Duration(milliseconds: 200),
|
||||||
padding: const EdgeInsets.all(12),
|
padding: const EdgeInsets.all(12),
|
||||||
decoration: BoxDecoration(
|
decoration: BoxDecoration(
|
||||||
color: item.color.withOpacity(0.1),
|
color: item.color.withValues(alpha: 0.1),
|
||||||
borderRadius: BorderRadius.circular(8),
|
borderRadius: BorderRadius.circular(8),
|
||||||
boxShadow: [
|
boxShadow: [
|
||||||
BoxShadow(
|
BoxShadow(
|
||||||
color: item.color.withOpacity(0.1),
|
color: item.color.withValues(alpha: 0.1),
|
||||||
blurRadius: 4,
|
blurRadius: 4,
|
||||||
offset: const Offset(0, 2),
|
offset: const Offset(0, 2),
|
||||||
),
|
),
|
||||||
|
|
@ -299,7 +287,7 @@ class _SystemSettingsPageState extends State<SystemSettingsPage> {
|
||||||
Text(
|
Text(
|
||||||
_getLocalizedText(t, item.description),
|
_getLocalizedText(t, item.description),
|
||||||
style: theme.textTheme.bodySmall?.copyWith(
|
style: theme.textTheme.bodySmall?.copyWith(
|
||||||
color: colorScheme.onSurface.withOpacity(0.6),
|
color: colorScheme.onSurface.withValues(alpha: 0.6),
|
||||||
fontSize: 11,
|
fontSize: 11,
|
||||||
),
|
),
|
||||||
textAlign: TextAlign.center,
|
textAlign: TextAlign.center,
|
||||||
|
|
@ -311,7 +299,7 @@ class _SystemSettingsPageState extends State<SystemSettingsPage> {
|
||||||
duration: const Duration(milliseconds: 200),
|
duration: const Duration(milliseconds: 200),
|
||||||
padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 4),
|
padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 4),
|
||||||
decoration: BoxDecoration(
|
decoration: BoxDecoration(
|
||||||
color: item.color.withOpacity(0.1),
|
color: item.color.withValues(alpha: 0.1),
|
||||||
borderRadius: BorderRadius.circular(12),
|
borderRadius: BorderRadius.circular(12),
|
||||||
),
|
),
|
||||||
child: Icon(
|
child: Icon(
|
||||||
|
|
@ -346,41 +334,15 @@ class _SystemSettingsPageState extends State<SystemSettingsPage> {
|
||||||
return t.systemLogs;
|
return t.systemLogs;
|
||||||
case 'systemLogsDescription':
|
case 'systemLogsDescription':
|
||||||
return t.systemLogsDescription;
|
return t.systemLogsDescription;
|
||||||
|
case 'emailSettings':
|
||||||
|
return t.emailSettings;
|
||||||
|
case 'emailSettingsDescription':
|
||||||
|
return t.emailSettingsDescription;
|
||||||
default:
|
default:
|
||||||
return key;
|
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 {
|
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;
|
_seed = c;
|
||||||
notifyListeners();
|
notifyListeners();
|
||||||
final p = await SharedPreferences.getInstance();
|
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,
|
begin: Alignment.topLeft,
|
||||||
end: Alignment.bottomRight,
|
end: Alignment.bottomRight,
|
||||||
colors: [
|
colors: [
|
||||||
theme.colorScheme.primary.withOpacity(0.05),
|
theme.colorScheme.primary.withValues(alpha: 0.05),
|
||||||
theme.colorScheme.primary.withOpacity(0.02),
|
theme.colorScheme.primary.withValues(alpha: 0.02),
|
||||||
],
|
],
|
||||||
)
|
)
|
||||||
: null,
|
: null,
|
||||||
|
|
@ -58,7 +58,7 @@ class StorageConfigCard extends StatelessWidget {
|
||||||
Container(
|
Container(
|
||||||
padding: const EdgeInsets.all(12),
|
padding: const EdgeInsets.all(12),
|
||||||
decoration: BoxDecoration(
|
decoration: BoxDecoration(
|
||||||
color: _getStorageColor(storageType).withOpacity(0.1),
|
color: _getStorageColor(storageType).withValues(alpha: 0.1),
|
||||||
borderRadius: BorderRadius.circular(12),
|
borderRadius: BorderRadius.circular(12),
|
||||||
),
|
),
|
||||||
child: Icon(
|
child: Icon(
|
||||||
|
|
@ -255,13 +255,13 @@ class StorageConfigCard extends StatelessWidget {
|
||||||
return Container(
|
return Container(
|
||||||
padding: const EdgeInsets.all(12),
|
padding: const EdgeInsets.all(12),
|
||||||
decoration: BoxDecoration(
|
decoration: BoxDecoration(
|
||||||
color: theme.colorScheme.surfaceVariant.withOpacity(0.5),
|
color: theme.colorScheme.surfaceContainerHighest.withValues(alpha: 0.5),
|
||||||
borderRadius: BorderRadius.circular(8),
|
borderRadius: BorderRadius.circular(8),
|
||||||
),
|
),
|
||||||
child: Text(
|
child: Text(
|
||||||
'نوع ذخیرهسازی نامشخص',
|
'نوع ذخیرهسازی نامشخص',
|
||||||
style: theme.textTheme.bodyMedium?.copyWith(
|
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(
|
return Container(
|
||||||
padding: const EdgeInsets.all(16),
|
padding: const EdgeInsets.all(16),
|
||||||
decoration: BoxDecoration(
|
decoration: BoxDecoration(
|
||||||
color: theme.colorScheme.surfaceVariant.withOpacity(0.3),
|
color: theme.colorScheme.surfaceContainerHighest.withValues(alpha: 0.3),
|
||||||
borderRadius: BorderRadius.circular(12),
|
borderRadius: BorderRadius.circular(12),
|
||||||
border: Border.all(
|
border: Border.all(
|
||||||
color: theme.colorScheme.outline.withOpacity(0.2),
|
color: theme.colorScheme.outline.withValues(alpha: 0.2),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
child: Column(
|
child: Column(
|
||||||
|
|
@ -306,7 +306,7 @@ class StorageConfigCard extends StatelessWidget {
|
||||||
Text(
|
Text(
|
||||||
l10n.basePath,
|
l10n.basePath,
|
||||||
style: theme.textTheme.bodySmall?.copyWith(
|
style: theme.textTheme.bodySmall?.copyWith(
|
||||||
color: theme.colorScheme.onSurface.withOpacity(0.6),
|
color: theme.colorScheme.onSurface.withValues(alpha: 0.6),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
const SizedBox(height: 4),
|
const SizedBox(height: 4),
|
||||||
|
|
@ -316,7 +316,7 @@ class StorageConfigCard extends StatelessWidget {
|
||||||
color: theme.colorScheme.surface,
|
color: theme.colorScheme.surface,
|
||||||
borderRadius: BorderRadius.circular(6),
|
borderRadius: BorderRadius.circular(6),
|
||||||
border: Border.all(
|
border: Border.all(
|
||||||
color: theme.colorScheme.outline.withOpacity(0.3),
|
color: theme.colorScheme.outline.withValues(alpha: 0.3),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
child: Row(
|
child: Row(
|
||||||
|
|
@ -355,10 +355,10 @@ class StorageConfigCard extends StatelessWidget {
|
||||||
return Container(
|
return Container(
|
||||||
padding: const EdgeInsets.all(16),
|
padding: const EdgeInsets.all(16),
|
||||||
decoration: BoxDecoration(
|
decoration: BoxDecoration(
|
||||||
color: theme.colorScheme.surfaceVariant.withOpacity(0.3),
|
color: theme.colorScheme.surfaceContainerHighest.withValues(alpha: 0.3),
|
||||||
borderRadius: BorderRadius.circular(12),
|
borderRadius: BorderRadius.circular(12),
|
||||||
border: Border.all(
|
border: Border.all(
|
||||||
color: theme.colorScheme.outline.withOpacity(0.2),
|
color: theme.colorScheme.outline.withValues(alpha: 0.2),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
child: Column(
|
child: Column(
|
||||||
|
|
@ -435,7 +435,7 @@ class StorageConfigCard extends StatelessWidget {
|
||||||
Text(
|
Text(
|
||||||
'$label: ',
|
'$label: ',
|
||||||
style: theme.textTheme.bodySmall?.copyWith(
|
style: theme.textTheme.bodySmall?.copyWith(
|
||||||
color: theme.colorScheme.onSurface.withOpacity(0.6),
|
color: theme.colorScheme.onSurface.withValues(alpha: 0.6),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
Expanded(
|
Expanded(
|
||||||
|
|
|
||||||
|
|
@ -252,43 +252,33 @@ class _StorageConfigFormDialogState extends State<StorageConfigFormDialog> {
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
const SizedBox(height: 8),
|
const SizedBox(height: 8),
|
||||||
Row(
|
Column(
|
||||||
children: [
|
children: [
|
||||||
Expanded(
|
ListTile(
|
||||||
child: RadioListTile<String>(
|
leading: Radio<String>(
|
||||||
title: Row(
|
|
||||||
children: [
|
|
||||||
Icon(Icons.storage, size: 20),
|
|
||||||
const SizedBox(width: 8),
|
|
||||||
Text(l10n.localStorage),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
value: 'local',
|
value: 'local',
|
||||||
|
// ignore: deprecated_member_use
|
||||||
groupValue: _selectedStorageType,
|
groupValue: _selectedStorageType,
|
||||||
onChanged: (value) {
|
// ignore: deprecated_member_use
|
||||||
setState(() {
|
onChanged: (value) => setState(() => _selectedStorageType = value!),
|
||||||
_selectedStorageType = value!;
|
|
||||||
});
|
|
||||||
},
|
|
||||||
),
|
),
|
||||||
|
title: Text(l10n.localStorage),
|
||||||
|
trailing: Icon(Icons.storage, size: 20),
|
||||||
|
onTap: () => setState(() => _selectedStorageType = 'local'),
|
||||||
|
contentPadding: EdgeInsets.zero,
|
||||||
),
|
),
|
||||||
Expanded(
|
ListTile(
|
||||||
child: RadioListTile<String>(
|
leading: Radio<String>(
|
||||||
title: Row(
|
|
||||||
children: [
|
|
||||||
Icon(Icons.cloud_upload, size: 20),
|
|
||||||
const SizedBox(width: 8),
|
|
||||||
Text('سرور FTP'),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
value: 'ftp',
|
value: 'ftp',
|
||||||
|
// ignore: deprecated_member_use
|
||||||
groupValue: _selectedStorageType,
|
groupValue: _selectedStorageType,
|
||||||
onChanged: (value) {
|
// ignore: deprecated_member_use
|
||||||
setState(() {
|
onChanged: (value) => setState(() => _selectedStorageType = value!),
|
||||||
_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(
|
Container(
|
||||||
padding: const EdgeInsets.all(24),
|
padding: const EdgeInsets.all(24),
|
||||||
decoration: BoxDecoration(
|
decoration: BoxDecoration(
|
||||||
color: theme.colorScheme.surfaceVariant.withOpacity(0.3),
|
color: theme.colorScheme.surfaceContainerHighest.withValues(alpha: 0.3),
|
||||||
borderRadius: const BorderRadius.only(
|
borderRadius: const BorderRadius.only(
|
||||||
bottomLeft: Radius.circular(20),
|
bottomLeft: Radius.circular(20),
|
||||||
bottomRight: Radius.circular(20),
|
bottomRight: Radius.circular(20),
|
||||||
|
|
|
||||||
|
|
@ -58,6 +58,7 @@ class StorageConfigListWidgetState extends State<StorageConfigListWidget> {
|
||||||
final api = ApiClient();
|
final api = ApiClient();
|
||||||
final response = await api.post('/api/v1/admin/files/storage-configs/$configId/test');
|
final response = await api.post('/api/v1/admin/files/storage-configs/$configId/test');
|
||||||
|
|
||||||
|
if (!mounted) return;
|
||||||
if (response.data != null && response.data['success'] == true) {
|
if (response.data != null && response.data['success'] == true) {
|
||||||
final testResult = response.data['data']['test_result'];
|
final testResult = response.data['data']['test_result'];
|
||||||
if (testResult['success'] == true) {
|
if (testResult['success'] == true) {
|
||||||
|
|
@ -79,6 +80,7 @@ class StorageConfigListWidgetState extends State<StorageConfigListWidget> {
|
||||||
throw Exception(response.data?['message'] ?? 'خطا در تست اتصال');
|
throw Exception(response.data?['message'] ?? 'خطا در تست اتصال');
|
||||||
}
|
}
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
|
if (!mounted) return;
|
||||||
ScaffoldMessenger.of(context).showSnackBar(
|
ScaffoldMessenger.of(context).showSnackBar(
|
||||||
SnackBar(
|
SnackBar(
|
||||||
content: Text('اتصال ناموفق: $e'),
|
content: Text('اتصال ناموفق: $e'),
|
||||||
|
|
@ -112,6 +114,7 @@ class StorageConfigListWidgetState extends State<StorageConfigListWidget> {
|
||||||
final api = ApiClient();
|
final api = ApiClient();
|
||||||
final response = await api.delete('/api/v1/admin/files/storage-configs/$configId');
|
final response = await api.delete('/api/v1/admin/files/storage-configs/$configId');
|
||||||
|
|
||||||
|
if (!mounted) return;
|
||||||
if (response.data != null && response.data['success'] == true) {
|
if (response.data != null && response.data['success'] == true) {
|
||||||
ScaffoldMessenger.of(context).showSnackBar(
|
ScaffoldMessenger.of(context).showSnackBar(
|
||||||
SnackBar(
|
SnackBar(
|
||||||
|
|
@ -142,6 +145,7 @@ class StorageConfigListWidgetState extends State<StorageConfigListWidget> {
|
||||||
errorMessage = e.toString().replaceFirst('Exception: ', '');
|
errorMessage = e.toString().replaceFirst('Exception: ', '');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (!mounted) return;
|
||||||
ScaffoldMessenger.of(context).showSnackBar(
|
ScaffoldMessenger.of(context).showSnackBar(
|
||||||
SnackBar(
|
SnackBar(
|
||||||
content: Text(errorMessage),
|
content: Text(errorMessage),
|
||||||
|
|
@ -158,6 +162,7 @@ class StorageConfigListWidgetState extends State<StorageConfigListWidget> {
|
||||||
final api = ApiClient();
|
final api = ApiClient();
|
||||||
final response = await api.put('/api/v1/admin/files/storage-configs/$configId/set-default');
|
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) {
|
if (response.data != null && response.data['success'] == true) {
|
||||||
ScaffoldMessenger.of(context).showSnackBar(
|
ScaffoldMessenger.of(context).showSnackBar(
|
||||||
SnackBar(
|
SnackBar(
|
||||||
|
|
@ -172,6 +177,7 @@ class StorageConfigListWidgetState extends State<StorageConfigListWidget> {
|
||||||
throw Exception(response.data?['message'] ?? 'خطا در تنظیم به عنوان پیشفرض');
|
throw Exception(response.data?['message'] ?? 'خطا در تنظیم به عنوان پیشفرض');
|
||||||
}
|
}
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
|
if (!mounted) return;
|
||||||
ScaffoldMessenger.of(context).showSnackBar(
|
ScaffoldMessenger.of(context).showSnackBar(
|
||||||
SnackBar(
|
SnackBar(
|
||||||
content: Text('خطا در تنظیم به عنوان پیشفرض: $e'),
|
content: Text('خطا در تنظیم به عنوان پیشفرض: $e'),
|
||||||
|
|
@ -264,20 +270,20 @@ class StorageConfigListWidgetState extends State<StorageConfigListWidget> {
|
||||||
Icon(
|
Icon(
|
||||||
Icons.storage_outlined,
|
Icons.storage_outlined,
|
||||||
size: 64,
|
size: 64,
|
||||||
color: theme.colorScheme.primary.withOpacity(0.5),
|
color: theme.colorScheme.primary.withValues(alpha: 0.5),
|
||||||
),
|
),
|
||||||
const SizedBox(height: 16),
|
const SizedBox(height: 16),
|
||||||
Text(
|
Text(
|
||||||
'هیچ پیکربندی ذخیرهسازی وجود ندارد',
|
'هیچ پیکربندی ذخیرهسازی وجود ندارد',
|
||||||
style: theme.textTheme.headlineSmall?.copyWith(
|
style: theme.textTheme.headlineSmall?.copyWith(
|
||||||
color: theme.colorScheme.onSurface.withOpacity(0.6),
|
color: theme.colorScheme.onSurface.withValues(alpha: 0.6),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
const SizedBox(height: 8),
|
const SizedBox(height: 8),
|
||||||
Text(
|
Text(
|
||||||
'اولین پیکربندی ذخیرهسازی را ایجاد کنید',
|
'اولین پیکربندی ذخیرهسازی را ایجاد کنید',
|
||||||
style: theme.textTheme.bodyMedium?.copyWith(
|
style: theme.textTheme.bodyMedium?.copyWith(
|
||||||
color: theme.colorScheme.onSurface.withOpacity(0.6),
|
color: theme.colorScheme.onSurface.withValues(alpha: 0.6),
|
||||||
),
|
),
|
||||||
textAlign: TextAlign.center,
|
textAlign: TextAlign.center,
|
||||||
),
|
),
|
||||||
|
|
@ -285,7 +291,7 @@ class StorageConfigListWidgetState extends State<StorageConfigListWidget> {
|
||||||
Text(
|
Text(
|
||||||
'از دکمه + در پایین صفحه استفاده کنید',
|
'از دکمه + در پایین صفحه استفاده کنید',
|
||||||
style: theme.textTheme.bodySmall?.copyWith(
|
style: theme.textTheme.bodySmall?.copyWith(
|
||||||
color: theme.colorScheme.onSurface.withOpacity(0.5),
|
color: theme.colorScheme.onSurface.withValues(alpha: 0.5),
|
||||||
fontStyle: FontStyle.italic,
|
fontStyle: FontStyle.italic,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
|
@ -321,7 +327,7 @@ class StorageConfigListWidgetState extends State<StorageConfigListWidget> {
|
||||||
Text(
|
Text(
|
||||||
'${_storageConfigs.length} پیکربندی',
|
'${_storageConfigs.length} پیکربندی',
|
||||||
style: theme.textTheme.bodyMedium?.copyWith(
|
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,
|
begin: Alignment.topCenter,
|
||||||
end: Alignment.bottomCenter,
|
end: Alignment.bottomCenter,
|
||||||
colors: [
|
colors: [
|
||||||
theme.colorScheme.primary.withOpacity(0.1),
|
theme.colorScheme.primary.withValues(alpha: 0.1),
|
||||||
theme.colorScheme.surface,
|
theme.colorScheme.surface,
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
|
|
|
||||||
|
|
@ -43,7 +43,7 @@ class DataTableSearchDialog extends StatefulWidget {
|
||||||
class _DataTableSearchDialogState extends State<DataTableSearchDialog> {
|
class _DataTableSearchDialogState extends State<DataTableSearchDialog> {
|
||||||
late TextEditingController _controller;
|
late TextEditingController _controller;
|
||||||
late String _selectedType;
|
late String _selectedType;
|
||||||
Set<String> _selectedValues = <String>{};
|
final Set<String> _selectedValues = <String>{};
|
||||||
DateTime? _fromDate;
|
DateTime? _fromDate;
|
||||||
DateTime? _toDate;
|
DateTime? _toDate;
|
||||||
|
|
||||||
|
|
@ -118,7 +118,7 @@ class _DataTableSearchDialogState extends State<DataTableSearchDialog> {
|
||||||
return [
|
return [
|
||||||
// Search type dropdown
|
// Search type dropdown
|
||||||
DropdownButtonFormField<String>(
|
DropdownButtonFormField<String>(
|
||||||
value: _selectedType,
|
initialValue: _selectedType,
|
||||||
decoration: InputDecoration(
|
decoration: InputDecoration(
|
||||||
labelText: t.searchType,
|
labelText: t.searchType,
|
||||||
border: const OutlineInputBorder(),
|
border: const OutlineInputBorder(),
|
||||||
|
|
@ -163,27 +163,7 @@ class _DataTableSearchDialogState extends State<DataTableSearchDialog> {
|
||||||
subtitle: Text(_fromDate != null
|
subtitle: Text(_fromDate != null
|
||||||
? HesabixDateUtils.formatForDisplay(_fromDate!, isJalali)
|
? HesabixDateUtils.formatForDisplay(_fromDate!, isJalali)
|
||||||
: t.selectDate),
|
: t.selectDate),
|
||||||
onTap: () async {
|
onTap: () => _selectFromDate(t, isJalali),
|
||||||
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;
|
|
||||||
});
|
|
||||||
}
|
|
||||||
},
|
|
||||||
),
|
),
|
||||||
// To date
|
// To date
|
||||||
ListTile(
|
ListTile(
|
||||||
|
|
@ -192,27 +172,7 @@ class _DataTableSearchDialogState extends State<DataTableSearchDialog> {
|
||||||
subtitle: Text(_toDate != null
|
subtitle: Text(_toDate != null
|
||||||
? HesabixDateUtils.formatForDisplay(_toDate!, isJalali)
|
? HesabixDateUtils.formatForDisplay(_toDate!, isJalali)
|
||||||
: t.selectDate),
|
: t.selectDate),
|
||||||
onTap: () async {
|
onTap: () => _selectToDate(t, isJalali),
|
||||||
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;
|
|
||||||
});
|
|
||||||
}
|
|
||||||
},
|
|
||||||
),
|
),
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|
@ -344,6 +304,56 @@ class _DataTableSearchDialogState extends State<DataTableSearchDialog> {
|
||||||
}
|
}
|
||||||
Navigator.of(context).pop();
|
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
|
/// Dialog for date range filter
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,4 @@
|
||||||
import 'dart:async';
|
import 'dart:async';
|
||||||
import 'dart:typed_data';
|
|
||||||
// import 'dart:html' as html; // Not available on Linux
|
// import 'dart:html' as html; // Not available on Linux
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:data_table_2/data_table_2.dart';
|
import 'package:data_table_2/data_table_2.dart';
|
||||||
|
|
@ -63,7 +62,7 @@ class _DataTableWidgetState<T> extends State<DataTableWidget<T>> {
|
||||||
bool _sortDesc = false;
|
bool _sortDesc = false;
|
||||||
|
|
||||||
// Row selection state
|
// Row selection state
|
||||||
Set<int> _selectedRows = <int>{};
|
final Set<int> _selectedRows = <int>{};
|
||||||
bool _isExporting = false;
|
bool _isExporting = false;
|
||||||
|
|
||||||
// Column settings state
|
// Column settings state
|
||||||
|
|
@ -144,7 +143,7 @@ class _DataTableWidgetState<T> extends State<DataTableWidget<T>> {
|
||||||
_visibleColumns = _getVisibleColumnsFromSettings(effectiveSettings);
|
_visibleColumns = _getVisibleColumnsFromSettings(effectiveSettings);
|
||||||
});
|
});
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
print('Error loading column settings: $e');
|
debugPrint('Error loading column settings: $e');
|
||||||
setState(() {
|
setState(() {
|
||||||
_visibleColumns = List.from(widget.config.columns);
|
_visibleColumns = List.from(widget.config.columns);
|
||||||
});
|
});
|
||||||
|
|
@ -466,7 +465,7 @@ class _DataTableWidgetState<T> extends State<DataTableWidget<T>> {
|
||||||
widget.config.onColumnSettingsChanged!(validatedSettings);
|
widget.config.onColumnSettingsChanged!(validatedSettings);
|
||||||
}
|
}
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
print('Error saving column settings: $e');
|
debugPrint('Error saving column settings: $e');
|
||||||
if (mounted) {
|
if (mounted) {
|
||||||
final t = Localizations.of<AppLocalizations>(context, AppLocalizations)!;
|
final t = Localizations.of<AppLocalizations>(context, AppLocalizations)!;
|
||||||
final messenger = ScaffoldMessenger.of(context);
|
final messenger = ScaffoldMessenger.of(context);
|
||||||
|
|
@ -624,43 +623,16 @@ class _DataTableWidgetState<T> extends State<DataTableWidget<T>> {
|
||||||
// Platform-specific download functions for Linux
|
// Platform-specific download functions for Linux
|
||||||
Future<void> _downloadPdf(dynamic data, String filename) async {
|
Future<void> _downloadPdf(dynamic data, String filename) async {
|
||||||
// For Linux desktop, we'll save to Downloads folder
|
// 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
|
// TODO: Implement proper file saving for Linux
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<void> _downloadExcel(dynamic data, String filename) async {
|
Future<void> _downloadExcel(dynamic data, String filename) async {
|
||||||
// For Linux desktop, we'll save to Downloads folder
|
// 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
|
// 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
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
|
|
@ -1451,7 +1423,7 @@ class _ColumnHeaderWithSearch extends StatelessWidget {
|
||||||
onTap: enabled ? () => onSort(sortBy) : null,
|
onTap: enabled ? () => onSort(sortBy) : null,
|
||||||
child: Padding(
|
child: Padding(
|
||||||
padding: const EdgeInsets.symmetric(vertical: 8.0),
|
padding: const EdgeInsets.symmetric(vertical: 8.0),
|
||||||
child: Container(
|
child: SizedBox(
|
||||||
width: double.infinity,
|
width: double.infinity,
|
||||||
child: Row(
|
child: Row(
|
||||||
mainAxisSize: MainAxisSize.max,
|
mainAxisSize: MainAxisSize.max,
|
||||||
|
|
|
||||||
|
|
@ -21,7 +21,7 @@ class DataTableExampleUsage {
|
||||||
DataTableAction(
|
DataTableAction(
|
||||||
icon: Icons.edit,
|
icon: Icons.edit,
|
||||||
label: 'ویرایش',
|
label: 'ویرایش',
|
||||||
onTap: (user) => print('Edit user: ${user.id}'),
|
onTap: (user) => debugPrint('Edit user: ${user.id}'),
|
||||||
),
|
),
|
||||||
]),
|
]),
|
||||||
],
|
],
|
||||||
|
|
@ -51,7 +51,7 @@ class DataTableExampleUsage {
|
||||||
DataTableAction(
|
DataTableAction(
|
||||||
icon: Icons.visibility,
|
icon: Icons.visibility,
|
||||||
label: 'مشاهده',
|
label: 'مشاهده',
|
||||||
onTap: (order) => print('View order: ${order.id}'),
|
onTap: (order) => debugPrint('View order: ${order.id}'),
|
||||||
),
|
),
|
||||||
]),
|
]),
|
||||||
],
|
],
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,5 @@
|
||||||
import 'dart:convert';
|
import 'dart:convert';
|
||||||
|
import 'package:flutter/foundation.dart';
|
||||||
import 'package:shared_preferences/shared_preferences.dart';
|
import 'package:shared_preferences/shared_preferences.dart';
|
||||||
|
|
||||||
/// Column settings for a specific table
|
/// Column settings for a specific table
|
||||||
|
|
@ -58,7 +59,7 @@ class ColumnSettingsService {
|
||||||
final json = jsonDecode(jsonString) as Map<String, dynamic>;
|
final json = jsonDecode(jsonString) as Map<String, dynamic>;
|
||||||
return ColumnSettings.fromJson(json);
|
return ColumnSettings.fromJson(json);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
print('Error loading column settings: $e');
|
debugPrint('Error loading column settings: $e');
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -71,7 +72,7 @@ class ColumnSettingsService {
|
||||||
final jsonString = jsonEncode(settings.toJson());
|
final jsonString = jsonEncode(settings.toJson());
|
||||||
await prefs.setString(key, jsonString);
|
await prefs.setString(key, jsonString);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
print('Error saving column settings: $e');
|
debugPrint('Error saving column settings: $e');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -82,7 +83,7 @@ class ColumnSettingsService {
|
||||||
final key = '$_keyPrefix$tableId';
|
final key = '$_keyPrefix$tableId';
|
||||||
await prefs.remove(key);
|
await prefs.remove(key);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
print('Error clearing column settings: $e');
|
debugPrint('Error clearing column settings: $e');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -19,7 +19,7 @@ class ErrorNotice extends StatelessWidget {
|
||||||
decoration: BoxDecoration(
|
decoration: BoxDecoration(
|
||||||
color: bg,
|
color: bg,
|
||||||
borderRadius: BorderRadius.circular(8),
|
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),
|
padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 10),
|
||||||
child: Row(
|
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) {
|
Color _getBorderColor(ThemeData theme, bool isUser) {
|
||||||
if (isUser) {
|
if (isUser) {
|
||||||
return theme.colorScheme.primary.withOpacity(0.3);
|
return theme.colorScheme.primary.withValues(alpha: 0.3);
|
||||||
} else {
|
} 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) {
|
Color _getSenderTextColor(ThemeData theme, bool isUser) {
|
||||||
if (isUser) {
|
if (isUser) {
|
||||||
return Colors.white.withOpacity(0.8);
|
return Colors.white.withValues(alpha: 0.8);
|
||||||
} else {
|
} else {
|
||||||
return theme.colorScheme.primary;
|
return theme.colorScheme.primary;
|
||||||
}
|
}
|
||||||
|
|
@ -165,9 +165,9 @@ class MessageBubble extends StatelessWidget {
|
||||||
|
|
||||||
Color _getTimeColor(ThemeData theme, bool isUser) {
|
Color _getTimeColor(ThemeData theme, bool isUser) {
|
||||||
if (isUser) {
|
if (isUser) {
|
||||||
return Colors.white.withOpacity(0.7);
|
return Colors.white.withValues(alpha: 0.7);
|
||||||
} else {
|
} 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,
|
vertical: isSmall ? 4 : 6,
|
||||||
),
|
),
|
||||||
decoration: BoxDecoration(
|
decoration: BoxDecoration(
|
||||||
color: color.withOpacity(0.1),
|
color: color.withValues(alpha: 0.1),
|
||||||
borderRadius: BorderRadius.circular(isSmall ? 12 : 16),
|
borderRadius: BorderRadius.circular(isSmall ? 12 : 16),
|
||||||
border: Border.all(
|
border: Border.all(
|
||||||
color: color.withOpacity(0.3),
|
color: color.withValues(alpha: 0.3),
|
||||||
width: 1,
|
width: 1,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
|
|
||||||
|
|
@ -29,7 +29,7 @@ class TicketCard extends StatelessWidget {
|
||||||
borderRadius: BorderRadius.circular(16),
|
borderRadius: BorderRadius.circular(16),
|
||||||
boxShadow: [
|
boxShadow: [
|
||||||
BoxShadow(
|
BoxShadow(
|
||||||
color: theme.colorScheme.shadow.withOpacity(0.08),
|
color: theme.colorScheme.shadow.withValues(alpha: 0.08),
|
||||||
blurRadius: 8,
|
blurRadius: 8,
|
||||||
offset: const Offset(0, 2),
|
offset: const Offset(0, 2),
|
||||||
),
|
),
|
||||||
|
|
@ -46,7 +46,7 @@ class TicketCard extends StatelessWidget {
|
||||||
decoration: BoxDecoration(
|
decoration: BoxDecoration(
|
||||||
borderRadius: BorderRadius.circular(16),
|
borderRadius: BorderRadius.circular(16),
|
||||||
border: Border.all(
|
border: Border.all(
|
||||||
color: theme.colorScheme.outline.withOpacity(0.1),
|
color: theme.colorScheme.outline.withValues(alpha: 0.1),
|
||||||
width: 1,
|
width: 1,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
|
@ -78,7 +78,7 @@ class TicketCard extends StatelessWidget {
|
||||||
Text(
|
Text(
|
||||||
ticket.description,
|
ticket.description,
|
||||||
style: theme.textTheme.bodyMedium?.copyWith(
|
style: theme.textTheme.bodyMedium?.copyWith(
|
||||||
color: theme.colorScheme.onSurface.withOpacity(0.8),
|
color: theme.colorScheme.onSurface.withValues(alpha: 0.8),
|
||||||
height: 1.4,
|
height: 1.4,
|
||||||
),
|
),
|
||||||
maxLines: 3,
|
maxLines: 3,
|
||||||
|
|
@ -129,7 +129,7 @@ class TicketCard extends StatelessWidget {
|
||||||
return Container(
|
return Container(
|
||||||
padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 4),
|
padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 4),
|
||||||
decoration: BoxDecoration(
|
decoration: BoxDecoration(
|
||||||
color: theme.colorScheme.surfaceVariant.withOpacity(0.5),
|
color: theme.colorScheme.surfaceContainerHighest.withValues(alpha: 0.5),
|
||||||
borderRadius: BorderRadius.circular(8),
|
borderRadius: BorderRadius.circular(8),
|
||||||
),
|
),
|
||||||
child: Row(
|
child: Row(
|
||||||
|
|
@ -138,13 +138,13 @@ class TicketCard extends StatelessWidget {
|
||||||
Icon(
|
Icon(
|
||||||
Icons.access_time,
|
Icons.access_time,
|
||||||
size: 12,
|
size: 12,
|
||||||
color: theme.colorScheme.onSurfaceVariant.withOpacity(0.7),
|
color: theme.colorScheme.onSurfaceVariant.withValues(alpha: 0.7),
|
||||||
),
|
),
|
||||||
const SizedBox(width: 4),
|
const SizedBox(width: 4),
|
||||||
Text(
|
Text(
|
||||||
_formatDate(dateTime, l10n),
|
_formatDate(dateTime, l10n),
|
||||||
style: theme.textTheme.bodySmall?.copyWith(
|
style: theme.textTheme.bodySmall?.copyWith(
|
||||||
color: theme.colorScheme.onSurfaceVariant.withOpacity(0.8),
|
color: theme.colorScheme.onSurfaceVariant.withValues(alpha: 0.8),
|
||||||
fontWeight: FontWeight.w500,
|
fontWeight: FontWeight.w500,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
|
@ -160,14 +160,14 @@ class TicketCard extends StatelessWidget {
|
||||||
return Container(
|
return Container(
|
||||||
padding: const EdgeInsets.all(12),
|
padding: const EdgeInsets.all(12),
|
||||||
decoration: BoxDecoration(
|
decoration: BoxDecoration(
|
||||||
color: theme.colorScheme.surfaceVariant.withOpacity(0.3),
|
color: theme.colorScheme.surfaceContainerHighest.withValues(alpha: 0.3),
|
||||||
borderRadius: BorderRadius.circular(12),
|
borderRadius: BorderRadius.circular(12),
|
||||||
),
|
),
|
||||||
child: Row(
|
child: Row(
|
||||||
children: [
|
children: [
|
||||||
CircleAvatar(
|
CircleAvatar(
|
||||||
radius: 16,
|
radius: 16,
|
||||||
backgroundColor: theme.colorScheme.primary.withOpacity(0.1),
|
backgroundColor: theme.colorScheme.primary.withValues(alpha: 0.1),
|
||||||
child: Icon(
|
child: Icon(
|
||||||
Icons.person,
|
Icons.person,
|
||||||
size: 16,
|
size: 16,
|
||||||
|
|
@ -182,7 +182,7 @@ class TicketCard extends StatelessWidget {
|
||||||
Text(
|
Text(
|
||||||
l10n.createdBy,
|
l10n.createdBy,
|
||||||
style: theme.textTheme.bodySmall?.copyWith(
|
style: theme.textTheme.bodySmall?.copyWith(
|
||||||
color: theme.colorScheme.onSurfaceVariant.withOpacity(0.7),
|
color: theme.colorScheme.onSurfaceVariant.withValues(alpha: 0.7),
|
||||||
fontWeight: FontWeight.w500,
|
fontWeight: FontWeight.w500,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
|
@ -202,7 +202,7 @@ class TicketCard extends StatelessWidget {
|
||||||
Container(
|
Container(
|
||||||
padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 4),
|
padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 4),
|
||||||
decoration: BoxDecoration(
|
decoration: BoxDecoration(
|
||||||
color: theme.colorScheme.secondary.withOpacity(0.1),
|
color: theme.colorScheme.secondary.withValues(alpha: 0.1),
|
||||||
borderRadius: BorderRadius.circular(8),
|
borderRadius: BorderRadius.circular(8),
|
||||||
),
|
),
|
||||||
child: Row(
|
child: Row(
|
||||||
|
|
@ -239,10 +239,10 @@ class TicketCard extends StatelessWidget {
|
||||||
return Container(
|
return Container(
|
||||||
padding: const EdgeInsets.symmetric(horizontal: 10, vertical: 6),
|
padding: const EdgeInsets.symmetric(horizontal: 10, vertical: 6),
|
||||||
decoration: BoxDecoration(
|
decoration: BoxDecoration(
|
||||||
color: color.withOpacity(0.1),
|
color: color.withValues(alpha: 0.1),
|
||||||
borderRadius: BorderRadius.circular(12),
|
borderRadius: BorderRadius.circular(12),
|
||||||
border: Border.all(
|
border: Border.all(
|
||||||
color: color.withOpacity(0.2),
|
color: color.withValues(alpha: 0.2),
|
||||||
width: 1,
|
width: 1,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
|
|
||||||
|
|
@ -62,7 +62,7 @@ class _TicketDetailsDialogState extends State<TicketDetailsDialog> {
|
||||||
borderRadius: BorderRadius.circular(12),
|
borderRadius: BorderRadius.circular(12),
|
||||||
boxShadow: [
|
boxShadow: [
|
||||||
BoxShadow(
|
BoxShadow(
|
||||||
color: Colors.black.withOpacity(0.2),
|
color: Colors.black.withValues(alpha: 0.2),
|
||||||
blurRadius: 8,
|
blurRadius: 8,
|
||||||
offset: const Offset(0, 4),
|
offset: const Offset(0, 4),
|
||||||
),
|
),
|
||||||
|
|
@ -275,7 +275,7 @@ class _TicketDetailsDialogState extends State<TicketDetailsDialog> {
|
||||||
Container(
|
Container(
|
||||||
padding: const EdgeInsets.all(20),
|
padding: const EdgeInsets.all(20),
|
||||||
decoration: BoxDecoration(
|
decoration: BoxDecoration(
|
||||||
color: theme.primaryColor.withOpacity(0.1),
|
color: theme.primaryColor.withValues(alpha: 0.1),
|
||||||
borderRadius: const BorderRadius.only(
|
borderRadius: const BorderRadius.only(
|
||||||
topLeft: Radius.circular(20),
|
topLeft: Radius.circular(20),
|
||||||
topRight: Radius.circular(20),
|
topRight: Radius.circular(20),
|
||||||
|
|
|
||||||
|
|
@ -22,10 +22,10 @@ class TicketStatusChip extends StatelessWidget {
|
||||||
vertical: isSmall ? 4 : 6,
|
vertical: isSmall ? 4 : 6,
|
||||||
),
|
),
|
||||||
decoration: BoxDecoration(
|
decoration: BoxDecoration(
|
||||||
color: color.withOpacity(0.1),
|
color: color.withValues(alpha: 0.1),
|
||||||
borderRadius: BorderRadius.circular(isSmall ? 12 : 16),
|
borderRadius: BorderRadius.circular(isSmall ? 12 : 16),
|
||||||
border: Border.all(
|
border: Border.all(
|
||||||
color: color.withOpacity(0.3),
|
color: color.withValues(alpha: 0.3),
|
||||||
width: 1,
|
width: 1,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
|
|
||||||
|
|
@ -185,7 +185,7 @@ packages:
|
||||||
source: sdk
|
source: sdk
|
||||||
version: "0.0.0"
|
version: "0.0.0"
|
||||||
flutter_web_plugins:
|
flutter_web_plugins:
|
||||||
dependency: transitive
|
dependency: "direct main"
|
||||||
description: flutter
|
description: flutter
|
||||||
source: sdk
|
source: sdk
|
||||||
version: "0.0.0"
|
version: "0.0.0"
|
||||||
|
|
@ -193,10 +193,10 @@ packages:
|
||||||
dependency: "direct main"
|
dependency: "direct main"
|
||||||
description:
|
description:
|
||||||
name: go_router
|
name: go_router
|
||||||
sha256: eb059dfe59f08546e9787f895bd01652076f996bcbf485a8609ef990419ad227
|
sha256: b1488741c9ce37b72e026377c69a59c47378493156fc38efb5a54f6def3f92a3
|
||||||
url: "https://pub.flutter-io.cn"
|
url: "https://pub.flutter-io.cn"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "16.2.1"
|
version: "16.2.2"
|
||||||
http_parser:
|
http_parser:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
|
|
|
||||||
|
|
@ -32,6 +32,8 @@ dependencies:
|
||||||
sdk: flutter
|
sdk: flutter
|
||||||
flutter_localizations:
|
flutter_localizations:
|
||||||
sdk: flutter
|
sdk: flutter
|
||||||
|
flutter_web_plugins:
|
||||||
|
sdk: flutter
|
||||||
|
|
||||||
# The following adds the Cupertino Icons font to your application.
|
# The following adds the Cupertino Icons font to your application.
|
||||||
# Use with the CupertinoIcons class for iOS style icons.
|
# Use with the CupertinoIcons class for iOS style icons.
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue