diff --git a/hesabixAPI/adapters/api/v1/business_users.py b/hesabixAPI/adapters/api/v1/business_users.py new file mode 100644 index 0000000..a2c0bb3 --- /dev/null +++ b/hesabixAPI/adapters/api/v1/business_users.py @@ -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="کاربر با موفقیت حذف شد" + ) diff --git a/hesabixAPI/adapters/api/v1/schemas.py b/hesabixAPI/adapters/api/v1/schemas.py index 48d74e6..2532062 100644 --- a/hesabixAPI/adapters/api/v1/schemas.py +++ b/hesabixAPI/adapters/api/v1/schemas.py @@ -1,6 +1,7 @@ from typing import Any, List, Optional, Union, Generic, TypeVar from pydantic import BaseModel, EmailStr, Field from enum import Enum +from datetime import datetime T = TypeVar('T') @@ -247,3 +248,79 @@ class PaginatedResponse(BaseModel, Generic[T]): ) +# Business User Schemas +class BusinessUserSchema(BaseModel): + id: int + business_id: int + user_id: int + user_name: str + user_email: str + user_phone: Optional[str] = None + role: str + status: str + added_at: datetime + last_active: Optional[datetime] = None + permissions: dict + + class Config: + from_attributes = True + + +class AddUserRequest(BaseModel): + email_or_phone: str + + class Config: + json_schema_extra = { + "example": { + "email_or_phone": "user@example.com" + } + } + + +class AddUserResponse(BaseModel): + success: bool + message: str + user: Optional[BusinessUserSchema] = None + + +class UpdatePermissionsRequest(BaseModel): + permissions: dict + + class Config: + json_schema_extra = { + "example": { + "permissions": { + "sales": { + "read": True, + "write": True, + "delete": False + }, + "reports": { + "read": True, + "export": True + }, + "settings": { + "manage_users": True + } + } + } + } + + +class UpdatePermissionsResponse(BaseModel): + success: bool + message: str + + +class RemoveUserResponse(BaseModel): + success: bool + message: str + + +class BusinessUsersListResponse(BaseModel): + success: bool + message: str + data: dict + calendar_type: Optional[str] = None + + diff --git a/hesabixAPI/adapters/db/models/__init__.py b/hesabixAPI/adapters/db/models/__init__.py index 7e44cb1..0e06545 100644 --- a/hesabixAPI/adapters/db/models/__init__.py +++ b/hesabixAPI/adapters/db/models/__init__.py @@ -7,6 +7,7 @@ from .captcha import Captcha # noqa: F401 from .password_reset import PasswordReset # noqa: F401 from .business import Business # noqa: F401 from .business_permission import BusinessPermission # noqa: F401 +# Business user models removed - using business_permissions instead # Import support models from .support import * # noqa: F401, F403 diff --git a/hesabixAPI/adapters/db/models/business.py b/hesabixAPI/adapters/db/models/business.py index aeee6cd..93a946c 100644 --- a/hesabixAPI/adapters/db/models/business.py +++ b/hesabixAPI/adapters/db/models/business.py @@ -4,7 +4,7 @@ from datetime import datetime from enum import Enum from sqlalchemy import String, DateTime, Integer, ForeignKey, Enum as SQLEnum, Text -from sqlalchemy.orm import Mapped, mapped_column +from sqlalchemy.orm import Mapped, mapped_column, relationship from adapters.db.session import Base @@ -53,3 +53,6 @@ class Business(Base): created_at: Mapped[datetime] = mapped_column(DateTime, default=datetime.utcnow, nullable=False) updated_at: Mapped[datetime] = mapped_column(DateTime, default=datetime.utcnow, onupdate=datetime.utcnow, nullable=False) + + # Relationships - using business_permissions instead + # users = relationship("BusinessUser", back_populates="business", cascade="all, delete-orphan") diff --git a/hesabixAPI/adapters/db/models/user.py b/hesabixAPI/adapters/db/models/user.py index 90dbc49..2e119be 100644 --- a/hesabixAPI/adapters/db/models/user.py +++ b/hesabixAPI/adapters/db/models/user.py @@ -28,5 +28,8 @@ class User(Base): # Support relationships tickets = relationship("Ticket", foreign_keys="Ticket.user_id", back_populates="user") + + # Business relationships - using business_permissions instead + # businesses = relationship("BusinessUser", back_populates="user", cascade="all, delete-orphan") diff --git a/hesabixAPI/app/core/auth_dependency.py b/hesabixAPI/app/core/auth_dependency.py index 37c205f..6a5dae1 100644 --- a/hesabixAPI/app/core/auth_dependency.py +++ b/hesabixAPI/app/core/auth_dependency.py @@ -121,14 +121,21 @@ class AuthContext: """بررسی دسترسی به پنل اپراتور پشتیبانی""" return self.has_app_permission("support_operator") - def is_business_owner(self) -> bool: + def is_business_owner(self, business_id: int = None) -> bool: """بررسی اینکه آیا کاربر مالک کسب و کار است یا نه""" - if not self.business_id or not self.db: + import logging + logger = logging.getLogger(__name__) + + target_business_id = business_id or self.business_id + if not target_business_id or not self.db: + logger.info(f"is_business_owner: no business_id ({target_business_id}) or db ({self.db is not None})") return False from adapters.db.models.business import Business - business = self.db.get(Business, self.business_id) - return business and business.owner_id == self.user.id + business = self.db.get(Business, target_business_id) + is_owner = business and business.owner_id == self.user.id + logger.info(f"is_business_owner: business_id={target_business_id}, business={business}, owner_id={business.owner_id if business else None}, user_id={self.user.id}, is_owner={is_owner}") + return is_owner # بررسی دسترسی‌های کسب و کار def has_business_permission(self, section: str, action: str) -> bool: @@ -188,9 +195,25 @@ class AuthContext: """بررسی دسترسی صادرات در بخش""" return self.has_business_permission(section, "export") - def can_manage_business_users(self) -> bool: + def can_manage_business_users(self, business_id: int = None) -> bool: """بررسی دسترسی مدیریت کاربران کسب و کار""" - return self.has_business_permission("settings", "manage_users") + import logging + logger = logging.getLogger(__name__) + + # SuperAdmin دسترسی کامل دارد + if self.is_superadmin(): + logger.info(f"can_manage_business_users: user {self.user.id} is superadmin") + return True + + # مالک کسب و کار دسترسی کامل دارد + if self.is_business_owner(business_id): + logger.info(f"can_manage_business_users: user {self.user.id} is business owner") + return True + + # بررسی دسترسی در سطح کسب و کار + has_permission = self.has_business_permission("settings", "manage_users") + logger.info(f"can_manage_business_users: user {self.user.id} has permission: {has_permission}") + return has_permission # ترکیب دسترسی‌ها def has_any_permission(self, section: str, action: str) -> bool: @@ -204,16 +227,25 @@ class AuthContext: def can_access_business(self, business_id: int) -> bool: """بررسی دسترسی به کسب و کار خاص""" + import logging + logger = logging.getLogger(__name__) + + logger.info(f"Checking business access: user {self.user.id}, business {business_id}, context business_id {self.business_id}") + # SuperAdmin دسترسی به همه کسب و کارها دارد if self.is_superadmin(): + logger.info(f"User {self.user.id} is superadmin, granting access to business {business_id}") return True # اگر مالک کسب و کار است، دسترسی دارد if self.is_business_owner() and business_id == self.business_id: + logger.info(f"User {self.user.id} is business owner of {business_id}, granting access") return True # بررسی دسترسی‌های کسب و کار - return business_id == self.business_id + has_access = business_id == self.business_id + logger.info(f"Business access check: {business_id} == {self.business_id} = {has_access}") + return has_access def to_dict(self) -> dict: """تبدیل به dictionary برای استفاده در API""" @@ -252,10 +284,15 @@ def get_current_user( db: Session = Depends(get_db) ) -> AuthContext: """دریافت اطلاعات کامل کاربر کنونی و تنظیمات از درخواست""" + import logging + logger = logging.getLogger(__name__) + # Get authorization from request headers auth_header = request.headers.get("Authorization") + logger.info(f"Auth header: {auth_header}") if not auth_header or not auth_header.startswith("ApiKey "): + logger.warning(f"Invalid auth header: {auth_header}") raise ApiError("UNAUTHORIZED", "Missing or invalid API key", http_status=401) api_key = auth_header[len("ApiKey ") :].strip() diff --git a/hesabixAPI/app/core/permissions.py b/hesabixAPI/app/core/permissions.py index f00d098..6107590 100644 --- a/hesabixAPI/app/core/permissions.py +++ b/hesabixAPI/app/core/permissions.py @@ -74,6 +74,9 @@ def require_business_access(business_id_param: str = "business_id"): def decorator(func: Callable) -> Callable: @wraps(func) def wrapper(*args, **kwargs) -> Any: + import logging + logger = logging.getLogger(__name__) + # Find request in args or kwargs request = None for arg in args: @@ -85,6 +88,7 @@ def require_business_access(business_id_param: str = "business_id"): request = kwargs['request'] if not request: + logger.error("Request not found in function arguments") raise ApiError("INTERNAL_ERROR", "Request not found", http_status=500) # Get database session @@ -92,8 +96,17 @@ def require_business_access(business_id_param: str = "business_id"): db = next(get_db()) ctx = get_current_user(request, db) business_id = kwargs.get(business_id_param) + + logger.info(f"Checking business access for user {ctx.get_user_id()} to business {business_id}") + logger.info(f"User business_id from context: {ctx.business_id}") + logger.info(f"User is superadmin: {ctx.is_superadmin()}") + logger.info(f"User is business owner: {ctx.is_business_owner()}") + if business_id and not ctx.can_access_business(business_id): + logger.warning(f"User {ctx.get_user_id()} does not have access to business {business_id}") raise ApiError("FORBIDDEN", f"No access to business {business_id}", http_status=403) + + logger.info(f"User {ctx.get_user_id()} has access to business {business_id}") return func(*args, **kwargs) return wrapper return decorator diff --git a/hesabixAPI/app/core/responses.py b/hesabixAPI/app/core/responses.py index 63aeeec..c44d1f1 100644 --- a/hesabixAPI/app/core/responses.py +++ b/hesabixAPI/app/core/responses.py @@ -7,8 +7,16 @@ from fastapi import HTTPException, status, Request from .calendar import CalendarConverter, CalendarType -def success_response(data: Any, request: Request = None) -> dict[str, Any]: - response = {"success": True, "data": data} +def success_response(data: Any, request: Request = None, message: str = None) -> dict[str, Any]: + response = {"success": True} + + # Add data if provided + if data is not None: + response["data"] = data + + # Add message if provided + if message is not None: + response["message"] = message # Add calendar type information if request is available if request and hasattr(request.state, 'calendar_type'): @@ -27,8 +35,17 @@ def format_datetime_fields(data: Any, request: Request) -> Any: if isinstance(data, dict): formatted_data = {} for key, value in data.items(): - if isinstance(value, datetime): - formatted_data[key] = CalendarConverter.format_datetime(value, calendar_type) + if value is None: + formatted_data[key] = None + elif isinstance(value, datetime): + # Format the main date field based on calendar type + if calendar_type == "jalali": + formatted_data[key] = CalendarConverter.to_jalali(value)["formatted"] + else: + formatted_data[key] = value.isoformat() + + # Add formatted date as additional field + formatted_data[f"{key}_formatted"] = CalendarConverter.format_datetime(value, calendar_type) # Convert raw date to the same calendar type as the formatted date if calendar_type == "jalali": formatted_data[f"{key}_raw"] = CalendarConverter.to_jalali(value)["formatted"] diff --git a/hesabixAPI/app/main.py b/hesabixAPI/app/main.py index 83315ca..e6d693a 100644 --- a/hesabixAPI/app/main.py +++ b/hesabixAPI/app/main.py @@ -8,6 +8,7 @@ from adapters.api.v1.auth import router as auth_router from adapters.api.v1.users import router as users_router from adapters.api.v1.businesses import router as businesses_router from adapters.api.v1.business_dashboard import router as business_dashboard_router +from adapters.api.v1.business_users import router as business_users_router from adapters.api.v1.support.tickets import router as support_tickets_router from adapters.api.v1.support.operator import router as support_operator_router from adapters.api.v1.support.categories import router as support_categories_router @@ -271,6 +272,7 @@ def create_app() -> FastAPI: application.include_router(users_router, prefix=settings.api_v1_prefix) application.include_router(businesses_router, prefix=settings.api_v1_prefix) application.include_router(business_dashboard_router, prefix=settings.api_v1_prefix) + application.include_router(business_users_router, prefix=settings.api_v1_prefix) # Support endpoints application.include_router(support_tickets_router, prefix=f"{settings.api_v1_prefix}/support") @@ -292,7 +294,7 @@ def create_app() -> FastAPI: ) def read_root() -> dict[str, str]: return {"service": settings.app_name, "version": settings.app_version} - + # اضافه کردن security schemes from fastapi.openapi.utils import get_openapi diff --git a/hesabixAPI/hesabix_api.egg-info/SOURCES.txt b/hesabixAPI/hesabix_api.egg-info/SOURCES.txt index 9f623f1..cbe31ed 100644 --- a/hesabixAPI/hesabix_api.egg-info/SOURCES.txt +++ b/hesabixAPI/hesabix_api.egg-info/SOURCES.txt @@ -4,6 +4,8 @@ adapters/__init__.py adapters/api/__init__.py adapters/api/v1/__init__.py adapters/api/v1/auth.py +adapters/api/v1/business_dashboard.py +adapters/api/v1/business_users.py adapters/api/v1/businesses.py adapters/api/v1/health.py adapters/api/v1/schemas.py @@ -68,6 +70,7 @@ app/core/settings.py app/core/smart_normalizer.py app/services/api_key_service.py app/services/auth_service.py +app/services/business_dashboard_service.py app/services/business_service.py app/services/captcha_service.py app/services/email_service.py diff --git a/hesabixUI/hesabix_ui/lib/core/auth_store.dart b/hesabixUI/hesabix_ui/lib/core/auth_store.dart index 188b4cc..dc4e214 100644 --- a/hesabixUI/hesabix_ui/lib/core/auth_store.dart +++ b/hesabixUI/hesabix_ui/lib/core/auth_store.dart @@ -10,6 +10,7 @@ class AuthStore with ChangeNotifier { static const _kDeviceId = 'device_id'; static const _kAppPermissions = 'app_permissions'; static const _kIsSuperAdmin = 'is_superadmin'; + static const _kLastUrl = 'last_url'; final FlutterSecureStorage _secure = const FlutterSecureStorage(); String? _apiKey; @@ -21,6 +22,8 @@ class AuthStore with ChangeNotifier { String get deviceId => _deviceId ?? ''; Map? get appPermissions => _appPermissions; bool get isSuperAdmin => _isSuperAdmin; + int? _currentUserId; + int? get currentUserId => _currentUserId; Future load() async { final prefs = await SharedPreferences.getInstance(); @@ -98,8 +101,9 @@ class AuthStore with ChangeNotifier { } catch (_) {} await prefs.remove(_kApiKey); } - // پاک کردن دسترسی‌ها هنگام خروج + // پاک کردن دسترسی‌ها و آخرین URL هنگام خروج await _clearAppPermissions(); + await clearLastUrl(); } else { if (kIsWeb) { await prefs.setString(_kApiKey, key); @@ -113,10 +117,13 @@ class AuthStore with ChangeNotifier { notifyListeners(); } - Future saveAppPermissions(Map? permissions, bool isSuperAdmin) async { + Future saveAppPermissions(Map? permissions, bool isSuperAdmin, {int? userId}) async { final prefs = await SharedPreferences.getInstance(); _appPermissions = permissions; _isSuperAdmin = isSuperAdmin; + if (userId != null) { + _currentUserId = userId; + } if (permissions == null) { await _clearAppPermissions(); @@ -174,10 +181,16 @@ class AuthStore with ChangeNotifier { if (user != null) { final appPermissions = user['app_permissions'] as Map?; final isSuperAdmin = appPermissions?['superadmin'] == true; + final userId = user['id'] as int?; if (appPermissions != null) { await saveAppPermissions(appPermissions, isSuperAdmin); } + + if (userId != null) { + _currentUserId = userId; + notifyListeners(); + } } } } @@ -195,6 +208,32 @@ class AuthStore with ChangeNotifier { } bool get canAccessSupportOperator => hasAppPermission('support_operator'); + + // ذخیره آخرین URL برای بازیابی بعد از refresh + Future saveLastUrl(String url) async { + try { + final prefs = await SharedPreferences.getInstance(); + await prefs.setString(_kLastUrl, url); + } catch (_) {} + } + + // بازیابی آخرین URL + Future getLastUrl() async { + try { + final prefs = await SharedPreferences.getInstance(); + return prefs.getString(_kLastUrl); + } catch (_) { + return null; + } + } + + // پاک کردن آخرین URL + Future clearLastUrl() async { + try { + final prefs = await SharedPreferences.getInstance(); + await prefs.remove(_kLastUrl); + } catch (_) {} + } } diff --git a/hesabixUI/hesabix_ui/lib/l10n/app_en.arb b/hesabixUI/hesabix_ui/lib/l10n/app_en.arb index aa584d8..2a0265b 100644 --- a/hesabixUI/hesabix_ui/lib/l10n/app_en.arb +++ b/hesabixUI/hesabix_ui/lib/l10n/app_en.arb @@ -483,6 +483,77 @@ "backToProfile": "Back to Profile", "noBusinessesFound": "No businesses found", "createFirstBusiness": "Create your first business", - "accessDenied": "Access denied" + "accessDenied": "Access denied", + "basicTools": "Basic Tools", + "businessSettings": "Business Settings", + "printDocuments": "Print Documents", + "people": "People", + "peopleList": "People List", + "receipts": "Receipts", + "payments": "Payments", + "practicalTools": "Practical Tools", + "usersAndPermissions": "Users and Permissions", + "businessUsers": "Business Users", + "addNewUser": "Add New User", + "userEmailOrPhone": "Email or Phone", + "userEmailOrPhoneHint": "Enter user email or phone number", + "addUser": "Add User", + "userAddedSuccessfully": "User added successfully", + "userAddFailed": "Failed to add user", + "userRemovedSuccessfully": "User removed successfully", + "userRemoveFailed": "Failed to remove user", + "permissionsUpdatedSuccessfully": "Permissions updated successfully", + "permissionsUpdateFailed": "Failed to update permissions", + "userNotFound": "User not found", + "invalidEmailOrPhone": "Invalid email or phone number", + "userAlreadyExists": "User already exists", + "removeUser": "Remove User", + "removeUserConfirm": "Are you sure you want to remove this user?", + "userPermissions": "User Permissions", + "permissions": "Permissions", + "permission": "Permission", + "hasPermission": "Has Permission", + "noPermission": "No Permission", + "viewUsers": "View Users", + "managePermissions": "Manage Permissions", + "totalUsers": "Total Users", + "activeUsers": "Active Users", + "pendingUsers": "Pending Users", + "userName": "User Name", + "userEmail": "Email", + "userPhone": "Phone", + "userStatus": "Status", + "userRole": "Role", + "userAddedAt": "Added At", + "lastActive": "Last Active", + "active": "Active", + "inactive": "Inactive", + "pending": "Pending", + "owner": "Owner", + "admin": "Admin", + "member": "Member", + "viewer": "Viewer", + "editPermissions": "Edit Permissions", + "savePermissions": "Save Permissions", + "cancel": "Cancel", + "loading": "Loading...", + "noUsersFound": "No users found", + "searchUsers": "Search users...", + "filterByStatus": "Filter by Status", + "filterByRole": "Filter by Role", + "allStatuses": "All Statuses", + "allRoles": "All Roles", + "permissionDashboard": "Dashboard Access", + "permissionPeople": "People Access", + "permissionReceipts": "Receipts Access", + "permissionPayments": "Payments Access", + "permissionReports": "Reports Access", + "permissionSettings": "Settings Access", + "permissionUsers": "Users Access", + "permissionPrint": "Print Access", + "ownerWarning": "Warning: Business owner does not need to be added and always has full access to all sections", + "ownerWarningTitle": "Business Owner", + "alreadyAddedWarning": "This user has already been added to the business", + "alreadyAddedWarningTitle": "Existing User" } diff --git a/hesabixUI/hesabix_ui/lib/l10n/app_fa.arb b/hesabixUI/hesabix_ui/lib/l10n/app_fa.arb index d44225b..4699ef7 100644 --- a/hesabixUI/hesabix_ui/lib/l10n/app_fa.arb +++ b/hesabixUI/hesabix_ui/lib/l10n/app_fa.arb @@ -482,6 +482,77 @@ "backToProfile": "بازگشت به پروفایل", "noBusinessesFound": "هیچ کسب و کاری یافت نشد", "createFirstBusiness": "اولین کسب و کار خود را ایجاد کنید", - "accessDenied": "دسترسی غیرمجاز" + "accessDenied": "دسترسی غیرمجاز", + "basicTools": "ابزارهای پایه", + "businessSettings": "تنظیمات کسب و کار", + "printDocuments": "چاپ اسناد", + "people": "اشخاص", + "peopleList": "لیست اشخاص", + "receipts": "دریافت‌ها", + "payments": "پرداخت‌ها", + "practicalTools": "ابزارهای کاربردی", + "usersAndPermissions": "کاربران و دسترسی‌ها", + "businessUsers": "کاربران کسب و کار", + "addNewUser": "افزودن کاربر جدید", + "userEmailOrPhone": "ایمیل یا شماره تلفن", + "userEmailOrPhoneHint": "ایمیل یا شماره تلفن کاربر را وارد کنید", + "addUser": "افزودن کاربر", + "userAddedSuccessfully": "کاربر با موفقیت اضافه شد", + "userAddFailed": "خطا در افزودن کاربر", + "userRemovedSuccessfully": "کاربر با موفقیت حذف شد", + "userRemoveFailed": "خطا در حذف کاربر", + "permissionsUpdatedSuccessfully": "دسترسی‌ها با موفقیت به‌روزرسانی شد", + "permissionsUpdateFailed": "خطا در به‌روزرسانی دسترسی‌ها", + "userNotFound": "کاربر یافت نشد", + "invalidEmailOrPhone": "ایمیل یا شماره تلفن نامعتبر است", + "userAlreadyExists": "کاربر قبلاً اضافه شده است", + "removeUser": "حذف کاربر", + "removeUserConfirm": "آیا مطمئن هستید که می‌خواهید این کاربر را حذف کنید؟", + "userPermissions": "دسترسی‌های کاربر", + "permissions": "دسترسی‌ها", + "permission": "دسترسی", + "hasPermission": "دارای دسترسی", + "noPermission": "بدون دسترسی", + "viewUsers": "مشاهده کاربران", + "managePermissions": "مدیریت دسترسی‌ها", + "totalUsers": "کل کاربران", + "activeUsers": "کاربران فعال", + "pendingUsers": "کاربران در انتظار", + "userName": "نام کاربر", + "userEmail": "ایمیل", + "userPhone": "تلفن", + "userStatus": "وضعیت", + "userRole": "نقش", + "userAddedAt": "تاریخ افزودن", + "lastActive": "آخرین فعالیت", + "active": "فعال", + "inactive": "غیرفعال", + "pending": "در انتظار", + "owner": "مالک", + "admin": "مدیر", + "member": "عضو", + "viewer": "مشاهده‌گر", + "editPermissions": "ویرایش دسترسی‌ها", + "savePermissions": "ذخیره دسترسی‌ها", + "cancel": "لغو", + "loading": "در حال بارگذاری...", + "noUsersFound": "هیچ کاربری یافت نشد", + "searchUsers": "جستجوی کاربران...", + "filterByStatus": "فیلتر بر اساس وضعیت", + "filterByRole": "فیلتر بر اساس نقش", + "allStatuses": "همه وضعیت‌ها", + "allRoles": "همه نقش‌ها", + "permissionDashboard": "دسترسی به داشبورد", + "permissionPeople": "دسترسی به اشخاص", + "permissionReceipts": "دسترسی به دریافت‌ها", + "permissionPayments": "دسترسی به پرداخت‌ها", + "permissionReports": "دسترسی به گزارش‌ها", + "permissionSettings": "دسترسی به تنظیمات", + "permissionUsers": "دسترسی به کاربران", + "permissionPrint": "دسترسی به چاپ اسناد", + "ownerWarning": "هشدار: کاربر مالک کسب و کار نیازی به افزودن ندارد و همیشه دسترسی کامل به همه بخش‌ها دارد", + "ownerWarningTitle": "کاربر مالک", + "alreadyAddedWarning": "این کاربر قبلاً به کسب و کار اضافه شده است", + "alreadyAddedWarningTitle": "کاربر موجود" } diff --git a/hesabixUI/hesabix_ui/lib/l10n/app_localizations.dart b/hesabixUI/hesabix_ui/lib/l10n/app_localizations.dart index c4951bc..a00220e 100644 --- a/hesabixUI/hesabix_ui/lib/l10n/app_localizations.dart +++ b/hesabixUI/hesabix_ui/lib/l10n/app_localizations.dart @@ -2713,6 +2713,402 @@ abstract class AppLocalizations { /// In en, this message translates to: /// **'Access denied'** String get accessDenied; + + /// No description provided for @basicTools. + /// + /// In en, this message translates to: + /// **'Basic Tools'** + String get basicTools; + + /// No description provided for @businessSettings. + /// + /// In en, this message translates to: + /// **'Business Settings'** + String get businessSettings; + + /// No description provided for @printDocuments. + /// + /// In en, this message translates to: + /// **'Print Documents'** + String get printDocuments; + + /// No description provided for @people. + /// + /// In en, this message translates to: + /// **'People'** + String get people; + + /// No description provided for @peopleList. + /// + /// In en, this message translates to: + /// **'People List'** + String get peopleList; + + /// No description provided for @receipts. + /// + /// In en, this message translates to: + /// **'Receipts'** + String get receipts; + + /// No description provided for @payments. + /// + /// In en, this message translates to: + /// **'Payments'** + String get payments; + + /// No description provided for @practicalTools. + /// + /// In en, this message translates to: + /// **'Practical Tools'** + String get practicalTools; + + /// No description provided for @usersAndPermissions. + /// + /// In en, this message translates to: + /// **'Users and Permissions'** + String get usersAndPermissions; + + /// No description provided for @businessUsers. + /// + /// In en, this message translates to: + /// **'Business Users'** + String get businessUsers; + + /// No description provided for @addNewUser. + /// + /// In en, this message translates to: + /// **'Add New User'** + String get addNewUser; + + /// No description provided for @userEmailOrPhone. + /// + /// In en, this message translates to: + /// **'Email or Phone'** + String get userEmailOrPhone; + + /// No description provided for @userEmailOrPhoneHint. + /// + /// In en, this message translates to: + /// **'Enter user email or phone number'** + String get userEmailOrPhoneHint; + + /// No description provided for @addUser. + /// + /// In en, this message translates to: + /// **'Add User'** + String get addUser; + + /// No description provided for @userAddedSuccessfully. + /// + /// In en, this message translates to: + /// **'User added successfully'** + String get userAddedSuccessfully; + + /// No description provided for @userAddFailed. + /// + /// In en, this message translates to: + /// **'Failed to add user'** + String get userAddFailed; + + /// No description provided for @userRemovedSuccessfully. + /// + /// In en, this message translates to: + /// **'User removed successfully'** + String get userRemovedSuccessfully; + + /// No description provided for @userRemoveFailed. + /// + /// In en, this message translates to: + /// **'Failed to remove user'** + String get userRemoveFailed; + + /// No description provided for @permissionsUpdatedSuccessfully. + /// + /// In en, this message translates to: + /// **'Permissions updated successfully'** + String get permissionsUpdatedSuccessfully; + + /// No description provided for @permissionsUpdateFailed. + /// + /// In en, this message translates to: + /// **'Failed to update permissions'** + String get permissionsUpdateFailed; + + /// No description provided for @userNotFound. + /// + /// In en, this message translates to: + /// **'User not found'** + String get userNotFound; + + /// No description provided for @invalidEmailOrPhone. + /// + /// In en, this message translates to: + /// **'Invalid email or phone number'** + String get invalidEmailOrPhone; + + /// No description provided for @userAlreadyExists. + /// + /// In en, this message translates to: + /// **'User already exists'** + String get userAlreadyExists; + + /// No description provided for @removeUser. + /// + /// In en, this message translates to: + /// **'Remove User'** + String get removeUser; + + /// No description provided for @removeUserConfirm. + /// + /// In en, this message translates to: + /// **'Are you sure you want to remove this user?'** + String get removeUserConfirm; + + /// No description provided for @userPermissions. + /// + /// In en, this message translates to: + /// **'User Permissions'** + String get userPermissions; + + /// No description provided for @permissions. + /// + /// In en, this message translates to: + /// **'Permissions'** + String get permissions; + + /// No description provided for @permission. + /// + /// In en, this message translates to: + /// **'Permission'** + String get permission; + + /// No description provided for @hasPermission. + /// + /// In en, this message translates to: + /// **'Has Permission'** + String get hasPermission; + + /// No description provided for @noPermission. + /// + /// In en, this message translates to: + /// **'No Permission'** + String get noPermission; + + /// No description provided for @viewUsers. + /// + /// In en, this message translates to: + /// **'View Users'** + String get viewUsers; + + /// No description provided for @managePermissions. + /// + /// In en, this message translates to: + /// **'Manage Permissions'** + String get managePermissions; + + /// No description provided for @totalUsers. + /// + /// In en, this message translates to: + /// **'Total Users'** + String get totalUsers; + + /// No description provided for @activeUsers. + /// + /// In en, this message translates to: + /// **'Active Users'** + String get activeUsers; + + /// No description provided for @pendingUsers. + /// + /// In en, this message translates to: + /// **'Pending Users'** + String get pendingUsers; + + /// No description provided for @userName. + /// + /// In en, this message translates to: + /// **'User Name'** + String get userName; + + /// No description provided for @userEmail. + /// + /// In en, this message translates to: + /// **'Email'** + String get userEmail; + + /// No description provided for @userPhone. + /// + /// In en, this message translates to: + /// **'Phone'** + String get userPhone; + + /// No description provided for @userStatus. + /// + /// In en, this message translates to: + /// **'Status'** + String get userStatus; + + /// No description provided for @userRole. + /// + /// In en, this message translates to: + /// **'Role'** + String get userRole; + + /// No description provided for @userAddedAt. + /// + /// In en, this message translates to: + /// **'Added At'** + String get userAddedAt; + + /// No description provided for @lastActive. + /// + /// In en, this message translates to: + /// **'Last Active'** + String get lastActive; + + /// No description provided for @inactive. + /// + /// In en, this message translates to: + /// **'Inactive'** + String get inactive; + + /// No description provided for @pending. + /// + /// In en, this message translates to: + /// **'Pending'** + String get pending; + + /// No description provided for @admin. + /// + /// In en, this message translates to: + /// **'Admin'** + String get admin; + + /// No description provided for @viewer. + /// + /// In en, this message translates to: + /// **'Viewer'** + String get viewer; + + /// No description provided for @editPermissions. + /// + /// In en, this message translates to: + /// **'Edit Permissions'** + String get editPermissions; + + /// No description provided for @savePermissions. + /// + /// In en, this message translates to: + /// **'Save Permissions'** + String get savePermissions; + + /// No description provided for @noUsersFound. + /// + /// In en, this message translates to: + /// **'No users found'** + String get noUsersFound; + + /// No description provided for @searchUsers. + /// + /// In en, this message translates to: + /// **'Search users...'** + String get searchUsers; + + /// No description provided for @filterByStatus. + /// + /// In en, this message translates to: + /// **'Filter by Status'** + String get filterByStatus; + + /// No description provided for @filterByRole. + /// + /// In en, this message translates to: + /// **'Filter by Role'** + String get filterByRole; + + /// No description provided for @allStatuses. + /// + /// In en, this message translates to: + /// **'All Statuses'** + String get allStatuses; + + /// No description provided for @allRoles. + /// + /// In en, this message translates to: + /// **'All Roles'** + String get allRoles; + + /// No description provided for @permissionDashboard. + /// + /// In en, this message translates to: + /// **'Dashboard Access'** + String get permissionDashboard; + + /// No description provided for @permissionPeople. + /// + /// In en, this message translates to: + /// **'People Access'** + String get permissionPeople; + + /// No description provided for @permissionReceipts. + /// + /// In en, this message translates to: + /// **'Receipts Access'** + String get permissionReceipts; + + /// No description provided for @permissionPayments. + /// + /// In en, this message translates to: + /// **'Payments Access'** + String get permissionPayments; + + /// No description provided for @permissionReports. + /// + /// In en, this message translates to: + /// **'Reports Access'** + String get permissionReports; + + /// No description provided for @permissionSettings. + /// + /// In en, this message translates to: + /// **'Settings Access'** + String get permissionSettings; + + /// No description provided for @permissionUsers. + /// + /// In en, this message translates to: + /// **'Users Access'** + String get permissionUsers; + + /// No description provided for @permissionPrint. + /// + /// In en, this message translates to: + /// **'Print Access'** + String get permissionPrint; + + /// No description provided for @ownerWarning. + /// + /// In en, this message translates to: + /// **'Warning: Business owner does not need to be added and always has full access to all sections'** + String get ownerWarning; + + /// No description provided for @ownerWarningTitle. + /// + /// In en, this message translates to: + /// **'Business Owner'** + String get ownerWarningTitle; + + /// No description provided for @alreadyAddedWarning. + /// + /// In en, this message translates to: + /// **'This user has already been added to the business'** + String get alreadyAddedWarning; + + /// No description provided for @alreadyAddedWarningTitle. + /// + /// In en, this message translates to: + /// **'Existing User'** + String get alreadyAddedWarningTitle; } class _AppLocalizationsDelegate diff --git a/hesabixUI/hesabix_ui/lib/l10n/app_localizations_en.dart b/hesabixUI/hesabix_ui/lib/l10n/app_localizations_en.dart index 5087499..e1d3d16 100644 --- a/hesabixUI/hesabix_ui/lib/l10n/app_localizations_en.dart +++ b/hesabixUI/hesabix_ui/lib/l10n/app_localizations_en.dart @@ -1356,4 +1356,205 @@ class AppLocalizationsEn extends AppLocalizations { @override String get accessDenied => 'Access denied'; + + @override + String get basicTools => 'Basic Tools'; + + @override + String get businessSettings => 'Business Settings'; + + @override + String get printDocuments => 'Print Documents'; + + @override + String get people => 'People'; + + @override + String get peopleList => 'People List'; + + @override + String get receipts => 'Receipts'; + + @override + String get payments => 'Payments'; + + @override + String get practicalTools => 'Practical Tools'; + + @override + String get usersAndPermissions => 'Users and Permissions'; + + @override + String get businessUsers => 'Business Users'; + + @override + String get addNewUser => 'Add New User'; + + @override + String get userEmailOrPhone => 'Email or Phone'; + + @override + String get userEmailOrPhoneHint => 'Enter user email or phone number'; + + @override + String get addUser => 'Add User'; + + @override + String get userAddedSuccessfully => 'User added successfully'; + + @override + String get userAddFailed => 'Failed to add user'; + + @override + String get userRemovedSuccessfully => 'User removed successfully'; + + @override + String get userRemoveFailed => 'Failed to remove user'; + + @override + String get permissionsUpdatedSuccessfully => + 'Permissions updated successfully'; + + @override + String get permissionsUpdateFailed => 'Failed to update permissions'; + + @override + String get userNotFound => 'User not found'; + + @override + String get invalidEmailOrPhone => 'Invalid email or phone number'; + + @override + String get userAlreadyExists => 'User already exists'; + + @override + String get removeUser => 'Remove User'; + + @override + String get removeUserConfirm => 'Are you sure you want to remove this user?'; + + @override + String get userPermissions => 'User Permissions'; + + @override + String get permissions => 'Permissions'; + + @override + String get permission => 'Permission'; + + @override + String get hasPermission => 'Has Permission'; + + @override + String get noPermission => 'No Permission'; + + @override + String get viewUsers => 'View Users'; + + @override + String get managePermissions => 'Manage Permissions'; + + @override + String get totalUsers => 'Total Users'; + + @override + String get activeUsers => 'Active Users'; + + @override + String get pendingUsers => 'Pending Users'; + + @override + String get userName => 'User Name'; + + @override + String get userEmail => 'Email'; + + @override + String get userPhone => 'Phone'; + + @override + String get userStatus => 'Status'; + + @override + String get userRole => 'Role'; + + @override + String get userAddedAt => 'Added At'; + + @override + String get lastActive => 'Last Active'; + + @override + String get inactive => 'Inactive'; + + @override + String get pending => 'Pending'; + + @override + String get admin => 'Admin'; + + @override + String get viewer => 'Viewer'; + + @override + String get editPermissions => 'Edit Permissions'; + + @override + String get savePermissions => 'Save Permissions'; + + @override + String get noUsersFound => 'No users found'; + + @override + String get searchUsers => 'Search users...'; + + @override + String get filterByStatus => 'Filter by Status'; + + @override + String get filterByRole => 'Filter by Role'; + + @override + String get allStatuses => 'All Statuses'; + + @override + String get allRoles => 'All Roles'; + + @override + String get permissionDashboard => 'Dashboard Access'; + + @override + String get permissionPeople => 'People Access'; + + @override + String get permissionReceipts => 'Receipts Access'; + + @override + String get permissionPayments => 'Payments Access'; + + @override + String get permissionReports => 'Reports Access'; + + @override + String get permissionSettings => 'Settings Access'; + + @override + String get permissionUsers => 'Users Access'; + + @override + String get permissionPrint => 'Print Access'; + + @override + String get ownerWarning => + 'Warning: Business owner does not need to be added and always has full access to all sections'; + + @override + String get ownerWarningTitle => 'Business Owner'; + + @override + String get alreadyAddedWarning => + 'This user has already been added to the business'; + + @override + String get alreadyAddedWarningTitle => 'Existing User'; } diff --git a/hesabixUI/hesabix_ui/lib/l10n/app_localizations_fa.dart b/hesabixUI/hesabix_ui/lib/l10n/app_localizations_fa.dart index 7bb21b6..0c73388 100644 --- a/hesabixUI/hesabix_ui/lib/l10n/app_localizations_fa.dart +++ b/hesabixUI/hesabix_ui/lib/l10n/app_localizations_fa.dart @@ -1346,4 +1346,206 @@ class AppLocalizationsFa extends AppLocalizations { @override String get accessDenied => 'دسترسی غیرمجاز'; + + @override + String get basicTools => 'ابزارهای پایه'; + + @override + String get businessSettings => 'تنظیمات کسب و کار'; + + @override + String get printDocuments => 'چاپ اسناد'; + + @override + String get people => 'اشخاص'; + + @override + String get peopleList => 'لیست اشخاص'; + + @override + String get receipts => 'دریافت‌ها'; + + @override + String get payments => 'پرداخت‌ها'; + + @override + String get practicalTools => 'ابزارهای کاربردی'; + + @override + String get usersAndPermissions => 'کاربران و دسترسی‌ها'; + + @override + String get businessUsers => 'کاربران کسب و کار'; + + @override + String get addNewUser => 'افزودن کاربر جدید'; + + @override + String get userEmailOrPhone => 'ایمیل یا شماره تلفن'; + + @override + String get userEmailOrPhoneHint => 'ایمیل یا شماره تلفن کاربر را وارد کنید'; + + @override + String get addUser => 'افزودن کاربر'; + + @override + String get userAddedSuccessfully => 'کاربر با موفقیت اضافه شد'; + + @override + String get userAddFailed => 'خطا در افزودن کاربر'; + + @override + String get userRemovedSuccessfully => 'کاربر با موفقیت حذف شد'; + + @override + String get userRemoveFailed => 'خطا در حذف کاربر'; + + @override + String get permissionsUpdatedSuccessfully => + 'دسترسی‌ها با موفقیت به‌روزرسانی شد'; + + @override + String get permissionsUpdateFailed => 'خطا در به‌روزرسانی دسترسی‌ها'; + + @override + String get userNotFound => 'کاربر یافت نشد'; + + @override + String get invalidEmailOrPhone => 'ایمیل یا شماره تلفن نامعتبر است'; + + @override + String get userAlreadyExists => 'کاربر قبلاً اضافه شده است'; + + @override + String get removeUser => 'حذف کاربر'; + + @override + String get removeUserConfirm => + 'آیا مطمئن هستید که می‌خواهید این کاربر را حذف کنید؟'; + + @override + String get userPermissions => 'دسترسی‌های کاربر'; + + @override + String get permissions => 'دسترسی‌ها'; + + @override + String get permission => 'دسترسی'; + + @override + String get hasPermission => 'دارای دسترسی'; + + @override + String get noPermission => 'بدون دسترسی'; + + @override + String get viewUsers => 'مشاهده کاربران'; + + @override + String get managePermissions => 'مدیریت دسترسی‌ها'; + + @override + String get totalUsers => 'کل کاربران'; + + @override + String get activeUsers => 'کاربران فعال'; + + @override + String get pendingUsers => 'کاربران در انتظار'; + + @override + String get userName => 'نام کاربر'; + + @override + String get userEmail => 'ایمیل'; + + @override + String get userPhone => 'تلفن'; + + @override + String get userStatus => 'وضعیت'; + + @override + String get userRole => 'نقش'; + + @override + String get userAddedAt => 'تاریخ افزودن'; + + @override + String get lastActive => 'آخرین فعالیت'; + + @override + String get inactive => 'غیرفعال'; + + @override + String get pending => 'در انتظار'; + + @override + String get admin => 'مدیر'; + + @override + String get viewer => 'مشاهده‌گر'; + + @override + String get editPermissions => 'ویرایش دسترسی‌ها'; + + @override + String get savePermissions => 'ذخیره دسترسی‌ها'; + + @override + String get noUsersFound => 'هیچ کاربری یافت نشد'; + + @override + String get searchUsers => 'جستجوی کاربران...'; + + @override + String get filterByStatus => 'فیلتر بر اساس وضعیت'; + + @override + String get filterByRole => 'فیلتر بر اساس نقش'; + + @override + String get allStatuses => 'همه وضعیت‌ها'; + + @override + String get allRoles => 'همه نقش‌ها'; + + @override + String get permissionDashboard => 'دسترسی به داشبورد'; + + @override + String get permissionPeople => 'دسترسی به اشخاص'; + + @override + String get permissionReceipts => 'دسترسی به دریافت‌ها'; + + @override + String get permissionPayments => 'دسترسی به پرداخت‌ها'; + + @override + String get permissionReports => 'دسترسی به گزارش‌ها'; + + @override + String get permissionSettings => 'دسترسی به تنظیمات'; + + @override + String get permissionUsers => 'دسترسی به کاربران'; + + @override + String get permissionPrint => 'دسترسی به چاپ اسناد'; + + @override + String get ownerWarning => + 'هشدار: کاربر مالک کسب و کار نیازی به افزودن ندارد و همیشه دسترسی کامل به همه بخش‌ها دارد'; + + @override + String get ownerWarningTitle => 'کاربر مالک'; + + @override + String get alreadyAddedWarning => + 'این کاربر قبلاً به کسب و کار اضافه شده است'; + + @override + String get alreadyAddedWarningTitle => 'کاربر موجود'; } diff --git a/hesabixUI/hesabix_ui/lib/main.dart b/hesabixUI/hesabix_ui/lib/main.dart index 5cb3947..42fa7e3 100644 --- a/hesabixUI/hesabix_ui/lib/main.dart +++ b/hesabixUI/hesabix_ui/lib/main.dart @@ -20,6 +20,7 @@ import 'pages/admin/system_logs_page.dart'; import 'pages/admin/email_settings_page.dart'; import 'pages/business/business_shell.dart'; import 'pages/business/dashboard/business_dashboard_page.dart'; +import 'pages/business/users_permissions_page.dart'; import 'package:hesabix_ui/l10n/app_localizations.dart'; import 'core/locale_controller.dart'; import 'core/calendar_controller.dart'; @@ -29,6 +30,7 @@ import 'theme/app_theme.dart'; import 'core/auth_store.dart'; import 'core/permission_guard.dart'; import 'widgets/simple_splash_screen.dart'; +import 'widgets/url_tracker.dart'; void main() { // Use path-based routing instead of hash routing @@ -98,15 +100,34 @@ class _MyAppState extends State { ApiClient.bindCalendarController(_calendarController!); ApiClient.bindAuthStore(_authStore!); - // اطمینان از حداقل 4 ثانیه نمایش splash screen + // اطمینان از حداقل 1 ثانیه نمایش splash screen final elapsed = DateTime.now().difference(_loadStartTime!); - const minimumDuration = Duration(seconds: 4); + const minimumDuration = Duration(seconds: 1); if (elapsed < minimumDuration) { await Future.delayed(minimumDuration - elapsed); } + // ذخیره URL فعلی قبل از اتمام loading + if (_authStore != null) { + try { + final currentUrl = Uri.base.path; + print('🔍 LOADING DEBUG: Current URL before finishing loading: $currentUrl'); + + if (currentUrl.isNotEmpty && + currentUrl != '/' && + currentUrl != '/login' && + (currentUrl.startsWith('/user/profile/') || currentUrl.startsWith('/business/'))) { + print('🔍 LOADING DEBUG: Saving current URL: $currentUrl'); + await _authStore!.saveLastUrl(currentUrl); + } + } catch (e) { + print('🔍 LOADING DEBUG: Error saving current URL: $e'); + } + } + // اتمام loading if (mounted) { + print('🔍 LOADING DEBUG: Finishing loading, setting _isLoading to false'); setState(() { _isLoading = false; }); @@ -116,12 +137,16 @@ class _MyAppState extends State { // Root of application with GoRouter @override Widget build(BuildContext context) { + print('🔍 BUILD DEBUG: Building app, _isLoading: $_isLoading'); + print('🔍 BUILD DEBUG: Controllers - locale: ${_controller != null}, calendar: ${_calendarController != null}, theme: ${_themeController != null}, auth: ${_authStore != null}'); + // اگر هنوز loading است، splash screen نمایش بده if (_isLoading || _controller == null || _calendarController == null || _themeController == null || _authStore == null) { + print('🔍 BUILD DEBUG: Still loading, showing splash screen'); final loadingRouter = GoRouter( redirect: (context, state) { // در حین loading، هیچ redirect نکن - URL را حفظ کن @@ -165,11 +190,13 @@ class _MyAppState extends State { return SimpleSplashScreen( message: loadingMessage, showLogo: true, - displayDuration: const Duration(seconds: 4), + displayDuration: const Duration(seconds: 1), locale: _controller?.locale, + authStore: _authStore, onComplete: () { // این callback زمانی فراخوانی می‌شود که splash screen تمام شود // اما ما از splash controller استفاده می‌کنیم + print('🔍 SPLASH DEBUG: Splash screen completed'); }, ); }, @@ -194,41 +221,69 @@ class _MyAppState extends State { final controller = _controller!; final themeController = _themeController!; + print('🔍 BUILD DEBUG: All controllers loaded, creating main router'); final router = GoRouter( initialLocation: '/', - redirect: (context, state) { + redirect: (context, state) async { final currentPath = state.uri.path; + final fullUri = state.uri.toString(); + print('🔍 REDIRECT DEBUG: Current path: $currentPath'); + print('🔍 REDIRECT DEBUG: Full URI: $fullUri'); // اگر authStore هنوز load نشده، منتظر بمان if (_authStore == null) { + print('🔍 REDIRECT DEBUG: AuthStore is null, staying on current path'); return null; } final hasKey = _authStore!.apiKey != null && _authStore!.apiKey!.isNotEmpty; + print('🔍 REDIRECT DEBUG: Has API key: $hasKey'); // اگر API key ندارد if (!hasKey) { + print('🔍 REDIRECT DEBUG: No API key'); // اگر در login نیست، به login برود if (currentPath != '/login') { + print('🔍 REDIRECT DEBUG: Redirecting to login from $currentPath'); return '/login'; } // اگر در login است، بماند + print('🔍 REDIRECT DEBUG: Already on login, staying'); return null; } // اگر API key دارد + print('🔍 REDIRECT DEBUG: Has API key, checking current path'); + // اگر در login است، به dashboard برود if (currentPath == '/login') { + print('🔍 REDIRECT DEBUG: On login page, redirecting to dashboard'); return '/user/profile/dashboard'; } - // اگر در root است، به dashboard برود + // اگر در root است، آخرین URL را بررسی کن if (currentPath == '/') { + print('🔍 REDIRECT DEBUG: On root path, checking last URL'); + // اگر آخرین URL موجود است و معتبر است، به آن برود + final lastUrl = await _authStore!.getLastUrl(); + print('🔍 REDIRECT DEBUG: Last URL: $lastUrl'); + + if (lastUrl != null && + lastUrl.isNotEmpty && + lastUrl != '/' && + lastUrl != '/login' && + (lastUrl.startsWith('/user/profile/') || lastUrl.startsWith('/business/'))) { + print('🔍 REDIRECT DEBUG: Redirecting to last URL: $lastUrl'); + return lastUrl; + } + // وگرنه به dashboard برود (فقط اگر در root باشیم) + print('🔍 REDIRECT DEBUG: No valid last URL, redirecting to dashboard'); return '/user/profile/dashboard'; } - // برای سایر صفحات (شامل صفحات profile)، redirect نکن (بماند) - // این مهم است: اگر کاربر در صفحات profile است، بماند + // برای سایر صفحات (شامل صفحات profile و business)، redirect نکن (بماند) + // این مهم است: اگر کاربر در صفحات profile یا business است، بماند + print('🔍 REDIRECT DEBUG: On other page ($currentPath), staying on current path'); return null; }, routes: [ @@ -373,7 +428,9 @@ class _MyAppState extends State { return BusinessShell( businessId: businessId, authStore: _authStore!, + localeController: controller, calendarController: _calendarController!, + themeController: themeController, child: const SizedBox.shrink(), // Will be replaced by child routes ); }, @@ -386,11 +443,32 @@ class _MyAppState extends State { return BusinessShell( businessId: businessId, authStore: _authStore!, + localeController: controller, calendarController: _calendarController!, + themeController: themeController, child: BusinessDashboardPage(businessId: businessId), ); }, ), + GoRoute( + path: 'users-permissions', + name: 'business_users_permissions', + builder: (context, state) { + final businessId = int.parse(state.pathParameters['business_id']!); + return BusinessShell( + businessId: businessId, + authStore: _authStore!, + localeController: controller, + calendarController: _calendarController!, + themeController: themeController, + child: UsersPermissionsPage( + businessId: businessId.toString(), + authStore: _authStore!, + calendarController: _calendarController!, + ), + ); + }, + ), // TODO: Add other business routes (sales, accounting, etc.) ], ), @@ -400,28 +478,31 @@ class _MyAppState extends State { return AnimatedBuilder( animation: Listenable.merge([controller, themeController]), builder: (context, _) { - return MaterialApp.router( - title: 'Hesabix', - theme: AppTheme.build( - isDark: false, + return UrlTracker( + authStore: _authStore!, + child: MaterialApp.router( + title: 'Hesabix', + theme: AppTheme.build( + isDark: false, + locale: controller.locale, + seed: themeController.seedColor, + ), + darkTheme: AppTheme.build( + isDark: true, + locale: controller.locale, + seed: themeController.seedColor, + ), + themeMode: themeController.mode, + routerConfig: router, locale: controller.locale, - seed: themeController.seedColor, + supportedLocales: AppLocalizations.supportedLocales, + localizationsDelegates: const [ + AppLocalizations.delegate, + GlobalMaterialLocalizations.delegate, + GlobalCupertinoLocalizations.delegate, + GlobalWidgetsLocalizations.delegate, + ], ), - darkTheme: AppTheme.build( - isDark: true, - locale: controller.locale, - seed: themeController.seedColor, - ), - themeMode: themeController.mode, - routerConfig: router, - locale: controller.locale, - supportedLocales: AppLocalizations.supportedLocales, - localizationsDelegates: const [ - AppLocalizations.delegate, - GlobalMaterialLocalizations.delegate, - GlobalCupertinoLocalizations.delegate, - GlobalWidgetsLocalizations.delegate, - ], ); }, ); diff --git a/hesabixUI/hesabix_ui/lib/models/business_user_model.dart b/hesabixUI/hesabix_ui/lib/models/business_user_model.dart new file mode 100644 index 0000000..f7753c8 --- /dev/null +++ b/hesabixUI/hesabix_ui/lib/models/business_user_model.dart @@ -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 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 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?) ?? {}, + ); + } + + 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 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? 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?; + 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 get availableSections { + return permissions.keys.toList(); + } + + // Get all actions for a section + List getActionsForSection(String section) { + final sectionPerms = permissions[section] as Map?; + 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 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 json) { + return AddUserResponse( + success: json['success'] as bool? ?? false, + message: json['message'] as String? ?? '', + user: json['user'] != null + ? BusinessUser.fromJson(json['user'] as Map) + : null, + ); + } +} + +class UpdatePermissionsRequest { + final int businessId; + final int userId; + final Map permissions; + + const UpdatePermissionsRequest({ + required this.businessId, + required this.userId, + required this.permissions, + }); + + Map 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 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 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 json) { + return RemoveUserResponse( + success: json['success'] as bool? ?? false, + message: json['message'] as String? ?? '', + ); + } +} + +class BusinessUsersResponse { + final bool success; + final String message; + final List users; + final int totalCount; + + const BusinessUsersResponse({ + required this.success, + required this.message, + required this.users, + required this.totalCount, + }); + + factory BusinessUsersResponse.fromJson(Map json) { + return BusinessUsersResponse( + success: json['success'] as bool? ?? false, + message: json['message'] as String? ?? '', + users: (json['users'] as List?) + ?.map((userJson) => BusinessUser.fromJson(userJson as Map)) + .toList() ?? [], + totalCount: json['total_count'] as int? ?? 0, + ); + } +} \ No newline at end of file diff --git a/hesabixUI/hesabix_ui/lib/models/person_model.dart b/hesabixUI/hesabix_ui/lib/models/person_model.dart new file mode 100644 index 0000000..e69de29 diff --git a/hesabixUI/hesabix_ui/lib/pages/admin/user_management_page.dart b/hesabixUI/hesabix_ui/lib/pages/admin/user_management_page.dart index 7161347..9254381 100644 --- a/hesabixUI/hesabix_ui/lib/pages/admin/user_management_page.dart +++ b/hesabixUI/hesabix_ui/lib/pages/admin/user_management_page.dart @@ -341,11 +341,11 @@ class _UserManagementPageState extends State { content: const Text('User creation form would go here'), actions: [ TextButton( - onPressed: () => Navigator.of(context).pop(), + onPressed: () => context.pop(), child: Text(AppLocalizations.of(context).cancel), ), ElevatedButton( - onPressed: () => Navigator.of(context).pop(), + onPressed: () => context.pop(), child: Text(AppLocalizations.of(context).save), ), ], @@ -361,11 +361,11 @@ class _UserManagementPageState extends State { content: const Text('User edit form would go here'), actions: [ TextButton( - onPressed: () => Navigator.of(context).pop(), + onPressed: () => context.pop(), child: Text(AppLocalizations.of(context).cancel), ), ElevatedButton( - onPressed: () => Navigator.of(context).pop(), + onPressed: () => context.pop(), child: Text(AppLocalizations.of(context).save), ), ], @@ -381,11 +381,11 @@ class _UserManagementPageState extends State { content: const Text('Permissions management would go here'), actions: [ TextButton( - onPressed: () => Navigator.of(context).pop(), + onPressed: () => context.pop(), child: Text(AppLocalizations.of(context).cancel), ), ElevatedButton( - onPressed: () => Navigator.of(context).pop(), + onPressed: () => context.pop(), child: Text(AppLocalizations.of(context).save), ), ], @@ -401,12 +401,12 @@ class _UserManagementPageState extends State { content: Text('Are you sure you want to delete ${user['name']}?'), actions: [ TextButton( - onPressed: () => Navigator.of(context).pop(), + onPressed: () => context.pop(), child: Text(AppLocalizations.of(context).cancel), ), ElevatedButton( onPressed: () { - Navigator.of(context).pop(); + context.pop(); // Delete user logic here }, style: ElevatedButton.styleFrom(backgroundColor: Colors.red), diff --git a/hesabixUI/hesabix_ui/lib/pages/business/business_shell.dart b/hesabixUI/hesabix_ui/lib/pages/business/business_shell.dart index a08f94b..aec6d5d 100644 --- a/hesabixUI/hesabix_ui/lib/pages/business/business_shell.dart +++ b/hesabixUI/hesabix_ui/lib/pages/business/business_shell.dart @@ -1,21 +1,29 @@ import 'package:flutter/material.dart'; import 'package:go_router/go_router.dart'; -import 'package:hesabix_ui/l10n/app_localizations.dart'; import '../../core/auth_store.dart'; +import '../../core/locale_controller.dart'; import '../../core/calendar_controller.dart'; +import '../../theme/theme_controller.dart'; +import '../../widgets/settings_menu_button.dart'; +import '../../widgets/user_account_menu_button.dart'; +import 'package:hesabix_ui/l10n/app_localizations.dart'; class BusinessShell extends StatefulWidget { final int businessId; - final AuthStore authStore; - final CalendarController calendarController; final Widget child; + final AuthStore authStore; + final LocaleController? localeController; + final CalendarController? calendarController; + final ThemeController? themeController; const BusinessShell({ super.key, required this.businessId, - required this.authStore, - required this.calendarController, required this.child, + required this.authStore, + this.localeController, + this.calendarController, + this.themeController, }); @override @@ -23,6 +31,9 @@ class BusinessShell extends StatefulWidget { } class _BusinessShellState extends State { + int _hoverIndex = -1; + bool _isBasicToolsExpanded = false; + bool _isPeopleExpanded = false; @override void initState() { @@ -41,40 +52,206 @@ class _BusinessShellState extends State { final bool useRail = width >= 700; final bool railExtended = width >= 1100; final ColorScheme scheme = Theme.of(context).colorScheme; - final String location = GoRouterState.of(context).uri.toString(); + String location = '/business/${widget.businessId}/dashboard'; // default location + try { + location = GoRouterState.of(context).uri.toString(); + } catch (e) { + // اگر GoRouterState در دسترس نیست، از default استفاده کن + } final bool isDark = Theme.of(context).brightness == Brightness.dark; final String logoAsset = isDark ? 'assets/images/logo-light.png' : 'assets/images/logo-light.png'; final t = AppLocalizations.of(context); - final destinations = <_Dest>[ - _Dest(t.businessDashboard, Icons.dashboard_outlined, Icons.dashboard, '/business/${widget.businessId}/dashboard'), - _Dest(t.sales, Icons.sell, Icons.sell, '/business/${widget.businessId}/sales'), - _Dest(t.accounting, Icons.account_balance, Icons.account_balance, '/business/${widget.businessId}/accounting'), - _Dest(t.inventory, Icons.inventory, Icons.inventory, '/business/${widget.businessId}/inventory'), - _Dest(t.reports, Icons.assessment, Icons.assessment, '/business/${widget.businessId}/reports'), - _Dest(t.members, Icons.people, Icons.people, '/business/${widget.businessId}/members'), - _Dest(t.settings, Icons.settings, Icons.settings, '/business/${widget.businessId}/settings'), + + // ساختار متمرکز منو + final menuItems = <_MenuItem>[ + _MenuItem( + label: t.businessDashboard, + icon: Icons.dashboard_outlined, + selectedIcon: Icons.dashboard, + path: '/business/${widget.businessId}/dashboard', + type: _MenuItemType.simple, + ), + _MenuItem( + label: t.practicalTools, + icon: Icons.category, + selectedIcon: Icons.category, + path: null, // آیتم جداکننده + type: _MenuItemType.separator, + ), + _MenuItem( + label: t.people, + icon: Icons.people, + selectedIcon: Icons.people, + path: null, // برای منوی بازشونده + type: _MenuItemType.expandable, + children: [ + _MenuItem( + label: t.peopleList, + icon: Icons.list, + selectedIcon: Icons.list, + path: '/business/${widget.businessId}/people-list', + type: _MenuItemType.simple, + ), + _MenuItem( + label: t.receipts, + icon: Icons.receipt, + selectedIcon: Icons.receipt, + path: '/business/${widget.businessId}/receipts', + type: _MenuItemType.simple, + hasAddButton: true, + ), + _MenuItem( + label: t.payments, + icon: Icons.payment, + selectedIcon: Icons.payment, + path: '/business/${widget.businessId}/payments', + type: _MenuItemType.simple, + hasAddButton: true, + ), + ], + ), + _MenuItem( + label: t.settings, + icon: Icons.settings, + selectedIcon: Icons.settings, + path: null, // برای منوی بازشونده + type: _MenuItemType.expandable, + children: [ + _MenuItem( + label: t.businessSettings, + icon: Icons.business, + selectedIcon: Icons.business, + path: '/business/${widget.businessId}/business-settings', + type: _MenuItemType.simple, + ), + _MenuItem( + label: t.printDocuments, + icon: Icons.print, + selectedIcon: Icons.print, + path: '/business/${widget.businessId}/print-documents', + type: _MenuItemType.simple, + ), + _MenuItem( + label: t.usersAndPermissions, + icon: Icons.people_outline, + selectedIcon: Icons.people, + path: '/business/${widget.businessId}/users-permissions', + type: _MenuItemType.simple, + ), + ], + ), ]; int selectedIndex = 0; - for (int i = 0; i < destinations.length; i++) { - if (location.startsWith(destinations[i].path)) { + for (int i = 0; i < menuItems.length; i++) { + final item = menuItems[i]; + if (item.type == _MenuItemType.separator) continue; // نادیده گرفتن آیتم جداکننده + + if (item.type == _MenuItemType.simple && item.path != null && location.startsWith(item.path!)) { selectedIndex = i; break; + } else if (item.type == _MenuItemType.expandable && item.children != null) { + for (int j = 0; j < item.children!.length; j++) { + final child = item.children![j]; + if (child.path != null && location.startsWith(child.path!)) { + selectedIndex = i; + // تنظیم وضعیت باز بودن منو + if (i == 2) _isPeopleExpanded = true; // اشخاص در ایندکس 2 + if (i == 3) _isBasicToolsExpanded = true; // تنظیمات در ایندکس 3 + break; + } + } } } Future onSelect(int index) async { - final path = destinations[index].path; - if (GoRouterState.of(context).uri.toString() != path) { - context.go(path); + final item = menuItems[index]; + if (item.type == _MenuItemType.separator) return; // آیتم جداکننده قابل کلیک نیست + + if (item.type == _MenuItemType.simple && item.path != null) { + try { + if (GoRouterState.of(context).uri.toString() != item.path!) { + context.go(item.path!); + } + } catch (e) { + // اگر GoRouterState در دسترس نیست، مستقیماً به مسیر برود + context.go(item.path!); + } + } else if (item.type == _MenuItemType.expandable) { + // تغییر وضعیت باز/بسته بودن منو + if (item.label == t.people) _isPeopleExpanded = !_isPeopleExpanded; + if (item.label == t.settings) _isBasicToolsExpanded = !_isBasicToolsExpanded; + setState(() {}); } } - Future onBackToProfile() async { - context.go('/user/profile/businesses'); + Future onSelectChild(int parentIndex, int childIndex) async { + final parent = menuItems[parentIndex]; + if (parent.type == _MenuItemType.expandable && parent.children != null) { + final child = parent.children![childIndex]; + if (child.path != null) { + try { + if (GoRouterState.of(context).uri.toString() != child.path!) { + context.go(child.path!); + } + } catch (e) { + // اگر GoRouterState در دسترس نیست، مستقیماً به مسیر برود + context.go(child.path!); + } + } + } + } + + + Future onLogout() async { + await widget.authStore.saveApiKey(null); + if (!context.mounted) return; + ScaffoldMessenger.of(context) + ..hideCurrentSnackBar() + ..showSnackBar(const SnackBar(content: Text('خروج انجام شد'))); + context.go('/login'); + } + + bool isExpanded(_MenuItem item) { + if (item.label == t.people) return _isPeopleExpanded; + if (item.label == t.settings) return _isBasicToolsExpanded; + return false; + } + + int getTotalMenuItemsCount() { + int count = 0; + for (final item in menuItems) { + if (item.type == _MenuItemType.separator) { + count++; // آیتم جداکننده هم شمرده می‌شود + } else { + count++; // آیتم اصلی + if (item.type == _MenuItemType.expandable && isExpanded(item) && railExtended) { + count += item.children?.length ?? 0; + } + } + } + return count; + } + + int getMenuIndexFromTotalIndex(int totalIndex) { + int currentIndex = 0; + for (int i = 0; i < menuItems.length; i++) { + if (currentIndex == totalIndex) return i; + currentIndex++; + + final item = menuItems[i]; + if (item.type == _MenuItemType.expandable && isExpanded(item) && railExtended) { + final childrenCount = item.children?.length ?? 0; + if (totalIndex >= currentIndex && totalIndex < currentIndex + childrenCount) { + return i; + } + currentIndex += childrenCount; + } + } + return 0; } // Brand top bar with contrast color @@ -88,86 +265,431 @@ class _BusinessShellState extends State { final appBar = AppBar( backgroundColor: appBarBg, foregroundColor: appBarFg, - elevation: 0, + automaticallyImplyLeading: !useRail, + titleSpacing: 0, title: Row( children: [ - Image.asset( - logoAsset, - height: 32, - width: 32, - errorBuilder: (context, error, stackTrace) => Icon( - Icons.business, - color: appBarFg, - size: 32, - ), - ), const SizedBox(width: 12), - Text( - 'Hesabix', - style: TextStyle( - color: appBarFg, - fontWeight: FontWeight.bold, - fontSize: 20, - ), - ), + Image.asset(logoAsset, height: 28), + const SizedBox(width: 12), + Text(t.appTitle, style: TextStyle(color: appBarFg, fontWeight: FontWeight.w700)), ], ), + leading: useRail + ? null + : Builder( + builder: (ctx) => IconButton( + icon: Icon(Icons.menu, color: appBarFg), + onPressed: () => Scaffold.of(ctx).openDrawer(), + tooltip: t.menu, + ), + ), actions: [ - IconButton( - icon: Icon(Icons.arrow_back, color: appBarFg), - onPressed: onBackToProfile, - tooltip: t.backToProfile, + SettingsMenuButton( + localeController: widget.localeController, + calendarController: widget.calendarController, + themeController: widget.themeController, ), const SizedBox(width: 8), + UserAccountMenuButton(authStore: widget.authStore), + const SizedBox(width: 8), ], ); + final content = Container( + color: scheme.surface, + child: SafeArea( + child: widget.child, + ), + ); + + // Side colors and styles + final Color sideBg = Theme.of(context).brightness == Brightness.dark + ? scheme.surfaceContainerHighest + : scheme.surfaceContainerLow; + final Color sideFg = scheme.onSurfaceVariant; + final Color activeBg = scheme.primaryContainer; + final Color activeFg = scheme.onPrimaryContainer; + if (useRail) { return Scaffold( appBar: appBar, body: Row( children: [ - NavigationRail( - selectedIndex: selectedIndex, - onDestinationSelected: onSelect, - labelType: railExtended ? NavigationRailLabelType.selected : NavigationRailLabelType.all, - extended: railExtended, - destinations: destinations.map((dest) => NavigationRailDestination( - icon: Icon(dest.icon), - selectedIcon: Icon(dest.selectedIcon), - label: Text(dest.label), - )).toList(), + Container( + width: railExtended ? 240 : 88, + height: double.infinity, + color: sideBg, + child: ListView.builder( + padding: EdgeInsets.zero, + itemCount: getTotalMenuItemsCount(), + itemBuilder: (ctx, index) { + final menuIndex = getMenuIndexFromTotalIndex(index); + final item = menuItems[menuIndex]; + final bool isHovered = index == _hoverIndex; + final bool isSelected = menuIndex == selectedIndex; + final bool active = isSelected || isHovered; + final BorderRadius br = (isSelected && useRail) + ? BorderRadius.zero + : (isHovered ? BorderRadius.zero : BorderRadius.circular(8)); + final Color bgColor = active + ? (isHovered && !isSelected ? activeBg.withValues(alpha: 0.85) : activeBg) + : Colors.transparent; + + // اگر آیتم بازشونده است و در حالت باز است، زیرآیتم‌ها را نمایش بده + if (item.type == _MenuItemType.expandable && isExpanded(item) && railExtended) { + if (index == getMenuIndexFromTotalIndex(index)) { + // آیتم اصلی + return MouseRegion( + onEnter: (_) => setState(() => _hoverIndex = index), + onExit: (_) => setState(() => _hoverIndex = -1), + child: InkWell( + borderRadius: br, + onTap: () { + setState(() { + if (item.label == t.people) _isPeopleExpanded = !_isPeopleExpanded; + if (item.label == t.settings) _isBasicToolsExpanded = !_isBasicToolsExpanded; + }); + }, + child: Container( + margin: EdgeInsets.zero, + padding: EdgeInsets.symmetric( + horizontal: railExtended ? 16 : 8, + vertical: 8, + ), + decoration: BoxDecoration( + color: bgColor, + borderRadius: br, + ), + child: Row( + children: [ + Icon( + active ? item.selectedIcon : item.icon, + color: active ? activeFg : sideFg, + size: 24, + ), + if (railExtended) ...[ + const SizedBox(width: 12), + Expanded( + child: Text( + item.label, + style: TextStyle( + color: active ? activeFg : sideFg, + fontWeight: active ? FontWeight.w600 : FontWeight.w400, + ), + ), + ), + Icon( + isExpanded(item) ? Icons.expand_less : Icons.expand_more, + color: sideFg, + size: 20, + ), + ], + ], + ), + ), + ), + ); + } else { + // زیرآیتم‌ها + final childIndex = index - getMenuIndexFromTotalIndex(index) - 1; + if (childIndex < (item.children?.length ?? 0)) { + final child = item.children![childIndex]; + return MouseRegion( + onEnter: (_) => setState(() => _hoverIndex = index), + onExit: (_) => setState(() => _hoverIndex = -1), + child: InkWell( + borderRadius: br, + onTap: () => onSelectChild(menuIndex, childIndex), + child: Container( + margin: EdgeInsets.zero, + padding: EdgeInsets.symmetric( + horizontal: railExtended ? 24 : 16, // بیشتر indent برای زیرآیتم + vertical: 8, + ), + decoration: BoxDecoration( + color: bgColor, + borderRadius: br, + ), + child: Row( + children: [ + Icon( + child.icon, + color: sideFg, + size: 20, + ), + if (railExtended) ...[ + const SizedBox(width: 12), + Expanded( + child: Text( + child.label, + style: TextStyle( + color: sideFg, + fontWeight: FontWeight.w400, + ), + ), + ), + if (child.hasAddButton) + IconButton( + icon: const Icon(Icons.add, size: 16), + onPressed: () { + // Navigate to add new receipt/payment + if (child.label == t.receipts) { + // Navigate to add receipt + } else if (child.label == t.payments) { + // Navigate to add payment + } + }, + ), + ], + ], + ), + ), + ), + ); + } + } + } else { + // آیتم ساده، آیتم بازشونده در حالت بسته، یا آیتم جداکننده + if (item.type == _MenuItemType.separator) { + // آیتم جداکننده + return Container( + margin: EdgeInsets.symmetric( + horizontal: railExtended ? 16 : 8, + vertical: 8, + ), + child: Row( + children: [ + if (railExtended) ...[ + Expanded( + child: Divider( + color: sideFg.withValues(alpha: 0.3), + thickness: 1, + ), + ), + const SizedBox(width: 12), + Text( + item.label, + style: TextStyle( + color: sideFg.withValues(alpha: 0.7), + fontSize: 12, + fontWeight: FontWeight.w500, + ), + ), + const SizedBox(width: 12), + Expanded( + child: Divider( + color: sideFg.withValues(alpha: 0.3), + thickness: 1, + ), + ), + ] else ...[ + Expanded( + child: Divider( + color: sideFg.withValues(alpha: 0.3), + thickness: 1, + ), + ), + ], + ], + ), + ); + } else { + // آیتم ساده یا آیتم بازشونده در حالت بسته + return MouseRegion( + onEnter: (_) => setState(() => _hoverIndex = index), + onExit: (_) => setState(() => _hoverIndex = -1), + child: InkWell( + borderRadius: br, + onTap: () { + if (item.type == _MenuItemType.expandable) { + setState(() { + if (item.label == t.people) _isPeopleExpanded = !_isPeopleExpanded; + if (item.label == t.settings) _isBasicToolsExpanded = !_isBasicToolsExpanded; + }); + } else { + onSelect(menuIndex); + } + }, + child: Container( + margin: EdgeInsets.zero, + padding: EdgeInsets.symmetric( + horizontal: railExtended ? 16 : 8, + vertical: 8, + ), + decoration: BoxDecoration( + color: bgColor, + borderRadius: br, + ), + child: Row( + children: [ + Icon( + active ? item.selectedIcon : item.icon, + color: active ? activeFg : sideFg, + size: 24, + ), + if (railExtended) ...[ + const SizedBox(width: 12), + Expanded( + child: Text( + item.label, + style: TextStyle( + color: active ? activeFg : sideFg, + fontWeight: active ? FontWeight.w600 : FontWeight.w400, + ), + ), + ), + if (item.type == _MenuItemType.expandable) + Icon( + isExpanded(item) ? Icons.expand_less : Icons.expand_more, + color: sideFg, + size: 20, + ), + ], + ], + ), + ), + ), + ); + } + } + return const SizedBox.shrink(); + }, + ), ), const VerticalDivider(thickness: 1, width: 1), - Expanded( - child: widget.child, - ), + Expanded(child: content), ], ), ); - } else { + } + return Scaffold( appBar: appBar, - body: widget.child, - bottomNavigationBar: NavigationBar( - selectedIndex: selectedIndex, - onDestinationSelected: onSelect, - destinations: destinations.map((dest) => NavigationDestination( - icon: Icon(dest.icon), - selectedIcon: Icon(dest.selectedIcon), - label: dest.label, - )).toList(), + drawer: Drawer( + backgroundColor: sideBg, + child: SafeArea( + child: ListView( + padding: const EdgeInsets.symmetric(vertical: 8), + children: [ + // آیتم‌های منو + for (int i = 0; i < menuItems.length; i++) ...[ + Builder(builder: (ctx) { + final item = menuItems[i]; + final bool active = i == selectedIndex; + + if (item.type == _MenuItemType.separator) { + // آیتم جداکننده در منوی موبایل + return Container( + margin: const EdgeInsets.symmetric(horizontal: 16, vertical: 8), + child: Row( + children: [ + Expanded( + child: Divider( + color: sideFg.withValues(alpha: 0.3), + thickness: 1, + ), + ), + const SizedBox(width: 12), + Text( + item.label, + style: TextStyle( + color: sideFg.withValues(alpha: 0.7), + fontSize: 12, + fontWeight: FontWeight.w500, + ), + ), + const SizedBox(width: 12), + Expanded( + child: Divider( + color: sideFg.withValues(alpha: 0.3), + thickness: 1, + ), + ), + ], + ), + ); + } else if (item.type == _MenuItemType.simple) { + return ListTile( + leading: Icon(item.selectedIcon, color: active ? activeFg : sideFg), + title: Text(item.label, style: TextStyle(color: active ? activeFg : sideFg, fontWeight: active ? FontWeight.w600 : FontWeight.w400)), + selected: active, + selectedTileColor: activeBg, + onTap: () { + context.pop(); + onSelect(i); + }, + ); + } else if (item.type == _MenuItemType.expandable) { + return ExpansionTile( + leading: Icon(item.icon, color: sideFg), + title: Text(item.label, style: TextStyle(color: sideFg)), + initiallyExpanded: isExpanded(item), + onExpansionChanged: (expanded) { + setState(() { + if (item.label == t.people) _isPeopleExpanded = expanded; + if (item.label == t.settings) _isBasicToolsExpanded = expanded; + }); + }, + children: item.children?.map((child) => ListTile( + leading: const SizedBox(width: 24), + title: Text(child.label), + trailing: child.hasAddButton ? IconButton( + icon: const Icon(Icons.add, size: 20), + onPressed: () { + context.pop(); + // Navigate to add new receipt/payment + if (child.label == t.receipts) { + // Navigate to add receipt + } else if (child.label == t.payments) { + // Navigate to add payment + } + }, + ) : null, + onTap: () { + context.pop(); + onSelectChild(i, item.children!.indexOf(child)); + }, + )).toList() ?? [], + ); + } + return const SizedBox.shrink(); + }), + ], + + const Divider(), + ListTile( + leading: const Icon(Icons.logout), + title: Text(t.logout), + onTap: onLogout, + ), + ], + ), ), - ); - } + ), + body: content, + ); } } -class _Dest { +enum _MenuItemType { simple, expandable, separator } + +class _MenuItem { final String label; final IconData icon; final IconData selectedIcon; - final String path; - - const _Dest(this.label, this.icon, this.selectedIcon, this.path); -} + final String? path; + final _MenuItemType type; + 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, + }); +} \ No newline at end of file diff --git a/hesabixUI/hesabix_ui/lib/pages/business/users_permissions_page.dart b/hesabixUI/hesabix_ui/lib/pages/business/users_permissions_page.dart new file mode 100644 index 0000000..ed9a3d4 --- /dev/null +++ b/hesabixUI/hesabix_ui/lib/pages/business/users_permissions_page.dart @@ -0,0 +1,1353 @@ +import 'package:flutter/material.dart'; +import 'package:go_router/go_router.dart'; +import 'package:hesabix_ui/l10n/app_localizations.dart'; +import '../../core/api_client.dart'; +import '../../core/auth_store.dart'; +import '../../core/calendar_controller.dart'; +import '../../services/business_user_service.dart'; +import '../../models/business_user_model.dart'; + +class UsersPermissionsPage extends StatefulWidget { + final String businessId; + final AuthStore authStore; + final CalendarController calendarController; + + const UsersPermissionsPage({ + super.key, + required this.businessId, + required this.authStore, + required this.calendarController, + }); + + @override + State createState() => _UsersPermissionsPageState(); +} + +class _UsersPermissionsPageState extends State { + final BusinessUserService _userService = BusinessUserService(ApiClient()); + final TextEditingController _emailOrPhoneController = TextEditingController(); + + List _users = []; + bool _loading = true; + String? _error; + + @override + void initState() { + super.initState(); + _loadUsers(); + } + + @override + void dispose() { + _emailOrPhoneController.dispose(); + super.dispose(); + } + + Future _loadUsers() async { + try { + setState(() { + _loading = true; + _error = null; + }); + + final response = await _userService.getBusinessUsers(int.parse(widget.businessId)); + + if (mounted) { + setState(() { + _users = response.users; + _loading = false; + }); + } + } catch (e) { + if (mounted) { + setState(() { + _loading = false; + _error = e.toString(); + }); + _showErrorSnackBar('خطا در بارگذاری کاربران: $e'); + } + } + } + + Future _addUser() async { + if (_emailOrPhoneController.text.trim().isEmpty) { + _showErrorSnackBar('لطفاً ایمیل یا شماره تلفن را وارد کنید'); + return; + } + + // Check if trying to add business owner + if (_isTryingToAddOwner(_emailOrPhoneController.text.trim())) { + _showOwnerWarning(); + return; + } + + // Check if user already exists + if (_isUserAlreadyAdded(_emailOrPhoneController.text.trim())) { + _showAlreadyAddedWarning(); + return; + } + + try { + final request = AddUserRequest( + businessId: int.parse(widget.businessId), + emailOrPhone: _emailOrPhoneController.text.trim(), + ); + + final response = await _userService.addUser(request); + + if (response.success) { + _showSuccessSnackBar(response.message); + _emailOrPhoneController.clear(); + _loadUsers(); // Refresh the list + } else { + _showErrorSnackBar(response.message); + } + } catch (e) { + _showErrorSnackBar('خطا در افزودن کاربر: $e'); + } + } + + Future _updatePermissions(BusinessUser user, Map newPermissions) async { + try { + final request = UpdatePermissionsRequest( + businessId: int.parse(widget.businessId), + userId: user.userId, + permissions: newPermissions, + ); + + final response = await _userService.updatePermissions(request); + + if (response.success) { + _showSuccessSnackBar(response.message); + _loadUsers(); // Refresh the list + } else { + _showErrorSnackBar(response.message); + } + } catch (e) { + _showErrorSnackBar('خطا در به‌روزرسانی دسترسی‌ها: $e'); + } + } + + Future _removeUser(BusinessUser user) async { + final t = AppLocalizations.of(context); + final confirmed = await showDialog( + context: context, + builder: (context) => AlertDialog( + title: Text(t.removeUser), + content: Text(t.removeUserConfirm), + actions: [ + TextButton( + onPressed: () => context.pop(false), + child: Text(t.cancel), + ), + TextButton( + onPressed: () => context.pop(true), + child: Text(t.removeUser), + ), + ], + ), + ); + + if (confirmed != true) return; + + try { + final request = RemoveUserRequest( + businessId: int.parse(widget.businessId), + userId: user.userId, + ); + + final response = await _userService.removeUser(request); + + if (response.success) { + _showSuccessSnackBar(response.message); + _loadUsers(); // Refresh the list + } else { + _showErrorSnackBar(response.message); + } + } catch (e) { + _showErrorSnackBar('خطا در حذف کاربر: $e'); + } + } + + void _showErrorSnackBar(String message) { + ScaffoldMessenger.of(context).showSnackBar( + SnackBar( + content: Text(message), + backgroundColor: Colors.red, + ), + ); + } + + void _showSuccessSnackBar(String message) { + ScaffoldMessenger.of(context).showSnackBar( + SnackBar( + content: Text(message), + backgroundColor: Colors.green, + ), + ); + } + + bool _isTryingToAddOwner(String emailOrPhone) { + // Check if the current user is trying to add themselves (as owner) + final currentUserId = widget.authStore.currentUserId; + if (currentUserId == null) return false; + + // Find the owner in the users list + final owner = _users.where((user) => user.role == 'owner').firstOrNull; + if (owner == null) return false; + + // Check if the email/phone matches the owner's email/phone + return owner.userEmail == emailOrPhone || + (owner.userPhone != null && owner.userPhone == emailOrPhone); + } + + bool _isUserAlreadyAdded(String emailOrPhone) { + // Check if user already exists in the users list + return _users.any((user) => + user.userEmail == emailOrPhone || + (user.userPhone != null && user.userPhone == emailOrPhone)); + } + + void _showOwnerWarning() { + final t = AppLocalizations.of(context); + showDialog( + context: context, + builder: (context) => AlertDialog( + title: Row( + children: [ + Icon(Icons.warning, color: Colors.orange), + const SizedBox(width: 8), + Text(t.ownerWarningTitle), + ], + ), + content: Text(t.ownerWarning), + actions: [ + TextButton( + onPressed: () => context.pop(), + child: Text(t.cancel), + ), + ], + ), + ); + } + + void _showAlreadyAddedWarning() { + final t = AppLocalizations.of(context); + showDialog( + context: context, + builder: (context) => AlertDialog( + title: Row( + children: [ + Icon(Icons.info, color: Colors.blue), + const SizedBox(width: 8), + Text(t.alreadyAddedWarningTitle), + ], + ), + content: Text(t.alreadyAddedWarning), + actions: [ + TextButton( + onPressed: () => context.pop(), + child: Text(t.cancel), + ), + ], + ), + ); + } + + List get _filteredUsers { + return _users; + } + + @override + Widget build(BuildContext context) { + final t = AppLocalizations.of(context); + final theme = Theme.of(context); + final colorScheme = theme.colorScheme; + + return Scaffold( + backgroundColor: colorScheme.surface, + body: SafeArea( + child: Column( + children: [ + // Header Section + Container( + padding: const EdgeInsets.all(20), + decoration: BoxDecoration( + color: colorScheme.surface, + border: Border( + bottom: BorderSide( + color: colorScheme.outline.withValues(alpha: 0.2), + width: 1, + ), + ), + ), + child: Row( + children: [ + Container( + padding: const EdgeInsets.all(12), + decoration: BoxDecoration( + color: colorScheme.primaryContainer, + borderRadius: BorderRadius.circular(12), + ), + child: Icon( + Icons.people_alt_outlined, + size: 24, + color: colorScheme.onPrimaryContainer, + ), + ), + const SizedBox(width: 16), + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + t.usersAndPermissions, + style: theme.textTheme.headlineSmall?.copyWith( + fontWeight: FontWeight.bold, + color: colorScheme.onSurface, + ), + ), + const SizedBox(height: 4), + Text( + 'مدیریت کاربران و دسترسی‌های کسب و کار', + style: theme.textTheme.bodyMedium?.copyWith( + color: colorScheme.onSurfaceVariant, + ), + ), + ], + ), + ), + Container( + padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 6), + decoration: BoxDecoration( + color: colorScheme.primaryContainer, + borderRadius: BorderRadius.circular(20), + ), + child: Text( + '${_users.length} کاربر', + style: theme.textTheme.labelMedium?.copyWith( + color: colorScheme.onPrimaryContainer, + fontWeight: FontWeight.w600, + ), + ), + ), + ], + ), + ), + + // Content Area + Expanded( + child: SingleChildScrollView( + padding: const EdgeInsets.all(20), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + // Add User Section + _buildAddUserSection(t, theme, colorScheme), + const SizedBox(height: 24), + + // Users List + _buildUsersList(t, theme, colorScheme), + ], + ), + ), + ), + ], + ), + ), + ); + } + + Widget _buildAddUserSection(AppLocalizations t, ThemeData theme, ColorScheme colorScheme) { + return Container( + padding: const EdgeInsets.all(20), + decoration: BoxDecoration( + color: colorScheme.surface, + borderRadius: BorderRadius.circular(16), + border: Border.all( + color: colorScheme.outline.withValues(alpha: 0.2), + width: 1, + ), + boxShadow: [ + BoxShadow( + color: colorScheme.shadow.withValues(alpha: 0.05), + blurRadius: 8, + offset: const Offset(0, 2), + ), + ], + ), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Row( + children: [ + Icon( + Icons.person_add_outlined, + size: 20, + color: colorScheme.primary, + ), + const SizedBox(width: 8), + Text( + t.addNewUser, + style: theme.textTheme.titleMedium?.copyWith( + fontWeight: FontWeight.w600, + color: colorScheme.onSurface, + ), + ), + ], + ), + const SizedBox(height: 16), + Row( + children: [ + Expanded( + child: TextField( + controller: _emailOrPhoneController, + decoration: InputDecoration( + labelText: t.userEmailOrPhone, + hintText: t.userEmailOrPhoneHint, + prefixIcon: const Icon(Icons.email_outlined), + border: OutlineInputBorder( + borderRadius: BorderRadius.circular(12), + ), + contentPadding: const EdgeInsets.symmetric(horizontal: 16, vertical: 12), + ), + ), + ), + const SizedBox(width: 12), + ElevatedButton.icon( + onPressed: _addUser, + icon: const Icon(Icons.add, size: 18), + label: Text(t.addUser), + style: ElevatedButton.styleFrom( + padding: const EdgeInsets.symmetric(horizontal: 20, vertical: 12), + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(12), + ), + ), + ), + ], + ), + ], + ), + ); + } + + + + Widget _buildUsersList(AppLocalizations t, ThemeData theme, ColorScheme colorScheme) { + if (_loading) { + return Container( + height: 200, + child: Center( + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + CircularProgressIndicator( + color: colorScheme.primary, + ), + const SizedBox(height: 16), + Text( + t.loading, + style: theme.textTheme.bodyMedium?.copyWith( + color: colorScheme.onSurfaceVariant, + ), + ), + ], + ), + ), + ); + } + + if (_error != null) { + return Container( + height: 200, + child: Center( + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Icon( + Icons.error_outline, + size: 48, + color: colorScheme.error, + ), + const SizedBox(height: 16), + Text( + _error!, + style: theme.textTheme.bodyMedium?.copyWith( + color: colorScheme.error, + ), + textAlign: TextAlign.center, + ), + const SizedBox(height: 16), + ElevatedButton.icon( + onPressed: _loadUsers, + icon: const Icon(Icons.refresh, size: 18), + label: Text(t.retry), + style: ElevatedButton.styleFrom( + backgroundColor: colorScheme.errorContainer, + foregroundColor: colorScheme.onErrorContainer, + ), + ), + ], + ), + ), + ); + } + + if (_filteredUsers.isEmpty) { + return Container( + height: 200, + child: Center( + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Icon( + Icons.people_outline, + size: 48, + color: colorScheme.onSurfaceVariant, + ), + const SizedBox(height: 16), + Text( + t.noUsersFound, + style: theme.textTheme.bodyLarge?.copyWith( + color: colorScheme.onSurfaceVariant, + ), + ), + ], + ), + ), + ); + } + + return ListView.builder( + shrinkWrap: true, + physics: const NeverScrollableScrollPhysics(), + itemCount: _filteredUsers.length, + itemBuilder: (context, index) { + final user = _filteredUsers[index]; + return Padding( + padding: const EdgeInsets.only(bottom: 8), + child: _buildUserCard(user, t, theme, colorScheme), + ); + }, + ); + } + + + Widget _buildUserCard(BusinessUser user, AppLocalizations t, ThemeData theme, ColorScheme colorScheme) { + return Container( + decoration: BoxDecoration( + color: colorScheme.surface, + borderRadius: BorderRadius.circular(12), + border: Border.all( + color: colorScheme.outline.withValues(alpha: 0.1), + width: 1, + ), + boxShadow: [ + BoxShadow( + color: colorScheme.shadow.withValues(alpha: 0.03), + blurRadius: 4, + offset: const Offset(0, 1), + ), + ], + ), + child: Padding( + padding: const EdgeInsets.all(12), + child: Row( + children: [ + // Avatar + Container( + width: 40, + height: 40, + decoration: BoxDecoration( + color: colorScheme.primaryContainer, + borderRadius: BorderRadius.circular(10), + ), + child: Center( + child: Text( + user.userName.isNotEmpty ? user.userName[0].toUpperCase() : 'U', + style: TextStyle( + color: colorScheme.onPrimaryContainer, + fontWeight: FontWeight.bold, + fontSize: 16, + ), + ), + ), + ), + const SizedBox(width: 12), + + // User Info + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Row( + children: [ + Expanded( + child: Text( + user.userName, + style: theme.textTheme.titleSmall?.copyWith( + fontWeight: FontWeight.w600, + color: colorScheme.onSurface, + ), + overflow: TextOverflow.ellipsis, + ), + ), + if (user.role == 'owner') ...[ + const SizedBox(width: 8), + _buildOwnerChip(theme, colorScheme), + ], + ], + ), + const SizedBox(height: 2), + Text( + user.userEmail, + style: theme.textTheme.bodySmall?.copyWith( + color: colorScheme.onSurfaceVariant, + ), + overflow: TextOverflow.ellipsis, + ), + if (user.userPhone != null) ...[ + const SizedBox(height: 1), + Text( + user.userPhone!, + style: theme.textTheme.bodySmall?.copyWith( + color: colorScheme.onSurfaceVariant, + fontSize: 11, + ), + ), + ], + ], + ), + ), + + // Actions Menu + if (user.role != 'owner') + PopupMenuButton( + onSelected: (value) { + switch (value) { + case 'permissions': + _showPermissionsDialog(user); + break; + case 'remove': + _removeUser(user); + break; + } + }, + itemBuilder: (context) => [ + PopupMenuItem( + value: 'permissions', + child: Row( + children: [ + Icon( + Icons.security_outlined, + size: 18, + color: colorScheme.primary, + ), + const SizedBox(width: 12), + Text(t.editPermissions), + ], + ), + ), + PopupMenuItem( + value: 'remove', + child: Row( + children: [ + Icon( + Icons.delete_outline, + size: 18, + color: colorScheme.error, + ), + const SizedBox(width: 12), + Text(t.removeUser), + ], + ), + ), + ], + child: Container( + padding: const EdgeInsets.all(8), + decoration: BoxDecoration( + color: colorScheme.surfaceContainerHighest, + borderRadius: BorderRadius.circular(8), + border: Border.all( + color: colorScheme.outline.withValues(alpha: 0.2), + width: 1, + ), + ), + child: Icon( + Icons.more_vert, + size: 18, + color: colorScheme.onSurfaceVariant, + ), + ), + ), + ], + ), + ), + ); + } + + Widget _buildOwnerChip(ThemeData theme, ColorScheme colorScheme) { + return Container( + padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 4), + decoration: BoxDecoration( + color: Colors.orange.withValues(alpha: 0.1), + borderRadius: BorderRadius.circular(12), + border: Border.all( + color: Colors.orange.withValues(alpha: 0.3), + width: 1, + ), + ), + child: Row( + mainAxisSize: MainAxisSize.min, + children: [ + Icon( + Icons.star, + color: Colors.orange, + size: 12, + ), + const SizedBox(width: 4), + Text( + 'مالک', + style: TextStyle( + color: Colors.orange.shade700, + fontSize: 11, + fontWeight: FontWeight.w600, + ), + ), + ], + ), + ); + } + + void _showPermissionsDialog(BusinessUser user) { + final t = AppLocalizations.of(context); + final theme = Theme.of(context); + final colorScheme = theme.colorScheme; + + showDialog( + context: context, + builder: (context) => StatefulBuilder( + builder: (context, setState) { + Map currentPermissions = Map.from(user.permissions); + + return Dialog( + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(20), + ), + child: Container( + width: 800, + height: 700, + child: Column( + children: [ + // Header + Container( + padding: const EdgeInsets.all(24), + decoration: BoxDecoration( + color: colorScheme.primaryContainer, + borderRadius: const BorderRadius.only( + topLeft: Radius.circular(20), + topRight: Radius.circular(20), + ), + ), + child: Row( + children: [ + Container( + padding: const EdgeInsets.all(12), + decoration: BoxDecoration( + color: colorScheme.primary, + borderRadius: BorderRadius.circular(12), + ), + child: Icon( + Icons.security, + color: colorScheme.onPrimary, + size: 24, + ), + ), + const SizedBox(width: 16), + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + '${t.userPermissions} - ${user.userName}', + style: theme.textTheme.titleLarge?.copyWith( + fontWeight: FontWeight.bold, + color: colorScheme.onPrimaryContainer, + ), + ), + const SizedBox(height: 4), + Text( + 'مدیریت دسترسی‌های کاربر', + style: theme.textTheme.bodyMedium?.copyWith( + color: colorScheme.onPrimaryContainer.withValues(alpha: 0.8), + ), + ), + ], + ), + ), + IconButton( + onPressed: () => context.pop(), + icon: Icon( + Icons.close, + color: colorScheme.onPrimaryContainer, + ), + ), + ], + ), + ), + + // Content + Expanded( + child: SingleChildScrollView( + padding: const EdgeInsets.all(24), + child: Column( + children: [ + // اشخاص + _buildPermissionSection( + 'اشخاص', + Icons.people, + [ + _buildPermissionGroup( + 'اشخاص', + [ + _buildPermissionItem('افزودن', 'افزودن شخص جدید', _getPermission(currentPermissions, 'people', 'add'), (value) => _setPermission(currentPermissions, 'people', 'add', value), theme, colorScheme), + _buildPermissionItem('مشاهده', 'مشاهده لیست اشخاص', _getPermission(currentPermissions, 'people', 'view'), (value) => _setPermission(currentPermissions, 'people', 'view', value), theme, colorScheme), + _buildPermissionItem('ویرایش', 'ویرایش اطلاعات اشخاص', _getPermission(currentPermissions, 'people', 'edit'), (value) => _setPermission(currentPermissions, 'people', 'edit', value), theme, colorScheme), + _buildPermissionItem('حذف', 'حذف اشخاص', _getPermission(currentPermissions, 'people', 'delete'), (value) => _setPermission(currentPermissions, 'people', 'delete', value), theme, colorScheme), + ], + theme, + colorScheme, + ), + _buildPermissionGroup( + 'دریافت از اشخاص', + [ + _buildPermissionItem('افزودن', 'افزودن دریافت جدید', _getPermission(currentPermissions, 'people_receipts', 'add'), (value) => _setPermission(currentPermissions, 'people_receipts', 'add', value), theme, colorScheme), + _buildPermissionItem('مشاهده', 'مشاهده دریافت‌ها', _getPermission(currentPermissions, 'people_receipts', 'view'), (value) => _setPermission(currentPermissions, 'people_receipts', 'view', value), theme, colorScheme), + _buildPermissionItem('ویرایش', 'ویرایش دریافت‌ها', _getPermission(currentPermissions, 'people_receipts', 'edit'), (value) => _setPermission(currentPermissions, 'people_receipts', 'edit', value), theme, colorScheme), + _buildPermissionItem('حذف', 'حذف دریافت‌ها', _getPermission(currentPermissions, 'people_receipts', 'delete'), (value) => _setPermission(currentPermissions, 'people_receipts', 'delete', value), theme, colorScheme), + _buildPermissionItem('مدیریت پیش‌نویس‌ها', 'مدیریت پیش‌نویس‌های دریافت', _getPermission(currentPermissions, 'people_receipts', 'draft'), (value) => _setPermission(currentPermissions, 'people_receipts', 'draft', value), theme, colorScheme), + ], + theme, + colorScheme, + ), + _buildPermissionGroup( + 'پرداخت به اشخاص', + [ + _buildPermissionItem('افزودن', 'افزودن پرداخت جدید', _getPermission(currentPermissions, 'people_payments', 'add'), (value) => _setPermission(currentPermissions, 'people_payments', 'add', value), theme, colorScheme), + _buildPermissionItem('مشاهده', 'مشاهده پرداخت‌ها', _getPermission(currentPermissions, 'people_payments', 'view'), (value) => _setPermission(currentPermissions, 'people_payments', 'view', value), theme, colorScheme), + _buildPermissionItem('ویرایش', 'ویرایش پرداخت‌ها', _getPermission(currentPermissions, 'people_payments', 'edit'), (value) => _setPermission(currentPermissions, 'people_payments', 'edit', value), theme, colorScheme), + _buildPermissionItem('حذف', 'حذف پرداخت‌ها', _getPermission(currentPermissions, 'people_payments', 'delete'), (value) => _setPermission(currentPermissions, 'people_payments', 'delete', value), theme, colorScheme), + _buildPermissionItem('مدیریت پیش‌نویس‌ها', 'مدیریت پیش‌نویس‌های پرداخت', _getPermission(currentPermissions, 'people_payments', 'draft'), (value) => _setPermission(currentPermissions, 'people_payments', 'draft', value), theme, colorScheme), + ], + theme, + colorScheme, + ), + ], + theme, + colorScheme, + ), + + const SizedBox(height: 20), + + // کالا و خدمات + _buildPermissionSection( + 'کالا و خدمات', + Icons.inventory, + [ + _buildPermissionGroup( + 'کالا‌ها و خدمات', + [ + _buildPermissionItem('افزودن', 'افزودن کالا یا خدمت', _getPermission(currentPermissions, 'products', 'add'), (value) => _setPermission(currentPermissions, 'products', 'add', value), theme, colorScheme), + _buildPermissionItem('مشاهده', 'مشاهده کالاها و خدمات', _getPermission(currentPermissions, 'products', 'view'), (value) => _setPermission(currentPermissions, 'products', 'view', value), theme, colorScheme), + _buildPermissionItem('ویرایش', 'ویرایش کالاها و خدمات', _getPermission(currentPermissions, 'products', 'edit'), (value) => _setPermission(currentPermissions, 'products', 'edit', value), theme, colorScheme), + _buildPermissionItem('حذف', 'حذف کالاها و خدمات', _getPermission(currentPermissions, 'products', 'delete'), (value) => _setPermission(currentPermissions, 'products', 'delete', value), theme, colorScheme), + ], + theme, + colorScheme, + ), + _buildPermissionGroup( + 'لیست‌های قیمت', + [ + _buildPermissionItem('افزودن', 'افزودن لیست قیمت', _getPermission(currentPermissions, 'price_lists', 'add'), (value) => _setPermission(currentPermissions, 'price_lists', 'add', value), theme, colorScheme), + _buildPermissionItem('مشاهده', 'مشاهده لیست‌های قیمت', _getPermission(currentPermissions, 'price_lists', 'view'), (value) => _setPermission(currentPermissions, 'price_lists', 'view', value), theme, colorScheme), + _buildPermissionItem('ویرایش', 'ویرایش لیست‌های قیمت', _getPermission(currentPermissions, 'price_lists', 'edit'), (value) => _setPermission(currentPermissions, 'price_lists', 'edit', value), theme, colorScheme), + _buildPermissionItem('حذف', 'حذف لیست‌های قیمت', _getPermission(currentPermissions, 'price_lists', 'delete'), (value) => _setPermission(currentPermissions, 'price_lists', 'delete', value), theme, colorScheme), + ], + theme, + colorScheme, + ), + _buildPermissionGroup( + 'دسته‌بندی‌ها', + [ + _buildPermissionItem('افزودن', 'افزودن دسته‌بندی', _getPermission(currentPermissions, 'categories', 'add'), (value) => _setPermission(currentPermissions, 'categories', 'add', value), theme, colorScheme), + _buildPermissionItem('مشاهده', 'مشاهده دسته‌بندی‌ها', _getPermission(currentPermissions, 'categories', 'view'), (value) => _setPermission(currentPermissions, 'categories', 'view', value), theme, colorScheme), + _buildPermissionItem('ویرایش', 'ویرایش دسته‌بندی‌ها', _getPermission(currentPermissions, 'categories', 'edit'), (value) => _setPermission(currentPermissions, 'categories', 'edit', value), theme, colorScheme), + _buildPermissionItem('حذف', 'حذف دسته‌بندی‌ها', _getPermission(currentPermissions, 'categories', 'delete'), (value) => _setPermission(currentPermissions, 'categories', 'delete', value), theme, colorScheme), + ], + theme, + colorScheme, + ), + _buildPermissionGroup( + 'ویژگی‌های کالا و خدمات', + [ + _buildPermissionItem('افزودن', 'افزودن ویژگی', _getPermission(currentPermissions, 'product_attributes', 'add'), (value) => _setPermission(currentPermissions, 'product_attributes', 'add', value), theme, colorScheme), + _buildPermissionItem('مشاهده', 'مشاهده ویژگی‌ها', _getPermission(currentPermissions, 'product_attributes', 'view'), (value) => _setPermission(currentPermissions, 'product_attributes', 'view', value), theme, colorScheme), + _buildPermissionItem('ویرایش', 'ویرایش ویژگی‌ها', _getPermission(currentPermissions, 'product_attributes', 'edit'), (value) => _setPermission(currentPermissions, 'product_attributes', 'edit', value), theme, colorScheme), + _buildPermissionItem('حذف', 'حذف ویژگی‌ها', _getPermission(currentPermissions, 'product_attributes', 'delete'), (value) => _setPermission(currentPermissions, 'product_attributes', 'delete', value), theme, colorScheme), + ], + theme, + colorScheme, + ), + ], + theme, + colorScheme, + ), + + const SizedBox(height: 20), + + // بانکداری + _buildPermissionSection( + 'بانکداری', + Icons.account_balance, + [ + _buildPermissionGroup( + 'حساب‌های بانکی', + [ + _buildPermissionItem('افزودن', 'افزودن حساب بانکی', _getPermission(currentPermissions, 'bank_accounts', 'add'), (value) => _setPermission(currentPermissions, 'bank_accounts', 'add', value), theme, colorScheme), + _buildPermissionItem('مشاهده', 'مشاهده حساب‌های بانکی', _getPermission(currentPermissions, 'bank_accounts', 'view'), (value) => _setPermission(currentPermissions, 'bank_accounts', 'view', value), theme, colorScheme), + _buildPermissionItem('ویرایش', 'ویرایش حساب‌های بانکی', _getPermission(currentPermissions, 'bank_accounts', 'edit'), (value) => _setPermission(currentPermissions, 'bank_accounts', 'edit', value), theme, colorScheme), + _buildPermissionItem('حذف', 'حذف حساب‌های بانکی', _getPermission(currentPermissions, 'bank_accounts', 'delete'), (value) => _setPermission(currentPermissions, 'bank_accounts', 'delete', value), theme, colorScheme), + ], + theme, + colorScheme, + ), + _buildPermissionGroup( + 'صندوق', + [ + _buildPermissionItem('افزودن', 'افزودن صندوق', _getPermission(currentPermissions, 'cash', 'add'), (value) => _setPermission(currentPermissions, 'cash', 'add', value), theme, colorScheme), + _buildPermissionItem('مشاهده', 'مشاهده صندوق‌ها', _getPermission(currentPermissions, 'cash', 'view'), (value) => _setPermission(currentPermissions, 'cash', 'view', value), theme, colorScheme), + _buildPermissionItem('ویرایش', 'ویرایش صندوق‌ها', _getPermission(currentPermissions, 'cash', 'edit'), (value) => _setPermission(currentPermissions, 'cash', 'edit', value), theme, colorScheme), + _buildPermissionItem('حذف', 'حذف صندوق‌ها', _getPermission(currentPermissions, 'cash', 'delete'), (value) => _setPermission(currentPermissions, 'cash', 'delete', value), theme, colorScheme), + ], + theme, + colorScheme, + ), + _buildPermissionGroup( + 'تنخواه گردان', + [ + _buildPermissionItem('افزودن', 'افزودن تنخواه', _getPermission(currentPermissions, 'petty_cash', 'add'), (value) => _setPermission(currentPermissions, 'petty_cash', 'add', value), theme, colorScheme), + _buildPermissionItem('مشاهده', 'مشاهده تنخواه‌ها', _getPermission(currentPermissions, 'petty_cash', 'view'), (value) => _setPermission(currentPermissions, 'petty_cash', 'view', value), theme, colorScheme), + _buildPermissionItem('ویرایش', 'ویرایش تنخواه‌ها', _getPermission(currentPermissions, 'petty_cash', 'edit'), (value) => _setPermission(currentPermissions, 'petty_cash', 'edit', value), theme, colorScheme), + _buildPermissionItem('حذف', 'حذف تنخواه‌ها', _getPermission(currentPermissions, 'petty_cash', 'delete'), (value) => _setPermission(currentPermissions, 'petty_cash', 'delete', value), theme, colorScheme), + ], + theme, + colorScheme, + ), + _buildPermissionGroup( + 'چک', + [ + _buildPermissionItem('افزودن', 'افزودن چک', _getPermission(currentPermissions, 'checks', 'add'), (value) => _setPermission(currentPermissions, 'checks', 'add', value), theme, colorScheme), + _buildPermissionItem('مشاهده', 'مشاهده چک‌ها', _getPermission(currentPermissions, 'checks', 'view'), (value) => _setPermission(currentPermissions, 'checks', 'view', value), theme, colorScheme), + _buildPermissionItem('ویرایش', 'ویرایش چک‌ها', _getPermission(currentPermissions, 'checks', 'edit'), (value) => _setPermission(currentPermissions, 'checks', 'edit', value), theme, colorScheme), + _buildPermissionItem('حذف', 'حذف چک‌ها', _getPermission(currentPermissions, 'checks', 'delete'), (value) => _setPermission(currentPermissions, 'checks', 'delete', value), theme, colorScheme), + _buildPermissionItem('وصول', 'وصول چک‌ها', _getPermission(currentPermissions, 'checks', 'collect'), (value) => _setPermission(currentPermissions, 'checks', 'collect', value), theme, colorScheme), + _buildPermissionItem('انتقال', 'انتقال چک‌ها', _getPermission(currentPermissions, 'checks', 'transfer'), (value) => _setPermission(currentPermissions, 'checks', 'transfer', value), theme, colorScheme), + _buildPermissionItem('برگشت', 'برگشت چک‌ها', _getPermission(currentPermissions, 'checks', 'return'), (value) => _setPermission(currentPermissions, 'checks', 'return', value), theme, colorScheme), + ], + theme, + colorScheme, + ), + _buildPermissionGroup( + 'کیف پول', + [ + _buildPermissionItem('مشاهده', 'مشاهده کیف پول', _getPermission(currentPermissions, 'wallet', 'view'), (value) => _setPermission(currentPermissions, 'wallet', 'view', value), theme, colorScheme), + _buildPermissionItem('شارژ', 'شارژ کیف پول', _getPermission(currentPermissions, 'wallet', 'charge'), (value) => _setPermission(currentPermissions, 'wallet', 'charge', value), theme, colorScheme), + ], + theme, + colorScheme, + ), + _buildPermissionGroup( + 'انتقال', + [ + _buildPermissionItem('افزودن', 'افزودن انتقال', _getPermission(currentPermissions, 'transfers', 'add'), (value) => _setPermission(currentPermissions, 'transfers', 'add', value), theme, colorScheme), + _buildPermissionItem('مشاهده', 'مشاهده انتقال‌ها', _getPermission(currentPermissions, 'transfers', 'view'), (value) => _setPermission(currentPermissions, 'transfers', 'view', value), theme, colorScheme), + _buildPermissionItem('ویرایش', 'ویرایش انتقال‌ها', _getPermission(currentPermissions, 'transfers', 'edit'), (value) => _setPermission(currentPermissions, 'transfers', 'edit', value), theme, colorScheme), + _buildPermissionItem('حذف', 'حذف انتقال‌ها', _getPermission(currentPermissions, 'transfers', 'delete'), (value) => _setPermission(currentPermissions, 'transfers', 'delete', value), theme, colorScheme), + _buildPermissionItem('مدیریت پیش‌نویس‌ها', 'مدیریت پیش‌نویس‌های انتقال', _getPermission(currentPermissions, 'transfers', 'draft'), (value) => _setPermission(currentPermissions, 'transfers', 'draft', value), theme, colorScheme), + ], + theme, + colorScheme, + ), + ], + theme, + colorScheme, + ), + + const SizedBox(height: 20), + + // فاکتورها و هزینه‌ها + _buildPermissionSection( + 'فاکتورها و هزینه‌ها', + Icons.receipt, + [ + _buildPermissionGroup( + 'فاکتورها', + [ + _buildPermissionItem('افزودن', 'افزودن فاکتور', _getPermission(currentPermissions, 'invoices', 'add'), (value) => _setPermission(currentPermissions, 'invoices', 'add', value), theme, colorScheme), + _buildPermissionItem('مشاهده', 'مشاهده فاکتورها', _getPermission(currentPermissions, 'invoices', 'view'), (value) => _setPermission(currentPermissions, 'invoices', 'view', value), theme, colorScheme), + _buildPermissionItem('ویرایش', 'ویرایش فاکتورها', _getPermission(currentPermissions, 'invoices', 'edit'), (value) => _setPermission(currentPermissions, 'invoices', 'edit', value), theme, colorScheme), + _buildPermissionItem('حذف', 'حذف فاکتورها', _getPermission(currentPermissions, 'invoices', 'delete'), (value) => _setPermission(currentPermissions, 'invoices', 'delete', value), theme, colorScheme), + _buildPermissionItem('مدیریت پیش‌نویس‌ها', 'مدیریت پیش‌نویس‌های فاکتور', _getPermission(currentPermissions, 'invoices', 'draft'), (value) => _setPermission(currentPermissions, 'invoices', 'draft', value), theme, colorScheme), + ], + theme, + colorScheme, + ), + _buildPermissionGroup( + 'هزینه و درآمد', + [ + _buildPermissionItem('افزودن', 'افزودن هزینه یا درآمد', _getPermission(currentPermissions, 'expenses_income', 'add'), (value) => _setPermission(currentPermissions, 'expenses_income', 'add', value), theme, colorScheme), + _buildPermissionItem('مشاهده', 'مشاهده هزینه‌ها و درآمدها', _getPermission(currentPermissions, 'expenses_income', 'view'), (value) => _setPermission(currentPermissions, 'expenses_income', 'view', value), theme, colorScheme), + _buildPermissionItem('ویرایش', 'ویرایش هزینه‌ها و درآمدها', _getPermission(currentPermissions, 'expenses_income', 'edit'), (value) => _setPermission(currentPermissions, 'expenses_income', 'edit', value), theme, colorScheme), + _buildPermissionItem('حذف', 'حذف هزینه‌ها و درآمدها', _getPermission(currentPermissions, 'expenses_income', 'delete'), (value) => _setPermission(currentPermissions, 'expenses_income', 'delete', value), theme, colorScheme), + _buildPermissionItem('مدیریت پیش‌نویس‌ها', 'مدیریت پیش‌نویس‌های هزینه و درآمد', _getPermission(currentPermissions, 'expenses_income', 'draft'), (value) => _setPermission(currentPermissions, 'expenses_income', 'draft', value), theme, colorScheme), + ], + theme, + colorScheme, + ), + ], + theme, + colorScheme, + ), + + const SizedBox(height: 20), + + // حسابداری + _buildPermissionSection( + 'حسابداری', + Icons.calculate, + [ + _buildPermissionGroup( + 'اسناد حسابداری', + [ + _buildPermissionItem('افزودن', 'افزودن سند حسابداری', _getPermission(currentPermissions, 'accounting_documents', 'add'), (value) => _setPermission(currentPermissions, 'accounting_documents', 'add', value), theme, colorScheme), + _buildPermissionItem('مشاهده', 'مشاهده اسناد حسابداری', _getPermission(currentPermissions, 'accounting_documents', 'view'), (value) => _setPermission(currentPermissions, 'accounting_documents', 'view', value), theme, colorScheme), + _buildPermissionItem('ویرایش', 'ویرایش اسناد حسابداری', _getPermission(currentPermissions, 'accounting_documents', 'edit'), (value) => _setPermission(currentPermissions, 'accounting_documents', 'edit', value), theme, colorScheme), + _buildPermissionItem('حذف', 'حذف اسناد حسابداری', _getPermission(currentPermissions, 'accounting_documents', 'delete'), (value) => _setPermission(currentPermissions, 'accounting_documents', 'delete', value), theme, colorScheme), + _buildPermissionItem('مدیریت پیش‌نویس‌ها', 'مدیریت پیش‌نویس‌های اسناد', _getPermission(currentPermissions, 'accounting_documents', 'draft'), (value) => _setPermission(currentPermissions, 'accounting_documents', 'draft', value), theme, colorScheme), + ], + theme, + colorScheme, + ), + _buildPermissionGroup( + 'جدول حساب‌ها', + [ + _buildPermissionItem('افزودن', 'افزودن حساب', _getPermission(currentPermissions, 'chart_of_accounts', 'add'), (value) => _setPermission(currentPermissions, 'chart_of_accounts', 'add', value), theme, colorScheme), + _buildPermissionItem('مشاهده', 'مشاهده جدول حساب‌ها', _getPermission(currentPermissions, 'chart_of_accounts', 'view'), (value) => _setPermission(currentPermissions, 'chart_of_accounts', 'view', value), theme, colorScheme), + _buildPermissionItem('ویرایش', 'ویرایش جدول حساب‌ها', _getPermission(currentPermissions, 'chart_of_accounts', 'edit'), (value) => _setPermission(currentPermissions, 'chart_of_accounts', 'edit', value), theme, colorScheme), + _buildPermissionItem('حذف', 'حذف حساب‌ها', _getPermission(currentPermissions, 'chart_of_accounts', 'delete'), (value) => _setPermission(currentPermissions, 'chart_of_accounts', 'delete', value), theme, colorScheme), + ], + theme, + colorScheme, + ), + _buildPermissionGroup( + 'تراز افتتاحیه', + [ + _buildPermissionItem('مشاهده', 'مشاهده تراز افتتاحیه', _getPermission(currentPermissions, 'opening_balance', 'view'), (value) => _setPermission(currentPermissions, 'opening_balance', 'view', value), theme, colorScheme), + _buildPermissionItem('ویرایش', 'ویرایش تراز افتتاحیه', _getPermission(currentPermissions, 'opening_balance', 'edit'), (value) => _setPermission(currentPermissions, 'opening_balance', 'edit', value), theme, colorScheme), + ], + theme, + colorScheme, + ), + ], + theme, + colorScheme, + ), + + const SizedBox(height: 20), + + // انبارداری + _buildPermissionSection( + 'انبارداری', + Icons.warehouse, + [ + _buildPermissionGroup( + 'مدیریت انبارها', + [ + _buildPermissionItem('افزودن', 'افزودن انبار', _getPermission(currentPermissions, 'warehouses', 'add'), (value) => _setPermission(currentPermissions, 'warehouses', 'add', value), theme, colorScheme), + _buildPermissionItem('مشاهده', 'مشاهده انبارها', _getPermission(currentPermissions, 'warehouses', 'view'), (value) => _setPermission(currentPermissions, 'warehouses', 'view', value), theme, colorScheme), + _buildPermissionItem('ویرایش', 'ویرایش انبارها', _getPermission(currentPermissions, 'warehouses', 'edit'), (value) => _setPermission(currentPermissions, 'warehouses', 'edit', value), theme, colorScheme), + _buildPermissionItem('حذف', 'حذف انبارها', _getPermission(currentPermissions, 'warehouses', 'delete'), (value) => _setPermission(currentPermissions, 'warehouses', 'delete', value), theme, colorScheme), + ], + theme, + colorScheme, + ), + _buildPermissionGroup( + 'صدور حواله', + [ + _buildPermissionItem('افزودن', 'افزودن حواله', _getPermission(currentPermissions, 'warehouse_transfers', 'add'), (value) => _setPermission(currentPermissions, 'warehouse_transfers', 'add', value), theme, colorScheme), + _buildPermissionItem('مشاهده', 'مشاهده حواله‌ها', _getPermission(currentPermissions, 'warehouse_transfers', 'view'), (value) => _setPermission(currentPermissions, 'warehouse_transfers', 'view', value), theme, colorScheme), + _buildPermissionItem('ویرایش', 'ویرایش حواله‌ها', _getPermission(currentPermissions, 'warehouse_transfers', 'edit'), (value) => _setPermission(currentPermissions, 'warehouse_transfers', 'edit', value), theme, colorScheme), + _buildPermissionItem('حذف', 'حذف حواله‌ها', _getPermission(currentPermissions, 'warehouse_transfers', 'delete'), (value) => _setPermission(currentPermissions, 'warehouse_transfers', 'delete', value), theme, colorScheme), + _buildPermissionItem('مدیریت پیش‌نویس‌ها', 'مدیریت پیش‌نویس‌های حواله', _getPermission(currentPermissions, 'warehouse_transfers', 'draft'), (value) => _setPermission(currentPermissions, 'warehouse_transfers', 'draft', value), theme, colorScheme), + ], + theme, + colorScheme, + ), + ], + theme, + colorScheme, + ), + + const SizedBox(height: 20), + + // تنظیمات + _buildPermissionSection( + 'تنظیمات', + Icons.settings, + [ + _buildPermissionGroup( + 'تنظیمات', + [ + _buildPermissionItem('تنظیمات کسب و کار', 'مدیریت تنظیمات کسب و کار', _getPermission(currentPermissions, 'settings', 'business'), (value) => _setPermission(currentPermissions, 'settings', 'business', value), theme, colorScheme), + _buildPermissionItem('تنظیمات چاپ اسناد', 'مدیریت تنظیمات چاپ', _getPermission(currentPermissions, 'settings', 'print'), (value) => _setPermission(currentPermissions, 'settings', 'print', value), theme, colorScheme), + _buildPermissionItem('تاریخچه رویدادها', 'مشاهده تاریخچه رویدادها', _getPermission(currentPermissions, 'settings', 'history'), (value) => _setPermission(currentPermissions, 'settings', 'history', value), theme, colorScheme), + _buildPermissionItem('کاربران و دسترسی‌ها', 'مدیریت کاربران و دسترسی‌ها', _getPermission(currentPermissions, 'settings', 'users'), (value) => _setPermission(currentPermissions, 'settings', 'users', value), theme, colorScheme), + ], + theme, + colorScheme, + ), + _buildPermissionGroup( + 'فضای ذخیره‌سازی', + [ + _buildPermissionItem('مشاهده', 'مشاهده فضای ذخیره‌سازی', _getPermission(currentPermissions, 'storage', 'view'), (value) => _setPermission(currentPermissions, 'storage', 'view', value), theme, colorScheme), + _buildPermissionItem('حذف', 'حذف فایل‌ها', _getPermission(currentPermissions, 'storage', 'delete'), (value) => _setPermission(currentPermissions, 'storage', 'delete', value), theme, colorScheme), + ], + theme, + colorScheme, + ), + _buildPermissionGroup( + 'پنل پیامک', + [ + _buildPermissionItem('مشاهده تاریخچه', 'مشاهده تاریخچه پیامک‌ها', _getPermission(currentPermissions, 'sms', 'history'), (value) => _setPermission(currentPermissions, 'sms', 'history', value), theme, colorScheme), + _buildPermissionItem('مدیریت قالب‌ها', 'مدیریت قالب‌های پیامک', _getPermission(currentPermissions, 'sms', 'templates'), (value) => _setPermission(currentPermissions, 'sms', 'templates', value), theme, colorScheme), + ], + theme, + colorScheme, + ), + _buildPermissionGroup( + 'بازار افزونه‌ها', + [ + _buildPermissionItem('مشاهده', 'مشاهده افزونه‌ها', _getPermission(currentPermissions, 'marketplace', 'view'), (value) => _setPermission(currentPermissions, 'marketplace', 'view', value), theme, colorScheme), + _buildPermissionItem('خرید', 'خرید افزونه‌ها', _getPermission(currentPermissions, 'marketplace', 'buy'), (value) => _setPermission(currentPermissions, 'marketplace', 'buy', value), theme, colorScheme), + _buildPermissionItem('صورت حساب‌ها', 'مشاهده صورت حساب‌ها', _getPermission(currentPermissions, 'marketplace', 'invoices'), (value) => _setPermission(currentPermissions, 'marketplace', 'invoices', value), theme, colorScheme), + ], + theme, + colorScheme, + ), + ], + theme, + colorScheme, + ), + ], + ), + ), + ), + + // Actions + Container( + padding: const EdgeInsets.all(24), + decoration: BoxDecoration( + color: colorScheme.surfaceContainerHighest, + borderRadius: const BorderRadius.only( + bottomLeft: Radius.circular(20), + bottomRight: Radius.circular(20), + ), + ), + child: Row( + mainAxisAlignment: MainAxisAlignment.end, + children: [ + TextButton( + onPressed: () => context.pop(), + child: Text(t.cancel), + ), + const SizedBox(width: 12), + ElevatedButton.icon( + onPressed: () { + _updatePermissions(user, currentPermissions); + context.pop(); + }, + icon: const Icon(Icons.save, size: 18), + label: Text(t.savePermissions), + style: ElevatedButton.styleFrom( + padding: const EdgeInsets.symmetric(horizontal: 24, vertical: 12), + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(12), + ), + ), + ), + ], + ), + ), + ], + ), + ), + ); + }, + ), + ); + } + + Widget _buildPermissionSection( + String title, + IconData icon, + List permissions, + ThemeData theme, + ColorScheme colorScheme, + ) { + return Container( + decoration: BoxDecoration( + color: colorScheme.surfaceContainerLowest, + borderRadius: BorderRadius.circular(16), + border: Border.all( + color: colorScheme.outline.withValues(alpha: 0.1), + width: 1, + ), + ), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Container( + padding: const EdgeInsets.all(16), + decoration: BoxDecoration( + color: colorScheme.primaryContainer.withValues(alpha: 0.3), + borderRadius: const BorderRadius.only( + topLeft: Radius.circular(16), + topRight: Radius.circular(16), + ), + ), + child: Row( + children: [ + Icon( + icon, + size: 20, + color: colorScheme.primary, + ), + const SizedBox(width: 12), + Text( + title, + style: theme.textTheme.titleMedium?.copyWith( + fontWeight: FontWeight.w600, + color: colorScheme.onSurface, + ), + ), + ], + ), + ), + Padding( + padding: const EdgeInsets.all(16), + child: Column( + children: permissions, + ), + ), + ], + ), + ); + } + + Widget _buildPermissionGroup( + String groupTitle, + List permissions, + ThemeData theme, + ColorScheme colorScheme, + ) { + return Container( + margin: const EdgeInsets.only(bottom: 16), + padding: const EdgeInsets.all(16), + decoration: BoxDecoration( + color: colorScheme.surfaceContainerLowest.withValues(alpha: 0.5), + borderRadius: BorderRadius.circular(12), + border: Border.all( + color: colorScheme.outline.withValues(alpha: 0.1), + width: 1, + ), + ), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + groupTitle, + style: theme.textTheme.titleSmall?.copyWith( + fontWeight: FontWeight.w600, + color: colorScheme.primary, + ), + ), + const SizedBox(height: 12), + ...permissions, + ], + ), + ); + } + + Widget _buildPermissionItem( + String title, + String description, + bool value, + ValueChanged onChanged, + ThemeData theme, + ColorScheme colorScheme, + ) { + return Container( + margin: const EdgeInsets.only(bottom: 8), + child: Row( + children: [ + Switch( + value: value, + onChanged: onChanged, + activeColor: colorScheme.primary, + materialTapTargetSize: MaterialTapTargetSize.shrinkWrap, + ), + const SizedBox(width: 12), + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + title, + style: theme.textTheme.bodyMedium?.copyWith( + fontWeight: FontWeight.w500, + color: colorScheme.onSurface, + ), + ), + const SizedBox(height: 2), + Text( + description, + style: theme.textTheme.bodySmall?.copyWith( + color: colorScheme.onSurfaceVariant, + fontSize: 11, + ), + ), + ], + ), + ), + ], + ), + ); + } + + + bool _getPermission(Map permissions, String section, String action) { + if (!permissions.containsKey(section)) return false; + final sectionPerms = permissions[section] as Map?; + if (sectionPerms == null) return false; + return sectionPerms[action] == true; + } + + void _setPermission(Map permissions, String section, String action, bool value) { + if (!permissions.containsKey(section)) { + permissions[section] = {}; + } + permissions[section][action] = value; + } + +} \ No newline at end of file diff --git a/hesabixUI/hesabix_ui/lib/pages/login_page.dart b/hesabixUI/hesabix_ui/lib/pages/login_page.dart index 371a06d..5c94226 100644 --- a/hesabixUI/hesabix_ui/lib/pages/login_page.dart +++ b/hesabixUI/hesabix_ui/lib/pages/login_page.dart @@ -248,20 +248,26 @@ class _LoginPageState extends State with SingleTickerProviderStateMix // ذخیره دسترسی‌های اپلیکیشن final appPermissions = user?['app_permissions'] as Map?; final isSuperAdmin = appPermissions?['superadmin'] == true; + final userId = user?['id'] as int?; if (appPermissions != null) { - await widget.authStore.saveAppPermissions(appPermissions, isSuperAdmin); + await widget.authStore.saveAppPermissions(appPermissions, isSuperAdmin, userId: userId); } if (!mounted) return; _showSnack(t.homeWelcome); // بعد از login موفق، به صفحه قبلی یا dashboard برود - final currentPath = GoRouterState.of(context).uri.path; - if (currentPath.startsWith('/user/profile/') || currentPath.startsWith('/acc/')) { - // اگر در صفحه محافظت شده بود، همان صفحه را refresh کند - context.go(currentPath); - } else { - // وگرنه به dashboard برود + try { + final currentPath = GoRouterState.of(context).uri.path; + if (currentPath.startsWith('/user/profile/') || currentPath.startsWith('/acc/') || currentPath.startsWith('/business/')) { + // اگر در صفحه محافظت شده بود، همان صفحه را refresh کند + context.go(currentPath); + } else { + // وگرنه به dashboard برود + context.go('/user/profile/dashboard'); + } + } catch (e) { + // اگر GoRouterState در دسترس نیست، به dashboard برود context.go('/user/profile/dashboard'); } } catch (e) { @@ -344,9 +350,10 @@ class _LoginPageState extends State with SingleTickerProviderStateMix // ذخیره دسترسی‌های اپلیکیشن final appPermissions = user?['app_permissions'] as Map?; final isSuperAdmin = appPermissions?['superadmin'] == true; + final userId = user?['id'] as int?; if (appPermissions != null) { - await widget.authStore.saveAppPermissions(appPermissions, isSuperAdmin); + await widget.authStore.saveAppPermissions(appPermissions, isSuperAdmin, userId: userId); } _showSnack(t.registerSuccess); // پاکسازی کد معرف پس از ثبت‌نام موفق diff --git a/hesabixUI/hesabix_ui/lib/pages/profile/create_ticket_page.dart b/hesabixUI/hesabix_ui/lib/pages/profile/create_ticket_page.dart index 428ef13..4726de7 100644 --- a/hesabixUI/hesabix_ui/lib/pages/profile/create_ticket_page.dart +++ b/hesabixUI/hesabix_ui/lib/pages/profile/create_ticket_page.dart @@ -1,4 +1,5 @@ import 'package:flutter/material.dart'; +import 'package:go_router/go_router.dart'; import 'package:hesabix_ui/l10n/app_localizations.dart'; import 'package:hesabix_ui/core/api_client.dart'; import 'package:hesabix_ui/services/support_service.dart'; @@ -235,7 +236,7 @@ class _CreateTicketPageState extends State { ), ), IconButton( - onPressed: () => Navigator.of(context).pop(), + onPressed: () => context.pop(), icon: const Icon(Icons.close), style: IconButton.styleFrom( backgroundColor: theme.colorScheme.surface, @@ -645,7 +646,7 @@ class _CreateTicketPageState extends State { ), const SizedBox(width: 12), OutlinedButton( - onPressed: _isSubmitting ? null : () => Navigator.of(context).pop(), + onPressed: _isSubmitting ? null : () => context.pop(), style: OutlinedButton.styleFrom( padding: const EdgeInsets.symmetric(vertical: 16, horizontal: 24), shape: RoundedRectangleBorder( diff --git a/hesabixUI/hesabix_ui/lib/pages/profile/new_business_page.dart b/hesabixUI/hesabix_ui/lib/pages/profile/new_business_page.dart index 1427457..99c13be 100644 --- a/hesabixUI/hesabix_ui/lib/pages/profile/new_business_page.dart +++ b/hesabixUI/hesabix_ui/lib/pages/profile/new_business_page.dart @@ -1,4 +1,5 @@ import 'package:flutter/material.dart'; +import 'package:go_router/go_router.dart'; import 'package:hesabix_ui/l10n/app_localizations.dart'; import '../../models/business_models.dart'; import '../../services/business_api_service.dart'; @@ -121,7 +122,7 @@ class _NewBusinessPageState extends State { duration: const Duration(seconds: 2), ), ); - Navigator.of(context).pop(); + context.pop(); } } catch (e) { if (mounted) { diff --git a/hesabixUI/hesabix_ui/lib/pages/profile/profile_shell.dart b/hesabixUI/hesabix_ui/lib/pages/profile/profile_shell.dart index 858ea42..986a5c3 100644 --- a/hesabixUI/hesabix_ui/lib/pages/profile/profile_shell.dart +++ b/hesabixUI/hesabix_ui/lib/pages/profile/profile_shell.dart @@ -42,7 +42,12 @@ class _ProfileShellState extends State { final bool useRail = width >= 700; final bool railExtended = width >= 1100; final ColorScheme scheme = Theme.of(context).colorScheme; - final String location = GoRouterState.of(context).uri.toString(); + String location = '/user/profile/dashboard'; // default location + try { + location = GoRouterState.of(context).uri.toString(); + } catch (e) { + // اگر GoRouterState در دسترس نیست، از default استفاده کن + } final bool isDark = Theme.of(context).brightness == Brightness.dark; final String logoAsset = isDark ? 'assets/images/logo-light.png' @@ -85,7 +90,12 @@ class _ProfileShellState extends State { Future onSelect(int index) async { final path = allDestinations[index].path; - if (GoRouterState.of(context).uri.toString() != path) { + try { + if (GoRouterState.of(context).uri.toString() != path) { + context.go(path); + } + } catch (e) { + // اگر GoRouterState در دسترس نیست، مستقیماً به مسیر برود context.go(path); } } @@ -251,7 +261,7 @@ class _ProfileShellState extends State { selected: active, selectedTileColor: activeBg, onTap: () { - Navigator.of(context).pop(); + context.pop(); onSelect(i); }, ); diff --git a/hesabixUI/hesabix_ui/lib/services/business_user_service.dart b/hesabixUI/hesabix_ui/lib/services/business_user_service.dart new file mode 100644 index 0000000..6636c82 --- /dev/null +++ b/hesabixUI/hesabix_ui/lib/services/business_user_service.dart @@ -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 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 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 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 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 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'); + } + } +} \ No newline at end of file diff --git a/hesabixUI/hesabix_ui/lib/services/person_service.dart b/hesabixUI/hesabix_ui/lib/services/person_service.dart new file mode 100644 index 0000000..e69de29 diff --git a/hesabixUI/hesabix_ui/lib/widgets/admin/file_storage/storage_config_form_dialog.dart b/hesabixUI/hesabix_ui/lib/widgets/admin/file_storage/storage_config_form_dialog.dart index 12e3d25..7fb49fa 100644 --- a/hesabixUI/hesabix_ui/lib/widgets/admin/file_storage/storage_config_form_dialog.dart +++ b/hesabixUI/hesabix_ui/lib/widgets/admin/file_storage/storage_config_form_dialog.dart @@ -1,4 +1,5 @@ import 'package:flutter/material.dart'; +import 'package:go_router/go_router.dart'; import 'package:hesabix_ui/l10n/app_localizations.dart'; import '../../../core/api_client.dart'; @@ -121,7 +122,7 @@ class _StorageConfigFormDialogState extends State { } if (mounted) { - Navigator.of(context).pop(); + context.pop(); // Only show SnackBar if there's no onSaved callback (parent will handle notification) if (widget.onSaved == null) { @@ -202,7 +203,7 @@ class _StorageConfigFormDialogState extends State { ), ), IconButton( - onPressed: () => Navigator.of(context).pop(), + onPressed: () => context.pop(), icon: const Icon(Icons.close), color: theme.colorScheme.onPrimary, ), @@ -344,7 +345,7 @@ class _StorageConfigFormDialogState extends State { mainAxisAlignment: MainAxisAlignment.end, children: [ TextButton( - onPressed: _isLoading ? null : () => Navigator.of(context).pop(), + onPressed: _isLoading ? null : () => context.pop(), child: Text(l10n.cancel), ), const SizedBox(width: 12), diff --git a/hesabixUI/hesabix_ui/lib/widgets/data_table/column_settings_dialog.dart b/hesabixUI/hesabix_ui/lib/widgets/data_table/column_settings_dialog.dart index 63f6675..ea8d612 100644 --- a/hesabixUI/hesabix_ui/lib/widgets/data_table/column_settings_dialog.dart +++ b/hesabixUI/hesabix_ui/lib/widgets/data_table/column_settings_dialog.dart @@ -1,4 +1,5 @@ import 'package:flutter/material.dart'; +import 'package:go_router/go_router.dart'; import 'package:hesabix_ui/l10n/app_localizations.dart'; import 'data_table_config.dart'; import 'helpers/column_settings_service.dart'; @@ -65,7 +66,7 @@ class _ColumnSettingsDialogState extends State { ), const Spacer(), IconButton( - onPressed: () => Navigator.of(context).pop(), + onPressed: () => context.pop(), icon: const Icon(Icons.close), ), ], @@ -96,7 +97,7 @@ class _ColumnSettingsDialogState extends State { ), const SizedBox(width: 12), OutlinedButton( - onPressed: () => Navigator.of(context).pop(), + onPressed: () => context.pop(), child: Text(t.cancel), ), const SizedBox(width: 12), diff --git a/hesabixUI/hesabix_ui/lib/widgets/data_table/data_table_search_dialog.dart b/hesabixUI/hesabix_ui/lib/widgets/data_table/data_table_search_dialog.dart index a4694eb..6f1a558 100644 --- a/hesabixUI/hesabix_ui/lib/widgets/data_table/data_table_search_dialog.dart +++ b/hesabixUI/hesabix_ui/lib/widgets/data_table/data_table_search_dialog.dart @@ -1,4 +1,5 @@ import 'package:flutter/material.dart'; +import 'package:go_router/go_router.dart'; import 'package:intl/intl.dart'; import 'package:hesabix_ui/l10n/app_localizations.dart'; import 'data_table_config.dart'; @@ -234,7 +235,7 @@ class _DataTableSearchDialogState extends State { return [ TextButton( onPressed: () { - Navigator.of(context).pop(); + context.pop(); }, child: Text(t.cancel), ), @@ -242,7 +243,7 @@ class _DataTableSearchDialogState extends State { TextButton( onPressed: () { widget.onClear(); - Navigator.of(context).pop(); + context.pop(); }, child: Text(t.clear), ), @@ -302,7 +303,7 @@ class _DataTableSearchDialogState extends State { widget.onApply(_controller.text.trim(), _selectedType); break; } - Navigator.of(context).pop(); + context.pop(); } Future _selectFromDate(AppLocalizations t, bool isJalali) async { @@ -449,7 +450,7 @@ class _DataTableDateRangeDialogState extends State { actions: [ TextButton( onPressed: () { - Navigator.of(context).pop(); + context.pop(); }, child: Text(t.cancel), ), @@ -457,7 +458,7 @@ class _DataTableDateRangeDialogState extends State { TextButton( onPressed: () { widget.onClear(); - Navigator.of(context).pop(); + context.pop(); }, child: Text(t.clear), ), @@ -465,7 +466,7 @@ class _DataTableDateRangeDialogState extends State { onPressed: _fromDate != null && _toDate != null ? () { widget.onApply(_fromDate, _toDate); - Navigator.of(context).pop(); + context.pop(); } : null, child: Text(t.applyFilter), diff --git a/hesabixUI/hesabix_ui/lib/widgets/jalali_date_picker.dart b/hesabixUI/hesabix_ui/lib/widgets/jalali_date_picker.dart index cae1e20..5db1b42 100644 --- a/hesabixUI/hesabix_ui/lib/widgets/jalali_date_picker.dart +++ b/hesabixUI/hesabix_ui/lib/widgets/jalali_date_picker.dart @@ -1,4 +1,5 @@ import 'package:flutter/material.dart'; +import 'package:go_router/go_router.dart'; import 'package:shamsi_date/shamsi_date.dart'; /// DatePicker سفارشی برای تقویم شمسی @@ -100,7 +101,7 @@ class _JalaliDatePickerState extends State { mainAxisAlignment: MainAxisAlignment.end, children: [ TextButton( - onPressed: () => Navigator.of(context).pop(), + onPressed: () => context.pop(), child: Text( 'انصراف', style: TextStyle(color: theme.textTheme.bodyMedium?.color), diff --git a/hesabixUI/hesabix_ui/lib/widgets/progress_splash_screen.dart b/hesabixUI/hesabix_ui/lib/widgets/progress_splash_screen.dart index 89e2776..fd7cb35 100644 --- a/hesabixUI/hesabix_ui/lib/widgets/progress_splash_screen.dart +++ b/hesabixUI/hesabix_ui/lib/widgets/progress_splash_screen.dart @@ -134,11 +134,11 @@ class _ProgressSplashScreenState extends State colors: isDark ? [ bgColor, - bgColor.withOpacity(0.95), + bgColor.withValues(alpha: 0.95), ] : [ bgColor, - bgColor.withOpacity(0.98), + bgColor.withValues(alpha: 0.98), ], ), ), @@ -161,7 +161,7 @@ class _ProgressSplashScreenState extends State borderRadius: BorderRadius.circular(20), boxShadow: [ BoxShadow( - color: primary.withOpacity(0.2), + color: primary.withValues(alpha: 0.2), blurRadius: 20, spreadRadius: 2, ), @@ -204,7 +204,7 @@ class _ProgressSplashScreenState extends State // Subtitle Text( - AppLocalizations.of(context)?.businessManagementPlatform ?? 'Business Management Platform', + AppLocalizations.of(context).businessManagementPlatform, style: theme.textTheme.bodyLarge?.copyWith( color: colorScheme.onSurfaceVariant, fontWeight: FontWeight.w400, @@ -243,7 +243,7 @@ class _ProgressSplashScreenState extends State decoration: BoxDecoration( borderRadius: BorderRadius.circular(2), gradient: LinearGradient( - colors: [primary, primary.withOpacity(0.8)], + colors: [primary, primary.withValues(alpha: 0.8)], ), ), ), @@ -255,7 +255,7 @@ class _ProgressSplashScreenState extends State // Loading Message Text( - widget.message ?? AppLocalizations.of(context)?.loading ?? 'Loading...', + widget.message ?? AppLocalizations.of(context).loading, style: theme.textTheme.bodyMedium?.copyWith( color: colorScheme.onSurfaceVariant, fontWeight: FontWeight.w500, @@ -268,7 +268,7 @@ class _ProgressSplashScreenState extends State Text( '${_remainingSeconds}s', style: theme.textTheme.bodySmall?.copyWith( - color: colorScheme.onSurfaceVariant.withOpacity(0.7), + color: colorScheme.onSurfaceVariant.withValues(alpha: 0.7), fontWeight: FontWeight.w400, ), ), @@ -281,7 +281,7 @@ class _ProgressSplashScreenState extends State Text( 'Version 1.0.0', style: theme.textTheme.bodySmall?.copyWith( - color: colorScheme.onSurfaceVariant.withOpacity(0.6), + color: colorScheme.onSurfaceVariant.withValues(alpha: 0.6), ), ), ], diff --git a/hesabixUI/hesabix_ui/lib/widgets/settings_menu_button.dart b/hesabixUI/hesabix_ui/lib/widgets/settings_menu_button.dart new file mode 100644 index 0000000..2c4ccba --- /dev/null +++ b/hesabixUI/hesabix_ui/lib/widgets/settings_menu_button.dart @@ -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, + ); + } +} diff --git a/hesabixUI/hesabix_ui/lib/widgets/simple_splash_screen.dart b/hesabixUI/hesabix_ui/lib/widgets/simple_splash_screen.dart index 8499ade..9b11052 100644 --- a/hesabixUI/hesabix_ui/lib/widgets/simple_splash_screen.dart +++ b/hesabixUI/hesabix_ui/lib/widgets/simple_splash_screen.dart @@ -1,5 +1,6 @@ import 'package:flutter/material.dart'; import 'dart:async'; +import '../core/auth_store.dart'; class SimpleSplashScreen extends StatefulWidget { final String? message; @@ -9,6 +10,7 @@ class SimpleSplashScreen extends StatefulWidget { final Duration displayDuration; final VoidCallback? onComplete; final Locale? locale; + final AuthStore? authStore; const SimpleSplashScreen({ super.key, @@ -16,9 +18,10 @@ class SimpleSplashScreen extends StatefulWidget { this.showLogo = true, this.backgroundColor, this.primaryColor, - this.displayDuration = const Duration(seconds: 4), + this.displayDuration = const Duration(seconds: 1), this.onComplete, this.locale, + this.authStore, }); @override @@ -68,12 +71,51 @@ class _SimpleSplashScreenState extends State _fadeController.forward(); _scaleController.forward(); - // Start display timer + // Start display timer with authentication check _displayTimer = Timer(widget.displayDuration, () { - widget.onComplete?.call(); + _checkAuthenticationAndComplete(); }); } + void _checkAuthenticationAndComplete() { + print('🔍 SPLASH DEBUG: Checking authentication and completing splash screen'); + + // اگر authStore موجود است، وضعیت احراز هویت را بررسی کن + if (widget.authStore != null) { + final hasApiKey = widget.authStore!.apiKey != null && widget.authStore!.apiKey!.isNotEmpty; + print('🔍 SPLASH DEBUG: AuthStore available, has API key: $hasApiKey'); + + // اگر کاربر وارد شده، URL فعلی را ذخیره کن + if (hasApiKey) { + // URL فعلی را از window.location بگیر + try { + // در web، URL فعلی را از window.location می‌گیریم + final currentUrl = Uri.base.path; + print('🔍 SPLASH DEBUG: Current URL from Uri.base: $currentUrl'); + + if (currentUrl.isNotEmpty && + currentUrl != '/' && + currentUrl != '/login' && + (currentUrl.startsWith('/user/profile/') || currentUrl.startsWith('/business/'))) { + print('🔍 SPLASH DEBUG: Saving current URL: $currentUrl'); + widget.authStore!.saveLastUrl(currentUrl); + } + } catch (e) { + print('🔍 SPLASH DEBUG: Error getting current URL: $e'); + } + } + + // اگر کاربر وارد نشده، به صفحه لاگین هدایت می‌شود + // اگر کاربر وارد شده، در صفحه کنونی می‌ماند + // این منطق در main.dart در GoRouter redirect مدیریت می‌شود + } else { + print('🔍 SPLASH DEBUG: AuthStore is null'); + } + + print('🔍 SPLASH DEBUG: Calling onComplete callback'); + widget.onComplete?.call(); + } + @override void dispose() { _fadeController.dispose(); diff --git a/hesabixUI/hesabix_ui/lib/widgets/splash_screen.dart b/hesabixUI/hesabix_ui/lib/widgets/splash_screen.dart index fb3d163..b30751b 100644 --- a/hesabixUI/hesabix_ui/lib/widgets/splash_screen.dart +++ b/hesabixUI/hesabix_ui/lib/widgets/splash_screen.dart @@ -36,11 +36,11 @@ class SplashScreen extends StatelessWidget { colors: isDark ? [ bgColor, - bgColor.withOpacity(0.95), + bgColor.withValues(alpha: 0.95), ] : [ bgColor, - bgColor.withOpacity(0.98), + bgColor.withValues(alpha: 0.98), ], ), ), @@ -56,7 +56,7 @@ class SplashScreen extends StatelessWidget { borderRadius: BorderRadius.circular(20), boxShadow: [ BoxShadow( - color: primary.withOpacity(0.2), + color: primary.withValues(alpha: 0.2), blurRadius: 20, spreadRadius: 2, ), @@ -99,7 +99,7 @@ class SplashScreen extends StatelessWidget { // Subtitle Text( - AppLocalizations.of(context)?.businessManagementPlatform ?? 'Business Management Platform', + AppLocalizations.of(context).businessManagementPlatform, style: theme.textTheme.bodyLarge?.copyWith( color: colorScheme.onSurfaceVariant, fontWeight: FontWeight.w400, @@ -122,7 +122,7 @@ class SplashScreen extends StatelessWidget { // Loading Message Text( - message ?? AppLocalizations.of(context)?.loading ?? 'Loading...', + message ?? AppLocalizations.of(context).loading, style: theme.textTheme.bodyMedium?.copyWith( color: colorScheme.onSurfaceVariant, fontWeight: FontWeight.w500, @@ -137,7 +137,7 @@ class SplashScreen extends StatelessWidget { Text( 'Version 1.0.0', style: theme.textTheme.bodySmall?.copyWith( - color: colorScheme.onSurfaceVariant.withOpacity(0.6), + color: colorScheme.onSurfaceVariant.withValues(alpha: 0.6), ), ), ], diff --git a/hesabixUI/hesabix_ui/lib/widgets/support/ticket_details_dialog.dart b/hesabixUI/hesabix_ui/lib/widgets/support/ticket_details_dialog.dart index 48724d1..7b17551 100644 --- a/hesabixUI/hesabix_ui/lib/widgets/support/ticket_details_dialog.dart +++ b/hesabixUI/hesabix_ui/lib/widgets/support/ticket_details_dialog.dart @@ -1,4 +1,5 @@ import 'package:flutter/material.dart'; +import 'package:go_router/go_router.dart'; import 'package:hesabix_ui/l10n/app_localizations.dart'; import 'package:hesabix_ui/models/support_models.dart'; import 'package:hesabix_ui/services/support_service.dart'; @@ -312,7 +313,7 @@ class _TicketDetailsDialogState extends State { ), ), IconButton( - onPressed: () => Navigator.of(context).pop(), + onPressed: () => context.pop(), icon: const Icon(Icons.close), style: IconButton.styleFrom( backgroundColor: Colors.grey[200], diff --git a/hesabixUI/hesabix_ui/lib/widgets/url_tracker.dart b/hesabixUI/hesabix_ui/lib/widgets/url_tracker.dart new file mode 100644 index 0000000..2888de5 --- /dev/null +++ b/hesabixUI/hesabix_ui/lib/widgets/url_tracker.dart @@ -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 createState() => _UrlTrackerState(); +} + +class _UrlTrackerState extends State { + 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; + } +} diff --git a/hesabixUI/hesabix_ui/lib/widgets/user_account_menu_button.dart b/hesabixUI/hesabix_ui/lib/widgets/user_account_menu_button.dart new file mode 100644 index 0000000..db62ebf --- /dev/null +++ b/hesabixUI/hesabix_ui/lib/widgets/user_account_menu_button.dart @@ -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, + ); + } +} diff --git a/hesabixUI/hesabix_ui/lib/widgets/user_menu_button.dart b/hesabixUI/hesabix_ui/lib/widgets/user_menu_button.dart new file mode 100644 index 0000000..db8d86d --- /dev/null +++ b/hesabixUI/hesabix_ui/lib/widgets/user_menu_button.dart @@ -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), + ], + ); + } +} \ No newline at end of file