more progress in permissions

This commit is contained in:
Hesabix 2025-09-25 01:01:27 +03:30
parent 798dd63627
commit 44eef85039
40 changed files with 4427 additions and 170 deletions

View file

@ -0,0 +1,443 @@
# 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 (
BusinessUsersListResponse, AddUserRequest, AddUserResponse,
UpdatePermissionsRequest, UpdatePermissionsResponse, RemoveUserResponse
)
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 adapters.db.repositories.business_permission_repo import BusinessPermissionRepository
from adapters.db.models.user import User
from adapters.db.models.business import Business
router = APIRouter(prefix="/business", tags=["business-users"])
@router.get("/{business_id}/users",
summary="لیست کاربران کسب و کار",
description="دریافت لیست کاربران یک کسب و کار",
response_model=BusinessUsersListResponse,
responses={
200: {
"description": "لیست کاربران با موفقیت دریافت شد",
"content": {
"application/json": {
"example": {
"success": True,
"message": "لیست کاربران دریافت شد",
"users": [
{
"id": 1,
"business_id": 1,
"user_id": 2,
"user_name": "علی احمدی",
"user_email": "ali@example.com",
"user_phone": "09123456789",
"role": "member",
"status": "active",
"added_at": "2024-01-01T00:00:00Z",
"last_active": "2024-01-01T12:00:00Z",
"permissions": {
"sales": {
"read": True,
"write": True,
"delete": False
},
"reports": {
"read": True,
"export": True
}
}
}
],
"total_count": 1
}
}
}
},
401: {
"description": "کاربر احراز هویت نشده است"
},
403: {
"description": "دسترسی غیرمجاز به کسب و کار"
}
}
)
@require_business_access("business_id")
def get_users(
request: Request,
business_id: int,
ctx: AuthContext = Depends(get_current_user),
db: Session = Depends(get_db)
) -> dict:
"""دریافت لیست کاربران کسب و کار"""
import logging
logger = logging.getLogger(__name__)
current_user_id = ctx.get_user_id()
logger.info(f"Getting users for business {business_id}, current user: {current_user_id}")
# Check if user is business owner or has permission to manage users
business = db.get(Business, business_id)
if not business:
logger.error(f"Business {business_id} not found")
raise HTTPException(status_code=404, detail="کسب و کار یافت نشد")
is_owner = business.owner_id == current_user_id
can_manage = ctx.can_manage_business_users()
logger.info(f"Business owner: {business.owner_id}, is_owner: {is_owner}, can_manage: {can_manage}")
if not is_owner and not can_manage:
logger.warning(f"User {current_user_id} does not have permission to manage users for business {business_id}")
raise HTTPException(status_code=403, detail="شما مجوز مدیریت کاربران ندارید")
# Get business permissions for this business
permission_repo = BusinessPermissionRepository(db)
business_permissions = permission_repo.get_business_users(business_id)
logger.info(f"Found {len(business_permissions)} business permissions for business {business_id}")
# Format users data
formatted_users = []
# Add business owner first
owner = db.get(User, business.owner_id)
if owner:
logger.info(f"Adding business owner: {owner.id} - {owner.email}")
owner_data = {
"id": business.owner_id, # Use owner_id as id
"business_id": business_id,
"user_id": business.owner_id,
"user_name": f"{owner.first_name or ''} {owner.last_name or ''}".strip(),
"user_email": owner.email or "",
"user_phone": owner.mobile,
"role": "owner",
"status": "active",
"added_at": business.created_at,
"last_active": business.updated_at,
"permissions": {}, # Owner has all permissions
}
formatted_users.append(owner_data)
else:
logger.warning(f"Business owner {business.owner_id} not found in users table")
# Add other users with permissions
for perm in business_permissions:
# Skip if this is the owner (already added)
if perm.user_id == business.owner_id:
logger.info(f"Skipping owner user {perm.user_id} as already added")
continue
user = db.get(User, perm.user_id)
if user:
logger.info(f"Adding user with permissions: {user.id} - {user.email}")
user_data = {
"id": perm.id,
"business_id": perm.business_id,
"user_id": perm.user_id,
"user_name": f"{user.first_name or ''} {user.last_name or ''}".strip(),
"user_email": user.email or "",
"user_phone": user.mobile,
"role": "member",
"status": "active",
"added_at": perm.created_at,
"last_active": perm.updated_at,
"permissions": perm.business_permissions or {},
}
formatted_users.append(user_data)
else:
logger.warning(f"User {perm.user_id} not found in users table")
logger.info(f"Returning {len(formatted_users)} users for business {business_id}")
# Format datetime fields based on calendar type
formatted_users = format_datetime_fields(formatted_users, request)
return success_response(
data={
"users": formatted_users,
"total_count": len(formatted_users)
},
request=request,
message="لیست کاربران دریافت شد"
)
@router.post("/{business_id}/users",
summary="افزودن کاربر به کسب و کار",
description="افزودن کاربر جدید به کسب و کار با ایمیل یا شماره تلفن",
response_model=AddUserResponse,
responses={
200: {
"description": "کاربر با موفقیت اضافه شد",
"content": {
"application/json": {
"example": {
"success": True,
"message": "کاربر با موفقیت اضافه شد",
"user": {
"id": 1,
"business_id": 1,
"user_id": 2,
"user_name": "علی احمدی",
"user_email": "ali@example.com",
"user_phone": "09123456789",
"role": "member",
"status": "active",
"added_at": "2024-01-01T00:00:00Z",
"last_active": None,
"permissions": {}
}
}
}
}
},
400: {
"description": "خطا در اعتبارسنجی داده‌ها"
},
401: {
"description": "کاربر احراز هویت نشده است"
},
403: {
"description": "دسترسی غیرمجاز یا مجوز کافی نیست"
},
404: {
"description": "کاربر یافت نشد"
}
}
)
@require_business_access("business_id")
def add_user(
request: Request,
business_id: int,
add_request: AddUserRequest,
ctx: AuthContext = Depends(get_current_user),
db: Session = Depends(get_db)
) -> dict:
"""افزودن کاربر به کسب و کار"""
import logging
logger = logging.getLogger(__name__)
current_user_id = ctx.get_user_id()
logger.info(f"Adding user to business {business_id}, current user: {current_user_id}")
logger.info(f"Add request: {add_request.email_or_phone}")
# Check if user is business owner or has permission to manage users
business = db.get(Business, business_id)
if not business:
logger.error(f"Business {business_id} not found")
raise HTTPException(status_code=404, detail="کسب و کار یافت نشد")
is_owner = business.owner_id == current_user_id
can_manage = ctx.can_manage_business_users(business_id)
logger.info(f"Business owner: {business.owner_id}, is_owner: {is_owner}, can_manage: {can_manage}")
logger.info(f"User {current_user_id} business_id from context: {ctx.business_id}")
logger.info(f"User {current_user_id} is superadmin: {ctx.is_superadmin()}")
if not is_owner and not can_manage:
logger.warning(f"User {current_user_id} does not have permission to add users to business {business_id}")
raise HTTPException(status_code=403, detail="شما مجوز مدیریت کاربران ندارید")
# Find user by email or phone
logger.info(f"Searching for user with email/phone: {add_request.email_or_phone}")
user = db.query(User).filter(
(User.email == add_request.email_or_phone) |
(User.mobile == add_request.email_or_phone)
).first()
if not user:
logger.warning(f"User not found with email/phone: {add_request.email_or_phone}")
raise HTTPException(status_code=404, detail="کاربر یافت نشد")
logger.info(f"Found user: {user.id} - {user.email}")
# Check if user is already added to this business
permission_repo = BusinessPermissionRepository(db)
existing_permission = permission_repo.get_by_user_and_business(user.id, business_id)
if existing_permission:
logger.warning(f"User {user.id} already exists in business {business_id}")
raise HTTPException(status_code=400, detail="کاربر قبلاً به این کسب و کار اضافه شده است")
# Add user to business with default permissions
logger.info(f"Adding user {user.id} to business {business_id}")
permission_obj = permission_repo.create_or_update(
user_id=user.id,
business_id=business_id,
permissions={} # Default empty permissions
)
logger.info(f"Created permission object: {permission_obj.id}")
# Format user data
user_data = {
"id": permission_obj.id,
"business_id": permission_obj.business_id,
"user_id": permission_obj.user_id,
"user_name": f"{user.first_name or ''} {user.last_name or ''}".strip(),
"user_email": user.email or "",
"user_phone": user.mobile,
"role": "member",
"status": "active",
"added_at": permission_obj.created_at,
"last_active": None,
"permissions": permission_obj.business_permissions or {},
}
logger.info(f"Returning user data: {user_data}")
# Format datetime fields based on calendar type
formatted_user_data = format_datetime_fields(user_data, request)
return success_response(
data={"user": formatted_user_data},
request=request,
message="کاربر با موفقیت اضافه شد"
)
@router.put("/{business_id}/users/{user_id}/permissions",
summary="به‌روزرسانی دسترسی‌های کاربر",
description="به‌روزرسانی دسترسی‌های یک کاربر در کسب و کار",
response_model=UpdatePermissionsResponse,
responses={
200: {
"description": "دسترسی‌ها با موفقیت به‌روزرسانی شد",
"content": {
"application/json": {
"example": {
"success": True,
"message": "دسترسی‌ها با موفقیت به‌روزرسانی شد"
}
}
}
},
400: {
"description": "خطا در اعتبارسنجی داده‌ها"
},
401: {
"description": "کاربر احراز هویت نشده است"
},
403: {
"description": "دسترسی غیرمجاز یا مجوز کافی نیست"
},
404: {
"description": "کاربر یافت نشد"
}
}
)
@require_business_access("business_id")
def update_permissions(
request: Request,
business_id: int,
user_id: int,
update_request: UpdatePermissionsRequest,
ctx: AuthContext = Depends(get_current_user),
db: Session = Depends(get_db)
) -> dict:
"""به‌روزرسانی دسترسی‌های کاربر"""
current_user_id = ctx.get_user_id()
# Check if user is business owner or has permission to manage users
business = db.get(Business, business_id)
if not business:
raise HTTPException(status_code=404, detail="کسب و کار یافت نشد")
is_owner = business.owner_id == current_user_id
can_manage = ctx.can_manage_business_users()
if not is_owner and not can_manage:
raise HTTPException(status_code=403, detail="شما مجوز مدیریت کاربران ندارید")
# Check if target user exists
target_user = db.get(User, user_id)
if not target_user:
raise HTTPException(status_code=404, detail="کاربر یافت نشد")
# Update permissions
permission_repo = BusinessPermissionRepository(db)
permission_obj = permission_repo.create_or_update(
user_id=user_id,
business_id=business_id,
permissions=update_request.permissions
)
return success_response(
data={},
request=request,
message="دسترسی‌ها با موفقیت به‌روزرسانی شد"
)
@router.delete("/{business_id}/users/{user_id}",
summary="حذف کاربر از کسب و کار",
description="حذف کاربر از کسب و کار",
response_model=RemoveUserResponse,
responses={
200: {
"description": "کاربر با موفقیت حذف شد",
"content": {
"application/json": {
"example": {
"success": True,
"message": "کاربر با موفقیت حذف شد"
}
}
}
},
401: {
"description": "کاربر احراز هویت نشده است"
},
403: {
"description": "دسترسی غیرمجاز یا مجوز کافی نیست"
},
404: {
"description": "کاربر یافت نشد"
}
}
)
@require_business_access("business_id")
def remove_user(
request: Request,
business_id: int,
user_id: int,
ctx: AuthContext = Depends(get_current_user),
db: Session = Depends(get_db)
) -> dict:
"""حذف کاربر از کسب و کار"""
current_user_id = ctx.get_user_id()
# Check if user is business owner or has permission to manage users
business = db.get(Business, business_id)
if not business:
raise HTTPException(status_code=404, detail="کسب و کار یافت نشد")
is_owner = business.owner_id == current_user_id
can_manage = ctx.can_manage_business_users()
if not is_owner and not can_manage:
raise HTTPException(status_code=403, detail="شما مجوز مدیریت کاربران ندارید")
# Check if target user is business owner
business = db.get(Business, business_id)
if business and business.owner_id == user_id:
raise HTTPException(status_code=400, detail="نمی‌توان مالک کسب و کار را حذف کرد")
# Remove user permissions
permission_repo = BusinessPermissionRepository(db)
success = permission_repo.delete_by_user_and_business(user_id, business_id)
if not success:
raise HTTPException(status_code=404, detail="کاربر یافت نشد")
return success_response(
data={},
request=request,
message="کاربر با موفقیت حذف شد"
)

View file

@ -1,6 +1,7 @@
from typing import Any, List, Optional, Union, Generic, TypeVar
from pydantic import BaseModel, EmailStr, Field
from enum import Enum
from datetime import datetime
T = TypeVar('T')
@ -247,3 +248,79 @@ class PaginatedResponse(BaseModel, Generic[T]):
)
# Business User Schemas
class BusinessUserSchema(BaseModel):
id: int
business_id: int
user_id: int
user_name: str
user_email: str
user_phone: Optional[str] = None
role: str
status: str
added_at: datetime
last_active: Optional[datetime] = None
permissions: dict
class Config:
from_attributes = True
class AddUserRequest(BaseModel):
email_or_phone: str
class Config:
json_schema_extra = {
"example": {
"email_or_phone": "user@example.com"
}
}
class AddUserResponse(BaseModel):
success: bool
message: str
user: Optional[BusinessUserSchema] = None
class UpdatePermissionsRequest(BaseModel):
permissions: dict
class Config:
json_schema_extra = {
"example": {
"permissions": {
"sales": {
"read": True,
"write": True,
"delete": False
},
"reports": {
"read": True,
"export": True
},
"settings": {
"manage_users": True
}
}
}
}
class UpdatePermissionsResponse(BaseModel):
success: bool
message: str
class RemoveUserResponse(BaseModel):
success: bool
message: str
class BusinessUsersListResponse(BaseModel):
success: bool
message: str
data: dict
calendar_type: Optional[str] = None

View file

@ -7,6 +7,7 @@ from .captcha import Captcha # noqa: F401
from .password_reset import PasswordReset # noqa: F401
from .business import Business # noqa: F401
from .business_permission import BusinessPermission # noqa: F401
# Business user models removed - using business_permissions instead
# Import support models
from .support import * # noqa: F401, F403

View file

@ -4,7 +4,7 @@ from datetime import datetime
from enum import Enum
from sqlalchemy import String, DateTime, Integer, ForeignKey, Enum as SQLEnum, Text
from sqlalchemy.orm import Mapped, mapped_column
from sqlalchemy.orm import Mapped, mapped_column, relationship
from adapters.db.session import Base
@ -53,3 +53,6 @@ class Business(Base):
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)
# Relationships - using business_permissions instead
# users = relationship("BusinessUser", back_populates="business", cascade="all, delete-orphan")

View file

@ -29,4 +29,7 @@ class User(Base):
# Support relationships
tickets = relationship("Ticket", foreign_keys="Ticket.user_id", back_populates="user")
# Business relationships - using business_permissions instead
# businesses = relationship("BusinessUser", back_populates="user", cascade="all, delete-orphan")

View file

@ -121,14 +121,21 @@ class AuthContext:
"""بررسی دسترسی به پنل اپراتور پشتیبانی"""
return self.has_app_permission("support_operator")
def is_business_owner(self) -> bool:
def is_business_owner(self, business_id: int = None) -> bool:
"""بررسی اینکه آیا کاربر مالک کسب و کار است یا نه"""
if not self.business_id or not self.db:
import logging
logger = logging.getLogger(__name__)
target_business_id = business_id or self.business_id
if not target_business_id or not self.db:
logger.info(f"is_business_owner: no business_id ({target_business_id}) or db ({self.db is not None})")
return False
from adapters.db.models.business import Business
business = self.db.get(Business, self.business_id)
return business and business.owner_id == self.user.id
business = self.db.get(Business, target_business_id)
is_owner = business and business.owner_id == self.user.id
logger.info(f"is_business_owner: business_id={target_business_id}, business={business}, owner_id={business.owner_id if business else None}, user_id={self.user.id}, is_owner={is_owner}")
return is_owner
# بررسی دسترسی‌های کسب و کار
def has_business_permission(self, section: str, action: str) -> bool:
@ -188,9 +195,25 @@ class AuthContext:
"""بررسی دسترسی صادرات در بخش"""
return self.has_business_permission(section, "export")
def can_manage_business_users(self) -> bool:
def can_manage_business_users(self, business_id: int = None) -> bool:
"""بررسی دسترسی مدیریت کاربران کسب و کار"""
return self.has_business_permission("settings", "manage_users")
import logging
logger = logging.getLogger(__name__)
# SuperAdmin دسترسی کامل دارد
if self.is_superadmin():
logger.info(f"can_manage_business_users: user {self.user.id} is superadmin")
return True
# مالک کسب و کار دسترسی کامل دارد
if self.is_business_owner(business_id):
logger.info(f"can_manage_business_users: user {self.user.id} is business owner")
return True
# بررسی دسترسی در سطح کسب و کار
has_permission = self.has_business_permission("settings", "manage_users")
logger.info(f"can_manage_business_users: user {self.user.id} has permission: {has_permission}")
return has_permission
# ترکیب دسترسی‌ها
def has_any_permission(self, section: str, action: str) -> bool:
@ -204,16 +227,25 @@ class AuthContext:
def can_access_business(self, business_id: int) -> bool:
"""بررسی دسترسی به کسب و کار خاص"""
import logging
logger = logging.getLogger(__name__)
logger.info(f"Checking business access: user {self.user.id}, business {business_id}, context business_id {self.business_id}")
# SuperAdmin دسترسی به همه کسب و کارها دارد
if self.is_superadmin():
logger.info(f"User {self.user.id} is superadmin, granting access to business {business_id}")
return True
# اگر مالک کسب و کار است، دسترسی دارد
if self.is_business_owner() and business_id == self.business_id:
logger.info(f"User {self.user.id} is business owner of {business_id}, granting access")
return True
# بررسی دسترسی‌های کسب و کار
return business_id == self.business_id
has_access = business_id == self.business_id
logger.info(f"Business access check: {business_id} == {self.business_id} = {has_access}")
return has_access
def to_dict(self) -> dict:
"""تبدیل به dictionary برای استفاده در API"""
@ -252,10 +284,15 @@ def get_current_user(
db: Session = Depends(get_db)
) -> AuthContext:
"""دریافت اطلاعات کامل کاربر کنونی و تنظیمات از درخواست"""
import logging
logger = logging.getLogger(__name__)
# Get authorization from request headers
auth_header = request.headers.get("Authorization")
logger.info(f"Auth header: {auth_header}")
if not auth_header or not auth_header.startswith("ApiKey "):
logger.warning(f"Invalid auth header: {auth_header}")
raise ApiError("UNAUTHORIZED", "Missing or invalid API key", http_status=401)
api_key = auth_header[len("ApiKey ") :].strip()

View file

@ -74,6 +74,9 @@ def require_business_access(business_id_param: str = "business_id"):
def decorator(func: Callable) -> Callable:
@wraps(func)
def wrapper(*args, **kwargs) -> Any:
import logging
logger = logging.getLogger(__name__)
# Find request in args or kwargs
request = None
for arg in args:
@ -85,6 +88,7 @@ def require_business_access(business_id_param: str = "business_id"):
request = kwargs['request']
if not request:
logger.error("Request not found in function arguments")
raise ApiError("INTERNAL_ERROR", "Request not found", http_status=500)
# Get database session
@ -92,8 +96,17 @@ def require_business_access(business_id_param: str = "business_id"):
db = next(get_db())
ctx = get_current_user(request, db)
business_id = kwargs.get(business_id_param)
logger.info(f"Checking business access for user {ctx.get_user_id()} to business {business_id}")
logger.info(f"User business_id from context: {ctx.business_id}")
logger.info(f"User is superadmin: {ctx.is_superadmin()}")
logger.info(f"User is business owner: {ctx.is_business_owner()}")
if business_id and not ctx.can_access_business(business_id):
logger.warning(f"User {ctx.get_user_id()} does not have access to business {business_id}")
raise ApiError("FORBIDDEN", f"No access to business {business_id}", http_status=403)
logger.info(f"User {ctx.get_user_id()} has access to business {business_id}")
return func(*args, **kwargs)
return wrapper
return decorator

View file

@ -7,8 +7,16 @@ from fastapi import HTTPException, status, Request
from .calendar import CalendarConverter, CalendarType
def success_response(data: Any, request: Request = None) -> dict[str, Any]:
response = {"success": True, "data": data}
def success_response(data: Any, request: Request = None, message: str = None) -> dict[str, Any]:
response = {"success": True}
# Add data if provided
if data is not None:
response["data"] = data
# Add message if provided
if message is not None:
response["message"] = message
# Add calendar type information if request is available
if request and hasattr(request.state, 'calendar_type'):
@ -27,8 +35,17 @@ def format_datetime_fields(data: Any, request: Request) -> Any:
if isinstance(data, dict):
formatted_data = {}
for key, value in data.items():
if isinstance(value, datetime):
formatted_data[key] = CalendarConverter.format_datetime(value, calendar_type)
if value is None:
formatted_data[key] = None
elif isinstance(value, datetime):
# Format the main date field based on calendar type
if calendar_type == "jalali":
formatted_data[key] = CalendarConverter.to_jalali(value)["formatted"]
else:
formatted_data[key] = value.isoformat()
# Add formatted date as additional field
formatted_data[f"{key}_formatted"] = CalendarConverter.format_datetime(value, calendar_type)
# Convert raw date to the same calendar type as the formatted date
if calendar_type == "jalali":
formatted_data[f"{key}_raw"] = CalendarConverter.to_jalali(value)["formatted"]

View file

@ -8,6 +8,7 @@ from adapters.api.v1.auth import router as auth_router
from adapters.api.v1.users import router as users_router
from adapters.api.v1.businesses import router as businesses_router
from adapters.api.v1.business_dashboard import router as business_dashboard_router
from adapters.api.v1.business_users import router as business_users_router
from adapters.api.v1.support.tickets import router as support_tickets_router
from adapters.api.v1.support.operator import router as support_operator_router
from adapters.api.v1.support.categories import router as support_categories_router
@ -271,6 +272,7 @@ def create_app() -> FastAPI:
application.include_router(users_router, prefix=settings.api_v1_prefix)
application.include_router(businesses_router, prefix=settings.api_v1_prefix)
application.include_router(business_dashboard_router, prefix=settings.api_v1_prefix)
application.include_router(business_users_router, prefix=settings.api_v1_prefix)
# Support endpoints
application.include_router(support_tickets_router, prefix=f"{settings.api_v1_prefix}/support")

View file

@ -4,6 +4,8 @@ adapters/__init__.py
adapters/api/__init__.py
adapters/api/v1/__init__.py
adapters/api/v1/auth.py
adapters/api/v1/business_dashboard.py
adapters/api/v1/business_users.py
adapters/api/v1/businesses.py
adapters/api/v1/health.py
adapters/api/v1/schemas.py
@ -68,6 +70,7 @@ app/core/settings.py
app/core/smart_normalizer.py
app/services/api_key_service.py
app/services/auth_service.py
app/services/business_dashboard_service.py
app/services/business_service.py
app/services/captcha_service.py
app/services/email_service.py

View file

@ -10,6 +10,7 @@ class AuthStore with ChangeNotifier {
static const _kDeviceId = 'device_id';
static const _kAppPermissions = 'app_permissions';
static const _kIsSuperAdmin = 'is_superadmin';
static const _kLastUrl = 'last_url';
final FlutterSecureStorage _secure = const FlutterSecureStorage();
String? _apiKey;
@ -21,6 +22,8 @@ class AuthStore with ChangeNotifier {
String get deviceId => _deviceId ?? '';
Map<String, dynamic>? get appPermissions => _appPermissions;
bool get isSuperAdmin => _isSuperAdmin;
int? _currentUserId;
int? get currentUserId => _currentUserId;
Future<void> load() async {
final prefs = await SharedPreferences.getInstance();
@ -98,8 +101,9 @@ class AuthStore with ChangeNotifier {
} catch (_) {}
await prefs.remove(_kApiKey);
}
// پاک کردن دسترسیها هنگام خروج
// پاک کردن دسترسیها و آخرین URL هنگام خروج
await _clearAppPermissions();
await clearLastUrl();
} else {
if (kIsWeb) {
await prefs.setString(_kApiKey, key);
@ -113,10 +117,13 @@ class AuthStore with ChangeNotifier {
notifyListeners();
}
Future<void> saveAppPermissions(Map<String, dynamic>? permissions, bool isSuperAdmin) async {
Future<void> saveAppPermissions(Map<String, dynamic>? permissions, bool isSuperAdmin, {int? userId}) async {
final prefs = await SharedPreferences.getInstance();
_appPermissions = permissions;
_isSuperAdmin = isSuperAdmin;
if (userId != null) {
_currentUserId = userId;
}
if (permissions == null) {
await _clearAppPermissions();
@ -174,10 +181,16 @@ class AuthStore with ChangeNotifier {
if (user != null) {
final appPermissions = user['app_permissions'] as Map<String, dynamic>?;
final isSuperAdmin = appPermissions?['superadmin'] == true;
final userId = user['id'] as int?;
if (appPermissions != null) {
await saveAppPermissions(appPermissions, isSuperAdmin);
}
if (userId != null) {
_currentUserId = userId;
notifyListeners();
}
}
}
}
@ -195,6 +208,32 @@ class AuthStore with ChangeNotifier {
}
bool get canAccessSupportOperator => hasAppPermission('support_operator');
// ذخیره آخرین URL برای بازیابی بعد از refresh
Future<void> saveLastUrl(String url) async {
try {
final prefs = await SharedPreferences.getInstance();
await prefs.setString(_kLastUrl, url);
} catch (_) {}
}
// بازیابی آخرین URL
Future<String?> getLastUrl() async {
try {
final prefs = await SharedPreferences.getInstance();
return prefs.getString(_kLastUrl);
} catch (_) {
return null;
}
}
// پاک کردن آخرین URL
Future<void> clearLastUrl() async {
try {
final prefs = await SharedPreferences.getInstance();
await prefs.remove(_kLastUrl);
} catch (_) {}
}
}

View file

@ -483,6 +483,77 @@
"backToProfile": "Back to Profile",
"noBusinessesFound": "No businesses found",
"createFirstBusiness": "Create your first business",
"accessDenied": "Access denied"
"accessDenied": "Access denied",
"basicTools": "Basic Tools",
"businessSettings": "Business Settings",
"printDocuments": "Print Documents",
"people": "People",
"peopleList": "People List",
"receipts": "Receipts",
"payments": "Payments",
"practicalTools": "Practical Tools",
"usersAndPermissions": "Users and Permissions",
"businessUsers": "Business Users",
"addNewUser": "Add New User",
"userEmailOrPhone": "Email or Phone",
"userEmailOrPhoneHint": "Enter user email or phone number",
"addUser": "Add User",
"userAddedSuccessfully": "User added successfully",
"userAddFailed": "Failed to add user",
"userRemovedSuccessfully": "User removed successfully",
"userRemoveFailed": "Failed to remove user",
"permissionsUpdatedSuccessfully": "Permissions updated successfully",
"permissionsUpdateFailed": "Failed to update permissions",
"userNotFound": "User not found",
"invalidEmailOrPhone": "Invalid email or phone number",
"userAlreadyExists": "User already exists",
"removeUser": "Remove User",
"removeUserConfirm": "Are you sure you want to remove this user?",
"userPermissions": "User Permissions",
"permissions": "Permissions",
"permission": "Permission",
"hasPermission": "Has Permission",
"noPermission": "No Permission",
"viewUsers": "View Users",
"managePermissions": "Manage Permissions",
"totalUsers": "Total Users",
"activeUsers": "Active Users",
"pendingUsers": "Pending Users",
"userName": "User Name",
"userEmail": "Email",
"userPhone": "Phone",
"userStatus": "Status",
"userRole": "Role",
"userAddedAt": "Added At",
"lastActive": "Last Active",
"active": "Active",
"inactive": "Inactive",
"pending": "Pending",
"owner": "Owner",
"admin": "Admin",
"member": "Member",
"viewer": "Viewer",
"editPermissions": "Edit Permissions",
"savePermissions": "Save Permissions",
"cancel": "Cancel",
"loading": "Loading...",
"noUsersFound": "No users found",
"searchUsers": "Search users...",
"filterByStatus": "Filter by Status",
"filterByRole": "Filter by Role",
"allStatuses": "All Statuses",
"allRoles": "All Roles",
"permissionDashboard": "Dashboard Access",
"permissionPeople": "People Access",
"permissionReceipts": "Receipts Access",
"permissionPayments": "Payments Access",
"permissionReports": "Reports Access",
"permissionSettings": "Settings Access",
"permissionUsers": "Users Access",
"permissionPrint": "Print Access",
"ownerWarning": "Warning: Business owner does not need to be added and always has full access to all sections",
"ownerWarningTitle": "Business Owner",
"alreadyAddedWarning": "This user has already been added to the business",
"alreadyAddedWarningTitle": "Existing User"
}

View file

@ -482,6 +482,77 @@
"backToProfile": "بازگشت به پروفایل",
"noBusinessesFound": "هیچ کسب و کاری یافت نشد",
"createFirstBusiness": "اولین کسب و کار خود را ایجاد کنید",
"accessDenied": "دسترسی غیرمجاز"
"accessDenied": "دسترسی غیرمجاز",
"basicTools": "ابزارهای پایه",
"businessSettings": "تنظیمات کسب و کار",
"printDocuments": "چاپ اسناد",
"people": "اشخاص",
"peopleList": "لیست اشخاص",
"receipts": "دریافت‌ها",
"payments": "پرداخت‌ها",
"practicalTools": "ابزارهای کاربردی",
"usersAndPermissions": "کاربران و دسترسی‌ها",
"businessUsers": "کاربران کسب و کار",
"addNewUser": "افزودن کاربر جدید",
"userEmailOrPhone": "ایمیل یا شماره تلفن",
"userEmailOrPhoneHint": "ایمیل یا شماره تلفن کاربر را وارد کنید",
"addUser": "افزودن کاربر",
"userAddedSuccessfully": "کاربر با موفقیت اضافه شد",
"userAddFailed": "خطا در افزودن کاربر",
"userRemovedSuccessfully": "کاربر با موفقیت حذف شد",
"userRemoveFailed": "خطا در حذف کاربر",
"permissionsUpdatedSuccessfully": "دسترسی‌ها با موفقیت به‌روزرسانی شد",
"permissionsUpdateFailed": "خطا در به‌روزرسانی دسترسی‌ها",
"userNotFound": "کاربر یافت نشد",
"invalidEmailOrPhone": "ایمیل یا شماره تلفن نامعتبر است",
"userAlreadyExists": "کاربر قبلاً اضافه شده است",
"removeUser": "حذف کاربر",
"removeUserConfirm": "آیا مطمئن هستید که می‌خواهید این کاربر را حذف کنید؟",
"userPermissions": "دسترسی‌های کاربر",
"permissions": "دسترسی‌ها",
"permission": "دسترسی",
"hasPermission": "دارای دسترسی",
"noPermission": "بدون دسترسی",
"viewUsers": "مشاهده کاربران",
"managePermissions": "مدیریت دسترسی‌ها",
"totalUsers": "کل کاربران",
"activeUsers": "کاربران فعال",
"pendingUsers": "کاربران در انتظار",
"userName": "نام کاربر",
"userEmail": "ایمیل",
"userPhone": "تلفن",
"userStatus": "وضعیت",
"userRole": "نقش",
"userAddedAt": "تاریخ افزودن",
"lastActive": "آخرین فعالیت",
"active": "فعال",
"inactive": "غیرفعال",
"pending": "در انتظار",
"owner": "مالک",
"admin": "مدیر",
"member": "عضو",
"viewer": "مشاهده‌گر",
"editPermissions": "ویرایش دسترسی‌ها",
"savePermissions": "ذخیره دسترسی‌ها",
"cancel": "لغو",
"loading": "در حال بارگذاری...",
"noUsersFound": "هیچ کاربری یافت نشد",
"searchUsers": "جستجوی کاربران...",
"filterByStatus": "فیلتر بر اساس وضعیت",
"filterByRole": "فیلتر بر اساس نقش",
"allStatuses": "همه وضعیت‌ها",
"allRoles": "همه نقش‌ها",
"permissionDashboard": "دسترسی به داشبورد",
"permissionPeople": "دسترسی به اشخاص",
"permissionReceipts": "دسترسی به دریافت‌ها",
"permissionPayments": "دسترسی به پرداخت‌ها",
"permissionReports": "دسترسی به گزارش‌ها",
"permissionSettings": "دسترسی به تنظیمات",
"permissionUsers": "دسترسی به کاربران",
"permissionPrint": "دسترسی به چاپ اسناد",
"ownerWarning": "هشدار: کاربر مالک کسب و کار نیازی به افزودن ندارد و همیشه دسترسی کامل به همه بخش‌ها دارد",
"ownerWarningTitle": "کاربر مالک",
"alreadyAddedWarning": "این کاربر قبلاً به کسب و کار اضافه شده است",
"alreadyAddedWarningTitle": "کاربر موجود"
}

View file

@ -2713,6 +2713,402 @@ abstract class AppLocalizations {
/// In en, this message translates to:
/// **'Access denied'**
String get accessDenied;
/// No description provided for @basicTools.
///
/// In en, this message translates to:
/// **'Basic Tools'**
String get basicTools;
/// No description provided for @businessSettings.
///
/// In en, this message translates to:
/// **'Business Settings'**
String get businessSettings;
/// No description provided for @printDocuments.
///
/// In en, this message translates to:
/// **'Print Documents'**
String get printDocuments;
/// No description provided for @people.
///
/// In en, this message translates to:
/// **'People'**
String get people;
/// No description provided for @peopleList.
///
/// In en, this message translates to:
/// **'People List'**
String get peopleList;
/// No description provided for @receipts.
///
/// In en, this message translates to:
/// **'Receipts'**
String get receipts;
/// No description provided for @payments.
///
/// In en, this message translates to:
/// **'Payments'**
String get payments;
/// No description provided for @practicalTools.
///
/// In en, this message translates to:
/// **'Practical Tools'**
String get practicalTools;
/// No description provided for @usersAndPermissions.
///
/// In en, this message translates to:
/// **'Users and Permissions'**
String get usersAndPermissions;
/// No description provided for @businessUsers.
///
/// In en, this message translates to:
/// **'Business Users'**
String get businessUsers;
/// No description provided for @addNewUser.
///
/// In en, this message translates to:
/// **'Add New User'**
String get addNewUser;
/// No description provided for @userEmailOrPhone.
///
/// In en, this message translates to:
/// **'Email or Phone'**
String get userEmailOrPhone;
/// No description provided for @userEmailOrPhoneHint.
///
/// In en, this message translates to:
/// **'Enter user email or phone number'**
String get userEmailOrPhoneHint;
/// No description provided for @addUser.
///
/// In en, this message translates to:
/// **'Add User'**
String get addUser;
/// No description provided for @userAddedSuccessfully.
///
/// In en, this message translates to:
/// **'User added successfully'**
String get userAddedSuccessfully;
/// No description provided for @userAddFailed.
///
/// In en, this message translates to:
/// **'Failed to add user'**
String get userAddFailed;
/// No description provided for @userRemovedSuccessfully.
///
/// In en, this message translates to:
/// **'User removed successfully'**
String get userRemovedSuccessfully;
/// No description provided for @userRemoveFailed.
///
/// In en, this message translates to:
/// **'Failed to remove user'**
String get userRemoveFailed;
/// No description provided for @permissionsUpdatedSuccessfully.
///
/// In en, this message translates to:
/// **'Permissions updated successfully'**
String get permissionsUpdatedSuccessfully;
/// No description provided for @permissionsUpdateFailed.
///
/// In en, this message translates to:
/// **'Failed to update permissions'**
String get permissionsUpdateFailed;
/// No description provided for @userNotFound.
///
/// In en, this message translates to:
/// **'User not found'**
String get userNotFound;
/// No description provided for @invalidEmailOrPhone.
///
/// In en, this message translates to:
/// **'Invalid email or phone number'**
String get invalidEmailOrPhone;
/// No description provided for @userAlreadyExists.
///
/// In en, this message translates to:
/// **'User already exists'**
String get userAlreadyExists;
/// No description provided for @removeUser.
///
/// In en, this message translates to:
/// **'Remove User'**
String get removeUser;
/// No description provided for @removeUserConfirm.
///
/// In en, this message translates to:
/// **'Are you sure you want to remove this user?'**
String get removeUserConfirm;
/// No description provided for @userPermissions.
///
/// In en, this message translates to:
/// **'User Permissions'**
String get userPermissions;
/// No description provided for @permissions.
///
/// In en, this message translates to:
/// **'Permissions'**
String get permissions;
/// No description provided for @permission.
///
/// In en, this message translates to:
/// **'Permission'**
String get permission;
/// No description provided for @hasPermission.
///
/// In en, this message translates to:
/// **'Has Permission'**
String get hasPermission;
/// No description provided for @noPermission.
///
/// In en, this message translates to:
/// **'No Permission'**
String get noPermission;
/// No description provided for @viewUsers.
///
/// In en, this message translates to:
/// **'View Users'**
String get viewUsers;
/// No description provided for @managePermissions.
///
/// In en, this message translates to:
/// **'Manage Permissions'**
String get managePermissions;
/// No description provided for @totalUsers.
///
/// In en, this message translates to:
/// **'Total Users'**
String get totalUsers;
/// No description provided for @activeUsers.
///
/// In en, this message translates to:
/// **'Active Users'**
String get activeUsers;
/// No description provided for @pendingUsers.
///
/// In en, this message translates to:
/// **'Pending Users'**
String get pendingUsers;
/// No description provided for @userName.
///
/// In en, this message translates to:
/// **'User Name'**
String get userName;
/// No description provided for @userEmail.
///
/// In en, this message translates to:
/// **'Email'**
String get userEmail;
/// No description provided for @userPhone.
///
/// In en, this message translates to:
/// **'Phone'**
String get userPhone;
/// No description provided for @userStatus.
///
/// In en, this message translates to:
/// **'Status'**
String get userStatus;
/// No description provided for @userRole.
///
/// In en, this message translates to:
/// **'Role'**
String get userRole;
/// No description provided for @userAddedAt.
///
/// In en, this message translates to:
/// **'Added At'**
String get userAddedAt;
/// No description provided for @lastActive.
///
/// In en, this message translates to:
/// **'Last Active'**
String get lastActive;
/// No description provided for @inactive.
///
/// In en, this message translates to:
/// **'Inactive'**
String get inactive;
/// No description provided for @pending.
///
/// In en, this message translates to:
/// **'Pending'**
String get pending;
/// No description provided for @admin.
///
/// In en, this message translates to:
/// **'Admin'**
String get admin;
/// No description provided for @viewer.
///
/// In en, this message translates to:
/// **'Viewer'**
String get viewer;
/// No description provided for @editPermissions.
///
/// In en, this message translates to:
/// **'Edit Permissions'**
String get editPermissions;
/// No description provided for @savePermissions.
///
/// In en, this message translates to:
/// **'Save Permissions'**
String get savePermissions;
/// No description provided for @noUsersFound.
///
/// In en, this message translates to:
/// **'No users found'**
String get noUsersFound;
/// No description provided for @searchUsers.
///
/// In en, this message translates to:
/// **'Search users...'**
String get searchUsers;
/// No description provided for @filterByStatus.
///
/// In en, this message translates to:
/// **'Filter by Status'**
String get filterByStatus;
/// No description provided for @filterByRole.
///
/// In en, this message translates to:
/// **'Filter by Role'**
String get filterByRole;
/// No description provided for @allStatuses.
///
/// In en, this message translates to:
/// **'All Statuses'**
String get allStatuses;
/// No description provided for @allRoles.
///
/// In en, this message translates to:
/// **'All Roles'**
String get allRoles;
/// No description provided for @permissionDashboard.
///
/// In en, this message translates to:
/// **'Dashboard Access'**
String get permissionDashboard;
/// No description provided for @permissionPeople.
///
/// In en, this message translates to:
/// **'People Access'**
String get permissionPeople;
/// No description provided for @permissionReceipts.
///
/// In en, this message translates to:
/// **'Receipts Access'**
String get permissionReceipts;
/// No description provided for @permissionPayments.
///
/// In en, this message translates to:
/// **'Payments Access'**
String get permissionPayments;
/// No description provided for @permissionReports.
///
/// In en, this message translates to:
/// **'Reports Access'**
String get permissionReports;
/// No description provided for @permissionSettings.
///
/// In en, this message translates to:
/// **'Settings Access'**
String get permissionSettings;
/// No description provided for @permissionUsers.
///
/// In en, this message translates to:
/// **'Users Access'**
String get permissionUsers;
/// No description provided for @permissionPrint.
///
/// In en, this message translates to:
/// **'Print Access'**
String get permissionPrint;
/// No description provided for @ownerWarning.
///
/// In en, this message translates to:
/// **'Warning: Business owner does not need to be added and always has full access to all sections'**
String get ownerWarning;
/// No description provided for @ownerWarningTitle.
///
/// In en, this message translates to:
/// **'Business Owner'**
String get ownerWarningTitle;
/// No description provided for @alreadyAddedWarning.
///
/// In en, this message translates to:
/// **'This user has already been added to the business'**
String get alreadyAddedWarning;
/// No description provided for @alreadyAddedWarningTitle.
///
/// In en, this message translates to:
/// **'Existing User'**
String get alreadyAddedWarningTitle;
}
class _AppLocalizationsDelegate

View file

@ -1356,4 +1356,205 @@ class AppLocalizationsEn extends AppLocalizations {
@override
String get accessDenied => 'Access denied';
@override
String get basicTools => 'Basic Tools';
@override
String get businessSettings => 'Business Settings';
@override
String get printDocuments => 'Print Documents';
@override
String get people => 'People';
@override
String get peopleList => 'People List';
@override
String get receipts => 'Receipts';
@override
String get payments => 'Payments';
@override
String get practicalTools => 'Practical Tools';
@override
String get usersAndPermissions => 'Users and Permissions';
@override
String get businessUsers => 'Business Users';
@override
String get addNewUser => 'Add New User';
@override
String get userEmailOrPhone => 'Email or Phone';
@override
String get userEmailOrPhoneHint => 'Enter user email or phone number';
@override
String get addUser => 'Add User';
@override
String get userAddedSuccessfully => 'User added successfully';
@override
String get userAddFailed => 'Failed to add user';
@override
String get userRemovedSuccessfully => 'User removed successfully';
@override
String get userRemoveFailed => 'Failed to remove user';
@override
String get permissionsUpdatedSuccessfully =>
'Permissions updated successfully';
@override
String get permissionsUpdateFailed => 'Failed to update permissions';
@override
String get userNotFound => 'User not found';
@override
String get invalidEmailOrPhone => 'Invalid email or phone number';
@override
String get userAlreadyExists => 'User already exists';
@override
String get removeUser => 'Remove User';
@override
String get removeUserConfirm => 'Are you sure you want to remove this user?';
@override
String get userPermissions => 'User Permissions';
@override
String get permissions => 'Permissions';
@override
String get permission => 'Permission';
@override
String get hasPermission => 'Has Permission';
@override
String get noPermission => 'No Permission';
@override
String get viewUsers => 'View Users';
@override
String get managePermissions => 'Manage Permissions';
@override
String get totalUsers => 'Total Users';
@override
String get activeUsers => 'Active Users';
@override
String get pendingUsers => 'Pending Users';
@override
String get userName => 'User Name';
@override
String get userEmail => 'Email';
@override
String get userPhone => 'Phone';
@override
String get userStatus => 'Status';
@override
String get userRole => 'Role';
@override
String get userAddedAt => 'Added At';
@override
String get lastActive => 'Last Active';
@override
String get inactive => 'Inactive';
@override
String get pending => 'Pending';
@override
String get admin => 'Admin';
@override
String get viewer => 'Viewer';
@override
String get editPermissions => 'Edit Permissions';
@override
String get savePermissions => 'Save Permissions';
@override
String get noUsersFound => 'No users found';
@override
String get searchUsers => 'Search users...';
@override
String get filterByStatus => 'Filter by Status';
@override
String get filterByRole => 'Filter by Role';
@override
String get allStatuses => 'All Statuses';
@override
String get allRoles => 'All Roles';
@override
String get permissionDashboard => 'Dashboard Access';
@override
String get permissionPeople => 'People Access';
@override
String get permissionReceipts => 'Receipts Access';
@override
String get permissionPayments => 'Payments Access';
@override
String get permissionReports => 'Reports Access';
@override
String get permissionSettings => 'Settings Access';
@override
String get permissionUsers => 'Users Access';
@override
String get permissionPrint => 'Print Access';
@override
String get ownerWarning =>
'Warning: Business owner does not need to be added and always has full access to all sections';
@override
String get ownerWarningTitle => 'Business Owner';
@override
String get alreadyAddedWarning =>
'This user has already been added to the business';
@override
String get alreadyAddedWarningTitle => 'Existing User';
}

View file

@ -1346,4 +1346,206 @@ class AppLocalizationsFa extends AppLocalizations {
@override
String get accessDenied => 'دسترسی غیرمجاز';
@override
String get basicTools => 'ابزارهای پایه';
@override
String get businessSettings => 'تنظیمات کسب و کار';
@override
String get printDocuments => 'چاپ اسناد';
@override
String get people => 'اشخاص';
@override
String get peopleList => 'لیست اشخاص';
@override
String get receipts => 'دریافت‌ها';
@override
String get payments => 'پرداخت‌ها';
@override
String get practicalTools => 'ابزارهای کاربردی';
@override
String get usersAndPermissions => 'کاربران و دسترسی‌ها';
@override
String get businessUsers => 'کاربران کسب و کار';
@override
String get addNewUser => 'افزودن کاربر جدید';
@override
String get userEmailOrPhone => 'ایمیل یا شماره تلفن';
@override
String get userEmailOrPhoneHint => 'ایمیل یا شماره تلفن کاربر را وارد کنید';
@override
String get addUser => 'افزودن کاربر';
@override
String get userAddedSuccessfully => 'کاربر با موفقیت اضافه شد';
@override
String get userAddFailed => 'خطا در افزودن کاربر';
@override
String get userRemovedSuccessfully => 'کاربر با موفقیت حذف شد';
@override
String get userRemoveFailed => 'خطا در حذف کاربر';
@override
String get permissionsUpdatedSuccessfully =>
'دسترسی‌ها با موفقیت به‌روزرسانی شد';
@override
String get permissionsUpdateFailed => 'خطا در به‌روزرسانی دسترسی‌ها';
@override
String get userNotFound => 'کاربر یافت نشد';
@override
String get invalidEmailOrPhone => 'ایمیل یا شماره تلفن نامعتبر است';
@override
String get userAlreadyExists => 'کاربر قبلاً اضافه شده است';
@override
String get removeUser => 'حذف کاربر';
@override
String get removeUserConfirm =>
'آیا مطمئن هستید که می‌خواهید این کاربر را حذف کنید؟';
@override
String get userPermissions => 'دسترسی‌های کاربر';
@override
String get permissions => 'دسترسی‌ها';
@override
String get permission => 'دسترسی';
@override
String get hasPermission => 'دارای دسترسی';
@override
String get noPermission => 'بدون دسترسی';
@override
String get viewUsers => 'مشاهده کاربران';
@override
String get managePermissions => 'مدیریت دسترسی‌ها';
@override
String get totalUsers => 'کل کاربران';
@override
String get activeUsers => 'کاربران فعال';
@override
String get pendingUsers => 'کاربران در انتظار';
@override
String get userName => 'نام کاربر';
@override
String get userEmail => 'ایمیل';
@override
String get userPhone => 'تلفن';
@override
String get userStatus => 'وضعیت';
@override
String get userRole => 'نقش';
@override
String get userAddedAt => 'تاریخ افزودن';
@override
String get lastActive => 'آخرین فعالیت';
@override
String get inactive => 'غیرفعال';
@override
String get pending => 'در انتظار';
@override
String get admin => 'مدیر';
@override
String get viewer => 'مشاهده‌گر';
@override
String get editPermissions => 'ویرایش دسترسی‌ها';
@override
String get savePermissions => 'ذخیره دسترسی‌ها';
@override
String get noUsersFound => 'هیچ کاربری یافت نشد';
@override
String get searchUsers => 'جستجوی کاربران...';
@override
String get filterByStatus => 'فیلتر بر اساس وضعیت';
@override
String get filterByRole => 'فیلتر بر اساس نقش';
@override
String get allStatuses => 'همه وضعیت‌ها';
@override
String get allRoles => 'همه نقش‌ها';
@override
String get permissionDashboard => 'دسترسی به داشبورد';
@override
String get permissionPeople => 'دسترسی به اشخاص';
@override
String get permissionReceipts => 'دسترسی به دریافت‌ها';
@override
String get permissionPayments => 'دسترسی به پرداخت‌ها';
@override
String get permissionReports => 'دسترسی به گزارش‌ها';
@override
String get permissionSettings => 'دسترسی به تنظیمات';
@override
String get permissionUsers => 'دسترسی به کاربران';
@override
String get permissionPrint => 'دسترسی به چاپ اسناد';
@override
String get ownerWarning =>
'هشدار: کاربر مالک کسب و کار نیازی به افزودن ندارد و همیشه دسترسی کامل به همه بخش‌ها دارد';
@override
String get ownerWarningTitle => 'کاربر مالک';
@override
String get alreadyAddedWarning =>
'این کاربر قبلاً به کسب و کار اضافه شده است';
@override
String get alreadyAddedWarningTitle => 'کاربر موجود';
}

View file

@ -20,6 +20,7 @@ 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 'pages/business/users_permissions_page.dart';
import 'package:hesabix_ui/l10n/app_localizations.dart';
import 'core/locale_controller.dart';
import 'core/calendar_controller.dart';
@ -29,6 +30,7 @@ import 'theme/app_theme.dart';
import 'core/auth_store.dart';
import 'core/permission_guard.dart';
import 'widgets/simple_splash_screen.dart';
import 'widgets/url_tracker.dart';
void main() {
// Use path-based routing instead of hash routing
@ -98,15 +100,34 @@ class _MyAppState extends State<MyApp> {
ApiClient.bindCalendarController(_calendarController!);
ApiClient.bindAuthStore(_authStore!);
// اطمینان از حداقل 4 ثانیه نمایش splash screen
// اطمینان از حداقل 1 ثانیه نمایش splash screen
final elapsed = DateTime.now().difference(_loadStartTime!);
const minimumDuration = Duration(seconds: 4);
const minimumDuration = Duration(seconds: 1);
if (elapsed < minimumDuration) {
await Future.delayed(minimumDuration - elapsed);
}
// ذخیره URL فعلی قبل از اتمام loading
if (_authStore != null) {
try {
final currentUrl = Uri.base.path;
print('🔍 LOADING DEBUG: Current URL before finishing loading: $currentUrl');
if (currentUrl.isNotEmpty &&
currentUrl != '/' &&
currentUrl != '/login' &&
(currentUrl.startsWith('/user/profile/') || currentUrl.startsWith('/business/'))) {
print('🔍 LOADING DEBUG: Saving current URL: $currentUrl');
await _authStore!.saveLastUrl(currentUrl);
}
} catch (e) {
print('🔍 LOADING DEBUG: Error saving current URL: $e');
}
}
// اتمام loading
if (mounted) {
print('🔍 LOADING DEBUG: Finishing loading, setting _isLoading to false');
setState(() {
_isLoading = false;
});
@ -116,12 +137,16 @@ class _MyAppState extends State<MyApp> {
// Root of application with GoRouter
@override
Widget build(BuildContext context) {
print('🔍 BUILD DEBUG: Building app, _isLoading: $_isLoading');
print('🔍 BUILD DEBUG: Controllers - locale: ${_controller != null}, calendar: ${_calendarController != null}, theme: ${_themeController != null}, auth: ${_authStore != null}');
// اگر هنوز loading است، splash screen نمایش بده
if (_isLoading ||
_controller == null ||
_calendarController == null ||
_themeController == null ||
_authStore == null) {
print('🔍 BUILD DEBUG: Still loading, showing splash screen');
final loadingRouter = GoRouter(
redirect: (context, state) {
// در حین loading، هیچ redirect نکن - URL را حفظ کن
@ -165,11 +190,13 @@ class _MyAppState extends State<MyApp> {
return SimpleSplashScreen(
message: loadingMessage,
showLogo: true,
displayDuration: const Duration(seconds: 4),
displayDuration: const Duration(seconds: 1),
locale: _controller?.locale,
authStore: _authStore,
onComplete: () {
// این callback زمانی فراخوانی میشود که splash screen تمام شود
// اما ما از splash controller استفاده میکنیم
print('🔍 SPLASH DEBUG: Splash screen completed');
},
);
},
@ -194,41 +221,69 @@ class _MyAppState extends State<MyApp> {
final controller = _controller!;
final themeController = _themeController!;
print('🔍 BUILD DEBUG: All controllers loaded, creating main router');
final router = GoRouter(
initialLocation: '/',
redirect: (context, state) {
redirect: (context, state) async {
final currentPath = state.uri.path;
final fullUri = state.uri.toString();
print('🔍 REDIRECT DEBUG: Current path: $currentPath');
print('🔍 REDIRECT DEBUG: Full URI: $fullUri');
// اگر authStore هنوز load نشده، منتظر بمان
if (_authStore == null) {
print('🔍 REDIRECT DEBUG: AuthStore is null, staying on current path');
return null;
}
final hasKey = _authStore!.apiKey != null && _authStore!.apiKey!.isNotEmpty;
print('🔍 REDIRECT DEBUG: Has API key: $hasKey');
// اگر API key ندارد
if (!hasKey) {
print('🔍 REDIRECT DEBUG: No API key');
// اگر در login نیست، به login برود
if (currentPath != '/login') {
print('🔍 REDIRECT DEBUG: Redirecting to login from $currentPath');
return '/login';
}
// اگر در login است، بماند
print('🔍 REDIRECT DEBUG: Already on login, staying');
return null;
}
// اگر API key دارد
print('🔍 REDIRECT DEBUG: Has API key, checking current path');
// اگر در login است، به dashboard برود
if (currentPath == '/login') {
print('🔍 REDIRECT DEBUG: On login page, redirecting to dashboard');
return '/user/profile/dashboard';
}
// اگر در root است، به dashboard برود
// اگر در root است، آخرین URL را بررسی کن
if (currentPath == '/') {
print('🔍 REDIRECT DEBUG: On root path, checking last URL');
// اگر آخرین URL موجود است و معتبر است، به آن برود
final lastUrl = await _authStore!.getLastUrl();
print('🔍 REDIRECT DEBUG: Last URL: $lastUrl');
if (lastUrl != null &&
lastUrl.isNotEmpty &&
lastUrl != '/' &&
lastUrl != '/login' &&
(lastUrl.startsWith('/user/profile/') || lastUrl.startsWith('/business/'))) {
print('🔍 REDIRECT DEBUG: Redirecting to last URL: $lastUrl');
return lastUrl;
}
// وگرنه به dashboard برود (فقط اگر در root باشیم)
print('🔍 REDIRECT DEBUG: No valid last URL, redirecting to dashboard');
return '/user/profile/dashboard';
}
// برای سایر صفحات (شامل صفحات profile)، redirect نکن (بماند)
// این مهم است: اگر کاربر در صفحات profile است، بماند
// برای سایر صفحات (شامل صفحات profile و business)، redirect نکن (بماند)
// این مهم است: اگر کاربر در صفحات profile یا business است، بماند
print('🔍 REDIRECT DEBUG: On other page ($currentPath), staying on current path');
return null;
},
routes: <RouteBase>[
@ -373,7 +428,9 @@ class _MyAppState extends State<MyApp> {
return BusinessShell(
businessId: businessId,
authStore: _authStore!,
localeController: controller,
calendarController: _calendarController!,
themeController: themeController,
child: const SizedBox.shrink(), // Will be replaced by child routes
);
},
@ -386,11 +443,32 @@ class _MyAppState extends State<MyApp> {
return BusinessShell(
businessId: businessId,
authStore: _authStore!,
localeController: controller,
calendarController: _calendarController!,
themeController: themeController,
child: BusinessDashboardPage(businessId: businessId),
);
},
),
GoRoute(
path: 'users-permissions',
name: 'business_users_permissions',
builder: (context, state) {
final businessId = int.parse(state.pathParameters['business_id']!);
return BusinessShell(
businessId: businessId,
authStore: _authStore!,
localeController: controller,
calendarController: _calendarController!,
themeController: themeController,
child: UsersPermissionsPage(
businessId: businessId.toString(),
authStore: _authStore!,
calendarController: _calendarController!,
),
);
},
),
// TODO: Add other business routes (sales, accounting, etc.)
],
),
@ -400,28 +478,31 @@ class _MyAppState extends State<MyApp> {
return AnimatedBuilder(
animation: Listenable.merge([controller, themeController]),
builder: (context, _) {
return MaterialApp.router(
title: 'Hesabix',
theme: AppTheme.build(
isDark: false,
return UrlTracker(
authStore: _authStore!,
child: MaterialApp.router(
title: 'Hesabix',
theme: AppTheme.build(
isDark: false,
locale: controller.locale,
seed: themeController.seedColor,
),
darkTheme: AppTheme.build(
isDark: true,
locale: controller.locale,
seed: themeController.seedColor,
),
themeMode: themeController.mode,
routerConfig: router,
locale: controller.locale,
seed: themeController.seedColor,
supportedLocales: AppLocalizations.supportedLocales,
localizationsDelegates: const [
AppLocalizations.delegate,
GlobalMaterialLocalizations.delegate,
GlobalCupertinoLocalizations.delegate,
GlobalWidgetsLocalizations.delegate,
],
),
darkTheme: AppTheme.build(
isDark: true,
locale: controller.locale,
seed: themeController.seedColor,
),
themeMode: themeController.mode,
routerConfig: router,
locale: controller.locale,
supportedLocales: AppLocalizations.supportedLocales,
localizationsDelegates: const [
AppLocalizations.delegate,
GlobalMaterialLocalizations.delegate,
GlobalCupertinoLocalizations.delegate,
GlobalWidgetsLocalizations.delegate,
],
);
},
);

View file

@ -0,0 +1,306 @@
import 'package:shamsi_date/shamsi_date.dart';
class BusinessUser {
final int id;
final int businessId;
final int userId;
final String userName;
final String userEmail;
final String? userPhone;
final String role;
final String status;
final DateTime addedAt;
final DateTime? lastActive;
final Map<String, dynamic> permissions;
const BusinessUser({
required this.id,
required this.businessId,
required this.userId,
required this.userName,
required this.userEmail,
this.userPhone,
required this.role,
required this.status,
required this.addedAt,
this.lastActive,
required this.permissions,
});
factory BusinessUser.fromJson(Map<String, dynamic> json) {
return BusinessUser(
id: json['id'] as int,
businessId: json['business_id'] as int,
userId: json['user_id'] as int,
userName: json['user_name'] as String,
userEmail: json['user_email'] as String,
userPhone: json['user_phone'] as String?,
role: json['role'] as String,
status: json['status'] as String,
addedAt: _parseDateTime(json['added_at']),
lastActive: json['last_active'] != null
? _parseDateTime(json['last_active'])
: null,
permissions: (json['permissions'] as Map<String, dynamic>?) ?? {},
);
}
static DateTime _parseDateTime(dynamic dateValue) {
if (dateValue == null) return DateTime.now();
if (dateValue is String) {
// Check if it's a Jalali date format (YYYY/MM/DD HH:MM:SS)
if (dateValue.contains('/') && !dateValue.contains('-')) {
try {
// Parse Jalali date format: YYYY/MM/DD HH:MM:SS
final parts = dateValue.split(' ');
if (parts.length >= 1) {
final dateParts = parts[0].split('/');
if (dateParts.length == 3) {
final year = int.parse(dateParts[0]);
final month = int.parse(dateParts[1]);
final day = int.parse(dateParts[2]);
final jalali = Jalali(year, month, day);
return jalali.toDateTime();
}
}
} catch (e) {
// Fall back to standard parsing
}
}
return DateTime.parse(dateValue);
} else if (dateValue is int) {
return DateTime.fromMillisecondsSinceEpoch(dateValue);
}
return DateTime.now();
}
Map<String, dynamic> toJson() {
return {
'id': id,
'business_id': businessId,
'user_id': userId,
'user_name': userName,
'user_email': userEmail,
'user_phone': userPhone,
'role': role,
'status': status,
'added_at': addedAt.toIso8601String(),
'last_active': lastActive?.toIso8601String(),
'permissions': permissions,
};
}
BusinessUser copyWith({
int? id,
int? businessId,
int? userId,
String? userName,
String? userEmail,
String? userPhone,
String? role,
String? status,
DateTime? addedAt,
DateTime? lastActive,
Map<String, dynamic>? permissions,
}) {
return BusinessUser(
id: id ?? this.id,
businessId: businessId ?? this.businessId,
userId: userId ?? this.userId,
userName: userName ?? this.userName,
userEmail: userEmail ?? this.userEmail,
userPhone: userPhone ?? this.userPhone,
role: role ?? this.role,
status: status ?? this.status,
addedAt: addedAt ?? this.addedAt,
lastActive: lastActive ?? this.lastActive,
permissions: permissions ?? this.permissions,
);
}
// Helper methods for permissions
bool hasPermission(String section, String action) {
if (permissions.isEmpty) return false;
if (!permissions.containsKey(section)) return false;
final sectionPerms = permissions[section] as Map<String, dynamic>?;
if (sectionPerms == null) return action == 'read';
return sectionPerms[action] == true;
}
bool canRead(String section) {
return hasPermission(section, 'read') || permissions.containsKey(section);
}
bool canWrite(String section) {
return hasPermission(section, 'write');
}
bool canDelete(String section) {
return hasPermission(section, 'delete');
}
bool canApprove(String section) {
return hasPermission(section, 'approve');
}
bool canExport(String section) {
return hasPermission(section, 'export');
}
bool canManageUsers() {
return hasPermission('settings', 'manage_users');
}
// Get all available sections
List<String> get availableSections {
return permissions.keys.toList();
}
// Get all actions for a section
List<String> getActionsForSection(String section) {
final sectionPerms = permissions[section] as Map<String, dynamic>?;
if (sectionPerms == null) return ['read'];
return sectionPerms.keys.where((key) => sectionPerms[key] == true).toList();
}
}
// Request/Response models
class AddUserRequest {
final int businessId;
final String emailOrPhone;
const AddUserRequest({
required this.businessId,
required this.emailOrPhone,
});
Map<String, dynamic> toJson() {
return {
'business_id': businessId,
'email_or_phone': emailOrPhone,
};
}
}
class AddUserResponse {
final bool success;
final String message;
final BusinessUser? user;
const AddUserResponse({
required this.success,
required this.message,
this.user,
});
factory AddUserResponse.fromJson(Map<String, dynamic> json) {
return AddUserResponse(
success: json['success'] as bool? ?? false,
message: json['message'] as String? ?? '',
user: json['user'] != null
? BusinessUser.fromJson(json['user'] as Map<String, dynamic>)
: null,
);
}
}
class UpdatePermissionsRequest {
final int businessId;
final int userId;
final Map<String, dynamic> permissions;
const UpdatePermissionsRequest({
required this.businessId,
required this.userId,
required this.permissions,
});
Map<String, dynamic> toJson() {
return {
'business_id': businessId,
'user_id': userId,
'permissions': permissions,
};
}
}
class UpdatePermissionsResponse {
final bool success;
final String message;
const UpdatePermissionsResponse({
required this.success,
required this.message,
});
factory UpdatePermissionsResponse.fromJson(Map<String, dynamic> json) {
return UpdatePermissionsResponse(
success: json['success'] as bool? ?? false,
message: json['message'] as String? ?? '',
);
}
}
class RemoveUserRequest {
final int businessId;
final int userId;
const RemoveUserRequest({
required this.businessId,
required this.userId,
});
Map<String, dynamic> toJson() {
return {
'business_id': businessId,
'user_id': userId,
};
}
}
class RemoveUserResponse {
final bool success;
final String message;
const RemoveUserResponse({
required this.success,
required this.message,
});
factory RemoveUserResponse.fromJson(Map<String, dynamic> json) {
return RemoveUserResponse(
success: json['success'] as bool? ?? false,
message: json['message'] as String? ?? '',
);
}
}
class BusinessUsersResponse {
final bool success;
final String message;
final List<BusinessUser> users;
final int totalCount;
const BusinessUsersResponse({
required this.success,
required this.message,
required this.users,
required this.totalCount,
});
factory BusinessUsersResponse.fromJson(Map<String, dynamic> json) {
return BusinessUsersResponse(
success: json['success'] as bool? ?? false,
message: json['message'] as String? ?? '',
users: (json['users'] as List<dynamic>?)
?.map((userJson) => BusinessUser.fromJson(userJson as Map<String, dynamic>))
.toList() ?? [],
totalCount: json['total_count'] as int? ?? 0,
);
}
}

View file

@ -341,11 +341,11 @@ class _UserManagementPageState extends State<UserManagementPage> {
content: const Text('User creation form would go here'),
actions: [
TextButton(
onPressed: () => Navigator.of(context).pop(),
onPressed: () => context.pop(),
child: Text(AppLocalizations.of(context).cancel),
),
ElevatedButton(
onPressed: () => Navigator.of(context).pop(),
onPressed: () => context.pop(),
child: Text(AppLocalizations.of(context).save),
),
],
@ -361,11 +361,11 @@ class _UserManagementPageState extends State<UserManagementPage> {
content: const Text('User edit form would go here'),
actions: [
TextButton(
onPressed: () => Navigator.of(context).pop(),
onPressed: () => context.pop(),
child: Text(AppLocalizations.of(context).cancel),
),
ElevatedButton(
onPressed: () => Navigator.of(context).pop(),
onPressed: () => context.pop(),
child: Text(AppLocalizations.of(context).save),
),
],
@ -381,11 +381,11 @@ class _UserManagementPageState extends State<UserManagementPage> {
content: const Text('Permissions management would go here'),
actions: [
TextButton(
onPressed: () => Navigator.of(context).pop(),
onPressed: () => context.pop(),
child: Text(AppLocalizations.of(context).cancel),
),
ElevatedButton(
onPressed: () => Navigator.of(context).pop(),
onPressed: () => context.pop(),
child: Text(AppLocalizations.of(context).save),
),
],
@ -401,12 +401,12 @@ class _UserManagementPageState extends State<UserManagementPage> {
content: Text('Are you sure you want to delete ${user['name']}?'),
actions: [
TextButton(
onPressed: () => Navigator.of(context).pop(),
onPressed: () => context.pop(),
child: Text(AppLocalizations.of(context).cancel),
),
ElevatedButton(
onPressed: () {
Navigator.of(context).pop();
context.pop();
// Delete user logic here
},
style: ElevatedButton.styleFrom(backgroundColor: Colors.red),

View file

@ -1,21 +1,29 @@
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/locale_controller.dart';
import '../../core/calendar_controller.dart';
import '../../theme/theme_controller.dart';
import '../../widgets/settings_menu_button.dart';
import '../../widgets/user_account_menu_button.dart';
import 'package:hesabix_ui/l10n/app_localizations.dart';
class BusinessShell extends StatefulWidget {
final int businessId;
final AuthStore authStore;
final CalendarController calendarController;
final Widget child;
final AuthStore authStore;
final LocaleController? localeController;
final CalendarController? calendarController;
final ThemeController? themeController;
const BusinessShell({
super.key,
required this.businessId,
required this.authStore,
required this.calendarController,
required this.child,
required this.authStore,
this.localeController,
this.calendarController,
this.themeController,
});
@override
@ -23,6 +31,9 @@ class BusinessShell extends StatefulWidget {
}
class _BusinessShellState extends State<BusinessShell> {
int _hoverIndex = -1;
bool _isBasicToolsExpanded = false;
bool _isPeopleExpanded = false;
@override
void initState() {
@ -41,40 +52,206 @@ class _BusinessShellState extends State<BusinessShell> {
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();
String location = '/business/${widget.businessId}/dashboard'; // default location
try {
location = GoRouterState.of(context).uri.toString();
} catch (e) {
// اگر GoRouterState در دسترس نیست، از default استفاده کن
}
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'),
// ساختار متمرکز منو
final menuItems = <_MenuItem>[
_MenuItem(
label: t.businessDashboard,
icon: Icons.dashboard_outlined,
selectedIcon: Icons.dashboard,
path: '/business/${widget.businessId}/dashboard',
type: _MenuItemType.simple,
),
_MenuItem(
label: t.practicalTools,
icon: Icons.category,
selectedIcon: Icons.category,
path: null, // آیتم جداکننده
type: _MenuItemType.separator,
),
_MenuItem(
label: t.people,
icon: Icons.people,
selectedIcon: Icons.people,
path: null, // برای منوی بازشونده
type: _MenuItemType.expandable,
children: [
_MenuItem(
label: t.peopleList,
icon: Icons.list,
selectedIcon: Icons.list,
path: '/business/${widget.businessId}/people-list',
type: _MenuItemType.simple,
),
_MenuItem(
label: t.receipts,
icon: Icons.receipt,
selectedIcon: Icons.receipt,
path: '/business/${widget.businessId}/receipts',
type: _MenuItemType.simple,
hasAddButton: true,
),
_MenuItem(
label: t.payments,
icon: Icons.payment,
selectedIcon: Icons.payment,
path: '/business/${widget.businessId}/payments',
type: _MenuItemType.simple,
hasAddButton: true,
),
],
),
_MenuItem(
label: t.settings,
icon: Icons.settings,
selectedIcon: Icons.settings,
path: null, // برای منوی بازشونده
type: _MenuItemType.expandable,
children: [
_MenuItem(
label: t.businessSettings,
icon: Icons.business,
selectedIcon: Icons.business,
path: '/business/${widget.businessId}/business-settings',
type: _MenuItemType.simple,
),
_MenuItem(
label: t.printDocuments,
icon: Icons.print,
selectedIcon: Icons.print,
path: '/business/${widget.businessId}/print-documents',
type: _MenuItemType.simple,
),
_MenuItem(
label: t.usersAndPermissions,
icon: Icons.people_outline,
selectedIcon: Icons.people,
path: '/business/${widget.businessId}/users-permissions',
type: _MenuItemType.simple,
),
],
),
];
int selectedIndex = 0;
for (int i = 0; i < destinations.length; i++) {
if (location.startsWith(destinations[i].path)) {
for (int i = 0; i < menuItems.length; i++) {
final item = menuItems[i];
if (item.type == _MenuItemType.separator) continue; // نادیده گرفتن آیتم جداکننده
if (item.type == _MenuItemType.simple && item.path != null && location.startsWith(item.path!)) {
selectedIndex = i;
break;
} else if (item.type == _MenuItemType.expandable && item.children != null) {
for (int j = 0; j < item.children!.length; j++) {
final child = item.children![j];
if (child.path != null && location.startsWith(child.path!)) {
selectedIndex = i;
// تنظیم وضعیت باز بودن منو
if (i == 2) _isPeopleExpanded = true; // اشخاص در ایندکس 2
if (i == 3) _isBasicToolsExpanded = true; // تنظیمات در ایندکس 3
break;
}
}
}
}
Future<void> onSelect(int index) async {
final path = destinations[index].path;
if (GoRouterState.of(context).uri.toString() != path) {
context.go(path);
final item = menuItems[index];
if (item.type == _MenuItemType.separator) return; // آیتم جداکننده قابل کلیک نیست
if (item.type == _MenuItemType.simple && item.path != null) {
try {
if (GoRouterState.of(context).uri.toString() != item.path!) {
context.go(item.path!);
}
} catch (e) {
// اگر GoRouterState در دسترس نیست، مستقیماً به مسیر برود
context.go(item.path!);
}
} else if (item.type == _MenuItemType.expandable) {
// تغییر وضعیت باز/بسته بودن منو
if (item.label == t.people) _isPeopleExpanded = !_isPeopleExpanded;
if (item.label == t.settings) _isBasicToolsExpanded = !_isBasicToolsExpanded;
setState(() {});
}
}
Future<void> onBackToProfile() async {
context.go('/user/profile/businesses');
Future<void> onSelectChild(int parentIndex, int childIndex) async {
final parent = menuItems[parentIndex];
if (parent.type == _MenuItemType.expandable && parent.children != null) {
final child = parent.children![childIndex];
if (child.path != null) {
try {
if (GoRouterState.of(context).uri.toString() != child.path!) {
context.go(child.path!);
}
} catch (e) {
// اگر GoRouterState در دسترس نیست، مستقیماً به مسیر برود
context.go(child.path!);
}
}
}
}
Future<void> onLogout() async {
await widget.authStore.saveApiKey(null);
if (!context.mounted) return;
ScaffoldMessenger.of(context)
..hideCurrentSnackBar()
..showSnackBar(const SnackBar(content: Text('خروج انجام شد')));
context.go('/login');
}
bool isExpanded(_MenuItem item) {
if (item.label == t.people) return _isPeopleExpanded;
if (item.label == t.settings) return _isBasicToolsExpanded;
return false;
}
int getTotalMenuItemsCount() {
int count = 0;
for (final item in menuItems) {
if (item.type == _MenuItemType.separator) {
count++; // آیتم جداکننده هم شمرده میشود
} else {
count++; // آیتم اصلی
if (item.type == _MenuItemType.expandable && isExpanded(item) && railExtended) {
count += item.children?.length ?? 0;
}
}
}
return count;
}
int getMenuIndexFromTotalIndex(int totalIndex) {
int currentIndex = 0;
for (int i = 0; i < menuItems.length; i++) {
if (currentIndex == totalIndex) return i;
currentIndex++;
final item = menuItems[i];
if (item.type == _MenuItemType.expandable && isExpanded(item) && railExtended) {
final childrenCount = item.children?.length ?? 0;
if (totalIndex >= currentIndex && totalIndex < currentIndex + childrenCount) {
return i;
}
currentIndex += childrenCount;
}
}
return 0;
}
// Brand top bar with contrast color
@ -88,86 +265,431 @@ class _BusinessShellState extends State<BusinessShell> {
final appBar = AppBar(
backgroundColor: appBarBg,
foregroundColor: appBarFg,
elevation: 0,
automaticallyImplyLeading: !useRail,
titleSpacing: 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,
),
),
Image.asset(logoAsset, height: 28),
const SizedBox(width: 12),
Text(t.appTitle, style: TextStyle(color: appBarFg, fontWeight: FontWeight.w700)),
],
),
leading: useRail
? null
: Builder(
builder: (ctx) => IconButton(
icon: Icon(Icons.menu, color: appBarFg),
onPressed: () => Scaffold.of(ctx).openDrawer(),
tooltip: t.menu,
),
),
actions: [
IconButton(
icon: Icon(Icons.arrow_back, color: appBarFg),
onPressed: onBackToProfile,
tooltip: t.backToProfile,
SettingsMenuButton(
localeController: widget.localeController,
calendarController: widget.calendarController,
themeController: widget.themeController,
),
const SizedBox(width: 8),
UserAccountMenuButton(authStore: widget.authStore),
const SizedBox(width: 8),
],
);
final content = Container(
color: scheme.surface,
child: SafeArea(
child: widget.child,
),
);
// Side colors and styles
final Color sideBg = Theme.of(context).brightness == Brightness.dark
? scheme.surfaceContainerHighest
: scheme.surfaceContainerLow;
final Color sideFg = scheme.onSurfaceVariant;
final Color activeBg = scheme.primaryContainer;
final Color activeFg = scheme.onPrimaryContainer;
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(),
Container(
width: railExtended ? 240 : 88,
height: double.infinity,
color: sideBg,
child: ListView.builder(
padding: EdgeInsets.zero,
itemCount: getTotalMenuItemsCount(),
itemBuilder: (ctx, index) {
final menuIndex = getMenuIndexFromTotalIndex(index);
final item = menuItems[menuIndex];
final bool isHovered = index == _hoverIndex;
final bool isSelected = menuIndex == selectedIndex;
final bool active = isSelected || isHovered;
final BorderRadius br = (isSelected && useRail)
? BorderRadius.zero
: (isHovered ? BorderRadius.zero : BorderRadius.circular(8));
final Color bgColor = active
? (isHovered && !isSelected ? activeBg.withValues(alpha: 0.85) : activeBg)
: Colors.transparent;
// اگر آیتم بازشونده است و در حالت باز است، زیرآیتمها را نمایش بده
if (item.type == _MenuItemType.expandable && isExpanded(item) && railExtended) {
if (index == getMenuIndexFromTotalIndex(index)) {
// آیتم اصلی
return MouseRegion(
onEnter: (_) => setState(() => _hoverIndex = index),
onExit: (_) => setState(() => _hoverIndex = -1),
child: InkWell(
borderRadius: br,
onTap: () {
setState(() {
if (item.label == t.people) _isPeopleExpanded = !_isPeopleExpanded;
if (item.label == t.settings) _isBasicToolsExpanded = !_isBasicToolsExpanded;
});
},
child: Container(
margin: EdgeInsets.zero,
padding: EdgeInsets.symmetric(
horizontal: railExtended ? 16 : 8,
vertical: 8,
),
decoration: BoxDecoration(
color: bgColor,
borderRadius: br,
),
child: Row(
children: [
Icon(
active ? item.selectedIcon : item.icon,
color: active ? activeFg : sideFg,
size: 24,
),
if (railExtended) ...[
const SizedBox(width: 12),
Expanded(
child: Text(
item.label,
style: TextStyle(
color: active ? activeFg : sideFg,
fontWeight: active ? FontWeight.w600 : FontWeight.w400,
),
),
),
Icon(
isExpanded(item) ? Icons.expand_less : Icons.expand_more,
color: sideFg,
size: 20,
),
],
],
),
),
),
);
} else {
// زیرآیتمها
final childIndex = index - getMenuIndexFromTotalIndex(index) - 1;
if (childIndex < (item.children?.length ?? 0)) {
final child = item.children![childIndex];
return MouseRegion(
onEnter: (_) => setState(() => _hoverIndex = index),
onExit: (_) => setState(() => _hoverIndex = -1),
child: InkWell(
borderRadius: br,
onTap: () => onSelectChild(menuIndex, childIndex),
child: Container(
margin: EdgeInsets.zero,
padding: EdgeInsets.symmetric(
horizontal: railExtended ? 24 : 16, // بیشتر indent برای زیرآیتم
vertical: 8,
),
decoration: BoxDecoration(
color: bgColor,
borderRadius: br,
),
child: Row(
children: [
Icon(
child.icon,
color: sideFg,
size: 20,
),
if (railExtended) ...[
const SizedBox(width: 12),
Expanded(
child: Text(
child.label,
style: TextStyle(
color: sideFg,
fontWeight: FontWeight.w400,
),
),
),
if (child.hasAddButton)
IconButton(
icon: const Icon(Icons.add, size: 16),
onPressed: () {
// Navigate to add new receipt/payment
if (child.label == t.receipts) {
// Navigate to add receipt
} else if (child.label == t.payments) {
// Navigate to add payment
}
},
),
],
],
),
),
),
);
}
}
} else {
// آیتم ساده، آیتم بازشونده در حالت بسته، یا آیتم جداکننده
if (item.type == _MenuItemType.separator) {
// آیتم جداکننده
return Container(
margin: EdgeInsets.symmetric(
horizontal: railExtended ? 16 : 8,
vertical: 8,
),
child: Row(
children: [
if (railExtended) ...[
Expanded(
child: Divider(
color: sideFg.withValues(alpha: 0.3),
thickness: 1,
),
),
const SizedBox(width: 12),
Text(
item.label,
style: TextStyle(
color: sideFg.withValues(alpha: 0.7),
fontSize: 12,
fontWeight: FontWeight.w500,
),
),
const SizedBox(width: 12),
Expanded(
child: Divider(
color: sideFg.withValues(alpha: 0.3),
thickness: 1,
),
),
] else ...[
Expanded(
child: Divider(
color: sideFg.withValues(alpha: 0.3),
thickness: 1,
),
),
],
],
),
);
} else {
// آیتم ساده یا آیتم بازشونده در حالت بسته
return MouseRegion(
onEnter: (_) => setState(() => _hoverIndex = index),
onExit: (_) => setState(() => _hoverIndex = -1),
child: InkWell(
borderRadius: br,
onTap: () {
if (item.type == _MenuItemType.expandable) {
setState(() {
if (item.label == t.people) _isPeopleExpanded = !_isPeopleExpanded;
if (item.label == t.settings) _isBasicToolsExpanded = !_isBasicToolsExpanded;
});
} else {
onSelect(menuIndex);
}
},
child: Container(
margin: EdgeInsets.zero,
padding: EdgeInsets.symmetric(
horizontal: railExtended ? 16 : 8,
vertical: 8,
),
decoration: BoxDecoration(
color: bgColor,
borderRadius: br,
),
child: Row(
children: [
Icon(
active ? item.selectedIcon : item.icon,
color: active ? activeFg : sideFg,
size: 24,
),
if (railExtended) ...[
const SizedBox(width: 12),
Expanded(
child: Text(
item.label,
style: TextStyle(
color: active ? activeFg : sideFg,
fontWeight: active ? FontWeight.w600 : FontWeight.w400,
),
),
),
if (item.type == _MenuItemType.expandable)
Icon(
isExpanded(item) ? Icons.expand_less : Icons.expand_more,
color: sideFg,
size: 20,
),
],
],
),
),
),
);
}
}
return const SizedBox.shrink();
},
),
),
const VerticalDivider(thickness: 1, width: 1),
Expanded(
child: widget.child,
),
Expanded(child: content),
],
),
);
} 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(),
drawer: Drawer(
backgroundColor: sideBg,
child: SafeArea(
child: ListView(
padding: const EdgeInsets.symmetric(vertical: 8),
children: [
// آیتمهای منو
for (int i = 0; i < menuItems.length; i++) ...[
Builder(builder: (ctx) {
final item = menuItems[i];
final bool active = i == selectedIndex;
if (item.type == _MenuItemType.separator) {
// آیتم جداکننده در منوی موبایل
return Container(
margin: const EdgeInsets.symmetric(horizontal: 16, vertical: 8),
child: Row(
children: [
Expanded(
child: Divider(
color: sideFg.withValues(alpha: 0.3),
thickness: 1,
),
),
const SizedBox(width: 12),
Text(
item.label,
style: TextStyle(
color: sideFg.withValues(alpha: 0.7),
fontSize: 12,
fontWeight: FontWeight.w500,
),
),
const SizedBox(width: 12),
Expanded(
child: Divider(
color: sideFg.withValues(alpha: 0.3),
thickness: 1,
),
),
],
),
);
} else if (item.type == _MenuItemType.simple) {
return ListTile(
leading: Icon(item.selectedIcon, color: active ? activeFg : sideFg),
title: Text(item.label, style: TextStyle(color: active ? activeFg : sideFg, fontWeight: active ? FontWeight.w600 : FontWeight.w400)),
selected: active,
selectedTileColor: activeBg,
onTap: () {
context.pop();
onSelect(i);
},
);
} else if (item.type == _MenuItemType.expandable) {
return ExpansionTile(
leading: Icon(item.icon, color: sideFg),
title: Text(item.label, style: TextStyle(color: sideFg)),
initiallyExpanded: isExpanded(item),
onExpansionChanged: (expanded) {
setState(() {
if (item.label == t.people) _isPeopleExpanded = expanded;
if (item.label == t.settings) _isBasicToolsExpanded = expanded;
});
},
children: item.children?.map((child) => ListTile(
leading: const SizedBox(width: 24),
title: Text(child.label),
trailing: child.hasAddButton ? IconButton(
icon: const Icon(Icons.add, size: 20),
onPressed: () {
context.pop();
// Navigate to add new receipt/payment
if (child.label == t.receipts) {
// Navigate to add receipt
} else if (child.label == t.payments) {
// Navigate to add payment
}
},
) : null,
onTap: () {
context.pop();
onSelectChild(i, item.children!.indexOf(child));
},
)).toList() ?? [],
);
}
return const SizedBox.shrink();
}),
],
const Divider(),
ListTile(
leading: const Icon(Icons.logout),
title: Text(t.logout),
onTap: onLogout,
),
],
),
),
);
}
),
body: content,
);
}
}
class _Dest {
enum _MenuItemType { simple, expandable, separator }
class _MenuItem {
final String label;
final IconData icon;
final IconData selectedIcon;
final String path;
final String? path;
final _MenuItemType type;
final List<_MenuItem>? children;
final bool hasAddButton;
const _Dest(this.label, this.icon, this.selectedIcon, this.path);
const _MenuItem({
required this.label,
required this.icon,
required this.selectedIcon,
this.path,
required this.type,
this.children,
this.hasAddButton = false,
});
}

File diff suppressed because it is too large Load diff

View file

@ -248,20 +248,26 @@ class _LoginPageState extends State<LoginPage> with SingleTickerProviderStateMix
// ذخیره دسترسیهای اپلیکیشن
final appPermissions = user?['app_permissions'] as Map<String, dynamic>?;
final isSuperAdmin = appPermissions?['superadmin'] == true;
final userId = user?['id'] as int?;
if (appPermissions != null) {
await widget.authStore.saveAppPermissions(appPermissions, isSuperAdmin);
await widget.authStore.saveAppPermissions(appPermissions, isSuperAdmin, userId: userId);
}
if (!mounted) return;
_showSnack(t.homeWelcome);
// بعد از login موفق، به صفحه قبلی یا dashboard برود
final currentPath = GoRouterState.of(context).uri.path;
if (currentPath.startsWith('/user/profile/') || currentPath.startsWith('/acc/')) {
// اگر در صفحه محافظت شده بود، همان صفحه را refresh کند
context.go(currentPath);
} else {
// وگرنه به dashboard برود
try {
final currentPath = GoRouterState.of(context).uri.path;
if (currentPath.startsWith('/user/profile/') || currentPath.startsWith('/acc/') || currentPath.startsWith('/business/')) {
// اگر در صفحه محافظت شده بود، همان صفحه را refresh کند
context.go(currentPath);
} else {
// وگرنه به dashboard برود
context.go('/user/profile/dashboard');
}
} catch (e) {
// اگر GoRouterState در دسترس نیست، به dashboard برود
context.go('/user/profile/dashboard');
}
} catch (e) {
@ -344,9 +350,10 @@ class _LoginPageState extends State<LoginPage> with SingleTickerProviderStateMix
// ذخیره دسترسیهای اپلیکیشن
final appPermissions = user?['app_permissions'] as Map<String, dynamic>?;
final isSuperAdmin = appPermissions?['superadmin'] == true;
final userId = user?['id'] as int?;
if (appPermissions != null) {
await widget.authStore.saveAppPermissions(appPermissions, isSuperAdmin);
await widget.authStore.saveAppPermissions(appPermissions, isSuperAdmin, userId: userId);
}
_showSnack(t.registerSuccess);
// پاکسازی کد معرف پس از ثبتنام موفق

View file

@ -1,4 +1,5 @@
import 'package:flutter/material.dart';
import 'package:go_router/go_router.dart';
import 'package:hesabix_ui/l10n/app_localizations.dart';
import 'package:hesabix_ui/core/api_client.dart';
import 'package:hesabix_ui/services/support_service.dart';
@ -235,7 +236,7 @@ class _CreateTicketPageState extends State<CreateTicketPage> {
),
),
IconButton(
onPressed: () => Navigator.of(context).pop(),
onPressed: () => context.pop(),
icon: const Icon(Icons.close),
style: IconButton.styleFrom(
backgroundColor: theme.colorScheme.surface,
@ -645,7 +646,7 @@ class _CreateTicketPageState extends State<CreateTicketPage> {
),
const SizedBox(width: 12),
OutlinedButton(
onPressed: _isSubmitting ? null : () => Navigator.of(context).pop(),
onPressed: _isSubmitting ? null : () => context.pop(),
style: OutlinedButton.styleFrom(
padding: const EdgeInsets.symmetric(vertical: 16, horizontal: 24),
shape: RoundedRectangleBorder(

View file

@ -1,4 +1,5 @@
import 'package:flutter/material.dart';
import 'package:go_router/go_router.dart';
import 'package:hesabix_ui/l10n/app_localizations.dart';
import '../../models/business_models.dart';
import '../../services/business_api_service.dart';
@ -121,7 +122,7 @@ class _NewBusinessPageState extends State<NewBusinessPage> {
duration: const Duration(seconds: 2),
),
);
Navigator.of(context).pop();
context.pop();
}
} catch (e) {
if (mounted) {

View file

@ -42,7 +42,12 @@ class _ProfileShellState extends State<ProfileShell> {
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();
String location = '/user/profile/dashboard'; // default location
try {
location = GoRouterState.of(context).uri.toString();
} catch (e) {
// اگر GoRouterState در دسترس نیست، از default استفاده کن
}
final bool isDark = Theme.of(context).brightness == Brightness.dark;
final String logoAsset = isDark
? 'assets/images/logo-light.png'
@ -85,7 +90,12 @@ class _ProfileShellState extends State<ProfileShell> {
Future<void> onSelect(int index) async {
final path = allDestinations[index].path;
if (GoRouterState.of(context).uri.toString() != path) {
try {
if (GoRouterState.of(context).uri.toString() != path) {
context.go(path);
}
} catch (e) {
// اگر GoRouterState در دسترس نیست، مستقیماً به مسیر برود
context.go(path);
}
}
@ -251,7 +261,7 @@ class _ProfileShellState extends State<ProfileShell> {
selected: active,
selectedTileColor: activeBg,
onTap: () {
Navigator.of(context).pop();
context.pop();
onSelect(i);
},
);

View file

@ -0,0 +1,66 @@
import '../core/api_client.dart';
import '../models/business_user_model.dart';
class BusinessUserService {
final ApiClient _apiClient;
BusinessUserService(this._apiClient);
/// Get all users for a business
Future<BusinessUsersResponse> getBusinessUsers(int businessId) async {
try {
final response = await _apiClient.get('/api/v1/business/$businessId/users');
return BusinessUsersResponse.fromJson(response.data['data']);
} catch (e) {
throw Exception('Failed to fetch business users: $e');
}
}
/// Add a new user to business
Future<AddUserResponse> addUser(AddUserRequest request) async {
try {
final response = await _apiClient.post(
'/api/v1/business/${request.businessId}/users',
data: request.toJson(),
);
return AddUserResponse.fromJson(response.data);
} catch (e) {
throw Exception('Failed to add user: $e');
}
}
/// Update user permissions
Future<UpdatePermissionsResponse> updatePermissions(UpdatePermissionsRequest request) async {
try {
final response = await _apiClient.put(
'/api/v1/business/${request.businessId}/users/${request.userId}/permissions',
data: request.toJson(),
);
return UpdatePermissionsResponse.fromJson(response.data['data']);
} catch (e) {
throw Exception('Failed to update permissions: $e');
}
}
/// Remove user from business
Future<RemoveUserResponse> removeUser(RemoveUserRequest request) async {
try {
final response = await _apiClient.delete(
'/api/v1/business/${request.businessId}/users/${request.userId}',
);
return RemoveUserResponse.fromJson(response.data);
} catch (e) {
throw Exception('Failed to remove user: $e');
}
}
/// Get user details
Future<BusinessUser> getUserDetails(int businessId, int userId) async {
try {
final response = await _apiClient.get('/api/v1/business/$businessId/users/$userId');
return BusinessUser.fromJson(response.data['data']['user']);
} catch (e) {
throw Exception('Failed to fetch user details: $e');
}
}
}

View file

@ -1,4 +1,5 @@
import 'package:flutter/material.dart';
import 'package:go_router/go_router.dart';
import 'package:hesabix_ui/l10n/app_localizations.dart';
import '../../../core/api_client.dart';
@ -121,7 +122,7 @@ class _StorageConfigFormDialogState extends State<StorageConfigFormDialog> {
}
if (mounted) {
Navigator.of(context).pop();
context.pop();
// Only show SnackBar if there's no onSaved callback (parent will handle notification)
if (widget.onSaved == null) {
@ -202,7 +203,7 @@ class _StorageConfigFormDialogState extends State<StorageConfigFormDialog> {
),
),
IconButton(
onPressed: () => Navigator.of(context).pop(),
onPressed: () => context.pop(),
icon: const Icon(Icons.close),
color: theme.colorScheme.onPrimary,
),
@ -344,7 +345,7 @@ class _StorageConfigFormDialogState extends State<StorageConfigFormDialog> {
mainAxisAlignment: MainAxisAlignment.end,
children: [
TextButton(
onPressed: _isLoading ? null : () => Navigator.of(context).pop(),
onPressed: _isLoading ? null : () => context.pop(),
child: Text(l10n.cancel),
),
const SizedBox(width: 12),

View file

@ -1,4 +1,5 @@
import 'package:flutter/material.dart';
import 'package:go_router/go_router.dart';
import 'package:hesabix_ui/l10n/app_localizations.dart';
import 'data_table_config.dart';
import 'helpers/column_settings_service.dart';
@ -65,7 +66,7 @@ class _ColumnSettingsDialogState extends State<ColumnSettingsDialog> {
),
const Spacer(),
IconButton(
onPressed: () => Navigator.of(context).pop(),
onPressed: () => context.pop(),
icon: const Icon(Icons.close),
),
],
@ -96,7 +97,7 @@ class _ColumnSettingsDialogState extends State<ColumnSettingsDialog> {
),
const SizedBox(width: 12),
OutlinedButton(
onPressed: () => Navigator.of(context).pop(),
onPressed: () => context.pop(),
child: Text(t.cancel),
),
const SizedBox(width: 12),

View file

@ -1,4 +1,5 @@
import 'package:flutter/material.dart';
import 'package:go_router/go_router.dart';
import 'package:intl/intl.dart';
import 'package:hesabix_ui/l10n/app_localizations.dart';
import 'data_table_config.dart';
@ -234,7 +235,7 @@ class _DataTableSearchDialogState extends State<DataTableSearchDialog> {
return [
TextButton(
onPressed: () {
Navigator.of(context).pop();
context.pop();
},
child: Text(t.cancel),
),
@ -242,7 +243,7 @@ class _DataTableSearchDialogState extends State<DataTableSearchDialog> {
TextButton(
onPressed: () {
widget.onClear();
Navigator.of(context).pop();
context.pop();
},
child: Text(t.clear),
),
@ -302,7 +303,7 @@ class _DataTableSearchDialogState extends State<DataTableSearchDialog> {
widget.onApply(_controller.text.trim(), _selectedType);
break;
}
Navigator.of(context).pop();
context.pop();
}
Future<void> _selectFromDate(AppLocalizations t, bool isJalali) async {
@ -449,7 +450,7 @@ class _DataTableDateRangeDialogState extends State<DataTableDateRangeDialog> {
actions: [
TextButton(
onPressed: () {
Navigator.of(context).pop();
context.pop();
},
child: Text(t.cancel),
),
@ -457,7 +458,7 @@ class _DataTableDateRangeDialogState extends State<DataTableDateRangeDialog> {
TextButton(
onPressed: () {
widget.onClear();
Navigator.of(context).pop();
context.pop();
},
child: Text(t.clear),
),
@ -465,7 +466,7 @@ class _DataTableDateRangeDialogState extends State<DataTableDateRangeDialog> {
onPressed: _fromDate != null && _toDate != null
? () {
widget.onApply(_fromDate, _toDate);
Navigator.of(context).pop();
context.pop();
}
: null,
child: Text(t.applyFilter),

View file

@ -1,4 +1,5 @@
import 'package:flutter/material.dart';
import 'package:go_router/go_router.dart';
import 'package:shamsi_date/shamsi_date.dart';
/// DatePicker سفارشی برای تقویم شمسی
@ -100,7 +101,7 @@ class _JalaliDatePickerState extends State<JalaliDatePicker> {
mainAxisAlignment: MainAxisAlignment.end,
children: [
TextButton(
onPressed: () => Navigator.of(context).pop(),
onPressed: () => context.pop(),
child: Text(
'انصراف',
style: TextStyle(color: theme.textTheme.bodyMedium?.color),

View file

@ -134,11 +134,11 @@ class _ProgressSplashScreenState extends State<ProgressSplashScreen>
colors: isDark
? [
bgColor,
bgColor.withOpacity(0.95),
bgColor.withValues(alpha: 0.95),
]
: [
bgColor,
bgColor.withOpacity(0.98),
bgColor.withValues(alpha: 0.98),
],
),
),
@ -161,7 +161,7 @@ class _ProgressSplashScreenState extends State<ProgressSplashScreen>
borderRadius: BorderRadius.circular(20),
boxShadow: [
BoxShadow(
color: primary.withOpacity(0.2),
color: primary.withValues(alpha: 0.2),
blurRadius: 20,
spreadRadius: 2,
),
@ -204,7 +204,7 @@ class _ProgressSplashScreenState extends State<ProgressSplashScreen>
// Subtitle
Text(
AppLocalizations.of(context)?.businessManagementPlatform ?? 'Business Management Platform',
AppLocalizations.of(context).businessManagementPlatform,
style: theme.textTheme.bodyLarge?.copyWith(
color: colorScheme.onSurfaceVariant,
fontWeight: FontWeight.w400,
@ -243,7 +243,7 @@ class _ProgressSplashScreenState extends State<ProgressSplashScreen>
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(2),
gradient: LinearGradient(
colors: [primary, primary.withOpacity(0.8)],
colors: [primary, primary.withValues(alpha: 0.8)],
),
),
),
@ -255,7 +255,7 @@ class _ProgressSplashScreenState extends State<ProgressSplashScreen>
// Loading Message
Text(
widget.message ?? AppLocalizations.of(context)?.loading ?? 'Loading...',
widget.message ?? AppLocalizations.of(context).loading,
style: theme.textTheme.bodyMedium?.copyWith(
color: colorScheme.onSurfaceVariant,
fontWeight: FontWeight.w500,
@ -268,7 +268,7 @@ class _ProgressSplashScreenState extends State<ProgressSplashScreen>
Text(
'${_remainingSeconds}s',
style: theme.textTheme.bodySmall?.copyWith(
color: colorScheme.onSurfaceVariant.withOpacity(0.7),
color: colorScheme.onSurfaceVariant.withValues(alpha: 0.7),
fontWeight: FontWeight.w400,
),
),
@ -281,7 +281,7 @@ class _ProgressSplashScreenState extends State<ProgressSplashScreen>
Text(
'Version 1.0.0',
style: theme.textTheme.bodySmall?.copyWith(
color: colorScheme.onSurfaceVariant.withOpacity(0.6),
color: colorScheme.onSurfaceVariant.withValues(alpha: 0.6),
),
),
],

View file

@ -0,0 +1,82 @@
import 'package:flutter/material.dart';
import 'package:hesabix_ui/l10n/app_localizations.dart';
import 'calendar_switcher.dart';
import 'language_switcher.dart';
import 'theme_mode_switcher.dart';
import '../core/locale_controller.dart';
import '../core/calendar_controller.dart';
import '../theme/theme_controller.dart';
class SettingsMenuButton extends StatelessWidget {
final LocaleController? localeController;
final CalendarController? calendarController;
final ThemeController? themeController;
const SettingsMenuButton({
super.key,
this.localeController,
this.calendarController,
this.themeController,
});
void _showSettingsDialog(BuildContext context) {
final t = AppLocalizations.of(context);
showDialog(
context: context,
builder: (context) => AlertDialog(
title: Text(t.settings),
content: Column(
mainAxisSize: MainAxisSize.min,
children: [
if (calendarController != null) ...[
Row(
children: [
Text(t.calendar),
const Spacer(),
CalendarSwitcher(controller: calendarController!),
],
),
const SizedBox(height: 16),
],
if (localeController != null) ...[
Row(
children: [
Text(t.language),
const Spacer(),
LanguageSwitcher(controller: localeController!),
],
),
const SizedBox(height: 16),
],
if (themeController != null) ...[
Row(
children: [
Text(t.theme),
const Spacer(),
ThemeModeSwitcher(controller: themeController!),
],
),
],
],
),
),
);
}
@override
Widget build(BuildContext context) {
final cs = Theme.of(context).colorScheme;
return IconButton(
icon: CircleAvatar(
radius: 16,
backgroundColor: cs.surfaceContainerHighest,
foregroundColor: cs.onSurface,
child: const Icon(Icons.settings, size: 18),
),
onPressed: () => _showSettingsDialog(context),
tooltip: AppLocalizations.of(context).settings,
);
}
}

View file

@ -1,5 +1,6 @@
import 'package:flutter/material.dart';
import 'dart:async';
import '../core/auth_store.dart';
class SimpleSplashScreen extends StatefulWidget {
final String? message;
@ -9,6 +10,7 @@ class SimpleSplashScreen extends StatefulWidget {
final Duration displayDuration;
final VoidCallback? onComplete;
final Locale? locale;
final AuthStore? authStore;
const SimpleSplashScreen({
super.key,
@ -16,9 +18,10 @@ class SimpleSplashScreen extends StatefulWidget {
this.showLogo = true,
this.backgroundColor,
this.primaryColor,
this.displayDuration = const Duration(seconds: 4),
this.displayDuration = const Duration(seconds: 1),
this.onComplete,
this.locale,
this.authStore,
});
@override
@ -68,12 +71,51 @@ class _SimpleSplashScreenState extends State<SimpleSplashScreen>
_fadeController.forward();
_scaleController.forward();
// Start display timer
// Start display timer with authentication check
_displayTimer = Timer(widget.displayDuration, () {
widget.onComplete?.call();
_checkAuthenticationAndComplete();
});
}
void _checkAuthenticationAndComplete() {
print('🔍 SPLASH DEBUG: Checking authentication and completing splash screen');
// اگر authStore موجود است، وضعیت احراز هویت را بررسی کن
if (widget.authStore != null) {
final hasApiKey = widget.authStore!.apiKey != null && widget.authStore!.apiKey!.isNotEmpty;
print('🔍 SPLASH DEBUG: AuthStore available, has API key: $hasApiKey');
// اگر کاربر وارد شده، URL فعلی را ذخیره کن
if (hasApiKey) {
// URL فعلی را از window.location بگیر
try {
// در web، URL فعلی را از window.location میگیریم
final currentUrl = Uri.base.path;
print('🔍 SPLASH DEBUG: Current URL from Uri.base: $currentUrl');
if (currentUrl.isNotEmpty &&
currentUrl != '/' &&
currentUrl != '/login' &&
(currentUrl.startsWith('/user/profile/') || currentUrl.startsWith('/business/'))) {
print('🔍 SPLASH DEBUG: Saving current URL: $currentUrl');
widget.authStore!.saveLastUrl(currentUrl);
}
} catch (e) {
print('🔍 SPLASH DEBUG: Error getting current URL: $e');
}
}
// اگر کاربر وارد نشده، به صفحه لاگین هدایت میشود
// اگر کاربر وارد شده، در صفحه کنونی میماند
// این منطق در main.dart در GoRouter redirect مدیریت میشود
} else {
print('🔍 SPLASH DEBUG: AuthStore is null');
}
print('🔍 SPLASH DEBUG: Calling onComplete callback');
widget.onComplete?.call();
}
@override
void dispose() {
_fadeController.dispose();

View file

@ -36,11 +36,11 @@ class SplashScreen extends StatelessWidget {
colors: isDark
? [
bgColor,
bgColor.withOpacity(0.95),
bgColor.withValues(alpha: 0.95),
]
: [
bgColor,
bgColor.withOpacity(0.98),
bgColor.withValues(alpha: 0.98),
],
),
),
@ -56,7 +56,7 @@ class SplashScreen extends StatelessWidget {
borderRadius: BorderRadius.circular(20),
boxShadow: [
BoxShadow(
color: primary.withOpacity(0.2),
color: primary.withValues(alpha: 0.2),
blurRadius: 20,
spreadRadius: 2,
),
@ -99,7 +99,7 @@ class SplashScreen extends StatelessWidget {
// Subtitle
Text(
AppLocalizations.of(context)?.businessManagementPlatform ?? 'Business Management Platform',
AppLocalizations.of(context).businessManagementPlatform,
style: theme.textTheme.bodyLarge?.copyWith(
color: colorScheme.onSurfaceVariant,
fontWeight: FontWeight.w400,
@ -122,7 +122,7 @@ class SplashScreen extends StatelessWidget {
// Loading Message
Text(
message ?? AppLocalizations.of(context)?.loading ?? 'Loading...',
message ?? AppLocalizations.of(context).loading,
style: theme.textTheme.bodyMedium?.copyWith(
color: colorScheme.onSurfaceVariant,
fontWeight: FontWeight.w500,
@ -137,7 +137,7 @@ class SplashScreen extends StatelessWidget {
Text(
'Version 1.0.0',
style: theme.textTheme.bodySmall?.copyWith(
color: colorScheme.onSurfaceVariant.withOpacity(0.6),
color: colorScheme.onSurfaceVariant.withValues(alpha: 0.6),
),
),
],

View file

@ -1,4 +1,5 @@
import 'package:flutter/material.dart';
import 'package:go_router/go_router.dart';
import 'package:hesabix_ui/l10n/app_localizations.dart';
import 'package:hesabix_ui/models/support_models.dart';
import 'package:hesabix_ui/services/support_service.dart';
@ -312,7 +313,7 @@ class _TicketDetailsDialogState extends State<TicketDetailsDialog> {
),
),
IconButton(
onPressed: () => Navigator.of(context).pop(),
onPressed: () => context.pop(),
icon: const Icon(Icons.close),
style: IconButton.styleFrom(
backgroundColor: Colors.grey[200],

View file

@ -0,0 +1,55 @@
import 'package:flutter/material.dart';
import 'package:go_router/go_router.dart';
import '../core/auth_store.dart';
class UrlTracker extends StatefulWidget {
final Widget child;
final AuthStore authStore;
const UrlTracker({
super.key,
required this.child,
required this.authStore,
});
@override
State<UrlTracker> createState() => _UrlTrackerState();
}
class _UrlTrackerState extends State<UrlTracker> {
String? _lastTrackedUrl;
@override
void initState() {
super.initState();
_trackCurrentUrl();
}
void _trackCurrentUrl() {
WidgetsBinding.instance.addPostFrameCallback((_) {
if (mounted) {
try {
final currentUrl = GoRouterState.of(context).uri.path;
if (currentUrl != _lastTrackedUrl &&
currentUrl.isNotEmpty &&
currentUrl != '/' &&
currentUrl != '/login' &&
(currentUrl.startsWith('/user/profile/') || currentUrl.startsWith('/business/'))) {
_lastTrackedUrl = currentUrl;
widget.authStore.saveLastUrl(currentUrl);
}
} catch (e) {
// اگر GoRouterState در دسترس نیست، URL را track نکن
// این ممکن است در splash screen یا loading state اتفاق بیفتد
}
}
});
}
@override
Widget build(BuildContext context) {
// هر بار که widget rebuild میشود، URL فعلی را track کن
_trackCurrentUrl();
return widget.child;
}
}

View file

@ -0,0 +1,101 @@
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';
class UserAccountMenuButton extends StatelessWidget {
final AuthStore authStore;
const UserAccountMenuButton({
super.key,
required this.authStore,
});
void _showUserMenu(BuildContext context) {
final t = AppLocalizations.of(context);
final cs = Theme.of(context).colorScheme;
showDialog(
context: context,
builder: (context) => AlertDialog(
title: Text(t.profile),
content: Column(
mainAxisSize: MainAxisSize.min,
children: [
ListTile(
leading: Icon(Icons.person, color: cs.onSurface),
title: Text(t.profile),
onTap: () {
context.pop();
// Navigate to profile page
},
),
ListTile(
leading: Icon(Icons.settings, color: cs.onSurface),
title: Text(t.settings),
onTap: () {
context.pop();
// Navigate to account settings
},
),
const Divider(),
ListTile(
leading: Icon(Icons.logout, color: cs.error),
title: Text(t.logout, style: TextStyle(color: cs.error)),
onTap: () {
context.pop();
// Trigger logout
_confirmLogout(context);
},
),
],
),
),
);
}
void _confirmLogout(BuildContext context) {
final t = AppLocalizations.of(context);
showDialog(
context: context,
builder: (context) => AlertDialog(
title: Text(t.logoutConfirmTitle),
content: Text(t.logoutConfirmMessage),
actions: [
TextButton(
onPressed: () => context.pop(),
child: Text(t.cancel),
),
FilledButton(
onPressed: () async {
context.pop();
await authStore.saveApiKey(null);
if (!context.mounted) return;
ScaffoldMessenger.of(context)
..hideCurrentSnackBar()
..showSnackBar(SnackBar(content: Text(t.logoutDone)));
},
child: Text(t.logout),
),
],
),
);
}
@override
Widget build(BuildContext context) {
final cs = Theme.of(context).colorScheme;
return IconButton(
icon: CircleAvatar(
radius: 16,
backgroundColor: cs.surfaceContainerHighest,
foregroundColor: cs.onSurface,
child: const Icon(Icons.person, size: 18),
),
onPressed: () => _showUserMenu(context),
tooltip: AppLocalizations.of(context).profile,
);
}
}

View file

@ -0,0 +1,46 @@
import 'package:flutter/material.dart';
import 'calendar_switcher.dart';
import 'language_switcher.dart';
import 'theme_mode_switcher.dart';
import 'logout_button.dart';
import '../core/auth_store.dart';
import '../core/locale_controller.dart';
import '../core/calendar_controller.dart';
import '../theme/theme_controller.dart';
class UserMenuButton extends StatelessWidget {
final AuthStore authStore;
final LocaleController? localeController;
final CalendarController? calendarController;
final ThemeController? themeController;
const UserMenuButton({
super.key,
required this.authStore,
this.localeController,
this.calendarController,
this.themeController,
});
@override
Widget build(BuildContext context) {
return Row(
mainAxisSize: MainAxisSize.min,
children: [
if (calendarController != null) ...[
CalendarSwitcher(controller: calendarController!),
const SizedBox(width: 8),
],
if (localeController != null) ...[
LanguageSwitcher(controller: localeController!),
const SizedBox(width: 8),
],
if (themeController != null) ...[
ThemeModeSwitcher(controller: themeController!),
const SizedBox(width: 8),
],
LogoutButton(authStore: authStore),
],
);
}
}