progress in permissions
This commit is contained in:
parent
898e0fb993
commit
2e0c68967b
|
|
@ -264,7 +264,8 @@ def get_business_info_with_permissions(
|
||||||
if not ctx.is_superadmin() and not ctx.is_business_owner(business_id):
|
if not ctx.is_superadmin() and not ctx.is_business_owner(business_id):
|
||||||
# دریافت دسترسیهای کسب و کار از business_permissions
|
# دریافت دسترسیهای کسب و کار از business_permissions
|
||||||
permission_repo = BusinessPermissionRepository(db)
|
permission_repo = BusinessPermissionRepository(db)
|
||||||
business_permission = permission_repo.get_by_business_and_user(business_id, ctx.get_user_id())
|
# ترتیب آرگومانها: (user_id, business_id)
|
||||||
|
business_permission = permission_repo.get_by_user_and_business(ctx.get_user_id(), business_id)
|
||||||
if business_permission:
|
if business_permission:
|
||||||
permissions = business_permission.business_permissions or {}
|
permissions = business_permission.business_permissions or {}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -18,6 +18,127 @@ from adapters.db.models.business import Business
|
||||||
router = APIRouter(prefix="/business", tags=["business-users"])
|
router = APIRouter(prefix="/business", tags=["business-users"])
|
||||||
|
|
||||||
|
|
||||||
|
@router.get("/{business_id}/users/{user_id}",
|
||||||
|
summary="دریافت جزئیات کاربر",
|
||||||
|
description="دریافت جزئیات کاربر و دسترسیهایش در کسب و کار",
|
||||||
|
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": "2024-01-01T12:00:00Z",
|
||||||
|
"permissions": {
|
||||||
|
"people": {
|
||||||
|
"add": True,
|
||||||
|
"view": True,
|
||||||
|
"edit": False,
|
||||||
|
"delete": False
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
401: {
|
||||||
|
"description": "کاربر احراز هویت نشده است"
|
||||||
|
},
|
||||||
|
403: {
|
||||||
|
"description": "دسترسی غیرمجاز به کسب و کار"
|
||||||
|
},
|
||||||
|
404: {
|
||||||
|
"description": "کاربر یافت نشد"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
)
|
||||||
|
@require_business_access("business_id")
|
||||||
|
def get_user_details(
|
||||||
|
request: Request,
|
||||||
|
business_id: int,
|
||||||
|
user_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 user details for user {user_id} in 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 view user details for business {business_id}")
|
||||||
|
raise HTTPException(status_code=403, detail="شما مجوز مشاهده جزئیات کاربران ندارید")
|
||||||
|
|
||||||
|
# Get user details
|
||||||
|
user = db.get(User, user_id)
|
||||||
|
if not user:
|
||||||
|
logger.warning(f"User {user_id} not found")
|
||||||
|
raise HTTPException(status_code=404, detail="کاربر یافت نشد")
|
||||||
|
|
||||||
|
# Get user permissions for this business
|
||||||
|
permission_repo = BusinessPermissionRepository(db)
|
||||||
|
permission_obj = permission_repo.get_by_user_and_business(user_id, business_id)
|
||||||
|
|
||||||
|
# Determine role and permissions
|
||||||
|
if business.owner_id == user_id:
|
||||||
|
role = "owner"
|
||||||
|
permissions = {} # Owner has all permissions
|
||||||
|
else:
|
||||||
|
role = "member"
|
||||||
|
permissions = permission_obj.business_permissions if permission_obj else {}
|
||||||
|
|
||||||
|
# Format user data
|
||||||
|
user_data = {
|
||||||
|
"id": permission_obj.id if permission_obj else user_id,
|
||||||
|
"business_id": business_id,
|
||||||
|
"user_id": user_id,
|
||||||
|
"user_name": f"{user.first_name or ''} {user.last_name or ''}".strip(),
|
||||||
|
"user_email": user.email or "",
|
||||||
|
"user_phone": user.mobile,
|
||||||
|
"role": role,
|
||||||
|
"status": "active",
|
||||||
|
"added_at": permission_obj.created_at if permission_obj else business.created_at,
|
||||||
|
"last_active": permission_obj.updated_at if permission_obj else business.updated_at,
|
||||||
|
"permissions": permissions,
|
||||||
|
}
|
||||||
|
|
||||||
|
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.get("/{business_id}/users",
|
@router.get("/{business_id}/users",
|
||||||
summary="لیست کاربران کسب و کار",
|
summary="لیست کاربران کسب و کار",
|
||||||
description="دریافت لیست کاربران یک کسب و کار",
|
description="دریافت لیست کاربران یک کسب و کار",
|
||||||
|
|
|
||||||
|
|
@ -28,15 +28,34 @@ class BusinessPermissionRepository(BaseRepository[BusinessPermission]):
|
||||||
existing = self.get_by_user_and_business(user_id, business_id)
|
existing = self.get_by_user_and_business(user_id, business_id)
|
||||||
|
|
||||||
if existing:
|
if existing:
|
||||||
existing.business_permissions = permissions
|
# Preserve existing permissions and enforce join=True
|
||||||
|
existing_permissions = existing.business_permissions or {}
|
||||||
|
|
||||||
|
# Always ignore incoming 'join' field from clients
|
||||||
|
incoming_permissions = dict(permissions or {})
|
||||||
|
if 'join' in incoming_permissions:
|
||||||
|
incoming_permissions.pop('join', None)
|
||||||
|
|
||||||
|
# Merge and enforce join flag
|
||||||
|
merged_permissions = dict(existing_permissions)
|
||||||
|
merged_permissions.update(incoming_permissions)
|
||||||
|
merged_permissions['join'] = True
|
||||||
|
|
||||||
|
existing.business_permissions = merged_permissions
|
||||||
self.db.commit()
|
self.db.commit()
|
||||||
self.db.refresh(existing)
|
self.db.refresh(existing)
|
||||||
return existing
|
return existing
|
||||||
else:
|
else:
|
||||||
|
# On creation, ensure join=True exists by default
|
||||||
|
base_permissions = {'join': True}
|
||||||
|
incoming_permissions = dict(permissions or {})
|
||||||
|
if 'join' in incoming_permissions:
|
||||||
|
incoming_permissions.pop('join', None)
|
||||||
|
|
||||||
new_permission = BusinessPermission(
|
new_permission = BusinessPermission(
|
||||||
user_id=user_id,
|
user_id=user_id,
|
||||||
business_id=business_id,
|
business_id=business_id,
|
||||||
business_permissions=permissions
|
business_permissions={**base_permissions, **incoming_permissions}
|
||||||
)
|
)
|
||||||
self.db.add(new_permission)
|
self.db.add(new_permission)
|
||||||
self.db.commit()
|
self.db.commit()
|
||||||
|
|
|
||||||
|
|
@ -71,6 +71,29 @@ class ApiClient {
|
||||||
if (calendarType != null && calendarType.isNotEmpty) {
|
if (calendarType != null && calendarType.isNotEmpty) {
|
||||||
options.headers['X-Calendar-Type'] = calendarType;
|
options.headers['X-Calendar-Type'] = calendarType;
|
||||||
}
|
}
|
||||||
|
// Inject X-Business-ID header when path targets a specific business
|
||||||
|
try {
|
||||||
|
final uri = options.uri;
|
||||||
|
final path = uri.path;
|
||||||
|
// If current business exists, prefer it
|
||||||
|
final currentBusinessId = _authStore?.currentBusiness?.id;
|
||||||
|
int? resolvedBusinessId = currentBusinessId;
|
||||||
|
// Fallback: detect business_id from URL like /api/v1/business/{id}/...
|
||||||
|
if (resolvedBusinessId == null) {
|
||||||
|
final match = RegExp(r"/api/v1/business/(\d+)/").firstMatch(path);
|
||||||
|
if (match != null) {
|
||||||
|
final idStr = match.group(1);
|
||||||
|
if (idStr != null) {
|
||||||
|
resolvedBusinessId = int.tryParse(idStr);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (resolvedBusinessId != null) {
|
||||||
|
options.headers['X-Business-ID'] = resolvedBusinessId.toString();
|
||||||
|
}
|
||||||
|
} catch (_) {
|
||||||
|
// ignore header injection failures
|
||||||
|
}
|
||||||
if (kDebugMode) {
|
if (kDebugMode) {
|
||||||
// ignore: avoid_print
|
// ignore: avoid_print
|
||||||
print('[API][REQ] ${options.method} ${options.uri}');
|
print('[API][REQ] ${options.method} ${options.uri}');
|
||||||
|
|
|
||||||
|
|
@ -317,15 +317,16 @@ class AuthStore with ChangeNotifier {
|
||||||
if (_businessPermissions == null) return false;
|
if (_businessPermissions == null) return false;
|
||||||
|
|
||||||
final sectionPerms = _businessPermissions![section] as Map<String, dynamic>?;
|
final sectionPerms = _businessPermissions![section] as Map<String, dynamic>?;
|
||||||
if (sectionPerms == null) return action == 'view'; // دسترسی خواندن پیشفرض
|
// اگر سکشن در دسترسیها موجود نیست، هیچ دسترسیای وجود ندارد
|
||||||
|
if (sectionPerms == null) return false;
|
||||||
|
|
||||||
return sectionPerms[action] == true;
|
return sectionPerms[action] == true;
|
||||||
}
|
}
|
||||||
|
|
||||||
// دسترسیهای کلی
|
// دسترسیهای کلی
|
||||||
bool canReadSection(String section) {
|
bool canReadSection(String section) {
|
||||||
return hasBusinessPermission(section, 'view') ||
|
// خواندن فقط زمانی مجاز است که بهصراحت در سکشن اجازه داده شده باشد
|
||||||
_businessPermissions?.containsKey(section) == true;
|
return hasBusinessPermission(section, 'view');
|
||||||
}
|
}
|
||||||
|
|
||||||
bool canWriteSection(String section) {
|
bool canWriteSection(String section) {
|
||||||
|
|
|
||||||
|
|
@ -837,6 +837,27 @@
|
||||||
"activePersons": "Active Persons",
|
"activePersons": "Active Persons",
|
||||||
"inactivePersons": "Inactive Persons",
|
"inactivePersons": "Inactive Persons",
|
||||||
"personsByType": "Persons by Type",
|
"personsByType": "Persons by Type",
|
||||||
"update": "Update"
|
"update": "Update",
|
||||||
|
"collect": "Collect",
|
||||||
|
"transfer": "Transfer",
|
||||||
|
"charge": "Charge",
|
||||||
|
"businessSettings": "Business Settings",
|
||||||
|
"printSettings": "Print Settings",
|
||||||
|
"eventHistory": "Event History",
|
||||||
|
"usersAndPermissions": "Users and Permissions",
|
||||||
|
"storageSpace": "Storage Space",
|
||||||
|
"deleteFiles": "Files",
|
||||||
|
"viewSmsHistory": "View SMS History",
|
||||||
|
"manageSmsTemplates": "Manage SMS Templates",
|
||||||
|
"viewMarketplace": "View Marketplace",
|
||||||
|
"buyPlugins": "Buy Plugins",
|
||||||
|
"viewInvoices": "View Invoices",
|
||||||
|
"saving": "Saving...",
|
||||||
|
"userPermissionsTitle": "User Permissions",
|
||||||
|
"dialogClose": "Close",
|
||||||
|
"buy": "Buy",
|
||||||
|
"templates": "Templates",
|
||||||
|
"history": "History",
|
||||||
|
"business": "Business"
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -836,6 +836,27 @@
|
||||||
"activePersons": "اشخاص فعال",
|
"activePersons": "اشخاص فعال",
|
||||||
"inactivePersons": "اشخاص غیرفعال",
|
"inactivePersons": "اشخاص غیرفعال",
|
||||||
"personsByType": "اشخاص بر اساس نوع",
|
"personsByType": "اشخاص بر اساس نوع",
|
||||||
"update": "ویرایش"
|
"update": "ویرایش",
|
||||||
|
"collect": "وصول",
|
||||||
|
"transfer": "انتقال",
|
||||||
|
"charge": "شارژ",
|
||||||
|
"businessSettings": "تنظیمات کسب و کار",
|
||||||
|
"printSettings": "تنظیمات چاپ اسناد",
|
||||||
|
"eventHistory": "تاریخچه رویدادها",
|
||||||
|
"usersAndPermissions": "کاربران و دسترسیها",
|
||||||
|
"storageSpace": "فضای ذخیرهسازی",
|
||||||
|
"deleteFiles": "فایلها",
|
||||||
|
"viewSmsHistory": "مشاهده تاریخچه پیامکها",
|
||||||
|
"manageSmsTemplates": "مدیریت قالبهای پیامک",
|
||||||
|
"viewMarketplace": "مشاهده افزونهها",
|
||||||
|
"buyPlugins": "خرید افزونهها",
|
||||||
|
"viewInvoices": "صورت حسابها",
|
||||||
|
"saving": "در حال ذخیره...",
|
||||||
|
"userPermissionsTitle": "دسترسیهای کاربر",
|
||||||
|
"dialogClose": "بستن",
|
||||||
|
"buy": "خرید",
|
||||||
|
"templates": "قالبها",
|
||||||
|
"history": "تاریخچه",
|
||||||
|
"business": "کسب و کار"
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -4037,7 +4037,7 @@ abstract class AppLocalizations {
|
||||||
/// No description provided for @deleteFiles.
|
/// No description provided for @deleteFiles.
|
||||||
///
|
///
|
||||||
/// In en, this message translates to:
|
/// In en, this message translates to:
|
||||||
/// **'Delete Files'**
|
/// **'Files'**
|
||||||
String get deleteFiles;
|
String get deleteFiles;
|
||||||
|
|
||||||
/// No description provided for @smsPanel.
|
/// No description provided for @smsPanel.
|
||||||
|
|
@ -4567,6 +4567,66 @@ abstract class AppLocalizations {
|
||||||
/// In en, this message translates to:
|
/// In en, this message translates to:
|
||||||
/// **'Update'**
|
/// **'Update'**
|
||||||
String get update;
|
String get update;
|
||||||
|
|
||||||
|
/// No description provided for @collect.
|
||||||
|
///
|
||||||
|
/// In en, this message translates to:
|
||||||
|
/// **'Collect'**
|
||||||
|
String get collect;
|
||||||
|
|
||||||
|
/// No description provided for @transfer.
|
||||||
|
///
|
||||||
|
/// In en, this message translates to:
|
||||||
|
/// **'Transfer'**
|
||||||
|
String get transfer;
|
||||||
|
|
||||||
|
/// No description provided for @charge.
|
||||||
|
///
|
||||||
|
/// In en, this message translates to:
|
||||||
|
/// **'Charge'**
|
||||||
|
String get charge;
|
||||||
|
|
||||||
|
/// No description provided for @saving.
|
||||||
|
///
|
||||||
|
/// In en, this message translates to:
|
||||||
|
/// **'Saving...'**
|
||||||
|
String get saving;
|
||||||
|
|
||||||
|
/// No description provided for @userPermissionsTitle.
|
||||||
|
///
|
||||||
|
/// In en, this message translates to:
|
||||||
|
/// **'User Permissions'**
|
||||||
|
String get userPermissionsTitle;
|
||||||
|
|
||||||
|
/// No description provided for @dialogClose.
|
||||||
|
///
|
||||||
|
/// In en, this message translates to:
|
||||||
|
/// **'Close'**
|
||||||
|
String get dialogClose;
|
||||||
|
|
||||||
|
/// No description provided for @buy.
|
||||||
|
///
|
||||||
|
/// In en, this message translates to:
|
||||||
|
/// **'Buy'**
|
||||||
|
String get buy;
|
||||||
|
|
||||||
|
/// No description provided for @templates.
|
||||||
|
///
|
||||||
|
/// In en, this message translates to:
|
||||||
|
/// **'Templates'**
|
||||||
|
String get templates;
|
||||||
|
|
||||||
|
/// No description provided for @history.
|
||||||
|
///
|
||||||
|
/// In en, this message translates to:
|
||||||
|
/// **'History'**
|
||||||
|
String get history;
|
||||||
|
|
||||||
|
/// No description provided for @business.
|
||||||
|
///
|
||||||
|
/// In en, this message translates to:
|
||||||
|
/// **'Business'**
|
||||||
|
String get business;
|
||||||
}
|
}
|
||||||
|
|
||||||
class _AppLocalizationsDelegate
|
class _AppLocalizationsDelegate
|
||||||
|
|
|
||||||
|
|
@ -2025,7 +2025,7 @@ class AppLocalizationsEn extends AppLocalizations {
|
||||||
String get viewStorage => 'View Storage';
|
String get viewStorage => 'View Storage';
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String get deleteFiles => 'Delete Files';
|
String get deleteFiles => 'Files';
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String get smsPanel => 'SMS Panel';
|
String get smsPanel => 'SMS Panel';
|
||||||
|
|
@ -2302,4 +2302,34 @@ class AppLocalizationsEn extends AppLocalizations {
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String get update => 'Update';
|
String get update => 'Update';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get collect => 'Collect';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get transfer => 'Transfer';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get charge => 'Charge';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get saving => 'Saving...';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get userPermissionsTitle => 'User Permissions';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get dialogClose => 'Close';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get buy => 'Buy';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get templates => 'Templates';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get history => 'History';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get business => 'Business';
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -2013,7 +2013,7 @@ class AppLocalizationsFa extends AppLocalizations {
|
||||||
String get viewStorage => 'مشاهده فضای ذخیرهسازی';
|
String get viewStorage => 'مشاهده فضای ذخیرهسازی';
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String get deleteFiles => 'حذف فایلها';
|
String get deleteFiles => 'فایلها';
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String get smsPanel => 'پنل پیامک';
|
String get smsPanel => 'پنل پیامک';
|
||||||
|
|
@ -2286,4 +2286,34 @@ class AppLocalizationsFa extends AppLocalizations {
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String get update => 'ویرایش';
|
String get update => 'ویرایش';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get collect => 'وصول';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get transfer => 'انتقال';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get charge => 'شارژ';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get saving => 'در حال ذخیره...';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get userPermissionsTitle => 'دسترسیهای کاربر';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get dialogClose => 'بستن';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get buy => 'خرید';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get templates => 'قالبها';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get history => 'تاریخچه';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get business => 'کسب و کار';
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -795,48 +795,32 @@ class _BusinessShellState extends State<BusinessShell> {
|
||||||
size: 20,
|
size: 20,
|
||||||
)
|
)
|
||||||
else if (item.hasAddButton)
|
else if (item.hasAddButton)
|
||||||
GestureDetector(
|
Builder(builder: (ctx) {
|
||||||
onTap: () {
|
final section = _sectionForLabel(item.label, t);
|
||||||
// Navigate to add new item
|
final canAdd = section != null && (widget.authStore.hasBusinessPermission(section, 'add'));
|
||||||
if (item.label == t.people) {
|
if (!canAdd) return const SizedBox.shrink();
|
||||||
// Navigate to add person
|
return GestureDetector(
|
||||||
_showAddPersonDialog();
|
onTap: () {
|
||||||
} else if (item.label == t.invoice) {
|
if (item.label == t.people) {
|
||||||
// Navigate to add invoice
|
_showAddPersonDialog();
|
||||||
} else if (item.label == t.receiptsAndPayments) {
|
}
|
||||||
// Navigate to add receipt/payment
|
// سایر مسیرهای افزودن در آینده متصل میشوند
|
||||||
} else if (item.label == t.transfers) {
|
},
|
||||||
// Navigate to add transfer
|
child: Container(
|
||||||
} else if (item.label == t.documents) {
|
width: 24,
|
||||||
// Navigate to add document
|
height: 24,
|
||||||
} else if (item.label == t.expenseAndIncome) {
|
decoration: BoxDecoration(
|
||||||
// Navigate to add expense/income
|
color: sideFg.withValues(alpha: 0.1),
|
||||||
} else if (item.label == t.reports) {
|
borderRadius: BorderRadius.circular(4),
|
||||||
// Navigate to add report
|
),
|
||||||
} else if (item.label == t.inquiries) {
|
child: Icon(
|
||||||
// Navigate to add inquiry
|
Icons.add,
|
||||||
} else if (item.label == t.storageSpace) {
|
size: 16,
|
||||||
// Navigate to add storage space
|
color: sideFg,
|
||||||
} else if (item.label == t.taxpayers) {
|
),
|
||||||
// Navigate to add taxpayer
|
|
||||||
} else if (item.label == t.pluginMarketplace) {
|
|
||||||
// Navigate to add plugin
|
|
||||||
}
|
|
||||||
},
|
|
||||||
child: Container(
|
|
||||||
width: 24,
|
|
||||||
height: 24,
|
|
||||||
decoration: BoxDecoration(
|
|
||||||
color: sideFg.withValues(alpha: 0.1),
|
|
||||||
borderRadius: BorderRadius.circular(4),
|
|
||||||
),
|
),
|
||||||
child: Icon(
|
);
|
||||||
Icons.add,
|
}),
|
||||||
size: 16,
|
|
||||||
color: sideFg,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
],
|
],
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
|
|
@ -901,17 +885,41 @@ class _BusinessShellState extends State<BusinessShell> {
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
} else if (item.type == _MenuItemType.simple) {
|
} else if (item.type == _MenuItemType.simple) {
|
||||||
|
final section = _sectionForLabel(item.label, t);
|
||||||
|
final canAdd = section != null && (widget.authStore.hasBusinessPermission(section, 'add'));
|
||||||
return ListTile(
|
return ListTile(
|
||||||
leading: Icon(item.selectedIcon, color: active ? activeFg : sideFg),
|
leading: Icon(item.selectedIcon, color: active ? activeFg : sideFg),
|
||||||
title: Text(item.label, style: TextStyle(color: active ? activeFg : sideFg, fontWeight: active ? FontWeight.w600 : FontWeight.w400)),
|
title: Text(item.label, style: TextStyle(color: active ? activeFg : sideFg, fontWeight: active ? FontWeight.w600 : FontWeight.w400)),
|
||||||
selected: active,
|
selected: active,
|
||||||
selectedTileColor: activeBg,
|
selectedTileColor: activeBg,
|
||||||
|
trailing: (item.hasAddButton && canAdd)
|
||||||
|
? GestureDetector(
|
||||||
|
onTap: () {
|
||||||
|
context.pop();
|
||||||
|
// در حال حاضر فقط اشخاص پشتیبانی میشود
|
||||||
|
if (item.label == t.people) {
|
||||||
|
_showAddPersonDialog();
|
||||||
|
}
|
||||||
|
},
|
||||||
|
child: Container(
|
||||||
|
width: 24,
|
||||||
|
height: 24,
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
color: sideFg.withValues(alpha: 0.1),
|
||||||
|
borderRadius: BorderRadius.circular(4),
|
||||||
|
),
|
||||||
|
child: Icon(Icons.add, size: 16, color: sideFg),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
: null,
|
||||||
onTap: () {
|
onTap: () {
|
||||||
context.pop();
|
context.pop();
|
||||||
onSelect(i);
|
onSelect(i);
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
} else if (item.type == _MenuItemType.expandable) {
|
} else if (item.type == _MenuItemType.expandable) {
|
||||||
|
// فیلتر کردن زیرآیتمها بر اساس دسترسی
|
||||||
|
final visibleChildren = (item.children ?? []).where((child) => _hasAccessToMenuItem(child)).toList();
|
||||||
return ExpansionTile(
|
return ExpansionTile(
|
||||||
leading: Icon(item.icon, color: sideFg),
|
leading: Icon(item.icon, color: sideFg),
|
||||||
title: Text(item.label, style: TextStyle(color: sideFg)),
|
title: Text(item.label, style: TextStyle(color: sideFg)),
|
||||||
|
|
@ -924,10 +932,13 @@ class _BusinessShellState extends State<BusinessShell> {
|
||||||
if (item.label == t.warehouseManagement) _isWarehouseManagementExpanded = expanded;
|
if (item.label == t.warehouseManagement) _isWarehouseManagementExpanded = expanded;
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
children: item.children?.map((child) => ListTile(
|
children: visibleChildren.map((child) {
|
||||||
|
final childSection = _sectionForLabel(child.label, t);
|
||||||
|
final childCanAdd = child.hasAddButton && (childSection != null && widget.authStore.hasBusinessPermission(childSection, 'add'));
|
||||||
|
return ListTile(
|
||||||
leading: const SizedBox(width: 24),
|
leading: const SizedBox(width: 24),
|
||||||
title: Text(child.label),
|
title: Text(child.label),
|
||||||
trailing: child.hasAddButton ? GestureDetector(
|
trailing: childCanAdd ? GestureDetector(
|
||||||
onTap: () {
|
onTap: () {
|
||||||
context.pop();
|
context.pop();
|
||||||
// Navigate to add new item
|
// Navigate to add new item
|
||||||
|
|
@ -977,7 +988,7 @@ class _BusinessShellState extends State<BusinessShell> {
|
||||||
context.pop();
|
context.pop();
|
||||||
onSelectChild(i, item.children!.indexOf(child));
|
onSelectChild(i, item.children!.indexOf(child));
|
||||||
},
|
},
|
||||||
)).toList() ?? [],
|
}).toList(),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
return const SizedBox.shrink();
|
return const SizedBox.shrink();
|
||||||
|
|
@ -1016,39 +1027,12 @@ class _BusinessShellState extends State<BusinessShell> {
|
||||||
}
|
}
|
||||||
|
|
||||||
bool _hasAccessToMenuItem(_MenuItem item) {
|
bool _hasAccessToMenuItem(_MenuItem item) {
|
||||||
final sectionMap = {
|
final section = _sectionForLabel(item.label, AppLocalizations.of(context));
|
||||||
'people': 'people',
|
// داشبورد همیشه قابل مشاهده است
|
||||||
'products': 'products',
|
if (item.path != null && item.path!.endsWith('/dashboard')) return true;
|
||||||
'priceLists': 'price_lists',
|
// اگر سکشن تعریف نشده، نمایش داده نشود
|
||||||
'categories': 'categories',
|
if (section == null) return false;
|
||||||
'productAttributes': 'product_attributes',
|
// فقط وقتی اجازه خواندن دارد نمایش بده
|
||||||
'accounts': 'bank_accounts',
|
|
||||||
'pettyCash': 'petty_cash',
|
|
||||||
'cashBox': 'cash',
|
|
||||||
'wallet': 'wallet',
|
|
||||||
'checks': 'checks',
|
|
||||||
'invoice': 'invoices',
|
|
||||||
'receiptsAndPayments': 'accounting_documents',
|
|
||||||
'expenseAndIncome': 'expenses_income',
|
|
||||||
'transfers': 'transfers',
|
|
||||||
'documents': 'accounting_documents',
|
|
||||||
'chartOfAccounts': 'chart_of_accounts',
|
|
||||||
'openingBalance': 'opening_balance',
|
|
||||||
'yearEndClosing': 'opening_balance',
|
|
||||||
'accountingSettings': 'settings',
|
|
||||||
'reports': 'reports',
|
|
||||||
'warehouses': 'warehouses',
|
|
||||||
'shipments': 'warehouse_transfers',
|
|
||||||
'inquiries': 'reports',
|
|
||||||
'storageSpace': 'storage',
|
|
||||||
'taxpayers': 'settings',
|
|
||||||
'settings': 'settings',
|
|
||||||
'pluginMarketplace': 'marketplace',
|
|
||||||
};
|
|
||||||
|
|
||||||
final section = sectionMap[item.label];
|
|
||||||
if (section == null) return true; // اگر بخشی تعریف نشده، نمایش داده شود
|
|
||||||
|
|
||||||
return widget.authStore.canReadSection(section);
|
return widget.authStore.canReadSection(section);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -1058,6 +1042,35 @@ class _BusinessShellState extends State<BusinessShell> {
|
||||||
// اگر حداقل یکی از زیرآیتمها قابل دسترسی باشد، منو نمایش داده شود
|
// اگر حداقل یکی از زیرآیتمها قابل دسترسی باشد، منو نمایش داده شود
|
||||||
return item.children!.any((child) => _hasAccessToMenuItem(child));
|
return item.children!.any((child) => _hasAccessToMenuItem(child));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// تبدیل برچسب محلیشده منو به کلید سکشن دسترسی
|
||||||
|
String? _sectionForLabel(String label, AppLocalizations t) {
|
||||||
|
if (label == t.people) return 'people';
|
||||||
|
if (label == t.products) return 'products';
|
||||||
|
if (label == t.priceLists) return 'price_lists';
|
||||||
|
if (label == t.categories) return 'categories';
|
||||||
|
if (label == t.productAttributes) return 'product_attributes';
|
||||||
|
if (label == t.accounts) return 'bank_accounts';
|
||||||
|
if (label == t.pettyCash) return 'petty_cash';
|
||||||
|
if (label == t.cashBox) return 'cash';
|
||||||
|
if (label == t.wallet) return 'wallet';
|
||||||
|
if (label == t.checks) return 'checks';
|
||||||
|
if (label == t.invoice) return 'invoices';
|
||||||
|
if (label == t.receiptsAndPayments) return 'people_transactions';
|
||||||
|
if (label == t.expenseAndIncome) return 'expenses_income';
|
||||||
|
if (label == t.transfers) return 'transfers';
|
||||||
|
if (label == t.documents) return 'accounting_documents';
|
||||||
|
if (label == t.chartOfAccounts) return 'chart_of_accounts';
|
||||||
|
if (label == t.openingBalance) return 'opening_balance';
|
||||||
|
if (label == t.warehouses) return 'warehouses';
|
||||||
|
if (label == t.shipments) return 'warehouse_transfers';
|
||||||
|
if (label == t.inquiries) return 'reports';
|
||||||
|
if (label == t.storageSpace) return 'storage';
|
||||||
|
if (label == t.taxpayers) return 'settings';
|
||||||
|
if (label == t.settings) return 'settings';
|
||||||
|
if (label == t.pluginMarketplace) return 'marketplace';
|
||||||
|
return null;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
enum _MenuItemType { simple, expandable, separator }
|
enum _MenuItemType { simple, expandable, separator }
|
||||||
|
|
|
||||||
|
|
@ -61,7 +61,7 @@ class _SettingsPageState extends State<SettingsPage> {
|
||||||
title: t.usersAndPermissions,
|
title: t.usersAndPermissions,
|
||||||
subtitle: t.usersAndPermissionsDescription,
|
subtitle: t.usersAndPermissionsDescription,
|
||||||
icon: Icons.people_outline,
|
icon: Icons.people_outline,
|
||||||
onTap: () => _showUsersPermissionsDialog(context),
|
onTap: () => context.go('/business/${widget.businessId}/users-permissions'),
|
||||||
),
|
),
|
||||||
_buildSettingItem(
|
_buildSettingItem(
|
||||||
context,
|
context,
|
||||||
|
|
@ -240,30 +240,6 @@ class _SettingsPageState extends State<SettingsPage> {
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
void _showUsersPermissionsDialog(BuildContext context) {
|
|
||||||
final t = AppLocalizations.of(context);
|
|
||||||
showDialog(
|
|
||||||
context: context,
|
|
||||||
builder: (context) => AlertDialog(
|
|
||||||
title: Text(t.usersAndPermissions),
|
|
||||||
content: Text(t.usersAndPermissionsDialogContent),
|
|
||||||
actions: [
|
|
||||||
TextButton(
|
|
||||||
onPressed: () => Navigator.pop(context),
|
|
||||||
child: Text(t.close),
|
|
||||||
),
|
|
||||||
FilledButton(
|
|
||||||
onPressed: () {
|
|
||||||
Navigator.pop(context);
|
|
||||||
// Navigate to users permissions page
|
|
||||||
context.go('/business/${widget.businessId}/users-permissions');
|
|
||||||
},
|
|
||||||
child: Text(t.manage),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
void _showPrintDocumentsDialog(BuildContext context) {
|
void _showPrintDocumentsDialog(BuildContext context) {
|
||||||
final t = AppLocalizations.of(context);
|
final t = AppLocalizations.of(context);
|
||||||
|
|
|
||||||
File diff suppressed because it is too large
Load diff
Loading…
Reference in a new issue