more progress in permissions
This commit is contained in:
parent
798dd63627
commit
44eef85039
443
hesabixAPI/adapters/api/v1/business_users.py
Normal file
443
hesabixAPI/adapters/api/v1/business_users.py
Normal 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="کاربر با موفقیت حذف شد"
|
||||||
|
)
|
||||||
|
|
@ -1,6 +1,7 @@
|
||||||
from typing import Any, List, Optional, Union, Generic, TypeVar
|
from typing import Any, List, Optional, Union, Generic, TypeVar
|
||||||
from pydantic import BaseModel, EmailStr, Field
|
from pydantic import BaseModel, EmailStr, Field
|
||||||
from enum import Enum
|
from enum import Enum
|
||||||
|
from datetime import datetime
|
||||||
|
|
||||||
T = TypeVar('T')
|
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
|
||||||
|
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -7,6 +7,7 @@ from .captcha import Captcha # noqa: F401
|
||||||
from .password_reset import PasswordReset # noqa: F401
|
from .password_reset import PasswordReset # noqa: F401
|
||||||
from .business import Business # noqa: F401
|
from .business import Business # noqa: F401
|
||||||
from .business_permission import BusinessPermission # noqa: F401
|
from .business_permission import BusinessPermission # noqa: F401
|
||||||
|
# Business user models removed - using business_permissions instead
|
||||||
|
|
||||||
# Import support models
|
# Import support models
|
||||||
from .support import * # noqa: F401, F403
|
from .support import * # noqa: F401, F403
|
||||||
|
|
|
||||||
|
|
@ -4,7 +4,7 @@ from datetime import datetime
|
||||||
from enum import Enum
|
from enum import Enum
|
||||||
|
|
||||||
from sqlalchemy import String, DateTime, Integer, ForeignKey, Enum as SQLEnum, Text
|
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
|
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)
|
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)
|
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")
|
||||||
|
|
|
||||||
|
|
@ -28,5 +28,8 @@ class User(Base):
|
||||||
|
|
||||||
# Support relationships
|
# Support relationships
|
||||||
tickets = relationship("Ticket", foreign_keys="Ticket.user_id", back_populates="user")
|
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")
|
||||||
|
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -121,14 +121,21 @@ class AuthContext:
|
||||||
"""بررسی دسترسی به پنل اپراتور پشتیبانی"""
|
"""بررسی دسترسی به پنل اپراتور پشتیبانی"""
|
||||||
return self.has_app_permission("support_operator")
|
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
|
return False
|
||||||
|
|
||||||
from adapters.db.models.business import Business
|
from adapters.db.models.business import Business
|
||||||
business = self.db.get(Business, self.business_id)
|
business = self.db.get(Business, target_business_id)
|
||||||
return business and business.owner_id == self.user.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:
|
def has_business_permission(self, section: str, action: str) -> bool:
|
||||||
|
|
@ -188,9 +195,25 @@ class AuthContext:
|
||||||
"""بررسی دسترسی صادرات در بخش"""
|
"""بررسی دسترسی صادرات در بخش"""
|
||||||
return self.has_business_permission(section, "export")
|
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:
|
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:
|
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 دسترسی به همه کسب و کارها دارد
|
# SuperAdmin دسترسی به همه کسب و کارها دارد
|
||||||
if self.is_superadmin():
|
if self.is_superadmin():
|
||||||
|
logger.info(f"User {self.user.id} is superadmin, granting access to business {business_id}")
|
||||||
return True
|
return True
|
||||||
|
|
||||||
# اگر مالک کسب و کار است، دسترسی دارد
|
# اگر مالک کسب و کار است، دسترسی دارد
|
||||||
if self.is_business_owner() and business_id == self.business_id:
|
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 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:
|
def to_dict(self) -> dict:
|
||||||
"""تبدیل به dictionary برای استفاده در API"""
|
"""تبدیل به dictionary برای استفاده در API"""
|
||||||
|
|
@ -252,10 +284,15 @@ def get_current_user(
|
||||||
db: Session = Depends(get_db)
|
db: Session = Depends(get_db)
|
||||||
) -> AuthContext:
|
) -> AuthContext:
|
||||||
"""دریافت اطلاعات کامل کاربر کنونی و تنظیمات از درخواست"""
|
"""دریافت اطلاعات کامل کاربر کنونی و تنظیمات از درخواست"""
|
||||||
|
import logging
|
||||||
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
# Get authorization from request headers
|
# Get authorization from request headers
|
||||||
auth_header = request.headers.get("Authorization")
|
auth_header = request.headers.get("Authorization")
|
||||||
|
logger.info(f"Auth header: {auth_header}")
|
||||||
|
|
||||||
if not auth_header or not auth_header.startswith("ApiKey "):
|
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)
|
raise ApiError("UNAUTHORIZED", "Missing or invalid API key", http_status=401)
|
||||||
|
|
||||||
api_key = auth_header[len("ApiKey ") :].strip()
|
api_key = auth_header[len("ApiKey ") :].strip()
|
||||||
|
|
|
||||||
|
|
@ -74,6 +74,9 @@ def require_business_access(business_id_param: str = "business_id"):
|
||||||
def decorator(func: Callable) -> Callable:
|
def decorator(func: Callable) -> Callable:
|
||||||
@wraps(func)
|
@wraps(func)
|
||||||
def wrapper(*args, **kwargs) -> Any:
|
def wrapper(*args, **kwargs) -> Any:
|
||||||
|
import logging
|
||||||
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
# Find request in args or kwargs
|
# Find request in args or kwargs
|
||||||
request = None
|
request = None
|
||||||
for arg in args:
|
for arg in args:
|
||||||
|
|
@ -85,6 +88,7 @@ def require_business_access(business_id_param: str = "business_id"):
|
||||||
request = kwargs['request']
|
request = kwargs['request']
|
||||||
|
|
||||||
if not request:
|
if not request:
|
||||||
|
logger.error("Request not found in function arguments")
|
||||||
raise ApiError("INTERNAL_ERROR", "Request not found", http_status=500)
|
raise ApiError("INTERNAL_ERROR", "Request not found", http_status=500)
|
||||||
|
|
||||||
# Get database session
|
# Get database session
|
||||||
|
|
@ -92,8 +96,17 @@ def require_business_access(business_id_param: str = "business_id"):
|
||||||
db = next(get_db())
|
db = next(get_db())
|
||||||
ctx = get_current_user(request, db)
|
ctx = get_current_user(request, db)
|
||||||
business_id = kwargs.get(business_id_param)
|
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):
|
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)
|
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 func(*args, **kwargs)
|
||||||
return wrapper
|
return wrapper
|
||||||
return decorator
|
return decorator
|
||||||
|
|
|
||||||
|
|
@ -7,8 +7,16 @@ from fastapi import HTTPException, status, Request
|
||||||
from .calendar import CalendarConverter, CalendarType
|
from .calendar import CalendarConverter, CalendarType
|
||||||
|
|
||||||
|
|
||||||
def success_response(data: Any, request: Request = None) -> dict[str, Any]:
|
def success_response(data: Any, request: Request = None, message: str = None) -> dict[str, Any]:
|
||||||
response = {"success": True, "data": data}
|
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
|
# Add calendar type information if request is available
|
||||||
if request and hasattr(request.state, 'calendar_type'):
|
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):
|
if isinstance(data, dict):
|
||||||
formatted_data = {}
|
formatted_data = {}
|
||||||
for key, value in data.items():
|
for key, value in data.items():
|
||||||
if isinstance(value, datetime):
|
if value is None:
|
||||||
formatted_data[key] = CalendarConverter.format_datetime(value, calendar_type)
|
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
|
# Convert raw date to the same calendar type as the formatted date
|
||||||
if calendar_type == "jalali":
|
if calendar_type == "jalali":
|
||||||
formatted_data[f"{key}_raw"] = CalendarConverter.to_jalali(value)["formatted"]
|
formatted_data[f"{key}_raw"] = CalendarConverter.to_jalali(value)["formatted"]
|
||||||
|
|
|
||||||
|
|
@ -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.users import router as users_router
|
||||||
from adapters.api.v1.businesses import router as businesses_router
|
from adapters.api.v1.businesses import router as businesses_router
|
||||||
from adapters.api.v1.business_dashboard import router as business_dashboard_router
|
from adapters.api.v1.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.tickets import router as support_tickets_router
|
||||||
from adapters.api.v1.support.operator import router as support_operator_router
|
from adapters.api.v1.support.operator import router as support_operator_router
|
||||||
from adapters.api.v1.support.categories import router as support_categories_router
|
from adapters.api.v1.support.categories import router as support_categories_router
|
||||||
|
|
@ -271,6 +272,7 @@ def create_app() -> FastAPI:
|
||||||
application.include_router(users_router, prefix=settings.api_v1_prefix)
|
application.include_router(users_router, prefix=settings.api_v1_prefix)
|
||||||
application.include_router(businesses_router, prefix=settings.api_v1_prefix)
|
application.include_router(businesses_router, prefix=settings.api_v1_prefix)
|
||||||
application.include_router(business_dashboard_router, prefix=settings.api_v1_prefix)
|
application.include_router(business_dashboard_router, prefix=settings.api_v1_prefix)
|
||||||
|
application.include_router(business_users_router, prefix=settings.api_v1_prefix)
|
||||||
|
|
||||||
# Support endpoints
|
# Support endpoints
|
||||||
application.include_router(support_tickets_router, prefix=f"{settings.api_v1_prefix}/support")
|
application.include_router(support_tickets_router, prefix=f"{settings.api_v1_prefix}/support")
|
||||||
|
|
@ -292,7 +294,7 @@ def create_app() -> FastAPI:
|
||||||
)
|
)
|
||||||
def read_root() -> dict[str, str]:
|
def read_root() -> dict[str, str]:
|
||||||
return {"service": settings.app_name, "version": settings.app_version}
|
return {"service": settings.app_name, "version": settings.app_version}
|
||||||
|
|
||||||
# اضافه کردن security schemes
|
# اضافه کردن security schemes
|
||||||
from fastapi.openapi.utils import get_openapi
|
from fastapi.openapi.utils import get_openapi
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -4,6 +4,8 @@ adapters/__init__.py
|
||||||
adapters/api/__init__.py
|
adapters/api/__init__.py
|
||||||
adapters/api/v1/__init__.py
|
adapters/api/v1/__init__.py
|
||||||
adapters/api/v1/auth.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/businesses.py
|
||||||
adapters/api/v1/health.py
|
adapters/api/v1/health.py
|
||||||
adapters/api/v1/schemas.py
|
adapters/api/v1/schemas.py
|
||||||
|
|
@ -68,6 +70,7 @@ app/core/settings.py
|
||||||
app/core/smart_normalizer.py
|
app/core/smart_normalizer.py
|
||||||
app/services/api_key_service.py
|
app/services/api_key_service.py
|
||||||
app/services/auth_service.py
|
app/services/auth_service.py
|
||||||
|
app/services/business_dashboard_service.py
|
||||||
app/services/business_service.py
|
app/services/business_service.py
|
||||||
app/services/captcha_service.py
|
app/services/captcha_service.py
|
||||||
app/services/email_service.py
|
app/services/email_service.py
|
||||||
|
|
|
||||||
|
|
@ -10,6 +10,7 @@ class AuthStore with ChangeNotifier {
|
||||||
static const _kDeviceId = 'device_id';
|
static const _kDeviceId = 'device_id';
|
||||||
static const _kAppPermissions = 'app_permissions';
|
static const _kAppPermissions = 'app_permissions';
|
||||||
static const _kIsSuperAdmin = 'is_superadmin';
|
static const _kIsSuperAdmin = 'is_superadmin';
|
||||||
|
static const _kLastUrl = 'last_url';
|
||||||
|
|
||||||
final FlutterSecureStorage _secure = const FlutterSecureStorage();
|
final FlutterSecureStorage _secure = const FlutterSecureStorage();
|
||||||
String? _apiKey;
|
String? _apiKey;
|
||||||
|
|
@ -21,6 +22,8 @@ class AuthStore with ChangeNotifier {
|
||||||
String get deviceId => _deviceId ?? '';
|
String get deviceId => _deviceId ?? '';
|
||||||
Map<String, dynamic>? get appPermissions => _appPermissions;
|
Map<String, dynamic>? get appPermissions => _appPermissions;
|
||||||
bool get isSuperAdmin => _isSuperAdmin;
|
bool get isSuperAdmin => _isSuperAdmin;
|
||||||
|
int? _currentUserId;
|
||||||
|
int? get currentUserId => _currentUserId;
|
||||||
|
|
||||||
Future<void> load() async {
|
Future<void> load() async {
|
||||||
final prefs = await SharedPreferences.getInstance();
|
final prefs = await SharedPreferences.getInstance();
|
||||||
|
|
@ -98,8 +101,9 @@ class AuthStore with ChangeNotifier {
|
||||||
} catch (_) {}
|
} catch (_) {}
|
||||||
await prefs.remove(_kApiKey);
|
await prefs.remove(_kApiKey);
|
||||||
}
|
}
|
||||||
// پاک کردن دسترسیها هنگام خروج
|
// پاک کردن دسترسیها و آخرین URL هنگام خروج
|
||||||
await _clearAppPermissions();
|
await _clearAppPermissions();
|
||||||
|
await clearLastUrl();
|
||||||
} else {
|
} else {
|
||||||
if (kIsWeb) {
|
if (kIsWeb) {
|
||||||
await prefs.setString(_kApiKey, key);
|
await prefs.setString(_kApiKey, key);
|
||||||
|
|
@ -113,10 +117,13 @@ class AuthStore with ChangeNotifier {
|
||||||
notifyListeners();
|
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();
|
final prefs = await SharedPreferences.getInstance();
|
||||||
_appPermissions = permissions;
|
_appPermissions = permissions;
|
||||||
_isSuperAdmin = isSuperAdmin;
|
_isSuperAdmin = isSuperAdmin;
|
||||||
|
if (userId != null) {
|
||||||
|
_currentUserId = userId;
|
||||||
|
}
|
||||||
|
|
||||||
if (permissions == null) {
|
if (permissions == null) {
|
||||||
await _clearAppPermissions();
|
await _clearAppPermissions();
|
||||||
|
|
@ -174,10 +181,16 @@ class AuthStore with ChangeNotifier {
|
||||||
if (user != null) {
|
if (user != null) {
|
||||||
final appPermissions = user['app_permissions'] as Map<String, dynamic>?;
|
final appPermissions = user['app_permissions'] as Map<String, dynamic>?;
|
||||||
final isSuperAdmin = appPermissions?['superadmin'] == true;
|
final isSuperAdmin = appPermissions?['superadmin'] == true;
|
||||||
|
final userId = user['id'] as int?;
|
||||||
|
|
||||||
if (appPermissions != null) {
|
if (appPermissions != null) {
|
||||||
await saveAppPermissions(appPermissions, isSuperAdmin);
|
await saveAppPermissions(appPermissions, isSuperAdmin);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (userId != null) {
|
||||||
|
_currentUserId = userId;
|
||||||
|
notifyListeners();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -195,6 +208,32 @@ class AuthStore with ChangeNotifier {
|
||||||
}
|
}
|
||||||
|
|
||||||
bool get canAccessSupportOperator => hasAppPermission('support_operator');
|
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 (_) {}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -483,6 +483,77 @@
|
||||||
"backToProfile": "Back to Profile",
|
"backToProfile": "Back to Profile",
|
||||||
"noBusinessesFound": "No businesses found",
|
"noBusinessesFound": "No businesses found",
|
||||||
"createFirstBusiness": "Create your first business",
|
"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"
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -482,6 +482,77 @@
|
||||||
"backToProfile": "بازگشت به پروفایل",
|
"backToProfile": "بازگشت به پروفایل",
|
||||||
"noBusinessesFound": "هیچ کسب و کاری یافت نشد",
|
"noBusinessesFound": "هیچ کسب و کاری یافت نشد",
|
||||||
"createFirstBusiness": "اولین کسب و کار خود را ایجاد کنید",
|
"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": "کاربر موجود"
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -2713,6 +2713,402 @@ abstract class AppLocalizations {
|
||||||
/// In en, this message translates to:
|
/// In en, this message translates to:
|
||||||
/// **'Access denied'**
|
/// **'Access denied'**
|
||||||
String get accessDenied;
|
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
|
class _AppLocalizationsDelegate
|
||||||
|
|
|
||||||
|
|
@ -1356,4 +1356,205 @@ class AppLocalizationsEn extends AppLocalizations {
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String get accessDenied => 'Access denied';
|
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';
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1346,4 +1346,206 @@ class AppLocalizationsFa extends AppLocalizations {
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String get accessDenied => 'دسترسی غیرمجاز';
|
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 => 'کاربر موجود';
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -20,6 +20,7 @@ import 'pages/admin/system_logs_page.dart';
|
||||||
import 'pages/admin/email_settings_page.dart';
|
import 'pages/admin/email_settings_page.dart';
|
||||||
import 'pages/business/business_shell.dart';
|
import 'pages/business/business_shell.dart';
|
||||||
import 'pages/business/dashboard/business_dashboard_page.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 'package:hesabix_ui/l10n/app_localizations.dart';
|
||||||
import 'core/locale_controller.dart';
|
import 'core/locale_controller.dart';
|
||||||
import 'core/calendar_controller.dart';
|
import 'core/calendar_controller.dart';
|
||||||
|
|
@ -29,6 +30,7 @@ import 'theme/app_theme.dart';
|
||||||
import 'core/auth_store.dart';
|
import 'core/auth_store.dart';
|
||||||
import 'core/permission_guard.dart';
|
import 'core/permission_guard.dart';
|
||||||
import 'widgets/simple_splash_screen.dart';
|
import 'widgets/simple_splash_screen.dart';
|
||||||
|
import 'widgets/url_tracker.dart';
|
||||||
|
|
||||||
void main() {
|
void main() {
|
||||||
// Use path-based routing instead of hash routing
|
// Use path-based routing instead of hash routing
|
||||||
|
|
@ -98,15 +100,34 @@ class _MyAppState extends State<MyApp> {
|
||||||
ApiClient.bindCalendarController(_calendarController!);
|
ApiClient.bindCalendarController(_calendarController!);
|
||||||
ApiClient.bindAuthStore(_authStore!);
|
ApiClient.bindAuthStore(_authStore!);
|
||||||
|
|
||||||
// اطمینان از حداقل 4 ثانیه نمایش splash screen
|
// اطمینان از حداقل 1 ثانیه نمایش splash screen
|
||||||
final elapsed = DateTime.now().difference(_loadStartTime!);
|
final elapsed = DateTime.now().difference(_loadStartTime!);
|
||||||
const minimumDuration = Duration(seconds: 4);
|
const minimumDuration = Duration(seconds: 1);
|
||||||
if (elapsed < minimumDuration) {
|
if (elapsed < minimumDuration) {
|
||||||
await Future.delayed(minimumDuration - elapsed);
|
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
|
// اتمام loading
|
||||||
if (mounted) {
|
if (mounted) {
|
||||||
|
print('🔍 LOADING DEBUG: Finishing loading, setting _isLoading to false');
|
||||||
setState(() {
|
setState(() {
|
||||||
_isLoading = false;
|
_isLoading = false;
|
||||||
});
|
});
|
||||||
|
|
@ -116,12 +137,16 @@ class _MyAppState extends State<MyApp> {
|
||||||
// Root of application with GoRouter
|
// Root of application with GoRouter
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
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 نمایش بده
|
// اگر هنوز loading است، splash screen نمایش بده
|
||||||
if (_isLoading ||
|
if (_isLoading ||
|
||||||
_controller == null ||
|
_controller == null ||
|
||||||
_calendarController == null ||
|
_calendarController == null ||
|
||||||
_themeController == null ||
|
_themeController == null ||
|
||||||
_authStore == null) {
|
_authStore == null) {
|
||||||
|
print('🔍 BUILD DEBUG: Still loading, showing splash screen');
|
||||||
final loadingRouter = GoRouter(
|
final loadingRouter = GoRouter(
|
||||||
redirect: (context, state) {
|
redirect: (context, state) {
|
||||||
// در حین loading، هیچ redirect نکن - URL را حفظ کن
|
// در حین loading، هیچ redirect نکن - URL را حفظ کن
|
||||||
|
|
@ -165,11 +190,13 @@ class _MyAppState extends State<MyApp> {
|
||||||
return SimpleSplashScreen(
|
return SimpleSplashScreen(
|
||||||
message: loadingMessage,
|
message: loadingMessage,
|
||||||
showLogo: true,
|
showLogo: true,
|
||||||
displayDuration: const Duration(seconds: 4),
|
displayDuration: const Duration(seconds: 1),
|
||||||
locale: _controller?.locale,
|
locale: _controller?.locale,
|
||||||
|
authStore: _authStore,
|
||||||
onComplete: () {
|
onComplete: () {
|
||||||
// این callback زمانی فراخوانی میشود که splash screen تمام شود
|
// این callback زمانی فراخوانی میشود که splash screen تمام شود
|
||||||
// اما ما از splash controller استفاده میکنیم
|
// اما ما از splash controller استفاده میکنیم
|
||||||
|
print('🔍 SPLASH DEBUG: Splash screen completed');
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
|
|
@ -194,41 +221,69 @@ class _MyAppState extends State<MyApp> {
|
||||||
final controller = _controller!;
|
final controller = _controller!;
|
||||||
final themeController = _themeController!;
|
final themeController = _themeController!;
|
||||||
|
|
||||||
|
print('🔍 BUILD DEBUG: All controllers loaded, creating main router');
|
||||||
final router = GoRouter(
|
final router = GoRouter(
|
||||||
initialLocation: '/',
|
initialLocation: '/',
|
||||||
redirect: (context, state) {
|
redirect: (context, state) async {
|
||||||
final currentPath = state.uri.path;
|
final currentPath = state.uri.path;
|
||||||
|
final fullUri = state.uri.toString();
|
||||||
|
print('🔍 REDIRECT DEBUG: Current path: $currentPath');
|
||||||
|
print('🔍 REDIRECT DEBUG: Full URI: $fullUri');
|
||||||
|
|
||||||
// اگر authStore هنوز load نشده، منتظر بمان
|
// اگر authStore هنوز load نشده، منتظر بمان
|
||||||
if (_authStore == null) {
|
if (_authStore == null) {
|
||||||
|
print('🔍 REDIRECT DEBUG: AuthStore is null, staying on current path');
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
final hasKey = _authStore!.apiKey != null && _authStore!.apiKey!.isNotEmpty;
|
final hasKey = _authStore!.apiKey != null && _authStore!.apiKey!.isNotEmpty;
|
||||||
|
print('🔍 REDIRECT DEBUG: Has API key: $hasKey');
|
||||||
|
|
||||||
// اگر API key ندارد
|
// اگر API key ندارد
|
||||||
if (!hasKey) {
|
if (!hasKey) {
|
||||||
|
print('🔍 REDIRECT DEBUG: No API key');
|
||||||
// اگر در login نیست، به login برود
|
// اگر در login نیست، به login برود
|
||||||
if (currentPath != '/login') {
|
if (currentPath != '/login') {
|
||||||
|
print('🔍 REDIRECT DEBUG: Redirecting to login from $currentPath');
|
||||||
return '/login';
|
return '/login';
|
||||||
}
|
}
|
||||||
// اگر در login است، بماند
|
// اگر در login است، بماند
|
||||||
|
print('🔍 REDIRECT DEBUG: Already on login, staying');
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
// اگر API key دارد
|
// اگر API key دارد
|
||||||
|
print('🔍 REDIRECT DEBUG: Has API key, checking current path');
|
||||||
|
|
||||||
// اگر در login است، به dashboard برود
|
// اگر در login است، به dashboard برود
|
||||||
if (currentPath == '/login') {
|
if (currentPath == '/login') {
|
||||||
|
print('🔍 REDIRECT DEBUG: On login page, redirecting to dashboard');
|
||||||
return '/user/profile/dashboard';
|
return '/user/profile/dashboard';
|
||||||
}
|
}
|
||||||
|
|
||||||
// اگر در root است، به dashboard برود
|
// اگر در root است، آخرین URL را بررسی کن
|
||||||
if (currentPath == '/') {
|
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';
|
return '/user/profile/dashboard';
|
||||||
}
|
}
|
||||||
|
|
||||||
// برای سایر صفحات (شامل صفحات profile)، redirect نکن (بماند)
|
// برای سایر صفحات (شامل صفحات profile و business)، redirect نکن (بماند)
|
||||||
// این مهم است: اگر کاربر در صفحات profile است، بماند
|
// این مهم است: اگر کاربر در صفحات profile یا business است، بماند
|
||||||
|
print('🔍 REDIRECT DEBUG: On other page ($currentPath), staying on current path');
|
||||||
return null;
|
return null;
|
||||||
},
|
},
|
||||||
routes: <RouteBase>[
|
routes: <RouteBase>[
|
||||||
|
|
@ -373,7 +428,9 @@ class _MyAppState extends State<MyApp> {
|
||||||
return BusinessShell(
|
return BusinessShell(
|
||||||
businessId: businessId,
|
businessId: businessId,
|
||||||
authStore: _authStore!,
|
authStore: _authStore!,
|
||||||
|
localeController: controller,
|
||||||
calendarController: _calendarController!,
|
calendarController: _calendarController!,
|
||||||
|
themeController: themeController,
|
||||||
child: const SizedBox.shrink(), // Will be replaced by child routes
|
child: const SizedBox.shrink(), // Will be replaced by child routes
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
|
|
@ -386,11 +443,32 @@ class _MyAppState extends State<MyApp> {
|
||||||
return BusinessShell(
|
return BusinessShell(
|
||||||
businessId: businessId,
|
businessId: businessId,
|
||||||
authStore: _authStore!,
|
authStore: _authStore!,
|
||||||
|
localeController: controller,
|
||||||
calendarController: _calendarController!,
|
calendarController: _calendarController!,
|
||||||
|
themeController: themeController,
|
||||||
child: BusinessDashboardPage(businessId: businessId),
|
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.)
|
// TODO: Add other business routes (sales, accounting, etc.)
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
|
|
@ -400,28 +478,31 @@ class _MyAppState extends State<MyApp> {
|
||||||
return AnimatedBuilder(
|
return AnimatedBuilder(
|
||||||
animation: Listenable.merge([controller, themeController]),
|
animation: Listenable.merge([controller, themeController]),
|
||||||
builder: (context, _) {
|
builder: (context, _) {
|
||||||
return MaterialApp.router(
|
return UrlTracker(
|
||||||
title: 'Hesabix',
|
authStore: _authStore!,
|
||||||
theme: AppTheme.build(
|
child: MaterialApp.router(
|
||||||
isDark: false,
|
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,
|
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,
|
|
||||||
],
|
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
|
|
||||||
306
hesabixUI/hesabix_ui/lib/models/business_user_model.dart
Normal file
306
hesabixUI/hesabix_ui/lib/models/business_user_model.dart
Normal 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,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
0
hesabixUI/hesabix_ui/lib/models/person_model.dart
Normal file
0
hesabixUI/hesabix_ui/lib/models/person_model.dart
Normal file
|
|
@ -341,11 +341,11 @@ class _UserManagementPageState extends State<UserManagementPage> {
|
||||||
content: const Text('User creation form would go here'),
|
content: const Text('User creation form would go here'),
|
||||||
actions: [
|
actions: [
|
||||||
TextButton(
|
TextButton(
|
||||||
onPressed: () => Navigator.of(context).pop(),
|
onPressed: () => context.pop(),
|
||||||
child: Text(AppLocalizations.of(context).cancel),
|
child: Text(AppLocalizations.of(context).cancel),
|
||||||
),
|
),
|
||||||
ElevatedButton(
|
ElevatedButton(
|
||||||
onPressed: () => Navigator.of(context).pop(),
|
onPressed: () => context.pop(),
|
||||||
child: Text(AppLocalizations.of(context).save),
|
child: Text(AppLocalizations.of(context).save),
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
|
|
@ -361,11 +361,11 @@ class _UserManagementPageState extends State<UserManagementPage> {
|
||||||
content: const Text('User edit form would go here'),
|
content: const Text('User edit form would go here'),
|
||||||
actions: [
|
actions: [
|
||||||
TextButton(
|
TextButton(
|
||||||
onPressed: () => Navigator.of(context).pop(),
|
onPressed: () => context.pop(),
|
||||||
child: Text(AppLocalizations.of(context).cancel),
|
child: Text(AppLocalizations.of(context).cancel),
|
||||||
),
|
),
|
||||||
ElevatedButton(
|
ElevatedButton(
|
||||||
onPressed: () => Navigator.of(context).pop(),
|
onPressed: () => context.pop(),
|
||||||
child: Text(AppLocalizations.of(context).save),
|
child: Text(AppLocalizations.of(context).save),
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
|
|
@ -381,11 +381,11 @@ class _UserManagementPageState extends State<UserManagementPage> {
|
||||||
content: const Text('Permissions management would go here'),
|
content: const Text('Permissions management would go here'),
|
||||||
actions: [
|
actions: [
|
||||||
TextButton(
|
TextButton(
|
||||||
onPressed: () => Navigator.of(context).pop(),
|
onPressed: () => context.pop(),
|
||||||
child: Text(AppLocalizations.of(context).cancel),
|
child: Text(AppLocalizations.of(context).cancel),
|
||||||
),
|
),
|
||||||
ElevatedButton(
|
ElevatedButton(
|
||||||
onPressed: () => Navigator.of(context).pop(),
|
onPressed: () => context.pop(),
|
||||||
child: Text(AppLocalizations.of(context).save),
|
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']}?'),
|
content: Text('Are you sure you want to delete ${user['name']}?'),
|
||||||
actions: [
|
actions: [
|
||||||
TextButton(
|
TextButton(
|
||||||
onPressed: () => Navigator.of(context).pop(),
|
onPressed: () => context.pop(),
|
||||||
child: Text(AppLocalizations.of(context).cancel),
|
child: Text(AppLocalizations.of(context).cancel),
|
||||||
),
|
),
|
||||||
ElevatedButton(
|
ElevatedButton(
|
||||||
onPressed: () {
|
onPressed: () {
|
||||||
Navigator.of(context).pop();
|
context.pop();
|
||||||
// Delete user logic here
|
// Delete user logic here
|
||||||
},
|
},
|
||||||
style: ElevatedButton.styleFrom(backgroundColor: Colors.red),
|
style: ElevatedButton.styleFrom(backgroundColor: Colors.red),
|
||||||
|
|
|
||||||
|
|
@ -1,21 +1,29 @@
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:go_router/go_router.dart';
|
import 'package:go_router/go_router.dart';
|
||||||
import 'package:hesabix_ui/l10n/app_localizations.dart';
|
|
||||||
import '../../core/auth_store.dart';
|
import '../../core/auth_store.dart';
|
||||||
|
import '../../core/locale_controller.dart';
|
||||||
import '../../core/calendar_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 {
|
class BusinessShell extends StatefulWidget {
|
||||||
final int businessId;
|
final int businessId;
|
||||||
final AuthStore authStore;
|
|
||||||
final CalendarController calendarController;
|
|
||||||
final Widget child;
|
final Widget child;
|
||||||
|
final AuthStore authStore;
|
||||||
|
final LocaleController? localeController;
|
||||||
|
final CalendarController? calendarController;
|
||||||
|
final ThemeController? themeController;
|
||||||
|
|
||||||
const BusinessShell({
|
const BusinessShell({
|
||||||
super.key,
|
super.key,
|
||||||
required this.businessId,
|
required this.businessId,
|
||||||
required this.authStore,
|
|
||||||
required this.calendarController,
|
|
||||||
required this.child,
|
required this.child,
|
||||||
|
required this.authStore,
|
||||||
|
this.localeController,
|
||||||
|
this.calendarController,
|
||||||
|
this.themeController,
|
||||||
});
|
});
|
||||||
|
|
||||||
@override
|
@override
|
||||||
|
|
@ -23,6 +31,9 @@ class BusinessShell extends StatefulWidget {
|
||||||
}
|
}
|
||||||
|
|
||||||
class _BusinessShellState extends State<BusinessShell> {
|
class _BusinessShellState extends State<BusinessShell> {
|
||||||
|
int _hoverIndex = -1;
|
||||||
|
bool _isBasicToolsExpanded = false;
|
||||||
|
bool _isPeopleExpanded = false;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
void initState() {
|
void initState() {
|
||||||
|
|
@ -41,40 +52,206 @@ class _BusinessShellState extends State<BusinessShell> {
|
||||||
final bool useRail = width >= 700;
|
final bool useRail = width >= 700;
|
||||||
final bool railExtended = width >= 1100;
|
final bool railExtended = width >= 1100;
|
||||||
final ColorScheme scheme = Theme.of(context).colorScheme;
|
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 bool isDark = Theme.of(context).brightness == Brightness.dark;
|
||||||
final String logoAsset = isDark
|
final String logoAsset = isDark
|
||||||
? 'assets/images/logo-light.png'
|
? 'assets/images/logo-light.png'
|
||||||
: 'assets/images/logo-light.png';
|
: 'assets/images/logo-light.png';
|
||||||
|
|
||||||
final t = AppLocalizations.of(context);
|
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'),
|
final menuItems = <_MenuItem>[
|
||||||
_Dest(t.accounting, Icons.account_balance, Icons.account_balance, '/business/${widget.businessId}/accounting'),
|
_MenuItem(
|
||||||
_Dest(t.inventory, Icons.inventory, Icons.inventory, '/business/${widget.businessId}/inventory'),
|
label: t.businessDashboard,
|
||||||
_Dest(t.reports, Icons.assessment, Icons.assessment, '/business/${widget.businessId}/reports'),
|
icon: Icons.dashboard_outlined,
|
||||||
_Dest(t.members, Icons.people, Icons.people, '/business/${widget.businessId}/members'),
|
selectedIcon: Icons.dashboard,
|
||||||
_Dest(t.settings, Icons.settings, Icons.settings, '/business/${widget.businessId}/settings'),
|
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;
|
int selectedIndex = 0;
|
||||||
for (int i = 0; i < destinations.length; i++) {
|
for (int i = 0; i < menuItems.length; i++) {
|
||||||
if (location.startsWith(destinations[i].path)) {
|
final item = menuItems[i];
|
||||||
|
if (item.type == _MenuItemType.separator) continue; // نادیده گرفتن آیتم جداکننده
|
||||||
|
|
||||||
|
if (item.type == _MenuItemType.simple && item.path != null && location.startsWith(item.path!)) {
|
||||||
selectedIndex = i;
|
selectedIndex = i;
|
||||||
break;
|
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 {
|
Future<void> onSelect(int index) async {
|
||||||
final path = destinations[index].path;
|
final item = menuItems[index];
|
||||||
if (GoRouterState.of(context).uri.toString() != path) {
|
if (item.type == _MenuItemType.separator) return; // آیتم جداکننده قابل کلیک نیست
|
||||||
context.go(path);
|
|
||||||
|
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 {
|
Future<void> onSelectChild(int parentIndex, int childIndex) async {
|
||||||
context.go('/user/profile/businesses');
|
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
|
// Brand top bar with contrast color
|
||||||
|
|
@ -88,86 +265,431 @@ class _BusinessShellState extends State<BusinessShell> {
|
||||||
final appBar = AppBar(
|
final appBar = AppBar(
|
||||||
backgroundColor: appBarBg,
|
backgroundColor: appBarBg,
|
||||||
foregroundColor: appBarFg,
|
foregroundColor: appBarFg,
|
||||||
elevation: 0,
|
automaticallyImplyLeading: !useRail,
|
||||||
|
titleSpacing: 0,
|
||||||
title: Row(
|
title: Row(
|
||||||
children: [
|
children: [
|
||||||
Image.asset(
|
|
||||||
logoAsset,
|
|
||||||
height: 32,
|
|
||||||
width: 32,
|
|
||||||
errorBuilder: (context, error, stackTrace) => Icon(
|
|
||||||
Icons.business,
|
|
||||||
color: appBarFg,
|
|
||||||
size: 32,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
const SizedBox(width: 12),
|
const SizedBox(width: 12),
|
||||||
Text(
|
Image.asset(logoAsset, height: 28),
|
||||||
'Hesabix',
|
const SizedBox(width: 12),
|
||||||
style: TextStyle(
|
Text(t.appTitle, style: TextStyle(color: appBarFg, fontWeight: FontWeight.w700)),
|
||||||
color: appBarFg,
|
|
||||||
fontWeight: FontWeight.bold,
|
|
||||||
fontSize: 20,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
|
leading: useRail
|
||||||
|
? null
|
||||||
|
: Builder(
|
||||||
|
builder: (ctx) => IconButton(
|
||||||
|
icon: Icon(Icons.menu, color: appBarFg),
|
||||||
|
onPressed: () => Scaffold.of(ctx).openDrawer(),
|
||||||
|
tooltip: t.menu,
|
||||||
|
),
|
||||||
|
),
|
||||||
actions: [
|
actions: [
|
||||||
IconButton(
|
SettingsMenuButton(
|
||||||
icon: Icon(Icons.arrow_back, color: appBarFg),
|
localeController: widget.localeController,
|
||||||
onPressed: onBackToProfile,
|
calendarController: widget.calendarController,
|
||||||
tooltip: t.backToProfile,
|
themeController: widget.themeController,
|
||||||
),
|
),
|
||||||
const SizedBox(width: 8),
|
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) {
|
if (useRail) {
|
||||||
return Scaffold(
|
return Scaffold(
|
||||||
appBar: appBar,
|
appBar: appBar,
|
||||||
body: Row(
|
body: Row(
|
||||||
children: [
|
children: [
|
||||||
NavigationRail(
|
Container(
|
||||||
selectedIndex: selectedIndex,
|
width: railExtended ? 240 : 88,
|
||||||
onDestinationSelected: onSelect,
|
height: double.infinity,
|
||||||
labelType: railExtended ? NavigationRailLabelType.selected : NavigationRailLabelType.all,
|
color: sideBg,
|
||||||
extended: railExtended,
|
child: ListView.builder(
|
||||||
destinations: destinations.map((dest) => NavigationRailDestination(
|
padding: EdgeInsets.zero,
|
||||||
icon: Icon(dest.icon),
|
itemCount: getTotalMenuItemsCount(),
|
||||||
selectedIcon: Icon(dest.selectedIcon),
|
itemBuilder: (ctx, index) {
|
||||||
label: Text(dest.label),
|
final menuIndex = getMenuIndexFromTotalIndex(index);
|
||||||
)).toList(),
|
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),
|
const VerticalDivider(thickness: 1, width: 1),
|
||||||
Expanded(
|
Expanded(child: content),
|
||||||
child: widget.child,
|
|
||||||
),
|
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
} else {
|
}
|
||||||
|
|
||||||
return Scaffold(
|
return Scaffold(
|
||||||
appBar: appBar,
|
appBar: appBar,
|
||||||
body: widget.child,
|
drawer: Drawer(
|
||||||
bottomNavigationBar: NavigationBar(
|
backgroundColor: sideBg,
|
||||||
selectedIndex: selectedIndex,
|
child: SafeArea(
|
||||||
onDestinationSelected: onSelect,
|
child: ListView(
|
||||||
destinations: destinations.map((dest) => NavigationDestination(
|
padding: const EdgeInsets.symmetric(vertical: 8),
|
||||||
icon: Icon(dest.icon),
|
children: [
|
||||||
selectedIcon: Icon(dest.selectedIcon),
|
// آیتمهای منو
|
||||||
label: dest.label,
|
for (int i = 0; i < menuItems.length; i++) ...[
|
||||||
)).toList(),
|
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 String label;
|
||||||
final IconData icon;
|
final IconData icon;
|
||||||
final IconData selectedIcon;
|
final IconData selectedIcon;
|
||||||
final String path;
|
final String? path;
|
||||||
|
final _MenuItemType type;
|
||||||
const _Dest(this.label, this.icon, this.selectedIcon, this.path);
|
final List<_MenuItem>? children;
|
||||||
}
|
final bool hasAddButton;
|
||||||
|
|
||||||
|
const _MenuItem({
|
||||||
|
required this.label,
|
||||||
|
required this.icon,
|
||||||
|
required this.selectedIcon,
|
||||||
|
this.path,
|
||||||
|
required this.type,
|
||||||
|
this.children,
|
||||||
|
this.hasAddButton = false,
|
||||||
|
});
|
||||||
|
}
|
||||||
1353
hesabixUI/hesabix_ui/lib/pages/business/users_permissions_page.dart
Normal file
1353
hesabixUI/hesabix_ui/lib/pages/business/users_permissions_page.dart
Normal file
File diff suppressed because it is too large
Load diff
|
|
@ -248,20 +248,26 @@ class _LoginPageState extends State<LoginPage> with SingleTickerProviderStateMix
|
||||||
// ذخیره دسترسیهای اپلیکیشن
|
// ذخیره دسترسیهای اپلیکیشن
|
||||||
final appPermissions = user?['app_permissions'] as Map<String, dynamic>?;
|
final appPermissions = user?['app_permissions'] as Map<String, dynamic>?;
|
||||||
final isSuperAdmin = appPermissions?['superadmin'] == true;
|
final isSuperAdmin = appPermissions?['superadmin'] == true;
|
||||||
|
final userId = user?['id'] as int?;
|
||||||
|
|
||||||
if (appPermissions != null) {
|
if (appPermissions != null) {
|
||||||
await widget.authStore.saveAppPermissions(appPermissions, isSuperAdmin);
|
await widget.authStore.saveAppPermissions(appPermissions, isSuperAdmin, userId: userId);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!mounted) return;
|
if (!mounted) return;
|
||||||
_showSnack(t.homeWelcome);
|
_showSnack(t.homeWelcome);
|
||||||
// بعد از login موفق، به صفحه قبلی یا dashboard برود
|
// بعد از login موفق، به صفحه قبلی یا dashboard برود
|
||||||
final currentPath = GoRouterState.of(context).uri.path;
|
try {
|
||||||
if (currentPath.startsWith('/user/profile/') || currentPath.startsWith('/acc/')) {
|
final currentPath = GoRouterState.of(context).uri.path;
|
||||||
// اگر در صفحه محافظت شده بود، همان صفحه را refresh کند
|
if (currentPath.startsWith('/user/profile/') || currentPath.startsWith('/acc/') || currentPath.startsWith('/business/')) {
|
||||||
context.go(currentPath);
|
// اگر در صفحه محافظت شده بود، همان صفحه را refresh کند
|
||||||
} else {
|
context.go(currentPath);
|
||||||
// وگرنه به dashboard برود
|
} else {
|
||||||
|
// وگرنه به dashboard برود
|
||||||
|
context.go('/user/profile/dashboard');
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
// اگر GoRouterState در دسترس نیست، به dashboard برود
|
||||||
context.go('/user/profile/dashboard');
|
context.go('/user/profile/dashboard');
|
||||||
}
|
}
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
|
|
@ -344,9 +350,10 @@ class _LoginPageState extends State<LoginPage> with SingleTickerProviderStateMix
|
||||||
// ذخیره دسترسیهای اپلیکیشن
|
// ذخیره دسترسیهای اپلیکیشن
|
||||||
final appPermissions = user?['app_permissions'] as Map<String, dynamic>?;
|
final appPermissions = user?['app_permissions'] as Map<String, dynamic>?;
|
||||||
final isSuperAdmin = appPermissions?['superadmin'] == true;
|
final isSuperAdmin = appPermissions?['superadmin'] == true;
|
||||||
|
final userId = user?['id'] as int?;
|
||||||
|
|
||||||
if (appPermissions != null) {
|
if (appPermissions != null) {
|
||||||
await widget.authStore.saveAppPermissions(appPermissions, isSuperAdmin);
|
await widget.authStore.saveAppPermissions(appPermissions, isSuperAdmin, userId: userId);
|
||||||
}
|
}
|
||||||
_showSnack(t.registerSuccess);
|
_showSnack(t.registerSuccess);
|
||||||
// پاکسازی کد معرف پس از ثبتنام موفق
|
// پاکسازی کد معرف پس از ثبتنام موفق
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,5 @@
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:go_router/go_router.dart';
|
||||||
import 'package:hesabix_ui/l10n/app_localizations.dart';
|
import 'package:hesabix_ui/l10n/app_localizations.dart';
|
||||||
import 'package:hesabix_ui/core/api_client.dart';
|
import 'package:hesabix_ui/core/api_client.dart';
|
||||||
import 'package:hesabix_ui/services/support_service.dart';
|
import 'package:hesabix_ui/services/support_service.dart';
|
||||||
|
|
@ -235,7 +236,7 @@ class _CreateTicketPageState extends State<CreateTicketPage> {
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
IconButton(
|
IconButton(
|
||||||
onPressed: () => Navigator.of(context).pop(),
|
onPressed: () => context.pop(),
|
||||||
icon: const Icon(Icons.close),
|
icon: const Icon(Icons.close),
|
||||||
style: IconButton.styleFrom(
|
style: IconButton.styleFrom(
|
||||||
backgroundColor: theme.colorScheme.surface,
|
backgroundColor: theme.colorScheme.surface,
|
||||||
|
|
@ -645,7 +646,7 @@ class _CreateTicketPageState extends State<CreateTicketPage> {
|
||||||
),
|
),
|
||||||
const SizedBox(width: 12),
|
const SizedBox(width: 12),
|
||||||
OutlinedButton(
|
OutlinedButton(
|
||||||
onPressed: _isSubmitting ? null : () => Navigator.of(context).pop(),
|
onPressed: _isSubmitting ? null : () => context.pop(),
|
||||||
style: OutlinedButton.styleFrom(
|
style: OutlinedButton.styleFrom(
|
||||||
padding: const EdgeInsets.symmetric(vertical: 16, horizontal: 24),
|
padding: const EdgeInsets.symmetric(vertical: 16, horizontal: 24),
|
||||||
shape: RoundedRectangleBorder(
|
shape: RoundedRectangleBorder(
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,5 @@
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:go_router/go_router.dart';
|
||||||
import 'package:hesabix_ui/l10n/app_localizations.dart';
|
import 'package:hesabix_ui/l10n/app_localizations.dart';
|
||||||
import '../../models/business_models.dart';
|
import '../../models/business_models.dart';
|
||||||
import '../../services/business_api_service.dart';
|
import '../../services/business_api_service.dart';
|
||||||
|
|
@ -121,7 +122,7 @@ class _NewBusinessPageState extends State<NewBusinessPage> {
|
||||||
duration: const Duration(seconds: 2),
|
duration: const Duration(seconds: 2),
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
Navigator.of(context).pop();
|
context.pop();
|
||||||
}
|
}
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
if (mounted) {
|
if (mounted) {
|
||||||
|
|
|
||||||
|
|
@ -42,7 +42,12 @@ class _ProfileShellState extends State<ProfileShell> {
|
||||||
final bool useRail = width >= 700;
|
final bool useRail = width >= 700;
|
||||||
final bool railExtended = width >= 1100;
|
final bool railExtended = width >= 1100;
|
||||||
final ColorScheme scheme = Theme.of(context).colorScheme;
|
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 bool isDark = Theme.of(context).brightness == Brightness.dark;
|
||||||
final String logoAsset = isDark
|
final String logoAsset = isDark
|
||||||
? 'assets/images/logo-light.png'
|
? 'assets/images/logo-light.png'
|
||||||
|
|
@ -85,7 +90,12 @@ class _ProfileShellState extends State<ProfileShell> {
|
||||||
|
|
||||||
Future<void> onSelect(int index) async {
|
Future<void> onSelect(int index) async {
|
||||||
final path = allDestinations[index].path;
|
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);
|
context.go(path);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -251,7 +261,7 @@ class _ProfileShellState extends State<ProfileShell> {
|
||||||
selected: active,
|
selected: active,
|
||||||
selectedTileColor: activeBg,
|
selectedTileColor: activeBg,
|
||||||
onTap: () {
|
onTap: () {
|
||||||
Navigator.of(context).pop();
|
context.pop();
|
||||||
onSelect(i);
|
onSelect(i);
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
|
|
||||||
66
hesabixUI/hesabix_ui/lib/services/business_user_service.dart
Normal file
66
hesabixUI/hesabix_ui/lib/services/business_user_service.dart
Normal 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');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -1,4 +1,5 @@
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:go_router/go_router.dart';
|
||||||
import 'package:hesabix_ui/l10n/app_localizations.dart';
|
import 'package:hesabix_ui/l10n/app_localizations.dart';
|
||||||
import '../../../core/api_client.dart';
|
import '../../../core/api_client.dart';
|
||||||
|
|
||||||
|
|
@ -121,7 +122,7 @@ class _StorageConfigFormDialogState extends State<StorageConfigFormDialog> {
|
||||||
}
|
}
|
||||||
|
|
||||||
if (mounted) {
|
if (mounted) {
|
||||||
Navigator.of(context).pop();
|
context.pop();
|
||||||
|
|
||||||
// Only show SnackBar if there's no onSaved callback (parent will handle notification)
|
// Only show SnackBar if there's no onSaved callback (parent will handle notification)
|
||||||
if (widget.onSaved == null) {
|
if (widget.onSaved == null) {
|
||||||
|
|
@ -202,7 +203,7 @@ class _StorageConfigFormDialogState extends State<StorageConfigFormDialog> {
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
IconButton(
|
IconButton(
|
||||||
onPressed: () => Navigator.of(context).pop(),
|
onPressed: () => context.pop(),
|
||||||
icon: const Icon(Icons.close),
|
icon: const Icon(Icons.close),
|
||||||
color: theme.colorScheme.onPrimary,
|
color: theme.colorScheme.onPrimary,
|
||||||
),
|
),
|
||||||
|
|
@ -344,7 +345,7 @@ class _StorageConfigFormDialogState extends State<StorageConfigFormDialog> {
|
||||||
mainAxisAlignment: MainAxisAlignment.end,
|
mainAxisAlignment: MainAxisAlignment.end,
|
||||||
children: [
|
children: [
|
||||||
TextButton(
|
TextButton(
|
||||||
onPressed: _isLoading ? null : () => Navigator.of(context).pop(),
|
onPressed: _isLoading ? null : () => context.pop(),
|
||||||
child: Text(l10n.cancel),
|
child: Text(l10n.cancel),
|
||||||
),
|
),
|
||||||
const SizedBox(width: 12),
|
const SizedBox(width: 12),
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,5 @@
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:go_router/go_router.dart';
|
||||||
import 'package:hesabix_ui/l10n/app_localizations.dart';
|
import 'package:hesabix_ui/l10n/app_localizations.dart';
|
||||||
import 'data_table_config.dart';
|
import 'data_table_config.dart';
|
||||||
import 'helpers/column_settings_service.dart';
|
import 'helpers/column_settings_service.dart';
|
||||||
|
|
@ -65,7 +66,7 @@ class _ColumnSettingsDialogState extends State<ColumnSettingsDialog> {
|
||||||
),
|
),
|
||||||
const Spacer(),
|
const Spacer(),
|
||||||
IconButton(
|
IconButton(
|
||||||
onPressed: () => Navigator.of(context).pop(),
|
onPressed: () => context.pop(),
|
||||||
icon: const Icon(Icons.close),
|
icon: const Icon(Icons.close),
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
|
|
@ -96,7 +97,7 @@ class _ColumnSettingsDialogState extends State<ColumnSettingsDialog> {
|
||||||
),
|
),
|
||||||
const SizedBox(width: 12),
|
const SizedBox(width: 12),
|
||||||
OutlinedButton(
|
OutlinedButton(
|
||||||
onPressed: () => Navigator.of(context).pop(),
|
onPressed: () => context.pop(),
|
||||||
child: Text(t.cancel),
|
child: Text(t.cancel),
|
||||||
),
|
),
|
||||||
const SizedBox(width: 12),
|
const SizedBox(width: 12),
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,5 @@
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:go_router/go_router.dart';
|
||||||
import 'package:intl/intl.dart';
|
import 'package:intl/intl.dart';
|
||||||
import 'package:hesabix_ui/l10n/app_localizations.dart';
|
import 'package:hesabix_ui/l10n/app_localizations.dart';
|
||||||
import 'data_table_config.dart';
|
import 'data_table_config.dart';
|
||||||
|
|
@ -234,7 +235,7 @@ class _DataTableSearchDialogState extends State<DataTableSearchDialog> {
|
||||||
return [
|
return [
|
||||||
TextButton(
|
TextButton(
|
||||||
onPressed: () {
|
onPressed: () {
|
||||||
Navigator.of(context).pop();
|
context.pop();
|
||||||
},
|
},
|
||||||
child: Text(t.cancel),
|
child: Text(t.cancel),
|
||||||
),
|
),
|
||||||
|
|
@ -242,7 +243,7 @@ class _DataTableSearchDialogState extends State<DataTableSearchDialog> {
|
||||||
TextButton(
|
TextButton(
|
||||||
onPressed: () {
|
onPressed: () {
|
||||||
widget.onClear();
|
widget.onClear();
|
||||||
Navigator.of(context).pop();
|
context.pop();
|
||||||
},
|
},
|
||||||
child: Text(t.clear),
|
child: Text(t.clear),
|
||||||
),
|
),
|
||||||
|
|
@ -302,7 +303,7 @@ class _DataTableSearchDialogState extends State<DataTableSearchDialog> {
|
||||||
widget.onApply(_controller.text.trim(), _selectedType);
|
widget.onApply(_controller.text.trim(), _selectedType);
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
Navigator.of(context).pop();
|
context.pop();
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<void> _selectFromDate(AppLocalizations t, bool isJalali) async {
|
Future<void> _selectFromDate(AppLocalizations t, bool isJalali) async {
|
||||||
|
|
@ -449,7 +450,7 @@ class _DataTableDateRangeDialogState extends State<DataTableDateRangeDialog> {
|
||||||
actions: [
|
actions: [
|
||||||
TextButton(
|
TextButton(
|
||||||
onPressed: () {
|
onPressed: () {
|
||||||
Navigator.of(context).pop();
|
context.pop();
|
||||||
},
|
},
|
||||||
child: Text(t.cancel),
|
child: Text(t.cancel),
|
||||||
),
|
),
|
||||||
|
|
@ -457,7 +458,7 @@ class _DataTableDateRangeDialogState extends State<DataTableDateRangeDialog> {
|
||||||
TextButton(
|
TextButton(
|
||||||
onPressed: () {
|
onPressed: () {
|
||||||
widget.onClear();
|
widget.onClear();
|
||||||
Navigator.of(context).pop();
|
context.pop();
|
||||||
},
|
},
|
||||||
child: Text(t.clear),
|
child: Text(t.clear),
|
||||||
),
|
),
|
||||||
|
|
@ -465,7 +466,7 @@ class _DataTableDateRangeDialogState extends State<DataTableDateRangeDialog> {
|
||||||
onPressed: _fromDate != null && _toDate != null
|
onPressed: _fromDate != null && _toDate != null
|
||||||
? () {
|
? () {
|
||||||
widget.onApply(_fromDate, _toDate);
|
widget.onApply(_fromDate, _toDate);
|
||||||
Navigator.of(context).pop();
|
context.pop();
|
||||||
}
|
}
|
||||||
: null,
|
: null,
|
||||||
child: Text(t.applyFilter),
|
child: Text(t.applyFilter),
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,5 @@
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:go_router/go_router.dart';
|
||||||
import 'package:shamsi_date/shamsi_date.dart';
|
import 'package:shamsi_date/shamsi_date.dart';
|
||||||
|
|
||||||
/// DatePicker سفارشی برای تقویم شمسی
|
/// DatePicker سفارشی برای تقویم شمسی
|
||||||
|
|
@ -100,7 +101,7 @@ class _JalaliDatePickerState extends State<JalaliDatePicker> {
|
||||||
mainAxisAlignment: MainAxisAlignment.end,
|
mainAxisAlignment: MainAxisAlignment.end,
|
||||||
children: [
|
children: [
|
||||||
TextButton(
|
TextButton(
|
||||||
onPressed: () => Navigator.of(context).pop(),
|
onPressed: () => context.pop(),
|
||||||
child: Text(
|
child: Text(
|
||||||
'انصراف',
|
'انصراف',
|
||||||
style: TextStyle(color: theme.textTheme.bodyMedium?.color),
|
style: TextStyle(color: theme.textTheme.bodyMedium?.color),
|
||||||
|
|
|
||||||
|
|
@ -134,11 +134,11 @@ class _ProgressSplashScreenState extends State<ProgressSplashScreen>
|
||||||
colors: isDark
|
colors: isDark
|
||||||
? [
|
? [
|
||||||
bgColor,
|
bgColor,
|
||||||
bgColor.withOpacity(0.95),
|
bgColor.withValues(alpha: 0.95),
|
||||||
]
|
]
|
||||||
: [
|
: [
|
||||||
bgColor,
|
bgColor,
|
||||||
bgColor.withOpacity(0.98),
|
bgColor.withValues(alpha: 0.98),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
|
@ -161,7 +161,7 @@ class _ProgressSplashScreenState extends State<ProgressSplashScreen>
|
||||||
borderRadius: BorderRadius.circular(20),
|
borderRadius: BorderRadius.circular(20),
|
||||||
boxShadow: [
|
boxShadow: [
|
||||||
BoxShadow(
|
BoxShadow(
|
||||||
color: primary.withOpacity(0.2),
|
color: primary.withValues(alpha: 0.2),
|
||||||
blurRadius: 20,
|
blurRadius: 20,
|
||||||
spreadRadius: 2,
|
spreadRadius: 2,
|
||||||
),
|
),
|
||||||
|
|
@ -204,7 +204,7 @@ class _ProgressSplashScreenState extends State<ProgressSplashScreen>
|
||||||
|
|
||||||
// Subtitle
|
// Subtitle
|
||||||
Text(
|
Text(
|
||||||
AppLocalizations.of(context)?.businessManagementPlatform ?? 'Business Management Platform',
|
AppLocalizations.of(context).businessManagementPlatform,
|
||||||
style: theme.textTheme.bodyLarge?.copyWith(
|
style: theme.textTheme.bodyLarge?.copyWith(
|
||||||
color: colorScheme.onSurfaceVariant,
|
color: colorScheme.onSurfaceVariant,
|
||||||
fontWeight: FontWeight.w400,
|
fontWeight: FontWeight.w400,
|
||||||
|
|
@ -243,7 +243,7 @@ class _ProgressSplashScreenState extends State<ProgressSplashScreen>
|
||||||
decoration: BoxDecoration(
|
decoration: BoxDecoration(
|
||||||
borderRadius: BorderRadius.circular(2),
|
borderRadius: BorderRadius.circular(2),
|
||||||
gradient: LinearGradient(
|
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
|
// Loading Message
|
||||||
Text(
|
Text(
|
||||||
widget.message ?? AppLocalizations.of(context)?.loading ?? 'Loading...',
|
widget.message ?? AppLocalizations.of(context).loading,
|
||||||
style: theme.textTheme.bodyMedium?.copyWith(
|
style: theme.textTheme.bodyMedium?.copyWith(
|
||||||
color: colorScheme.onSurfaceVariant,
|
color: colorScheme.onSurfaceVariant,
|
||||||
fontWeight: FontWeight.w500,
|
fontWeight: FontWeight.w500,
|
||||||
|
|
@ -268,7 +268,7 @@ class _ProgressSplashScreenState extends State<ProgressSplashScreen>
|
||||||
Text(
|
Text(
|
||||||
'${_remainingSeconds}s',
|
'${_remainingSeconds}s',
|
||||||
style: theme.textTheme.bodySmall?.copyWith(
|
style: theme.textTheme.bodySmall?.copyWith(
|
||||||
color: colorScheme.onSurfaceVariant.withOpacity(0.7),
|
color: colorScheme.onSurfaceVariant.withValues(alpha: 0.7),
|
||||||
fontWeight: FontWeight.w400,
|
fontWeight: FontWeight.w400,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
|
@ -281,7 +281,7 @@ class _ProgressSplashScreenState extends State<ProgressSplashScreen>
|
||||||
Text(
|
Text(
|
||||||
'Version 1.0.0',
|
'Version 1.0.0',
|
||||||
style: theme.textTheme.bodySmall?.copyWith(
|
style: theme.textTheme.bodySmall?.copyWith(
|
||||||
color: colorScheme.onSurfaceVariant.withOpacity(0.6),
|
color: colorScheme.onSurfaceVariant.withValues(alpha: 0.6),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
|
|
|
||||||
82
hesabixUI/hesabix_ui/lib/widgets/settings_menu_button.dart
Normal file
82
hesabixUI/hesabix_ui/lib/widgets/settings_menu_button.dart
Normal 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,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -1,5 +1,6 @@
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'dart:async';
|
import 'dart:async';
|
||||||
|
import '../core/auth_store.dart';
|
||||||
|
|
||||||
class SimpleSplashScreen extends StatefulWidget {
|
class SimpleSplashScreen extends StatefulWidget {
|
||||||
final String? message;
|
final String? message;
|
||||||
|
|
@ -9,6 +10,7 @@ class SimpleSplashScreen extends StatefulWidget {
|
||||||
final Duration displayDuration;
|
final Duration displayDuration;
|
||||||
final VoidCallback? onComplete;
|
final VoidCallback? onComplete;
|
||||||
final Locale? locale;
|
final Locale? locale;
|
||||||
|
final AuthStore? authStore;
|
||||||
|
|
||||||
const SimpleSplashScreen({
|
const SimpleSplashScreen({
|
||||||
super.key,
|
super.key,
|
||||||
|
|
@ -16,9 +18,10 @@ class SimpleSplashScreen extends StatefulWidget {
|
||||||
this.showLogo = true,
|
this.showLogo = true,
|
||||||
this.backgroundColor,
|
this.backgroundColor,
|
||||||
this.primaryColor,
|
this.primaryColor,
|
||||||
this.displayDuration = const Duration(seconds: 4),
|
this.displayDuration = const Duration(seconds: 1),
|
||||||
this.onComplete,
|
this.onComplete,
|
||||||
this.locale,
|
this.locale,
|
||||||
|
this.authStore,
|
||||||
});
|
});
|
||||||
|
|
||||||
@override
|
@override
|
||||||
|
|
@ -68,12 +71,51 @@ class _SimpleSplashScreenState extends State<SimpleSplashScreen>
|
||||||
_fadeController.forward();
|
_fadeController.forward();
|
||||||
_scaleController.forward();
|
_scaleController.forward();
|
||||||
|
|
||||||
// Start display timer
|
// Start display timer with authentication check
|
||||||
_displayTimer = Timer(widget.displayDuration, () {
|
_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
|
@override
|
||||||
void dispose() {
|
void dispose() {
|
||||||
_fadeController.dispose();
|
_fadeController.dispose();
|
||||||
|
|
|
||||||
|
|
@ -36,11 +36,11 @@ class SplashScreen extends StatelessWidget {
|
||||||
colors: isDark
|
colors: isDark
|
||||||
? [
|
? [
|
||||||
bgColor,
|
bgColor,
|
||||||
bgColor.withOpacity(0.95),
|
bgColor.withValues(alpha: 0.95),
|
||||||
]
|
]
|
||||||
: [
|
: [
|
||||||
bgColor,
|
bgColor,
|
||||||
bgColor.withOpacity(0.98),
|
bgColor.withValues(alpha: 0.98),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
|
@ -56,7 +56,7 @@ class SplashScreen extends StatelessWidget {
|
||||||
borderRadius: BorderRadius.circular(20),
|
borderRadius: BorderRadius.circular(20),
|
||||||
boxShadow: [
|
boxShadow: [
|
||||||
BoxShadow(
|
BoxShadow(
|
||||||
color: primary.withOpacity(0.2),
|
color: primary.withValues(alpha: 0.2),
|
||||||
blurRadius: 20,
|
blurRadius: 20,
|
||||||
spreadRadius: 2,
|
spreadRadius: 2,
|
||||||
),
|
),
|
||||||
|
|
@ -99,7 +99,7 @@ class SplashScreen extends StatelessWidget {
|
||||||
|
|
||||||
// Subtitle
|
// Subtitle
|
||||||
Text(
|
Text(
|
||||||
AppLocalizations.of(context)?.businessManagementPlatform ?? 'Business Management Platform',
|
AppLocalizations.of(context).businessManagementPlatform,
|
||||||
style: theme.textTheme.bodyLarge?.copyWith(
|
style: theme.textTheme.bodyLarge?.copyWith(
|
||||||
color: colorScheme.onSurfaceVariant,
|
color: colorScheme.onSurfaceVariant,
|
||||||
fontWeight: FontWeight.w400,
|
fontWeight: FontWeight.w400,
|
||||||
|
|
@ -122,7 +122,7 @@ class SplashScreen extends StatelessWidget {
|
||||||
|
|
||||||
// Loading Message
|
// Loading Message
|
||||||
Text(
|
Text(
|
||||||
message ?? AppLocalizations.of(context)?.loading ?? 'Loading...',
|
message ?? AppLocalizations.of(context).loading,
|
||||||
style: theme.textTheme.bodyMedium?.copyWith(
|
style: theme.textTheme.bodyMedium?.copyWith(
|
||||||
color: colorScheme.onSurfaceVariant,
|
color: colorScheme.onSurfaceVariant,
|
||||||
fontWeight: FontWeight.w500,
|
fontWeight: FontWeight.w500,
|
||||||
|
|
@ -137,7 +137,7 @@ class SplashScreen extends StatelessWidget {
|
||||||
Text(
|
Text(
|
||||||
'Version 1.0.0',
|
'Version 1.0.0',
|
||||||
style: theme.textTheme.bodySmall?.copyWith(
|
style: theme.textTheme.bodySmall?.copyWith(
|
||||||
color: colorScheme.onSurfaceVariant.withOpacity(0.6),
|
color: colorScheme.onSurfaceVariant.withValues(alpha: 0.6),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,5 @@
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:go_router/go_router.dart';
|
||||||
import 'package:hesabix_ui/l10n/app_localizations.dart';
|
import 'package:hesabix_ui/l10n/app_localizations.dart';
|
||||||
import 'package:hesabix_ui/models/support_models.dart';
|
import 'package:hesabix_ui/models/support_models.dart';
|
||||||
import 'package:hesabix_ui/services/support_service.dart';
|
import 'package:hesabix_ui/services/support_service.dart';
|
||||||
|
|
@ -312,7 +313,7 @@ class _TicketDetailsDialogState extends State<TicketDetailsDialog> {
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
IconButton(
|
IconButton(
|
||||||
onPressed: () => Navigator.of(context).pop(),
|
onPressed: () => context.pop(),
|
||||||
icon: const Icon(Icons.close),
|
icon: const Icon(Icons.close),
|
||||||
style: IconButton.styleFrom(
|
style: IconButton.styleFrom(
|
||||||
backgroundColor: Colors.grey[200],
|
backgroundColor: Colors.grey[200],
|
||||||
|
|
|
||||||
55
hesabixUI/hesabix_ui/lib/widgets/url_tracker.dart
Normal file
55
hesabixUI/hesabix_ui/lib/widgets/url_tracker.dart
Normal 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;
|
||||||
|
}
|
||||||
|
}
|
||||||
101
hesabixUI/hesabix_ui/lib/widgets/user_account_menu_button.dart
Normal file
101
hesabixUI/hesabix_ui/lib/widgets/user_account_menu_button.dart
Normal 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,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
46
hesabixUI/hesabix_ui/lib/widgets/user_menu_button.dart
Normal file
46
hesabixUI/hesabix_ui/lib/widgets/user_menu_button.dart
Normal 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),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
Loading…
Reference in a new issue